@yemi33/minions 0.1.2370 → 0.1.2371

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/bin/minions.js +38 -11
  2. package/dashboard/js/render-other.js +2 -30
  3. package/dashboard/js/render-work-items.js +0 -34
  4. package/dashboard/js/settings.js +12 -27
  5. package/dashboard/pages/tools.html +1 -1
  6. package/dashboard.js +80 -257
  7. package/docs/README.md +2 -3
  8. package/docs/architecture.excalidraw +2 -2
  9. package/docs/auto-discovery.md +3 -3
  10. package/docs/completion-reports.md +0 -121
  11. package/docs/copilot-cli-schema.md +9 -17
  12. package/docs/deprecated.json +0 -51
  13. package/docs/design-state-storage.md +4 -4
  14. package/docs/harness-propagation.md +33 -263
  15. package/docs/human-vs-automated.md +1 -1
  16. package/docs/named-agents.md +0 -2
  17. package/docs/plan-lifecycle.md +5 -6
  18. package/docs/qa-runbook-lifecycle.md +10 -25
  19. package/docs/shared-lifecycle-module-map.md +2 -10
  20. package/engine/ado-comment.js +5 -11
  21. package/engine/ado.js +10 -5
  22. package/engine/cleanup.js +16 -36
  23. package/engine/cli.js +13 -175
  24. package/engine/comment-format.js +8 -182
  25. package/engine/db/index.js +60 -10
  26. package/engine/db/migrations/018-sql-only-cutover.js +56 -0
  27. package/engine/dispatch-store.js +0 -114
  28. package/engine/dispatch.js +1 -6
  29. package/engine/gh-comment.js +2 -10
  30. package/engine/github.js +8 -6
  31. package/engine/lifecycle.js +58 -173
  32. package/engine/llm.js +9 -11
  33. package/engine/managed-spawn.js +1 -6
  34. package/engine/memory-store.js +8 -6
  35. package/engine/metrics-store.js +4 -128
  36. package/engine/pipeline.js +4 -7
  37. package/engine/playbook.js +8 -196
  38. package/engine/prd-store.js +115 -141
  39. package/engine/preflight.js +8 -55
  40. package/engine/projects.js +34 -41
  41. package/engine/pull-requests-store.js +9 -234
  42. package/engine/qa-runs.js +4 -15
  43. package/engine/qa-sessions.js +5 -17
  44. package/engine/queries.js +23 -103
  45. package/engine/routing.js +1 -1
  46. package/engine/runtimes/claude.js +1 -2
  47. package/engine/runtimes/codex.js +0 -2
  48. package/engine/runtimes/copilot.js +1 -4
  49. package/engine/shared-branch-pr-reconcile.js +2 -5
  50. package/engine/shared.js +174 -533
  51. package/engine/small-state-store.js +12 -647
  52. package/engine/spawn-agent.js +5 -96
  53. package/engine/state-operations.js +166 -0
  54. package/engine/supervisor.js +0 -1
  55. package/engine/watch-actions.js +2 -3
  56. package/engine/watches-store.js +2 -128
  57. package/engine/work-items-store.js +3 -254
  58. package/engine.js +102 -334
  59. package/package.json +2 -2
  60. package/playbooks/build-fix-complex.md +0 -4
  61. package/playbooks/fix.md +0 -4
  62. package/playbooks/implement-shared.md +0 -4
  63. package/playbooks/implement.md +0 -4
  64. package/playbooks/plan-to-prd.md +16 -11
  65. package/playbooks/plan.md +0 -4
  66. package/playbooks/review.md +0 -6
  67. package/playbooks/shared-rules.md +0 -65
  68. package/docs/harness-transparency.md +0 -199
  69. package/docs/project-skills.md +0 -193
  70. package/engine/discover-project-skills.js +0 -673
  71. package/engine/playbook-intents.js +0 -76
@@ -43,7 +43,6 @@ The full per-agent config shape supported under `config.agents.<id>`:
43
43
  | `model` | string | Per-agent model override (omitted → runtime picks its own default). |
44
44
  | `maxBudgetUsd` | number | Per-spawn USD cap. **`0` is a valid cap** (read-only / dry-run agents), not "unlimited". |
45
45
  | `bareMode` | boolean | Run Claude in `--bare` mode (Claude-runtime knob). |
46
- | `hermeticHarness` | boolean | Strip user-scope skill/command/MCP roots from the worktree; `--add-dir` resolves to `[minionsDir]` only. |
47
46
  | `workspace_manifest` | object | Optional permission scoping — see below. |
48
47
 
49
48
  ### Three-tier resolution chain
@@ -57,7 +56,6 @@ The resolver helpers live in [`engine/shared.js`](../engine/shared.js):
57
56
  | `resolveAgentModel(agent, engine)` | `agent.model` → `engine.defaultModel` → `undefined` (adapter omits `--model`, CLI uses its own default). |
58
57
  | `resolveAgentMaxBudget(agent, engine)` | `agent.maxBudgetUsd` → `engine.maxBudgetUsd` → `undefined` (no cap). Nullish-coalesced so `0` is honored. |
