@entelligentsia/forgecli 0.10.0 → 0.10.1

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 (45) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/CHANGELOG-forge-plugin.md +68 -0
  3. package/dist/bin/config.js +6 -0
  4. package/dist/bin/config.js.map +1 -1
  5. package/dist/extensions/forgecli/fix-bug.js +85 -37
  6. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  7. package/dist/extensions/forgecli/forge-subagent.d.ts +16 -1
  8. package/dist/extensions/forgecli/forge-subagent.js +11 -1
  9. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  10. package/dist/extensions/forgecli/run-sprint.js +2 -3
  11. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  12. package/dist/extensions/forgecli/run-task.js +2 -3
  13. package/dist/extensions/forgecli/run-task.js.map +1 -1
  14. package/dist/extensions/forgecli/session-registry.d.ts +13 -0
  15. package/dist/extensions/forgecli/session-registry.js +19 -0
  16. package/dist/extensions/forgecli/session-registry.js.map +1 -1
  17. package/dist/extensions/forgecli/test-orchestrate.js +1 -0
  18. package/dist/extensions/forgecli/test-orchestrate.js.map +1 -1
  19. package/dist/extensions/forgecli/thread-switcher.js +76 -16
  20. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  21. package/dist/extensions/forgecli/transition-guard.js +7 -2
  22. package/dist/extensions/forgecli/transition-guard.js.map +1 -1
  23. package/dist/extensions/forgecli/viewport-events.js +10 -0
  24. package/dist/extensions/forgecli/viewport-events.js.map +1 -1
  25. package/dist/extensions/forgecli/viewport-renderer.d.ts +18 -0
  26. package/dist/extensions/forgecli/viewport-renderer.js +27 -0
  27. package/dist/extensions/forgecli/viewport-renderer.js.map +1 -1
  28. package/dist/extensions/forgecli/whats-new-widget.d.ts +13 -8
  29. package/dist/extensions/forgecli/whats-new-widget.js +111 -42
  30. package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
  31. package/dist/forge-payload/.base-pack/workflows/architect_approve.md +29 -3
  32. package/dist/forge-payload/.base-pack/workflows/commit_task.md +15 -8
  33. package/dist/forge-payload/.base-pack/workflows/fix_bug.md +327 -185
  34. package/dist/forge-payload/.base-pack/workflows/implement_plan.md +18 -10
  35. package/dist/forge-payload/.base-pack/workflows/plan_task.md +15 -9
  36. package/dist/forge-payload/.base-pack/workflows/review_code.md +14 -6
  37. package/dist/forge-payload/.base-pack/workflows/review_plan.md +18 -10
  38. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  39. package/dist/forge-payload/.schemas/bug.schema.json +3 -2
  40. package/dist/forge-payload/tools/collate.cjs +34 -9
  41. package/dist/forge-payload/tools/parse-gates.cjs +8 -2
  42. package/dist/forge-payload/tools/store-cli.cjs +29 -10
  43. package/dist/forge-payload/tools/store.cjs +61 -0
  44. package/dist/forge-payload/tools/validate-store.cjs +6 -2
  45. package/package.json +2 -2
@@ -27,7 +27,8 @@ deps:
27
27
 
28
28
  0. Pre-flight Gate Check:
29
29
  - Resolve FORGE_ROOT (`node -e "console.log(require('./.forge/config.json').paths.forgeRoot)"`).
30
- - Run: `node "$FORGE_ROOT/tools/preflight-gate.cjs" --phase implement --task {taskId}`
30
+ - **Entity-mode resolution:** read the kickoff arguments. `--task {id}` → `entity_kind = "task"`, `record_id = {id}`. `--bug {id}` → `entity_kind = "bug"`, `record_id = {id}`. All store-cli calls below substitute `{entity_kind}` and `{record_id}` for the literal "task"/{taskId} placeholders.
31
+ - Run: `node "$FORGE_ROOT/tools/preflight-gate.cjs" --phase implement --{entity_kind} {record_id}`
31
32
  - Exit 1 (gate failed) → print stderr and HALT. Do not proceed; do not attempt to produce the artifact.
32
33
  - Exit 2 (misconfiguration) → print stderr and HALT.
33
34
  - Exit 0 → continue.
@@ -59,15 +60,17 @@ deps:
59
60
  - Tag updates: `<!-- Discovered during {TASK_ID} — {date} -->`
60
61
 
61
62
  6. Finalize:
