Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ pm` default, `NPM_CONFIG_PREFIX`, nvm/Volta) installed `codex` successfully and
| `assetRoutePrefixes.js` | Import-free leaf holding the URL prefixes the server owns: `ASSET_ROUTE_PREFIXES` (every `/data/**` static mount) and `SERVER_OWNED_PREFIXES` (what must never reach the SPA fallback, each with the exact `spaPaths` that ARE client routes). `scripts/dev-proxy-drift.test.js` checks the dev proxy's `^/data/` wildcard against the mounts, pins the route-registration order in `server/index.js` (a router added below the terminators is shadowed), and fails if a client route — from `NAV_COMMANDS` or `App.jsx`'s nested `<Route>` tree — is ever added under a server-owned prefix without being declared. |
| `asyncMutex.js` | Promise-based async mutex. |
| `concurrencyGate.js` | `createConcurrencyGate(limit)` → `run(fn)` — cap on simultaneous async work for ONE module-scoped budget, released FIFO. Sibling to `mapWithConcurrency.js`, which caps in-flight work *within one array map*; a gate is shared state, so several call sites fanning out at the same remote respect one budget instead of each respecting its own while the host sees the sum. `createMutex` (`asyncMutex.js`) is this with `limit` fixed at 1 — prefer it for mutual exclusion. Note the budget is per-MODULE, not per-host: two modules calling one host each get their own gate. Used by `huggingFaceCatalog.js` (4) and `ollamaRegistryCatalog.js` (16), whose cold catalog-enrichment bursts otherwise arrive at a free public API as a thundering herd — which the Hub answers with an HTTP/2 GOAWAY that surfaces as a bare `fetch failed`. |
| `dispatchLabels.js` | slashdo dispatch-hint contract: `model:light/medium/heavy` + `effort:low/medium/high/xhigh/max` vocabulary, prescribed forge colors, validation (`normalizeDispatchModel` / `normalizeDispatchEffort`), GitHub/GitLab vs Jira label formatting, optional contributor labels (`good first issue` / `help wanted`, never implied by `model:light`, and released at claim time by `formatContributorLabelReleaseCommands` — one best-effort command per label, since a forge fails the whole edit when a named label is absent), the open-ended planner-attribution axis (`planner:<model>`, `normalizePlannerId` / `resolvePlannerId` / `formatPlannerLabelGuidance` — records WHICH model wrote the plan, prefix-matched by `dispatchLabelSpec` so it lazily creates like the fixed labels; a filing agent takes the value from its prompt, never from self-identification), the `decomposed` epic workflow marker (`EPIC_DECOMPOSED_LABEL`, shared with perpetualWork.js#isActionableIssue and the claim prompts), lazy-create command text, the optional `--label` slots a rendered `issue create` example offers (`OPTIONAL_ISSUE_LABEL_FLAG_SLOTS` / `formatOptionalIssueLabelFlags` — one list so a new axis reaches every prompt template's copy-pasteable command, not just its prose), and shared dispatch plus issue-quality guidance (`ISSUE_QUALITY_GUIDANCE`, `DISPATCH_HINT_GUIDANCE`, `JIRA_DISPATCH_HINT_GUIDANCE`). Omit an unjustified axis; never invent `medium`; reject future-only/speculative work while keeping useful current refactors claimable. Consumed by work-tracker instructions, quota-burn audits, Layered Intelligence filing, and claim follow-up prompts. |
| `dispatchLabels.js` | slashdo dispatch-hint contract: `model:light/medium/heavy` + `effort:low/medium/high/xhigh/max` vocabulary, prescribed forge colors, validation (`normalizeDispatchModel` / `normalizeDispatchEffort`), GitHub/GitLab vs Jira label formatting, optional contributor labels (`good first issue` / `help wanted`, never implied by `model:light`, and released at claim time by `formatContributorLabelReleaseCommands` — one best-effort command per label, since a forge fails the whole edit when a named label is absent), the open-ended planner-attribution axis (`planner:<model>`, `normalizePlannerId` / `resolvePlannerId` / `formatPlannerLabelGuidance` — records WHICH model wrote the plan, prefix-matched by `dispatchLabelSpec` so it lazily creates like the fixed labels; a filing agent takes the value from its prompt, never from self-identification), the workflow-state markers (`EPIC_LABEL`/`EPIC_DECOMPOSED_LABEL` and `IN_PROGRESS_LABEL` — state, not hints: shared with perpetualWork.js#isActionableIssue, issueReconcile.js's zombie scan, issueWatcher.js's volunteer assignment, and the claim prompts), lazy-create command text, the optional `--label` slots a rendered `issue create` example offers (`OPTIONAL_ISSUE_LABEL_FLAG_SLOTS` / `formatOptionalIssueLabelFlags` — one list so a new axis reaches every prompt template's copy-pasteable command, not just its prose), and shared dispatch plus issue-quality guidance (`ISSUE_QUALITY_GUIDANCE`, `DISPATCH_HINT_GUIDANCE`, `JIRA_DISPATCH_HINT_GUIDANCE`). Omit an unjustified axis; never invent `medium`; reject future-only/speculative work while keeping useful current refactors claimable. Consumed by work-tracker instructions, quota-burn audits, Layered Intelligence filing, and claim follow-up prompts. |
| `domainAutonomy.js` | Per-domain autonomy guardrails (pure). `AUTONOMY_DOMAINS`/`DOMAIN_IDS`/`DOMAIN_MODES` (`off`/`dry-run`/`execute`), `getDomainMode(config, id)`, and `normalizeDomainAutonomy(raw)` to coerce a hand-edited/partial map. Default per domain is `execute` (reproduces pre-#711 behavior, so no migration needed). Also `CREATIVE_DOMAIN`/`getCreativeAutonomyMode(config)` (#2183) — the Creative Director orchestrator domain, kept out of `DOMAIN_IDS` and defaulting to mirror the `cos` mode. |
| `domainBudgets.js` | Per-domain daily autonomy budgets (pure). `BUDGET_LIMIT_FIELDS` (`maxActionsPerDay`/`maxMinutesPerDay`), `getDomainBudget(config, id)`, `normalizeDomainBudgets(raw)`, `hasBudget(budget)`, and `evaluateBudget(budget, usage)` → `{ withinBudget, exceeded }`. A `null`/non-positive cap means unlimited (default per domain, so no migration needed). Token/$ caps are intentionally absent — CLI subscription providers expose no per-run metering. Usage ledger + gate wiring live in `services/domainUsage.js`. |
| `eidoverseWorldDesign.js` | Immutable Eidoverse World Design V1/V2 registry, legacy override migration, semantic district contract, 48-signal ceiling, and deterministic library-only asset-recipe resolution/locking for the PortOS Luminous Systems Garden. |
Expand Down
17 changes: 17 additions & 0 deletions server/lib/dispatchLabels.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
* Contributor labels (`good first issue`, `help wanted`) are a third, equally
* optional axis: apply them when the work is actually onboarding-shaped, not
* because `model` happened to be `light`.
*
* The WORKFLOW labels below (`epic`, `decomposed`, `in-progress`) are not hints
* at all — they are state every claim/reconcile flow reads and writes. They live
* here because `dispatchLabelSpec` is what lazily creates any of them on a repo
* that has never defined it.
*/