59
58
  | `resolveAgentBareMode(agent, engine)` | `agent.bareMode` → `engine.claudeBareMode` → `false`. Strict null check so a per-agent `false` overrides an engine `true`. |
60
- | `resolveAgentHermeticHarness(agent, engine)` | `agent.hermeticHarness` → `engine.hermeticHarness` → `false`. Same strict-override semantics. |
61
59
 
62
60
  Empty string / `null` / `undefined` all count as "unset" for the string knobs, so an agent
63
61
  inherits the fleet default rather than blanking the value.
@@ -116,18 +116,18 @@ The verify agent and the user can then build and run from these paths.
116
116
 
117
117
  ## Manual Archive Lifecycle
118
118
 
119
- Archiving is a deliberate dashboard action, not a post-verify side effect. When a PRD completes, `checkPlanCompletion()` writes the completion summary, persists `status: "completed"` plus `_completionNotified`, and creates the verify work item. The PRD remains in `prd/` and its source plan remains in `plans/` so humans can review the guide, inspect the final state, resume stale work, or reopen items before hiding the plan from active views.
119
+ Archiving is a deliberate dashboard action, not a post-verify side effect. When a PRD completes, `checkPlanCompletion()` writes the completion summary, persists `status: "completed"` plus `_completionNotified` in SQL, and creates the verify work item. The PRD remains active and its source plan remains in `plans/` so humans can review the guide, inspect the final state, resume stale work, or reopen items before hiding the plan from active views.
120
120
 
121
121
  The verify agent does not call `archivePlan()`. After the testing guide and any follow-up PR work are satisfactory, use the dashboard archive action, which calls `POST /api/plans/archive` with the active PRD or plan filename.
122
122
 
123
123
  Manual archive behavior:
124
124
 
125
- 1. For PRD JSON files, the dashboard archives them **in-place** in `prd/` — the file is not moved. The JSON is stamped with `archived: true`, `status: "archived"`, and `archivedAt` (Phase 10 step 4.2b). **No `.backup` sidecar is written or restored for any `prd/*.json`** — Phase 10 step 4.3 dropped the PRD backup/restore lifecycle entirely (root-level PRDs used to get one to survive the now-retired rename race; SQL is canonical, so a stale `.backup` is pure resurrection fuel, not a safety net).
125
+ 1. The dashboard updates the SQL PRD row with `archived: true`, `status: "archived"`, and `archivedAt`.
126
126
  2. If the PRD has `source_plan`, the matching `plans/<source>.md` is moved to `plans/archive/<source>.md`.
127
127
  3. Pending or queued work items linked to the archived PRD are cancelled with `_cancelledBy: "plan-archived"`; completed work items remain as history.
128
128
  4. Plan worktrees are cleaned up by the archive handler/lifecycle cleanup path.
129
129
 
130
- Use unarchive only when the archived PRD/plan should reappear in active views. Reopening completed work should happen before archive, or after unarchive, so the materializer can see the active PRD file.
130
+ Use unarchive only when the archived PRD/plan should reappear in active views. Reopening completed work should happen before archive, or after unarchive, so the materializer can see the active SQL PRD.
131
131
 
132
132
  ## Human Feedback on PRs
133
133
 
@@ -151,10 +151,9 @@ After PRs are created, humans can leave comments containing `@minions` to trigge
151
151
  | File | Purpose |
152
152
  |------|---------|
153
153
  | `plans/*.md` | Source plans awaiting review or linked to active PRDs |
154
- | `prd/*.json` | Active PRDs with materializable items |
154
+ | SQL `prds` + `prd_items` | Active and archived PRDs with materializable items |
155
155
  | `plans/archive/` | Manually archived plan Markdown files |
156
- | `prd/*.json` (with `archived: true`) | PRDs archived in-place (Phase 10 step 4.2b — not moved; flag + `archivedAt` stamped on the file) |
157
- | `prd/archive/` | Legacy: PRDs archived before Phase 10 (still scanned for filename-collision detection) |
156
+ | `prd/` | Legacy upgrade input only; migration 018 imports and retires these files |
158
157
  | `playbooks/verify.md` | Verification task playbook |
159
158
  | `playbooks/implement.md` | Implementation playbook |
160
159
  | `playbooks/plan-to-prd.md` | Plan → PRD conversion playbook |
@@ -17,7 +17,7 @@ engine-assigned `id`). `target` is a name from `/api/managed-processes` or
17
17
 
18
18
  ## Run-record path
19
19
 
20
- `qa-runs.json` (newest-first, capped). Each run:
20
+ The SQL `qa_runs` table stores newest-first, capped run records. Each run:
21
21
  `{ id, runbookId, runbookName, target, status, startedAt, completedAt, workItemId, agentId, artifacts }`.
22
22
  `status` ∈ `pending|dispatched|running|passed|failed|error`. Created by
23
23
  `POST /api/qa/runbooks/run` (`{ id }`), which dispatches a `qa-validate`
@@ -27,29 +27,14 @@ and clears the interval on page navigation via the `switchPage` wrapper in
27
27
  `dashboard/js/qa.js` (matches `_stopPlanPoll`/`_stopMeetingPoll` pattern in
28
28
  `dashboard/js/state.js`).
29
29
 