62
- - Transitions: task FSM legal predecessors are `planned`, `plan-approved`, or `implementing`; target is `implemented`.
63
- - `planned` `implemented` (workflow-prose path — direct)
64
- - `plan-approved` → `implementing` → `implemented` (supervisor-review path)
65
- - Out-of-band escapes (any state): `plan-revision-required`, `code-revision-required`, `blocked`, `escalated`, `abandoned`
66
- - Update task status via `node "$FORGE_ROOT/tools/store-cli.cjs" update-status task {taskId} status implemented`
63
+ - Transitions:
64
+ - **Task mode** — legal predecessors are `planned`, `plan-approved`, or `implementing`; target is `implemented`.
65
+ - `planned` → `implemented` (workflow-prose path — direct)
66
+ - `plan-approved` `implementing` `implemented` (supervisor-review path)
67
+ - Out-of-band escapes (any state): `plan-revision-required`, `code-revision-required`, `blocked`, `escalated`, `abandoned`
68
+ Update status: `node "$FORGE_ROOT/tools/store-cli.cjs" update-status task {taskId} status implemented`
69
+ - **Bug mode** — NO status write. The bug remains `in-progress` until the commit phase transitions it to `fixed`. Writing `bug.status` here violates `meta-fix-bug.md § Iron Laws #2`.
67
70
  - **Do NOT emit a phase event yourself.** The orchestrator owns event emission — it composes the canonical event from runtime telemetry (model, provider, tokens, wall times) plus the SUMMARY you write in the next step. Subagents that call `store-cli emit` for phase events hallucinate runtime facts (see Plan 11 / Slice 2). Write the SUMMARY and return.
68
71
 
69
72
  7. Emit Summary Sidecar:
70
- - Write `IMPLEMENTATION-SUMMARY.json` to the task directory with the following shape:
73
+ - Write `IMPLEMENTATION-SUMMARY.json` to the record's directory with the following shape:
71
74
  ```json
72
75
  {
73
76
  "objective": "<one sentence — what this implementation delivered>",
@@ -77,12 +80,17 @@ deps:
77
80
  "artifact_ref":"PROGRESS.md"
78
81
  }
79
82
  ```
80
- - Call:
83
+ - Call (task mode):
81
84
  ```
82
- node "$FORGE_ROOT/tools/store-cli.cjs" set-summary {task_id} implementation \
85
+ node "$FORGE_ROOT/tools/store-cli.cjs" set-summary {taskId} implementation \
83
86
  engineering/sprints/{sprint}/{task}/IMPLEMENTATION-SUMMARY.json
84
87
  ```
85
- - If set-summary exits non-zero, fix the sidecar JSON and retry. Do not proceed without a valid summary.
88
+ Or (bug mode):
89
+ ```
90
+ node "$FORGE_ROOT/tools/store-cli.cjs" set-bug-summary {bugId} implementation \
91
+ engineering/bugs/{bugDir}/IMPLEMENTATION-SUMMARY.json
92
+ ```
93
+ - If the set-summary call exits non-zero, fix the sidecar JSON and retry. Do not proceed without a valid summary.
86
94
  ```
87
95
 
88
96
  ## Iron Laws
@@ -27,7 +27,8 @@ deps:
27
27
 
28
28
  0. Pre-flight Gate Check:
29
29
  - Resolve FORGE_ROOT (`node -e "console.log(require('./.forge/config.json').paths.forgeRoot)"`).
30
- - Run: `node "$FORGE_ROOT/tools/preflight-gate.cjs" --phase plan --task {taskId}`
30
+ - **Entity-mode resolution:** read the kickoff arguments. `--task {id}` → `entity_kind = "task"`, `record_id = {id}`. `--bug {id}` → `entity_kind = "bug"`, `record_id = {id}`. All store-cli calls below substitute `{entity_kind}` and `{record_id}` for the literal "task"/{taskId} placeholders.
31
+ - Run: `node "$FORGE_ROOT/tools/preflight-gate.cjs" --phase plan --{entity_kind} {record_id}`
31
32
  - Exit 1 (gate failed) → print stderr and HALT. Do not proceed; do not attempt to produce the artifact.
32
33
  - Exit 2 (misconfiguration) → print stderr and HALT.
33
34
  - Exit 0 → continue.
@@ -64,14 +65,14 @@ deps:
64
65
  - If new patterns were discovered, update architecture or business domain docs
65
66
 
66
67
  5. Finalize:
67
- - Transitions: task FSM legal targets from this step
68
- - `draft``planned` (this workflow's only legal target)
69
- - Out-of-band escapes (any state): `plan-revision-required`, `code-revision-required`, `blocked`, `escalated`, `abandoned`
70
- - Update task status via `node "$FORGE_ROOT/tools/store-cli.cjs" update-status task {taskId} status planned`
68
+ - Transitions:
69
+ - **Task mode** — legal target from this step: `draft → planned`. Out-of-band escapes (any state): `plan-revision-required`, `code-revision-required`, `blocked`, `escalated`, `abandoned`.
70
+ Update status: `node "$FORGE_ROOT/tools/store-cli.cjs" update-status task {taskId} status planned`
71
+ - **Bug mode** — NO status write. The bug remains `in-progress` until the commit phase transitions it to `fixed`. Writing `bug.status` here violates `meta-fix-bug.md § Iron Laws #2`.
71
72
  - **Do NOT emit a phase event yourself.** The orchestrator owns event emission — it composes the canonical event from runtime telemetry (model, provider, tokens, wall times) plus the SUMMARY you write in the next step. Subagents that call `store-cli emit` for phase events hallucinate runtime facts (see Plan 11 / Slice 2). Write the SUMMARY and return.