import { shellQuote } from './shellQuote.js';
Expand Down Expand Up @@ -80,14 +85,26 @@ export const EPIC_DECOMPOSED_LABEL = 'decomposed';
*/
export const EPIC_LABEL = 'epic';

/**
* The claim marker: 'this issue is claimed and being worked'. Written by every
* flow that takes ownership of an issue — a CoS claim, and the issue-watcher's
* deterministic volunteer assignment — and read as a hard skip by the claim
* queue (`perpetualWork.js#NON_ACTIONABLE_ISSUE_LABELS`) and by the zombie scan
* in `issueReconcile.js`. One spelling here so an assigning flow and a
* releasing flow can never drift apart.
*/
export const IN_PROGRESS_LABEL = 'in-progress';

export const WORKFLOW_LABEL_COLORS = Object.freeze({
[EPIC_DECOMPOSED_LABEL]: 'BFD4F2',
[EPIC_LABEL]: 'B60205',
[IN_PROGRESS_LABEL]: 'FFA500',
});

export const WORKFLOW_LABEL_DESCRIPTIONS = Object.freeze({
[EPIC_DECOMPOSED_LABEL]: 'Epic already split into per-slice child issues',
[EPIC_LABEL]: 'Umbrella/tracking issue — shipped as per-slice children, never as one PR',
[IN_PROGRESS_LABEL]: 'Claimed and being worked',
});