30
- ### Persistence (Phase 8: SQLite source of truth)
31
-
32
- Since the Phase 8 migration (`migrate-qa-state-to-sqlite`), both
33
- `qa-runs.json` and `qa-sessions.json` are dual-written sidecars and SQLite
34
- (`engine/state.db`, tables `qa_runs` + `qa_sessions`) is the source of
35
- truth. Every mutation goes through `shared.mutateQaRuns` /
36
- `shared.mutateQaSessions`, which apply inside a SQLite transaction
37
- (`BEGIN IMMEDIATE`) then best-effort mirror to JSON under a `withFileLock`
38
- on the sidecar path so cross-process writers serialize. Reads go through
39
- `small-state-store.readQaRuns` / `readQaSessions`, which compare the SQL
40
- content-hash to the JSON sidecar and rehydrate SQL from JSON when an
41
- external editor (operator hand-edit, manual rollback) has diverged.
42
-
43
- The JSON mirror is gated by `engine.qaDualWriteJson` (default true). Once
44
- operators trust SQL as the source of truth and have migrated external
45
- tooling off the JSON files, flip it false via Dashboard → Settings →
46
- Advanced → "QA: dual-write JSON sidecars" to halve write I/O. With the
47
- toggle off, `qa-runs.json` / `qa-sessions.json` stop receiving updates
48
- (stale snapshot at the moment of the flip), so don't disable until any
49
- external readers have moved to the SQL tables or the dashboard `/api/qa/*`
50
- endpoints. The qa-runs cap (`QA_RUNS_MAX_RECORDS=2000`) and qa-sessions
51
- cap (`QA_SESSIONS_MAX_RECORDS=500`) still apply, enforced in-memory at
52
- createRun/createSession time.
30
+ ### Persistence
31
+
32
+ SQLite (`engine/state.db`, tables `qa_runs` and `qa_sessions`) is the sole
33
+ source of truth. Every mutation goes through `shared.mutateQaRuns` or
34
+ `shared.mutateQaSessions` inside a transaction. Legacy JSON is imported only
35
+ during upgrade or through the explicit offline recovery command. The qa-runs
36
+ cap (`QA_RUNS_MAX_RECORDS=2000`) and qa-sessions cap
37
+ (`QA_SESSIONS_MAX_RECORDS=500`) remain enforced at creation time.
53
38
 
54
39
  ### Deleting runs
55
40
 
@@ -253,7 +238,7 @@ errors are mapped to HTTP via `_qaSessionsErrorToStatus`:
253
238
  DRAFT agent. Path is sandboxed by `_isSafeSessionId` (kebab-case ≤ 64
254
239
  chars, no `..`, no separators) before every read/write that maps id →
255
240
  path. Gitignored.
256
- - **Managed-spawn**: `engine/managed-processes.json` entry with
241
+ - **Managed-spawn**: SQL managed-process entry with
257
242
  `name='qa-session-<sessionId>'`. Cleaned up by `/kill` or by the
258
243
  session's `cancelSession` if the operator opts for the cancel-without-
259
244
  spawn-kill path.
@@ -105,7 +105,6 @@ because either is a bug:
105
105
  |---|---|---|
106
106
  | minions root pointer | `readMinionsRootPointer`, `saveMinionsRootPointer` | shared.js |
107
107
  | dashboard port file | `readDashboardPortFile`, `writeDashboardPortFile` | shared.js |
108
- | agent Copilot home dir | `resolveAgentCopilotHome`, `ensureAgentCopilotHome` | shared.js |
109
108
  | canonical PR id | `parseCanonicalPrId`, `getCanonicalPrId` | shared.js |
110
109
  | PR record | `findPrRecord`, `normalizePrRecord` | shared.js |
111
110
  | worktree owner marker | `writeWorktreeOwnerMarker`, `hasWorktreeOwnerMarker` | shared.js |
@@ -214,7 +213,7 @@ means zero such files were found), **Proposed target module**.
214
213
  | `redactPromptDescription` | 447-458 | Redactor applied to WI description text before it is injected into an agent // prompt. Applies the standard log redactor (_redactString) first, then catches // any remaining Authorization: Bearer header — including alrea | yes | `engine.js` | engine/shared.js (keep) |
215
214
  | `log` | 459-531 | Log. | yes | `engine/abandoned-pr-reconciliation.js`, `engine/ado-git-auth.js`, `engine/ado-status.js`, `engine/ado.js`, `engine/cc-worker-pool.js`, `engine/check-status.js` +72 more | engine/shared.js (keep) — logging/timestamp primitives |
216
215
  | `_currentLogPath` | 532-547 | Resolve the log file path at write time, not at module load time. `LOG_PATH` is captured eagerly when shared.js first loads. In production this is fine — there's a single shared.js instance per process. In tests the requ | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