72
73
 
73
74
  6. Emit Summary Sidecar:
74
- - Write `PLAN-SUMMARY.json` to the task directory with the following shape:
75
+ - Write `PLAN-SUMMARY.json` (task mode) or `BUG-FIX-PLAN-SUMMARY.json` (bug mode) to the record's directory. Shape:
75
76
  ```json
76
77
  {
77
78
  "objective": "<one sentence — what this plan sets out to build>",
@@ -81,12 +82,17 @@ deps:
81
82
  "artifact_ref":"PLAN.md"
82
83
  }
83
84
  ```
84
- - Call:
85
+ - Call (task mode):
85
86
  ```
86
- node "$FORGE_ROOT/tools/store-cli.cjs" set-summary {task_id} plan \
87
+ node "$FORGE_ROOT/tools/store-cli.cjs" set-summary {taskId} plan \
87
88
  engineering/sprints/{sprint}/{task}/PLAN-SUMMARY.json
88
89
  ```
89
- - If set-summary exits non-zero, fix the sidecar JSON and retry. Do not proceed without a valid summary.
90
+ Or (bug mode):
91
+ ```
92
+ node "$FORGE_ROOT/tools/store-cli.cjs" set-bug-summary {bugId} plan \
93
+ engineering/bugs/{bugDir}/BUG-FIX-PLAN-SUMMARY.json
94
+ ```
95
+ - If the set-summary call exits non-zero, fix the sidecar JSON and retry. Do not proceed without a valid summary.
90
96
  ```
91
97
 
92
98
  ## Iron Laws
@@ -45,7 +45,8 @@ Never set `FORGE_SKIP_WRITE_VALIDATION=1` — operator-only emergency switch.
45
45
 
46
46
  0. Pre-flight Gate Check:
47
47
  - Resolve FORGE_ROOT (`node -e "console.log(require('./.forge/config.json').paths.forgeRoot)"`).
48
- - Run: `node "$FORGE_ROOT/tools/preflight-gate.cjs" --phase review-code --task {taskId}`
48
+ - **Entity-mode resolution:** read the kickoff arguments. `--task {id}` → `entity_kind = "task"`, `record_id = {id}`. `--bug {id}` → `entity_kind = "bug"`, `record_id = {id}`. All store-cli calls below substitute `{entity_kind}` and `{record_id}` for the literal "task"/{taskId} placeholders.
49
+ - Run: `node "$FORGE_ROOT/tools/preflight-gate.cjs" --phase review-code --{entity_kind} {record_id}`
49
50
  - Exit 1 (gate failed) → print stderr and HALT. Do not proceed; do not attempt to produce the artifact.
50
51
  - Exit 2 (misconfiguration) → print stderr and HALT.
51
52
  - Exit 0 → continue.
@@ -71,11 +72,13 @@ Never set `FORGE_SKIP_WRITE_VALIDATION=1` — operator-only emergency switch.
71
72
  - Update stack-checklist.md if new patterns or pitfalls were discovered
72
73
 
73
74
  5. Finalize:
74
- - Update task status via `node "$FORGE_ROOT/tools/store-cli.cjs" update-status task {taskId} status review-approved` (if Approved) or `node "$FORGE_ROOT/tools/store-cli.cjs" update-status task {taskId} status code-revision-required` (if Revision Required)
75
+ - Transitions:
76
+ - **Task mode** — Update status: `node "$FORGE_ROOT/tools/store-cli.cjs" update-status task {taskId} status review-approved` (if Approved) or `... status code-revision-required` (if Revision Required).
77
+ - **Bug mode** — NO status write. The bug remains `in-progress`. The verdict signal travels through `summaries.code_review.verdict` (read by `read-verdict.cjs § BUG_PHASE_VERDICT_SOURCE`), not `bug.status`. Writing `bug.status` here violates `meta-fix-bug.md § Iron Laws #2`.
75
78
  - **Do NOT emit a phase event yourself.** The orchestrator (or kickoff handler) owns event emission — it composes the canonical event from runtime telemetry (model, provider, tokens, wall times) plus the SUMMARY you write in the next step. Subagents that call `store-cli emit` for phase events hallucinate runtime facts (see Plan 11 / Slice 2). Write the SUMMARY and return.
76
79
 