/**
Expand Down
12 changes: 12 additions & 0 deletions server/lib/dispatchLabels.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
dispatchLabelSpec,
allDispatchLabelSpecs,
formatLabelCreateCommand,
IN_PROGRESS_LABEL,
formatRepeatedLabelFlags,
CONTRIBUTOR_LABELS,
JIRA_CONTRIBUTOR_LABELS,
Expand Down Expand Up @@ -121,6 +122,17 @@ describe('label specs and CLI formatting', () => {
expect(dispatchLabelSpec('model-light')).toBe(null);
});

it('resolves the in-progress workflow marker so it can be lazily created', () => {
// The claim marker is state every claim/reconcile flow reads, so it must be
// creatable on a repo (or fork) that has never defined it.
expect(dispatchLabelSpec(IN_PROGRESS_LABEL)).toEqual({
name: 'in-progress',
color: 'FFA500',
description: 'Claimed and being worked',
});
expect(formatLabelCreateCommand(IN_PROGRESS_LABEL)).toContain('gh label create in-progress');
});

it('lists all eight specs without dropping an axis', () => {
const specs = allDispatchLabelSpecs();
expect(specs).toHaveLength(8);
Expand Down
5 changes: 1 addition & 4 deletions server/services/issueReconcile.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { execGit } from '../lib/execGit.js';
import { execGh, ensureForgeReachable } from './github.js';
import { execGlabJson } from './gitlab.js';
import { fetchMyCurrentSprintTickets } from './jira.js';
import { IN_PROGRESS_LABEL } from '../lib/dispatchLabels.js';
import { resolveAppForgeTarget, resolveRepoForgeTarget } from '../lib/workTracker.js';
import { safeJSONParse, PATHS } from '../lib/fileUtils.js';

Expand All @@ -62,10 +63,6 @@ const GH_LIST_LIMIT = 200;
// glab paginates via `--per-page`; its practical max page is 100.
const GL_PER_PAGE = 100;

// The `in-progress` label = "claimed and being worked". Kept as a constant so
// the scan, the classifier docs, and any future config share one spelling.
export const IN_PROGRESS_LABEL = 'in-progress';

/**
* Extract the issue number a git ref claims, or null. Recognizes both claim
* conventions:
Expand Down
46 changes: 42 additions & 4 deletions server/services/issueWatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
*
* The gather pass does the forge work that does not need a model: it reads only
* activity newer than the per-app cursor, assigns explicit volunteer comments
* on currently-unassigned issues, finds external PRs without an owner review on
* their current head, and supplies bounded diffs to one reasoning agent. The
* output pass validates that agent's structured decisions against fresh forge
* on currently-unassigned issues and marks them `in-progress`, finds external
* PRs without an owner review on their current head, and supplies bounded diffs
* to one reasoning agent. The output pass validates that agent's structured decisions against fresh forge
* state before it replies, posts inline reviews, updates stale branches, or
* merges. No model is asked to discover/filter forge records or execute a forge
* mutation itself.
Expand All @@ -18,6 +18,7 @@ import {
detectDeterministicModelAbuseSignals,
modelAbuseContentFingerprint,
} from '../lib/modelAbuseGuard.js';
import { IN_PROGRESS_LABEL, dispatchLabelSpec } from '../lib/dispatchLabels.js';
import { getOriginInfo } from '../lib/gitRemote.js';
import {
MAX_REVIEW_BODY_CHARS,
Expand All @@ -35,6 +36,8 @@ import { addNotification, NOTIFICATION_TYPES, PRIORITY_LEVELS } from './notifica
import { normalizeEligibilityFacts, runModelAbuseScan } from './modelAbuseGuard.js';
import { issuePrerequisiteWaived } from '../lib/modelAbuseGuard.js';

const IN_PROGRESS_LABEL_SPEC = dispatchLabelSpec(IN_PROGRESS_LABEL);

const GH_TIMEOUT_MS = 60_000;
const LIST_LIMIT = 100;
const MAX_PULL_REQUESTS_PER_RUN = 3;
Expand Down Expand Up @@ -245,13 +248,48 @@ async function getRepositoryIdentity(ctx) {
return ownerLogin ? { ownerLogin } : null;
}

/**
* Take an unassigned issue on a volunteer's behalf: set the assignee and stamp
* `in-progress`.
*
* The assignee is what actually removes the issue from the autonomous claim
* queue — `perpetualWork.js#isActionableIssue` already rejects any issue held
* by someone other than this install's login. The label is for the humans and
* the UI: it is what the Issues tab hides on, and it keeps a volunteer-claimed
* issue reading the same as an agent-claimed one.
*
* ONE combined edit is the fast path. `gh issue edit --add-label` fails the
* WHOLE call when the repo has never defined the label, so the fallback splits
* it — assign alone (the half that must not be lost on a fork), create the
* label, then apply it. `--add-assignee` is idempotent, so the retry is safe
* whether or not the combined attempt got as far as assigning. The label is
* created without `--force` so an install that recolored it keeps its color.
*
* Returns whether the ASSIGNMENT landed, never whether the label did: a missing
* label leaves the issue claimable, which a later pass can repair, while a
* missing assignment means the volunteer's comment still needs an answer and
* must go on to the reasoning agent.
*/
async function assignVolunteer(ctx, issueNumber, login) {
return runGh(['issue', 'edit', String(issueNumber), '--repo', ctx.repoSpec, '--add-assignee', login], ctx)
const edit = (...flags) => runGh(['issue', 'edit', String(issueNumber), '--repo', ctx.repoSpec, ...flags], ctx);
const assignFlags = ['--add-assignee', login];
const labelFlags = ['--add-label', IN_PROGRESS_LABEL];
if (await edit(...assignFlags, ...labelFlags).then(() => true, () => false)) return true;

const assigned = await edit(...assignFlags)
.then(() => true)
.catch((err) => {
console.error(`❌ issue-watcher: could not assign issue #${issueNumber} to ${login}: ${err.message}`);
return false;
});
if (!assigned) return false;
await runGh(['label', 'create', IN_PROGRESS_LABEL_SPEC.name, '--repo', ctx.repoSpec,
'--color', IN_PROGRESS_LABEL_SPEC.color, '--description', IN_PROGRESS_LABEL_SPEC.description], ctx)
.catch(() => null);
await edit(...labelFlags).catch((err) => {
console.error(`❌ issue-watcher: could not mark issue #${issueNumber} ${IN_PROGRESS_LABEL}: ${err.message}`);
});
return true;
}

/**
Expand Down
Loading