217
- | `_currentLogDbPath` | 548-559 | Resolve the SQL state.db path at write time same isolation contract // as _currentLogPath. MINIONS_LOG_PATH only redirects the JSON sidecar; // the SQL store stays attached to MINIONS_TEST_DIR / MINIONS_HOME because // | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
216
+ | `_currentLogDbPath` | 548-559 | Resolve the SQL state.db path at write time with test/home isolation. | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
218
217
  | `resolveEngineCacheDir` | 560-593 | MINIONS_TEST_DIR-aware engine dir for cache files (runtime caps, models, // spawn-debug.log). Returns `<MINIONS_TEST_DIR>/engine` under tests; otherwise // the caller's source-adjacent fallback (typically `__dirname`-der | yes | `engine/runtimes/claude.js`, `engine/runtimes/codex.js`, `engine/runtimes/copilot.js` | engine/shared.js (keep) |
219
218
  | `openUrlInBrowser` | 594-645 | Cross-platform URL opener. Uses execSync so failures fall through the // try/catch and the caller sees them. Dashboard self-open and `minions dash` // / `minions restart` post-health open all funnel through here. // // E | yes | `test/unit/dashboard-port-resolution.test.js`, `dashboard.js`, `engine.js` | engine/shared.js (keep) |
220
219
  | `_bumpBrowserOpenMetric` | 646-662 | Bump per-reason counter under metrics._engine.browserOpens. Best-effort — // metrics.json writes must never throw out of openUrlInBrowser. Schema: // _engine.browserOpens.<reason> = { count, suppressedCount, lastAt } | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
@@ -324,7 +323,6 @@ means zero such files were found), **Proposed target module**.
324
323
  | `resolveAgentBareMode` | 3974-3991 | Resolve whether this agent should run in Claude `--bare` mode. Priority: 1. `agent.bareMode` — per-agent override (boolean) 2. `engine.claudeBareMode` — fleet default 3. `false` — hardcoded fallback Strict undefined/null | yes | `test/unit.test.js`, `engine.js` | engine/shared.js (keep) |
325
324
  | `_normalizeMcpServerList` | 3992-4005 | P-mcp-storm — Resolve the list of MCP server names to disable for spawned // Copilot agents. The engine FILTERS these out of the agent's seeded // COPILOT_HOME mcp-config (engine.js `_applyAgentCopilotHome`) — a server a | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
326
325
  | `resolveCopilotAgentDisabledMcpServers` | 4006-4030 | Resolve copilot agent disabled mcp servers. | yes | `test/unit/agent-copilot-home.test.js` | engine/shared.js (keep) |
327
- | `resolveAgentHermeticHarness` | 4031-4062 | P-49e1c8b7 — Resolve whether this agent should run with a hermetic harness. Priority (mirrors `resolveAgentBareMode`): 1. `agent.hermeticHarness` — per-agent override (boolean) 2. `engine.hermeticHarness` — fleet default | yes | `engine/spawn-agent.js`, `engine.js` | engine/shared.js (keep) |
328
326
  | `applyLegacyCcModelMigration` | 4063-4082 | If `config.engine.ccModel` is set but `config.engine.defaultModel` is unset, mutate the in-memory `config.engine` so `defaultModel` mirrors `ccModel` and log a one-time deprecation notice. Does NOT write to disk. Returns | yes | `engine/cli.js`, `test/unit.test.js` | engine/shared.js (keep) |
329
327
  | `_resetLegacyCcModelMigrationFlag` | 4083-4113 | Test helper: reset the dedup flag so repeated tests can re-trigger the log. | yes | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
330
328
  | `runtimeConfigWarnings` | 4114-4194 | Inspect runtime fleet config and return warning entries for misconfiguration. Warnings emitted: - Unknown CLI: any `cli` value (per-agent, ccCli, defaultCli) not in `registeredRuntimes`. Each unknown value produces one e | yes | `engine/preflight.js` | engine/process-utils.js (new) — process spawn/pid/tmp-dir lifecycle helpers |
@@ -337,13 +335,11 @@ means zero such files were found), **Proposed target module**.
337
335
  | `_qaDualWriteEnabled` | 4673-4681 | Phase 8 — QA state mutators. Both qa-runs and qa-sessions are top-level JSON arrays (mutator receives the array; mutates in place or returns a replacement). The store diffs by `id`. Falls back to mutateJsonFileLocked on | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
338
336
  | `_qaMutator` | 4682-4720 | Qa mutator. | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
339
337
  | `mutateWatches` | 4721-4741 | Route a watches mutation through the SQL store. Same shape as mutateWorkItems / mutatePullRequests: mutator receives the watches array, mutates in place or returns a replacement, and the store diffs by id. SQL-canonical | yes | `engine/watches.js` | engine/shared.js (keep) |
340
- | `mutateMetrics` | 4742-4763 | Route a metrics mutation through the SQL store with a JSON dual-write mirror. Same shape as mutateWorkItems / mutatePullRequests: mutator receives the full legacy-shape metrics object, mutates in place or returns a repla | yes | `engine/cleanup.js`, `engine/llm.js`, `test/unit/auto-recovery.test.js`, `engine.js` | engine/shared.js (keep) |
338
+ | `mutateMetrics` | 4742-4763 | Route a metrics mutation through the SQL store. | yes | `engine/cleanup.js`, `engine/llm.js`, `test/unit/auto-recovery.test.js`, `engine.js` | engine/shared.js (keep) |
341
339
  | `trackReviewMetric` | 4764-4784 | Update per-agent review metrics (prsApproved/prsRejected). Only writes for configured agents. | yes | `engine/ado.js`, `engine/github.js`, `engine/queries.js`, `test/unit.test.js` | engine/shared.js (keep) |