77
80
  6. Emit Summary Sidecar:
78
- - Write `REVIEW-IMPL-SUMMARY.json` to the task directory with the following shape:
81
+ - Write `REVIEW-IMPL-SUMMARY.json` to the record's directory with the following shape:
79
82
  ```json
80
83
  {
81
84
  "objective": "<one sentence — what this review assessed>",
@@ -85,10 +88,15 @@ Never set `FORGE_SKIP_WRITE_VALIDATION=1` — operator-only emergency switch.
85
88
  "artifact_ref":"CODE_REVIEW.md"
86
89
  }
87
90
  ```
88
- - Call:
91
+ - Call (task mode):
89
92
  ```
90
- node "$FORGE_ROOT/tools/store-cli.cjs" set-summary {task_id} code_review \
93
+ node "$FORGE_ROOT/tools/store-cli.cjs" set-summary {taskId} code_review \
91
94
  engineering/sprints/{sprint}/{task}/REVIEW-IMPL-SUMMARY.json
92
95
  ```
93
- - If set-summary exits non-zero, fix the sidecar JSON and retry. Do not proceed without a valid summary.
96
+ Or (bug mode):
97
+ ```
98
+ node "$FORGE_ROOT/tools/store-cli.cjs" set-bug-summary {bugId} code_review \
99
+ engineering/bugs/{bugDir}/REVIEW-IMPL-SUMMARY.json
100
+ ```
101
+ - If the set-summary call exits non-zero, fix the sidecar JSON and retry. Do not proceed without a valid summary.
94
102
  ```
@@ -46,7 +46,8 @@ Never set `FORGE_SKIP_WRITE_VALIDATION=1` — operator-only emergency switch.
46
46
 
47
47
  0. Pre-flight Gate Check:
48
48
  - Resolve FORGE_ROOT (`node -e "console.log(require('./.forge/config.json').paths.forgeRoot)"`).
49
- - Run: `node "$FORGE_ROOT/tools/preflight-gate.cjs" --phase review-plan --task {taskId}`
49
+ - **Entity-mode resolution:** read the kickoff arguments. `--task {id}` → `entity_kind = "task"`, `record_id = {id}`. `--bug {id}` → `entity_kind = "bug"`, `record_id = {id}`. All store-cli calls below substitute `{entity_kind}` and `{record_id}` for the literal "task"/{taskId} placeholders.
50
+ - Run: `node "$FORGE_ROOT/tools/preflight-gate.cjs" --phase review-plan --{entity_kind} {record_id}`
50
51
  - Exit 1 (gate failed) → print stderr and HALT. Do not proceed; do not attempt to produce the artifact.
51
52
  - Exit 2 (misconfiguration) → print stderr and HALT.
52
53
  - Exit 0 → continue.
@@ -66,15 +67,17 @@ Never set `FORGE_SKIP_WRITE_VALIDATION=1` — operator-only emergency switch.
66
67
  - If Approved: provide any advisory notes
67
68
 
68
69
  4. Finalize:
69
- - Transitions: task FSM predecessor must be `planned`.
70
- - Approved → `plan-approved`
71
- - Revision Required → `plan-revision-required`
72
- - Out-of-band escapes (any state): `code-revision-required`, `blocked`, `escalated`, `abandoned`
73
- - Update task status via `node "$FORGE_ROOT/tools/store-cli.cjs" update-status task {taskId} status plan-approved` (if Approved) or `node "$FORGE_ROOT/tools/store-cli.cjs" update-status task {taskId} status plan-revision-required` (if Revision Required)
70
+ - Transitions:
71
+ - **Task mode** — predecessor must be `planned`.
72
+ - Approved → `plan-approved`
73
+ - Revision Required `plan-revision-required`
74
+ - Out-of-band escapes (any state): `code-revision-required`, `blocked`, `escalated`, `abandoned`
75
+ Update status: `node "$FORGE_ROOT/tools/store-cli.cjs" update-status task {taskId} status plan-approved` (if Approved) or `... status plan-revision-required` (if Revision Required)
76
+ - **Bug mode** — NO status write. The bug remains `in-progress`. The verdict signal travels through `summaries.review_plan.verdict` (read by `read-verdict.cjs § BUG_PHASE_VERDICT_SOURCE`), not `bug.status`. Writing `bug.status` here violates `meta-fix-bug.md § Iron Laws #2`.
74
77
  - **Do NOT emit a phase event yourself.** The orchestrator owns event emission — it composes the canonical event from runtime telemetry (model, provider, tokens, wall times) plus the SUMMARY you write in the next step. Subagents that call `store-cli emit` for phase events hallucinate runtime facts (see Plan 11 / Slice 2). Write the SUMMARY and return.
75
78
 
76
79
  5. Emit Summary Sidecar:
77
- - Write `REVIEW-PLAN-SUMMARY.json` to the task directory with the following shape:
80
+ - Write `REVIEW-PLAN-SUMMARY.json` to the record's directory with the following shape:
78
81
  ```json
79
82
  {
80
83
  "objective": "<one sentence — what this review assessed>",
@@ -84,12 +87,17 @@ Never set `FORGE_SKIP_WRITE_VALIDATION=1` — operator-only emergency switch.
84
87
  "artifact_ref":"PLAN_REVIEW.md"
85
88
  }
86
89
  ```
87
- - Call:
90
+ - Call (task mode):
88
91
  ```
89
- node "$FORGE_ROOT/tools/store-cli.cjs" set-summary {task_id} review_plan \
92
+ node "$FORGE_ROOT/tools/store-cli.cjs" set-summary {taskId} review_plan \
90
93
  engineering/sprints/{sprint}/{task}/REVIEW-PLAN-SUMMARY.json
91
94
  ```
92
- - If set-summary exits non-zero, fix the sidecar JSON and retry. Do not proceed without a valid summary.
95
+ Or (bug mode):
96
+ ```
97
+ node "$FORGE_ROOT/tools/store-cli.cjs" set-bug-summary {bugId} review_plan \
98
+ engineering/bugs/{bugDir}/REVIEW-PLAN-SUMMARY.json
99
+ ```
100
+ - If the set-summary call exits non-zero, fix the sidecar JSON and retry. Do not proceed without a valid summary.
93
101
  ```
94
102
 
95
103
  ## Generation Notes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge",
3
- "version": "0.43.19",
3
+ "version": "0.44.4",
4
4
  "description": "Self-enhancing AI software development lifecycle \u2014 generates project-specific SDLC instances from meta-definitions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -9,7 +9,7 @@
9
9
  "title": { "type": "string" },
10
10
  "description": { "type": "string" },
11
11
  "severity": { "type": "string", "enum": ["critical", "major", "minor"] },
12
- "status": { "type": "string", "enum": ["reported", "triaged", "in-progress", "fixed", "approved", "verified"] },
12
+ "status": { "type": "string", "enum": ["reported", "triaged", "in-progress", "fixed"] },
13
13
  "path": { "type": "string" },
14
14
  "rootCauseCategory": {
15
15
  "type": "string",
@@ -47,7 +47,8 @@
47
47
  "findings": { "type": "array", "items": { "type": "string", "maxLength": 200 }, "maxItems": 12 },
48
48
  "verdict": { "type": "string", "enum": ["approved", "revision", "n/a"] },
49
49
  "written_at": { "type": "string", "format": "date-time" },
50
- "artifact_ref":{ "type": "string", "description": "Relative path to the full artifact" }
50
+ "artifact_ref":{ "type": "string", "description": "Relative path to the full artifact" },
51
+ "route": { "type": "string", "enum": ["A", "B"], "description": "Fix-bug pipeline route decision. Only set on the triage phase summary by meta-fix-bug.md. A = short-circuit (skip plan-fix + review-plan); B = full loop (default). Optional and meaningless on non-triage phases — see meta-fix-bug.md § Triage Judgement." }
51
52
  },
52
53
  "additionalProperties": false
53
54
  }
@@ -952,10 +952,16 @@ for (const bug of allBugs) {
952
952
  // When purging events for a bug, aggregate cost data from event files
953
953
  // before they are deleted. The aggregated cost summary is embedded in
954
954
  // the bug's INDEX.md so the information survives the purge.
955
+ //
956
+ // Bug-phase events live in the shared `events/bugs/` virtual sprint dir
957
+ // (see meta-fix-bug.md § Event Emission). Read from that dir and filter
958
+ // by `event.bugId === bug.bugId` — the per-bug `events/{bugId}/` path
959
+ // never existed (silent data loss before this fix).
955
960
  let costTotals;
956
961
  if (PURGE_EVENTS && SPRINT_ARG && SPRINT_ARG === bug.bugId) {
957
- const { events } = loadSprintEvents(bug.bugId);
958
- const tokenEvents = events.filter(e => e.inputTokens !== undefined);
962
+ const { events } = loadSprintEvents('bugs');
963
+ const bugEvents = events.filter(e => e.bugId === bug.bugId);
964
+ const tokenEvents = bugEvents.filter(e => e.inputTokens !== undefined);
959
965
  if (tokenEvents.length > 0) {
960
966
  const totals = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, estimatedCostUSD: 0, sources: new Set() };
961
967
  for (const e of tokenEvents) {
@@ -1022,16 +1028,35 @@ const tag = DRY_RUN ? '[dry-run] ' : '';
1022
1028
  console.log(`${tag}Collated: ${targetSprints.length} sprint(s), ${allBugs.length} bug(s) → MASTER_INDEX.md updated, ${sprintIndexesWritten} sprint INDEX(es), ${taskIndexesWritten} task INDEX(es), ${bugIndexesWritten} bug INDEX(es), ${costReportsWritten} COST_REPORT(s) written`);
1023
1029
 
1024
1030
  // --- Purge event directory if requested ---
1031
+ //
1032
+ // Bug arg → purge only the bug-matching events from the shared
1033
+ // `events/bugs/` dir (filter by event.bugId). Sprint arg → purge the
1034
+ // whole sprint event directory as before.
1025
1035
  if (PURGE_EVENTS) {
1026
- const relDir = path.relative(cwd, path.join(storeRoot, 'events', SPRINT_ARG));
1027
1036
  try {
1028
- const result = _getStore().purgeEvents(SPRINT_ARG, { dryRun: DRY_RUN });
1029
- if (result.fileCount === 0) {
1030
- console.log(`${tag}Purge: no events directory found for '${SPRINT_ARG}' — nothing to delete`);
1031
- } else if (DRY_RUN) {
1032
- console.log(`[dry-run] would purge: ${relDir}/ (${result.fileCount} file(s))`);
1037
+ let result;
1038
+ let relDir;
1039
+ if (IS_BUG_ARG) {
1040
+ relDir = path.relative(cwd, path.join(storeRoot, 'events', 'bugs'));
1041
+ result = _getStore().purgeBugEvents(SPRINT_ARG, { dryRun: DRY_RUN });
1042
+ const label = `'${SPRINT_ARG}' in ${relDir}/`;
1043
+ if (result.fileCount === 0) {
1044
+ console.log(`${tag}Purge: no events for ${label} — nothing to delete`);
1045
+ } else if (DRY_RUN) {
1046
+ console.log(`[dry-run] would purge: ${label} (${result.fileCount} file(s))`);
1047
+ } else {
1048
+ console.log(`Purged: ${label} (${result.fileCount} event file(s) deleted)`);
1049
+ }
1033
1050
  } else {
1034
- console.log(`Purged: ${relDir}/ (${result.fileCount} event file(s) deleted)`);
1051
+ relDir = path.relative(cwd, path.join(storeRoot, 'events', SPRINT_ARG));
1052
+ result = _getStore().purgeEvents(SPRINT_ARG, { dryRun: DRY_RUN });
1053
+ if (result.fileCount === 0) {
1054
+ console.log(`${tag}Purge: no events directory found for '${SPRINT_ARG}' — nothing to delete`);
1055
+ } else if (DRY_RUN) {
1056
+ console.log(`[dry-run] would purge: ${relDir}/ (${result.fileCount} file(s))`);
1057
+ } else {
1058
+ console.log(`Purged: ${relDir}/ (${result.fileCount} event file(s) deleted)`);
1059
+ }
1035
1060
  }
1036
1061
  } catch (err) {
1037
1062
  console.error(`Error: ${err.message}`);
@@ -18,7 +18,13 @@
18
18
  const FENCE_OPEN = /^```gates\s+phase=([A-Za-z0-9_-]+)\s*$/;