342
340
  | `queuePlanToPrd` | 4785-4822 | Queue a plan-to-prd work item with dedup check inside lock. Returns { queued, alreadyQueued, id, item } so callers (e.g. POST /api/plans/execute) can surface the new or existing work item id back to the dashboard. Return | yes | `test/unit.test.js`, `dashboard.js` | engine/shared.js (keep) |
343
341
  | `extractPlanDeclaredProject` | 4823-4845 | Extract plan declared project. | yes | `dashboard.js`, `engine.js` | engine/shared.js (keep) |
344
342
  | `extractPlanTargetProjects` | 4846-4974 | P-7a3f1c08: Parse a cross-repo plan's list of target projects. // Primary signal: `<!-- minions:targetProjects=a, b, c -->` HTML comment. // Fallback (human): ` Projects: a, b \| c` markdown line in the first 80 lines. // | yes | `dashboard.js`, `engine.js` | engine/shared.js (keep) |
345
- | `normalizeHarnessUsed` | 4975-5027 | normalizeHarnessUsed(raw) — validate/clamp an untrusted harnessUsed-shaped // object into the canonical { skills, mcpServers, commands, docs } form. // Drops malformed entries (non-objects, or missing the kind's required | yes | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
346
- | `groundHarnessUsed` | 5028-5099 | groundHarnessUsed(harnessUsed, propagated) — Stage 2 grounding cross-check // (P-c6f5e8b3). Normalizes an agent's self-reported `harnessUsed` and annotates // every surviving entry with a strict-boolean `grounded` field | yes | `engine/cli.js` | engine/shared.js (keep) |
347
343
  | `_bumpPruneDefaultClaudeStrip` | 5100-5114 | Bump rolling-daily strip counter for docs/deprecated.json#prune-default-claude-config // removal gate (>=30 consecutive days of zero strips). Best-effort; mirrors // engine/cleanup.js:1200-1213 _engine.legacyStatusMigrat | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
348
344
  | `pruneDefaultClaudeConfig` | 5115-5185 | Prune default claude config. | yes | `test/unit.test.js`, `dashboard.js`, `minions.js` | engine/shared.js (keep) |
349
345
  | `_isStringArrayOrNull` | 5186-5202 | Is string array or null. | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
@@ -398,14 +394,11 @@ means zero such files were found), **Proposed target module**.
398
394
  | `parseWorktreePorcelain` | 6139-6169 | Parse `git worktree list --porcelain` output. Pure — callers run the subprocess (sync `execSilent` or async `execAsync`) and feed stdout in. Returns `[{path, head, branch, bare, detached, locked, prunable}]`. `branch` st | yes | `engine/cleanup.js`, `engine/worktree-gc.js`, `test/unit.test.js`, `engine.js` | engine/worktree-utils.js (new) — general worktree path/branch/owner-marker helpers (worktree-pool.js/worktree-gc.js already own pool sizing + GC sweep; this new module would take the shared low-level helpers those two currently import from shared.js) |
399
395
  | `assertWorktreeOutsideProject` | 6170-6203 | Throws when `worktreePath` would land inside (or equal) `projectRoot`. Called by the engine spawn path before `git worktree add`, and by the cleanup sweep that audits already-registered worktrees per linked project. The | yes | `test/unit.test.js`, `dashboard.js`, `engine.js` | engine/worktree-utils.js (new) — general worktree path/branch/owner-marker helpers (worktree-pool.js/worktree-gc.js already own pool sizing + GC sweep; this new module would take the shared low-level helpers those two currently import from shared.js) |