19
19
  const FENCE_CLOSE = /^```\s*$/;
20
20
 
21
- const VALID_VERDICTS = new Set(['approved', 'revision']);
21
+ // Verdicts a workflow can carry in an `after <phase> = <verdict>` directive.
22
+ // Mirrors `read-verdict.cjs § ALLOWED_VERDICTS` — both must accept the same
23
+ // set. `n/a` is the legitimate verdict for setup phases (plan, implement,
24
+ // triage) that do not produce an approval/revision signal. Rejecting it
25
+ // here caused EMBERGLOW-BUG-001 (v0.44.1) to halt at preflight exit 2
26
+ // when the new fix-bug meta tried to use `after triage = n/a`.
27
+ const VALID_VERDICTS = new Set(['approved', 'revision', 'n/a']);
22
28
 
23
29
  function parseGates(markdown) {
24
30
  if (typeof markdown !== 'string' || markdown.length === 0) return {};
@@ -142,7 +148,7 @@ function parseAfter(rest, lineNo) {
142
148
  const verdict = m[2].toLowerCase();
143
149
  if (!VALID_VERDICTS.has(verdict)) {
144
150
  throw new Error(
145
- `parse-gates: line ${lineNo}: "after" verdict must be "approved" or "revision", got "${m[2]}"`,
151
+ `parse-gates: line ${lineNo}: "after" verdict must be "approved", "revision", or "n/a", got "${m[2]}"`,
146
152
  );
147
153
  }
148
154
  return { phase: m[1], verdict };
@@ -140,6 +140,12 @@ const { suggest, suggestEntityType, formatSuggestion } = require('./lib/suggest.
140
140
  const VALID_SUMMARY_PHASES = new Set(['plan', 'review_plan', 'implementation', 'code_review', 'validation', 'triage', 'approve']);
141
141
 
142
142
  // Schema for a single phase summary (used by set-summary / set-bug-summary)
143
+ // Mirror of bug.schema.json § $defs.phaseSummary. Both definitions MUST stay
144
+ // in sync — this constant validates set-summary / set-bug-summary writes;
145
+ // the JSON schema validates entity reads. `route` is an optional triage-only
146
+ // field carrying the fix-bug pipeline route decision (A | B) and exists here
147
+ // for bug.summaries.triage in particular; non-triage phases simply do not
148
+ // set it.
143
149
  const PHASE_SUMMARY_SCHEMA = {
144
150
  type: 'object',
145
151
  required: ['objective', 'written_at'],
@@ -149,7 +155,8 @@ const PHASE_SUMMARY_SCHEMA = {
149
155
  findings: { type: 'array', items: { type: 'string', maxLength: 200 }, maxItems: 12 },
150
156
  verdict: { type: 'string', enum: ['approved', 'revision', 'n/a'] },
151
157
  written_at: { type: 'string' },
152
- artifact_ref:{ type: 'string' }
158
+ artifact_ref:{ type: 'string' },
159
+ route: { type: 'string', enum: ['A', 'B'] }
153
160
  },
154
161
  additionalProperties: false
155
162
  };
@@ -181,12 +188,21 @@ const SPRINT_TRANSITIONS = {
181
188
  };
182
189
 
183
190
  const BUG_TRANSITIONS = {
184
- reported: ['triaged'],
185
- triaged: ['in-progress'],
186
- 'in-progress': ['fixed', 'approved'], // fixed: code complete; approved: architect direct sign-off
187
- fixed: ['approved', 'in-progress'], // approved: architect sign-off; in-progress: revision
188
- approved: ['verified', 'in-progress', 'fixed'], // verified: commit; in-progress/ fixed: revision
189
- // Terminal: verified
191
+ reported: ['triaged'],
192
+ triaged: ['in-progress'],
193
+ 'in-progress': ['fixed'],
194
+ // Terminal: fixed
195
+ //
196
+ // The `approved` and `verified` enum members were removed (forge#GH-NNN).
197
+ // The architect-approve verdict signal for bugs travels through
198
+ // `bug.summaries.approve.verdict` (see read-verdict.cjs §
199
+ // BUG_PHASE_VERDICT_SOURCE), not `bug.status`. Keeping them in the enum
200
+ // created an LLM-translation trap whereby a task-shaped approve workflow
201
+ // run on a bug improvised `update-status bug ... approved`, which failed
202
+ // either at the schema layer (illegal transition out of terminal
203
+ // `verified`) or, post-FORGE-BUG-002 preflight defence, at the gate
204
+ // layer (forbid bug.status == approved). Dropping the enum removes the
205
+ // trap at its source.
190
206
  };
191
207
 
192
208
  const FEATURE_TRANSITIONS = {
@@ -205,7 +221,7 @@ const TRANSITION_MAP = {
205
221
  const TERMINAL_STATES = new Set([
206
222
  'committed', 'abandoned', // task
207
223
  'retrospective-done', // sprint
208
- 'verified', // bug
224
+ 'fixed', // bug
209
225
  'shipped', 'retired' // feature
210
226
  ]);
211
227
 
@@ -785,10 +801,13 @@ function cmdEmit() {
785
801
  }
786
802
 
787
803
  // FK check: reject sprintIds that are not in the store and not reserved.
788
- // SYS-* prefix is reserved for system-generated events that predate sprint records.
804
+ // Reserved sprintIds:
805
+ // - SYS-* — system-generated events that predate sprint records
806
+ // - bugs — virtual sprint dir for fix-bug phase events (see
807
+ // meta/tool-specs/validate-store.spec.md §"event.sprintId")
789
808
  // --allow-synthetic bypasses the check for test-harness or synthetic events.
790
809
  if (!allowSynthetic) {
791
- const RESERVED_GLOB = /^SYS-/;
810
+ const RESERVED_GLOB = /^(SYS-|bugs$)/;
792
811
  if (!RESERVED_GLOB.test(sprintId)) {
793
812
  const validSprintIds = resolveValidSprintIds();
794
813
  if (!validSprintIds.includes(sprintId)) {
@@ -56,6 +56,7 @@ class Store {
56
56
  * @returns {{ purged: boolean, fileCount: number, files: string[] }}
57
57
  */
58
58
  purgeEvents(sprintId, opts) { return this.impl.purgeEvents(sprintId, opts); }
59
+ purgeBugEvents(bugId, opts) { return this.impl.purgeBugEvents(bugId, opts); }
59
60
  /**
60
61
  * List event filenames for a sprint directory.
61
62
  * @param {string} sprintId
@@ -287,6 +288,66 @@ class FSImpl {
287
288
  return { purged: true, fileCount: files.length, files };
288
289
  }
289
290
 
291
+ /**
292
+ * Purge only the events belonging to a specific bug from the shared
293
+ * `.forge/store/events/bugs/` virtual sprint dir.
294
+ *
295
+ * Bugs share a single events directory (`events/bugs/`) — see
296
+ * `meta-fix-bug.md § Event Emission` and `validate-store.spec.md`. Purging
297
+ * by bug therefore must filter primary events by `event.bugId === bugId`
298
+ * and sweep sidecars whose filename pattern `_{eventId}_usage.json`
299
+ * matches a purged primary. The `events/bugs/` directory itself is
300
+ * never removed — other bugs' events remain.
301
+ *
302
+ * Returns `{ purged, fileCount, files }` matching `purgeEvents`.
303
+ */
304
+ purgeBugEvents(bugId, { dryRun = false } = {}) {
305
+ const eventsBugsDir = path.join(this.storeRoot, 'events', 'bugs');
306
+ if (!fs.existsSync(eventsBugsDir)) {
307
+ return { purged: false, fileCount: 0, files: [] };
308
+ }
309
+
310
+ const all = fs.readdirSync(eventsBugsDir).filter(f => f.endsWith('.json'));
311
+ const primaries = all.filter(f => !f.startsWith('_'));
312
+ const sidecars = all.filter(f => f.startsWith('_') && f.endsWith('_usage.json'));
313
+
314
+ // Identify primaries whose payload.bugId matches the requested bug.
315
+ const matchedPrimaries = [];
316
+ const matchedEventIds = new Set();
317
+ for (const filename of primaries) {
318
+ const filePath = path.join(eventsBugsDir, filename);
319
+ let payload;
320
+ try {
321
+ payload = JSON.parse(fs.readFileSync(filePath, 'utf8'));
322
+ } catch (_) {
323
+ continue; // malformed file — skip
324
+ }
325
+ if (payload && payload.bugId === bugId) {
326
+ matchedPrimaries.push(filename);
327
+ if (payload.eventId) matchedEventIds.add(payload.eventId);
328
+ }
329
+ }
330
+
331
+ // Match sidecars by filename pattern: `_{eventId}_usage.json`.
332
+ const matchedSidecars = sidecars.filter(filename => {
333
+ const m = filename.match(/^_(.+)_usage\.json$/);
334
+ return m && matchedEventIds.has(m[1]);
335
+ });
336
+
337
+ const allMatches = [...matchedPrimaries, ...matchedSidecars];
338
+ if (dryRun) {
339
+ return { purged: false, fileCount: allMatches.length, files: allMatches };
340
+ }
341
+ for (const filename of allMatches) {
342
+ fs.unlinkSync(path.join(eventsBugsDir, filename));
343
+ }
344
+ return {
345
+ purged: allMatches.length > 0,
346
+ fileCount: allMatches.length,
347
+ files: allMatches,
348
+ };
349
+ }
350
+
290
351
  /**
291
352
  * List all event filenames for a sprint directory.
292
353
  * Returns { filename, id } objects for ALL .json files including
@@ -494,8 +494,12 @@ for (const sprint of allSprints) {
494
494
 
495
495
  // --- Pass 2b: Orphan event directories ---
496
496
  // Scan .forge/store/events/ for subdirectories whose name does not match any
497
- // known sprintId and is not a reserved SYS-* prefix.
498
- const RESERVED_EVENT_PREFIX = /^SYS-/;
497
+ // known sprintId and is not a reserved virtual dir.
498
+ // Reserved:
499
+ // - SYS-* — system-generated events that predate sprint records
500
+ // - bugs — virtual sprint dir for fix-bug phase events (see
501
+ // meta/tool-specs/validate-store.spec.md §"event.sprintId")
502
+ const RESERVED_EVENT_PREFIX = /^(SYS-|bugs$)/;
499
503
  const eventsBaseDir = path.join(storeRootFromConfig, 'events');
500
504
  if (fs.existsSync(eventsBaseDir)) {
501
505
  let eventDirEntries;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entelligentsia/forgecli",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Forge SDLC ported onto @earendil-works/pi-coding-agent — production launcher with three bin aliases (forge/forgecli/4ge). Bundles a curated fork of pi-coding-agent vendored under earendil-works names.",
5
5
  "license": "MIT",
6
6
  "author": "Entelligentsia",
@@ -49,7 +49,7 @@
49
49
  ]
50
50
  },
51
51
  "forge": {
52
- "bundledVersion": "0.43.19",
52
+ "bundledVersion": "0.44.3",
53
53
  "forgeRoot": "../forge/forge"
54
54
  },
55
55
  "scripts": {