400
396
  | `resolveProjectRootDir` | 6204-6259 | Resolve the project root directory used as the parent for git worktree paths during dispatch. Centralizes the fallback that engine spawnAgent used to do inline (`project.localPath ? path.resolve(project.localPath) : path | yes | `engine/create-pr-worktree.js`, `test/unit.test.js`, `engine.js` | engine/shared.js (keep) |
401
- | `resolveAgentCopilotHome` | 6260-6286 | Resolve a stable, repo-EXTERNAL COPILOT_HOME for spawned copilot agents. Copilot loads `$COPILOT_HOME/mcp-config.json` and SPAWNS every server in it on every process start. `--disable-mcp-server <name>` only hides a serv | yes | `test/unit/agent-copilot-home.test.js`, `engine.js` | engine/shared.js (keep) |
402
397
  | `resolveAgentTempBaseDir` | 6287-6308 | Derive a scratch/temp base dir on the same volume agent work already lives on — the configured worktree location (engine.worktreeRoot, default `../worktrees`). Agents (copilot/node/git + JVM-based MCP servers) write heav | yes | `test/unit/agent-temp-dir.test.js`, `engine.js` | engine/shared.js (keep) |
403
- | `ensureAgentCopilotHome` | 6309-6412 | Seed the agent COPILOT_HOME's mcp-config (copy of `~/.copilot/mcp-config.json` // MINUS `engine.copilotAgentDisabledMcpServers`) and return the home path. This // is the single source of truth for "what MCP servers may a | yes | `test/unit/agent-copilot-home.test.js`, `dashboard.js`, `engine.js` | engine/shared.js (keep) |
404
398
  | `resolveSpawnPaths` | 6413-6505 | Resolve the agent's working directory and (when needed) the parent dir for git worktree placement. Decouples the two concerns that spawnAgent used to conflate (W-mp73x32w000l143d): 1. cwd — where the agent process actual | yes | `engine/live-checkout.js`, `test/unit.test.js`, `engine.js` | engine/shared.js (keep) |
405
399
  | `_applyWorkdirOrThrow` | 6506-6518 | ── meta.workdir helpers (P-714ef144) // // Per-WI `meta.workdir` is a relative POSIX subpath under the spawn base // (project.localPath for read-only / live; worktree root for mutating). // When set, the engine lands the | no | _internal-only (no callers found outside shared.js)_ | engine/shared.js (keep) |
406
400
  | `validateWorkItemWorkdir` | 6519-6561 | Validate work item workdir. | yes | `dashboard.js`, `engine.js` | engine/shared.js (keep) |
407
401
  | `applyWorkdir` | 6562-6586 | Apply workdir. | yes | `engine.js` | engine/shared.js (keep) |
408
- | `filterProjectHarnessDirsForWorkdir` | 6587-6650 | Filter project harness dirs for workdir. | yes | `engine.js` | engine/shared.js (keep) |
409
402
  | `isAllowedOrigin` | 6651-6686 | Returns true if the origin-like header value (either an `Origin` header value such as `http://localhost:7331` or a full `Referer` URL) belongs to the local dashboard allowlist. Port-agnostic. Returns false for null/undef | yes | `test/unit.test.js`, `dashboard.js` | engine/shared.js (keep) |
410
403
  | `isAllowedDashboardOrigin` | 6687-6705 | Returns true if the given `Origin` header value should receive an `Access-Control-Allow-Origin` echo on GET/HEAD responses. STRICTER than `isAllowedOrigin` — that helper is the mutating-request gate (allows any localhost | yes | `test/unit.test.js`, `dashboard.js` | engine/shared.js (keep) |
411
404
  | `buildSecurityHeaders` | 6706-6721 | Returns the baseline set of security response headers to apply on every HTTP response from the dashboard. Values match OWASP defaults for a same-origin SPA served from 127.0.0.1. The HTML entry-point response intentional | yes | `test/unit.test.js`, `dashboard.js` | engine/shared.js (keep) |
@@ -609,7 +602,6 @@ Same columns as above, relative to `lifecycle.js`.
609
602
  | `parseAgentOutput` | 4020-4024 | Agent Output Parsing | yes | `engine/timeout.js`, `test/unit.test.js`, `engine.js` | engine/lifecycle.js (keep) |
610
603
  | `hasActionableFailureClass` | 4025-4047 | Has actionable failure class. | no | _internal-only (no callers found outside lifecycle.js)_ | engine/lifecycle.js (keep) |
611
604
  | `handleInjectionFlag` | 4048-4134 | F5 (W-mpeklod3000we69c): handle agent-reported injection attempts. The agent set `securityFlags.injectionAttempt: true` in its completion report after spotting attacker-controlled instructions inside an `<UNTRUSTED-INPUT | no | _internal-only (no callers found outside lifecycle.js)_ | engine/lifecycle.js (keep) |
612
- | `resolveHarnessPropagated` | 4135-4149 | resolveHarnessPropagated(dispatchItem) — P-c6f5e8b3. Locate the spawn-time // `_harnessPropagated` manifest for a dispatch. The in-memory dispatchItem // (spawnAgent's closure object) normally carries it directly; after | yes | `engine.js` | engine/lifecycle.js (keep) |
613
605
  | `parseCompletionReportFile` | 4150-4172 | Parse completion report file. | yes | `engine/timeout.js`, `test/unit.test.js` | engine/lifecycle.js (keep) |
614
606
  | `normalizeCompletionArtifacts` | 4173-4198 | Normalize completion artifacts. | yes | _internal-only (no callers found outside lifecycle.js)_ | engine/lifecycle.js (keep) |
615
607
  | `normalizeArtifactPath` | 4199-4212 | Normalize artifact path. | no | _internal-only (no callers found outside lifecycle.js)_ | engine/lifecycle.js (keep) |
@@ -2,8 +2,7 @@
2
2
  * engine/ado-comment.js — Azure DevOps mirror of engine/gh-comment.js.
3
3
  *
4
4
  * Posts a minions PR-comment thread to ADO with the same hidden marker, the
5
- * same collapsible "Harnesses used" section, and the same deterministic brand
6
- * link as the GitHub path — because all three are produced by the ONE neutral
5
+ * same deterministic brand link as the GitHub path — because both are produced by the ONE neutral
7
6
  * builder `engine/comment-format.js#buildMinionsCommentBody`. This module owns
8
7
  * only the ADO transport (REST `…/threads` POST) and ADO-specific input
9
8
  * validation (orgBase / project / repositoryId / prNumber).
@@ -13,7 +12,7 @@
13
12
  * unlinked on ADO (the GitHub backstop in buildMinionsCommentBody never ran).
14
13
  * Routing ADO comments through this poster gives ADO the same guarantees as
15
14
  * GitHub: marker present (so pollers classify the comment as agent-authored),
16
- * harness section folded in, brand trailer hyperlinked.
15
+ * brand trailer hyperlinked.
17
16
  *
18
17
  * Auth: a bearer token from `az account get-access-token` (via
19
18
  * engine/ado-token.js#acquireAdoToken), threaded as `Authorization: Bearer`.
@@ -34,7 +33,7 @@
34
33
  * changes) stay `active` so they block/notify the author.
35
34
  */
36
35
 
37
- const { buildMinionsCommentBody, buildSkippedSkillSection } = require('./comment-format');
36
+ const { buildMinionsCommentBody } = require('./comment-format');
38
37
  const { acquireAdoToken } = require('./ado-token');
39
38
 
40
39
  const ADO_API_VERSION = '7.1';
@@ -95,7 +94,7 @@ async function _defaultAcquireToken() {
95
94
  /**
96
95
  * Post a minions PR comment to an Azure DevOps pull request.
97
96
  *
98
- * Mirrors engine/gh-comment.js#postPrComment: builds the marked + harness +
97
+ * Mirrors engine/gh-comment.js#postPrComment: builds the marked +
99
98
  * brand-linked body via the shared builder, then POSTs a new active thread.
100
99
  *
101
100
  * @param {object} args
@@ -107,8 +106,6 @@ async function _defaultAcquireToken() {
107
106
  * @param {string} args.agentId minions agent id (marker)
108
107
  * @param {string} args.kind comment kind (marker)
109
108
  * @param {string} [args.workItemId] originating work-item id (marker)
110
- * @param {object} [args.harnessUsed] grounded harnessUsed record (folded into body)
111
- * @param {object} [args.skillSkipped] skipped-project-skill record `{name, reason}` (folded into body, visible)
112
109
  * @param {number} [args.timeoutMs=30000]
113
110
  * @param {Function} [args.acquireToken] () => Promise<string> — injectable token source
114
111
  * @param {Function} [args.fetchImpl=fetch] injectable fetch
@@ -123,8 +120,6 @@ async function postAdoPrComment({
123
120
  agentId,
124
121
  kind,
125
122
  workItemId,
126
- harnessUsed,
127
- skillSkipped,
128
123
  resolved = false,
129
124
  timeoutMs = 30000,
130
125
  acquireToken = _defaultAcquireToken,
@@ -136,7 +131,7 @@ async function postAdoPrComment({
136
131
  _validatePrNumber(prNumber);
137
132
 
138
133
  // buildMinionsCommentBody validates marker fields and throws on bad input.
139
- const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed, skillSkipped });
134
+ const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body });
140
135
 
141
136
  const token = await acquireToken();
142
137
  if (!token || typeof token !== 'string') {
@@ -186,7 +181,6 @@ module.exports = {
186
181
  // Re-export the neutral builder so ADO callers have a single import surface,
187
182
  // mirroring engine/gh-comment.js.
188
183
  buildMinionsCommentBody,
189
- buildSkippedSkillSection,
190
184
  // Internal validators exported for tests.
191
185
  _validateOrgBase,
192
186
  _validateProject,
package/engine/ado.js CHANGED
@@ -409,11 +409,13 @@ function applyAdoPrMetadata(pr, prData) {
409
409
  updated = true;
410
410
  }
411
411
 
412
- // W-mpej044m00076d63: backfill `pr.created` from ADO's real creation
413
- // timestamp. lifecycle.js attach paths omit `created` so this poll is the
414
- // single source of truth for fresh records, and historical records missing
415
- // `created` get backfilled the next time they're polled. Idempotent.
416
- if (!pr.created && prData.creationDate) {
412
+ // W-mpej044m00076d63 / W-mrezh0yb0007f733: keep `pr.created` aligned with
413
+ // ADO's real creationDate. The platform is authoritative for when a PR was
414
+ // opened, so adopt its value whenever the stored one is missing, a legacy
415
+ // date-only stamp, or a link/attach-time `now()` default (the bug where
416
+ // manually-linked PRs showed a "created today" date). `shouldAdoptPlatformCreated`
417
+ // compares instants so equivalent representations don't churn. Idempotent.
418
+ if (shared.shouldAdoptPlatformCreated(pr.created, prData.creationDate)) {
417
419
  pr.created = prData.creationDate;
418
420
  updated = true;
419
421
  }
@@ -2606,6 +2608,9 @@ async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo, project) {
2606
2608
  description: pr.description || '',
2607
2609
  branch: stripRefsHeads(pr.sourceRefName),
2608
2610
  author: pr.createdBy?.displayName || '',
2611
+ // W-mrezh0yb0007f733 — surface ADO's real creationDate so manual-link paths
2612
+ // can seed `pr.created` with the true value instead of link-time now().
2613
+ created: pr.creationDate || '',
2609
2614
  };
2610
2615
  }
2611
2616
 
package/engine/cleanup.js CHANGED
@@ -1388,23 +1388,13 @@ async function runCleanup(config, verbose = false) {
1388
1388
  }
1389
1389
  // PRD items (missing_features[].status)
1390
1390
  try {
1391
- let prdDirEntries;
1392
- try {
1393
- prdDirEntries = fs.readdirSync(PRD_DIR);
1394
- } catch (e) {
1395
- log('warn', `migrate PRD statuses: failed to read ${PRD_DIR} — ${e.message}`);
1396
- prdDirEntries = [];
1397
- }
1398
- const prdFiles = prdDirEntries.filter(f => f.endsWith('.json'));
1391
+ const prdStore = require('./prd-store');
1392
+ const prdFiles = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
1399
1393
  for (const pf of prdFiles) {
1400
1394
  const prdPath = path.join(PRD_DIR, pf);
1395
+ const prd = prdStore.readPrd(prdPath);
1401
1396
  let migrated = 0;
1402
- shared.withFileLock(`${prdPath}.lock`, () => {
1403
- // safeJsonNoRestore: PRDs are terminal artifacts. If the active PRD
1404
- // was archived but a stale .backup sidecar remains, never resurrect
1405
- // it during a routine migration sweep (W-mouptdh1000h9f39).
1406
- const prd = safeJsonNoRestore(prdPath);
1407
- if (!prd?.missing_features) return;
1397
+ if (prd?.missing_features) {
1408
1398
  for (const feat of prd.missing_features) {
1409
1399
  if (LEGACY_DONE_ALIASES.has(feat.status)) {
1410
1400
  feat.status = shared.WI_STATUS.DONE;
@@ -1414,8 +1404,8 @@ async function runCleanup(config, verbose = false) {
1414
1404
  migrated++;
1415
1405
  }
1416
1406
  }
1417
- if (migrated > 0) safeWrite(prdPath, prd);
1418
- });
1407
+ }
1408
+ if (migrated > 0) prdStore.writePrd(prdPath, prd);
1419
1409
  if (migrated > 0) {
1420
1410
  log('info', `Migrated ${migrated} legacy PRD item status(es) in ${pf}`);
1421
1411
  }
@@ -1425,36 +1415,27 @@ async function runCleanup(config, verbose = false) {
1425
1415
  // Reset orphaned PRD item statuses — dispatched/failed with no matching work item (#779)
1426
1416
  cleaned.orphanedPrdStatuses = 0;
1427
1417
  try {
1428
- const wiIds = new Set();
1429
- for (const project of projects) {
1430
- const items = safeJsonArr(projectWorkItemsPath(project));
1431
- for (const wi of items) { if (wi?.id) wiIds.add(wi.id); }
1432
- }
1433
- try {
1434
- const centralWi = safeJsonArr(path.join(MINIONS_DIR, 'work-items.json'));
1435
- for (const wi of centralWi) { if (wi?.id) wiIds.add(wi.id); }
1436
- } catch { /* optional */ }
1418
+ const wiIds = new Set(
1419
+ queries.getWorkItems(config, { enrich: false }).map(wi => wi?.id).filter(Boolean)
1420
+ );
1437
1421
 
1438
- let orphanPrdEntries;
1439
- try { orphanPrdEntries = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json')); }
1440
- catch { orphanPrdEntries = []; }
1422
+ const prdStore = require('./prd-store');
1423
+ const orphanPrdEntries = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
1441
1424
  for (const pf of orphanPrdEntries) {
1442
1425
  const prdPath = path.join(PRD_DIR, pf);
1443
- // safeJsonNoRestore — peek must not auto-restore archived PRDs
1444
- // from a stale .backup sidecar (W-mouptdh1000h9f39).
1445
- const peek = safeJsonNoRestore(prdPath);
1426
+ const peek = prdStore.readPrd(prdPath);
1446
1427
  if (!peek?.missing_features) continue;
1447
1428
  let reset = 0;
1448
- mutateJsonFileLocked(prdPath, (prd) => {
1449
- if (!prd?.missing_features) return prd;
1429
+ const prd = peek;
1430
+ if (prd?.missing_features) {
1450
1431
  for (const feat of prd.missing_features) {
1451
1432
  if ((feat.status === shared.WI_STATUS.DISPATCHED || feat.status === shared.WI_STATUS.FAILED) && !wiIds.has(feat.id)) {
1452
1433
  feat.status = shared.WI_STATUS.PENDING;
1453
1434
  reset++;
1454
1435
  }
1455
1436
  }
1456
- return prd;
1457
- }, { skipWriteIfUnchanged: true });
1437
+ }
1438
+ if (reset > 0) prdStore.writePrd(prdPath, prd);
1458
1439
  if (reset > 0) {
1459
1440
  log('info', `Reset ${reset} orphaned PRD item status(es) → pending in ${pf}`);
1460
1441
  cleaned.orphanedPrdStatuses += reset;
@@ -1669,7 +1650,6 @@ async function runCleanup(config, verbose = false) {
1669
1650
  * that were written before that guard existed. */
1670
1651
  function scrubStaleMetrics() {
1671
1652
  const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
1672
- if (!fs.existsSync(metricsPath)) return;
1673
1653
  mutateJsonFileLocked(metricsPath, metrics => {
1674
1654
  for (const key of Object.keys(metrics)) {
1675
1655
  if (key.startsWith('temp-') || key === 'agent1' || key.startsWith('_test')) {