@jamie-tam/forge 6.0.0

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +389 -0
  3. package/agents/architect.md +92 -0
  4. package/agents/builder.md +122 -0
  5. package/agents/code-reviewer.md +107 -0
  6. package/agents/concept-designer.md +207 -0
  7. package/agents/craft-reviewer.md +132 -0
  8. package/agents/critic.md +130 -0
  9. package/agents/doc-writer.md +85 -0
  10. package/agents/dreamer.md +129 -0
  11. package/agents/e2e-runner.md +89 -0
  12. package/agents/gotcha-hunter.md +127 -0
  13. package/agents/prototype-builder.md +193 -0
  14. package/agents/prototype-codifier.md +204 -0
  15. package/agents/prototype-reviewer.md +163 -0
  16. package/agents/security-reviewer.md +108 -0
  17. package/agents/spec-reviewer.md +94 -0
  18. package/agents/tracer.md +98 -0
  19. package/agents/wireframer.md +109 -0
  20. package/commands/abort.md +25 -0
  21. package/commands/bugfix.md +151 -0
  22. package/commands/evolve.md +118 -0
  23. package/commands/feature.md +236 -0
  24. package/commands/forge.md +100 -0
  25. package/commands/greenfield.md +185 -0
  26. package/commands/hotfix.md +98 -0
  27. package/commands/refactor.md +147 -0
  28. package/commands/resume.md +25 -0
  29. package/commands/setup.md +201 -0
  30. package/commands/status.md +27 -0
  31. package/commands/task-force.md +110 -0
  32. package/commands/validate.md +12 -0
  33. package/dist/__tests__/active-manifest.test.js +272 -0
  34. package/dist/__tests__/copy.test.js +96 -0
  35. package/dist/__tests__/gate-check.test.js +384 -0
  36. package/dist/__tests__/wiki.test.js +472 -0
  37. package/dist/__tests__/work-manifest.test.js +304 -0
  38. package/dist/active-manifest.js +229 -0
  39. package/dist/cli.js +158 -0
  40. package/dist/copy.js +124 -0
  41. package/dist/gate-check.js +326 -0
  42. package/dist/hooks.js +60 -0
  43. package/dist/init.js +140 -0
  44. package/dist/manifest.js +90 -0
  45. package/dist/merge.js +77 -0
  46. package/dist/paths.js +36 -0
  47. package/dist/uninstall.js +216 -0
  48. package/dist/update.js +158 -0
  49. package/dist/verify-manifest.js +65 -0
  50. package/dist/verify.js +98 -0
  51. package/dist/wiki-ui.js +310 -0
  52. package/dist/wiki.js +364 -0
  53. package/dist/work-manifest.js +798 -0
  54. package/hooks/config/gate-requirements.json +79 -0
  55. package/hooks/hooks.json +143 -0
  56. package/hooks/scripts/analyze-telemetry.sh +114 -0
  57. package/hooks/scripts/gate-enforcer.sh +164 -0
  58. package/hooks/scripts/pre-compact.sh +90 -0
  59. package/hooks/scripts/session-start.sh +81 -0
  60. package/hooks/scripts/telemetry.sh +41 -0
  61. package/hooks/scripts/wiki-lint.sh +87 -0
  62. package/hooks/templates/AGENTS.md.template +48 -0
  63. package/hooks/templates/CLAUDE.md.template +45 -0
  64. package/package.json +55 -0
  65. package/protocols/README.md +40 -0
  66. package/protocols/codex.md +151 -0
  67. package/protocols/graphify.md +156 -0
  68. package/references/common/agent-coordination.md +65 -0
  69. package/references/common/coding-standards.md +54 -0
  70. package/references/common/feature-tracking.md +21 -0
  71. package/references/common/io-protocol.md +36 -0
  72. package/references/common/phases.md +57 -0
  73. package/references/common/quality-gates.md +130 -0
  74. package/references/common/skill-authoring.md +154 -0
  75. package/references/common/skill-compliance.md +30 -0
  76. package/references/python/standards.md +44 -0
  77. package/references/react/standards.md +61 -0
  78. package/references/typescript/standards.md +42 -0
  79. package/rules/common/forge-system.md +59 -0
  80. package/rules/common/git-workflow.md +40 -0
  81. package/rules/common/guardrails.md +37 -0
  82. package/rules/common/quality-gates.md +18 -0
  83. package/rules/common/security.md +50 -0
  84. package/rules/common/skill-selection.md +78 -0
  85. package/rules/common/testing.md +58 -0
  86. package/rules/common/verification.md +39 -0
  87. package/skills/build-pr-workflow/SKILL.md +301 -0
  88. package/skills/build-pr-workflow/references/pr-template.md +62 -0
  89. package/skills/build-pr-workflow/references/subagent-merge.md +47 -0
  90. package/skills/build-pr-workflow/references/worktree-setup.md +125 -0
  91. package/skills/build-prototype/SKILL.md +264 -0
  92. package/skills/build-scaffold/SKILL.md +340 -0
  93. package/skills/build-tdd/SKILL.md +89 -0
  94. package/skills/build-wireframe/SKILL.md +110 -0
  95. package/skills/build-wireframe/assets/baseline-template.html +486 -0
  96. package/skills/build-wireframe/references/demo-walkthroughs.md +170 -0
  97. package/skills/build-wireframe/references/gotchas.md +188 -0
  98. package/skills/build-wireframe/references/legend-lines.md +141 -0
  99. package/skills/concept-slides/SKILL.md +192 -0
  100. package/skills/deliver-db-migration/SKILL.md +466 -0
  101. package/skills/deliver-deploy/SKILL.md +407 -0
  102. package/skills/deliver-onboarding/SKILL.md +198 -0
  103. package/skills/deliver-onboarding/references/document-templates.md +393 -0
  104. package/skills/deliver-onboarding/templates/getting-started.md +122 -0
  105. package/skills/discover-codebase-analysis/SKILL.md +448 -0
  106. package/skills/discover-requirements/SKILL.md +418 -0
  107. package/skills/discover-requirements/templates/prd.md +99 -0
  108. package/skills/discover-requirements/templates/technical-spec.md +123 -0
  109. package/skills/discover-requirements/templates/user-stories.md +76 -0
  110. package/skills/harden/SKILL.md +214 -0
  111. package/skills/iterate-prototype/SKILL.md +241 -0
  112. package/skills/plan-architecture/SKILL.md +457 -0
  113. package/skills/plan-architecture/templates/adr-template.md +52 -0
  114. package/skills/plan-architecture/templates/api-contract.md +99 -0
  115. package/skills/plan-architecture/templates/db-schema.md +81 -0
  116. package/skills/plan-architecture/templates/system-design.md +111 -0
  117. package/skills/plan-brainstorm/SKILL.md +433 -0
  118. package/skills/plan-design-system/SKILL.md +279 -0
  119. package/skills/plan-task-decompose/SKILL.md +454 -0
  120. package/skills/quality-code-review/SKILL.md +286 -0
  121. package/skills/quality-security-audit/SKILL.md +292 -0
  122. package/skills/quality-security-audit/references/audit-report-template.md +89 -0
  123. package/skills/quality-security-audit/references/owasp-checks.md +178 -0
  124. package/skills/quality-test-execution/SKILL.md +435 -0
  125. package/skills/quality-test-plan/SKILL.md +297 -0
  126. package/skills/quality-test-plan/references/test-type-guide.md +263 -0
  127. package/skills/quality-test-plan/templates/e2e-test-plan.md +72 -0
  128. package/skills/quality-test-plan/templates/integration-test-plan.md +74 -0
  129. package/skills/quality-test-plan/templates/load-test-plan.md +111 -0
  130. package/skills/quality-test-plan/templates/smoke-test-plan.md +68 -0
  131. package/skills/quality-test-plan/templates/unit-test-plan.md +56 -0
  132. package/skills/quality-uiux/SKILL.md +481 -0
  133. package/skills/support-debug/SKILL.md +464 -0
  134. package/skills/support-dream/SKILL.md +213 -0
  135. package/skills/support-gotcha/SKILL.md +249 -0
  136. package/skills/support-runtime-reachability/SKILL.md +190 -0
  137. package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/app.ts +7 -0
  138. package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/handlers/cases.ts +7 -0
  139. package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/app.ts +8 -0
  140. package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/handlers/cases.ts +7 -0
  141. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/App.tsx +5 -0
  142. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/components/RingingBanner.tsx +7 -0
  143. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/hooks/useTwilio.ts +6 -0
  144. package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/App.tsx +5 -0
  145. package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/components/MyComp.tsx +3 -0
  146. package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/App.tsx +3 -0
  147. package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/components/Orphan.tsx +3 -0
  148. package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/lib/Service.ts +6 -0
  149. package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/main.ts +4 -0
  150. package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/lib/Lonely.ts +5 -0
  151. package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/main.ts +2 -0
  152. package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/handler.ts +3 -0
  153. package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/main.ts +3 -0
  154. package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/handler.ts +3 -0
  155. package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/main.ts +2 -0
  156. package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/lib.ts +5 -0
  157. package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/main.ts +3 -0
  158. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/index.ts +1 -0
  159. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/internal.ts +3 -0
  160. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/main.ts +3 -0
  161. package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.test.ts +5 -0
  162. package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.ts +3 -0
  163. package/skills/support-runtime-reachability/scripts/__fixtures__/case-13-gated-pending-annotation/src/future.ts +4 -0
  164. package/skills/support-runtime-reachability/scripts/__fixtures__/case-14-untraceable-annotation/src/decorated.ts +4 -0
  165. package/skills/support-runtime-reachability/scripts/__fixtures__/case-15-untraceable-empty/src/lazy.ts +4 -0
  166. package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/lib.py +15 -0
  167. package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/main.py +5 -0
  168. package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/parent.ts +5 -0
  169. package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/routes/cases.ts +5 -0
  170. package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/lib/foo.ts +3 -0
  171. package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/other.ts +8 -0
  172. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/cases.ts +4 -0
  173. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/users.ts +4 -0
  174. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/main.ts +5 -0
  175. package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/handlers/cases.ts +3 -0
  176. package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/main.ts +4 -0
  177. package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/lib.ts +5 -0
  178. package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/main.ts +5 -0
  179. package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/lib.ts +3 -0
  180. package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/main.ts +8 -0
  181. package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/lib.ts +3 -0
  182. package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/main.ts +7 -0
  183. package/skills/support-runtime-reachability/scripts/check.mjs +638 -0
  184. package/skills/support-runtime-reachability/scripts/check.test.mjs +244 -0
  185. package/skills/support-skill-validator/SKILL.md +194 -0
  186. package/skills/support-skill-validator/references/false-positives.md +59 -0
  187. package/skills/support-skill-validator/references/validation-checks.md +280 -0
  188. package/skills/support-system-guide/SKILL.md +311 -0
  189. package/skills/support-task-force/SKILL.md +265 -0
  190. package/skills/support-task-force/references/dispatch-pattern.md +178 -0
  191. package/skills/support-task-force/references/synthesis-template.md +126 -0
  192. package/skills/support-wiki-bootstrap/SKILL.md +37 -0
  193. package/skills/support-wiki-lint/SKILL.md +196 -0
  194. package/skills/support-wiki-lint/scripts/lint.mjs +488 -0
  195. package/skills/support-wiki-lint/scripts/lint.test.mjs +196 -0
  196. package/templates/README.md +23 -0
  197. package/templates/aiwiki/CLAUDE.md.template +78 -0
  198. package/templates/aiwiki/schemas/architecture.md +118 -0
  199. package/templates/aiwiki/schemas/convention.md +112 -0
  200. package/templates/aiwiki/schemas/decision.md +144 -0
  201. package/templates/aiwiki/schemas/gotcha.md +118 -0
  202. package/templates/aiwiki/schemas/oracle.md +105 -0
  203. package/templates/aiwiki/schemas/session.md +125 -0
  204. package/templates/manifests/bugfix.yaml +41 -0
  205. package/templates/manifests/feature.yaml +69 -0
  206. package/templates/manifests/greenfield.yaml +61 -0
  207. package/templates/manifests/hotfix.yaml +45 -0
  208. package/templates/manifests/refactor.yaml +44 -0
  209. package/templates/manifests/v5/SCHEMA.md +327 -0
  210. package/templates/manifests/v5/feature.yaml +77 -0
  211. package/templates/manifests/v6/SCHEMA.md +199 -0
  212. package/templates/wiki-html/dream-detail.html +378 -0
  213. package/templates/wiki-html/dreams-list.html +155 -0
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Smoke test for src/work-manifest.ts.
3
+ *
4
+ * Run with: npm run build && node dist/__tests__/work-manifest.test.js
5
+ *
6
+ * Zero-test-framework approach (consistent with project minimalism). Asserts
7
+ * by throwing; prints PASS/FAIL per case; non-zero exit on any failure.
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import { dirname, join } from "node:path";
12
+ import { parseManifest, } from "../work-manifest.js";
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ // Fixtures live in src/__fixtures__/ — at runtime we are in dist/__tests__/, so go up two.
16
+ const FIXTURES = join(__dirname, "..", "..", "src", "__fixtures__", "work-manifest");
17
+ let passed = 0;
18
+ let failed = 0;
19
+ const fails = [];
20
+ function test(name, fn) {
21
+ try {
22
+ fn();
23
+ passed++;
24
+ console.log(` PASS ${name}`);
25
+ }
26
+ catch (e) {
27
+ failed++;
28
+ const msg = e instanceof Error ? e.message : String(e);
29
+ fails.push(`${name}: ${msg}`);
30
+ console.log(` FAIL ${name}\n ${msg}`);
31
+ }
32
+ }
33
+ function loadFixture(name) {
34
+ return readFileSync(join(FIXTURES, name), "utf-8");
35
+ }
36
+ function expectOk(result, schema) {
37
+ if (!result.ok) {
38
+ throw new Error(`expected ok, got errors:\n${result.errors.map((e) => ` ${e.code} @ ${e.path}: ${e.message}`).join("\n")}`);
39
+ }
40
+ if (result.schema !== schema) {
41
+ throw new Error(`expected schema=${schema}, got ${result.schema}`);
42
+ }
43
+ return result.manifest;
44
+ }
45
+ function expectErrors(result, codes) {
46
+ if (result.ok) {
47
+ throw new Error(`expected errors [${codes.join(", ")}], but result was ok`);
48
+ }
49
+ const present = new Set(result.errors.map((e) => e.code));
50
+ const missing = codes.filter((c) => !present.has(c));
51
+ if (missing.length > 0) {
52
+ throw new Error(`expected errors ${codes.join(", ")} — missing ${missing.join(", ")}\n got: ${[...present].join(", ")}`);
53
+ }
54
+ }
55
+ console.log("\n=== work-manifest.ts smoke tests ===\n");
56
+ console.log("Structural: happy paths");
57
+ test("v5 good manifest parses cleanly", () => {
58
+ expectOk(parseManifest(loadFixture("v5-good.yaml")), "v5");
59
+ });
60
+ test("v4 manifest synthesizes to v5 single-slice", () => {
61
+ const m = expectOk(parseManifest(loadFixture("v4-legacy.yaml")), "v4");
62
+ if (!m.slice_graph.slices["legacy-build"]) {
63
+ throw new Error("expected synthesized 'legacy-build' slice");
64
+ }
65
+ if (m.slice_graph.slices["legacy-build"].status !== "complete") {
66
+ throw new Error(`v4 with all complete tasks should synthesize legacy-build status=complete; got ${m.slice_graph.slices["legacy-build"].status}`);
67
+ }
68
+ });
69
+ console.log("\nStructural: error paths (verifier returns ALL errors)");
70
+ test("cycle detected", () => {
71
+ expectErrors(parseManifest(loadFixture("bad-cycle.yaml")), ["E_CYCLE"]);
72
+ });
73
+ test("multi-error fixture surfaces all listed errors", () => {
74
+ expectErrors(parseManifest(loadFixture("bad-multi-error.yaml")), [
75
+ "E_BAD_CURRENT_SLICE",
76
+ "E_BAD_SLICE_ID",
77
+ "E_MULTIPLE_SKELETONS",
78
+ "E_SKELETON_NOT_ROOT",
79
+ "E_VARIANT_REQUIRED",
80
+ "E_VARIANT_FORBIDDEN",
81
+ "E_SELF_DEP",
82
+ "E_UNRESOLVED_DEP",
83
+ "E_GATE_COLLISION",
84
+ "E_BAD_ENUM_VALUE",
85
+ ]);
86
+ });
87
+ test("premature code-review-final blocked", () => {
88
+ expectErrors(parseManifest(loadFixture("bad-premature-final.yaml")), ["E_PHASE_GATE_PREMATURE"]);
89
+ });
90
+ test("orphan slice detected when skeleton present", () => {
91
+ expectErrors(parseManifest(loadFixture("bad-orphan-slice.yaml")), ["E_ORPHAN_SLICE"]);
92
+ });
93
+ test("shape violations: bad name regex, missing current_slice, gates: null", () => {
94
+ expectErrors(parseManifest(loadFixture("bad-shape.yaml")), [
95
+ "E_BAD_ENUM_VALUE", // name not kebab-case
96
+ "E_BAD_CURRENT_SLICE", // current_slice missing
97
+ "E_MISSING_REQUIRED_FIELD", // gates: null
98
+ ]);
99
+ });
100
+ test("gate payload: bad status value caught", () => {
101
+ const yaml = `schema_version: "5"
102
+ name: ok
103
+ type: feature
104
+ description: "x"
105
+ status: in-progress
106
+ created: "2026-05-03"
107
+ command: feature
108
+ phases:
109
+ discover: { codebase-analysis: { status: pending, gate-passed: false } }
110
+ plan: { brainstorm: { status: pending, gate-passed: false } }
111
+ quality: { code-review-final: { status: pending, gate-passed: false } }
112
+ deliver: { pr-created: false }
113
+ support: { gotchas-recorded: false }
114
+ slice_graph:
115
+ current_slice: a
116
+ slices:
117
+ a:
118
+ type: feature-slice
119
+ depends_on: []
120
+ status: pending
121
+ gates:
122
+ build-tdd: { status: bogus-status, gate-passed: "yes" }
123
+ `;
124
+ expectErrors(parseManifest(yaml), ["E_BAD_ENUM_VALUE", "E_MISSING_REQUIRED_FIELD"]);
125
+ });
126
+ test("depends_on must be an array", () => {
127
+ const yaml = `schema_version: "5"
128
+ name: ok
129
+ type: feature
130
+ description: "x"
131
+ status: in-progress
132
+ created: "2026-05-03"
133
+ command: feature
134
+ phases:
135
+ discover: { codebase-analysis: { status: pending, gate-passed: false } }
136
+ plan: { brainstorm: { status: pending, gate-passed: false } }
137
+ quality: { code-review-final: { status: pending, gate-passed: false } }
138
+ deliver: { pr-created: false }
139
+ support: { gotchas-recorded: false }
140
+ slice_graph:
141
+ current_slice: a
142
+ slices:
143
+ a:
144
+ type: feature-slice
145
+ depends_on: "skeleton"
146
+ status: pending
147
+ gates: { build-tdd: { status: pending, gate-passed: false } }
148
+ `;
149
+ expectErrors(parseManifest(yaml), ["E_MISSING_REQUIRED_FIELD"]);
150
+ });
151
+ console.log("\nv6: phase_plan");
152
+ test("v6 good manifest parses cleanly", () => {
153
+ const m = expectOk(parseManifest(loadFixture("v6-good.yaml")), "v6");
154
+ if (!m.phase_plan)
155
+ throw new Error("expected phase_plan to be present");
156
+ // Scalar form normalized to object
157
+ if (m.phase_plan.concept?.status !== "skipped") {
158
+ throw new Error(`expected phase_plan.concept.status=skipped, got ${m.phase_plan.concept?.status}`);
159
+ }
160
+ // Object form preserves reason
161
+ if (m.phase_plan["test-plan"]?.reason !== "typecheck + browser verify only — no test runner installed") {
162
+ throw new Error(`expected phase_plan.test-plan.reason preserved`);
163
+ }
164
+ if (m.phase_plan["test-plan"]?.status !== "active-light") {
165
+ throw new Error(`expected phase_plan.test-plan.status=active-light`);
166
+ }
167
+ });
168
+ test("v6 manifest missing phase_plan is rejected", () => {
169
+ const yaml = `schema_version: "6"
170
+ name: missing-plan
171
+ type: feature
172
+ description: "x"
173
+ status: in-progress
174
+ created: "2026-05-12"
175
+ command: feature
176
+ phases:
177
+ discover: { codebase-analysis: { status: pending, gate-passed: false } }
178
+ plan: { brainstorm: { status: pending, gate-passed: false } }
179
+ quality: { code-review-final: { status: pending, gate-passed: false } }
180
+ deliver: { pr-created: false }
181
+ support: { gotchas-recorded: false }
182
+ slice_graph:
183
+ current_slice: a
184
+ slices:
185
+ a: { type: feature-slice, depends_on: [], status: pending, gates: { build-tdd: { status: pending, gate-passed: false } } }
186
+ `;
187
+ expectErrors(parseManifest(yaml), ["E_MISSING_REQUIRED_FIELD"]);
188
+ });
189
+ test("v6 bad phase_plan: bad enum + bad shape", () => {
190
+ expectErrors(parseManifest(loadFixture("v6-bad-phase-plan.yaml")), [
191
+ "E_BAD_PHASE_PLAN_STATUS",
192
+ "E_BAD_PHASE_PLAN_SHAPE",
193
+ ]);
194
+ });
195
+ test("v5 manifest parsed under v6 reader leaves phase_plan undefined", () => {
196
+ const m = expectOk(parseManifest(loadFixture("v5-good.yaml")), "v5");
197
+ if (m.phase_plan !== undefined) {
198
+ throw new Error(`expected v5 manifest to have phase_plan=undefined, got ${JSON.stringify(m.phase_plan)}`);
199
+ }
200
+ });
201
+ test("v6 typo'd phase_plan key triggers W_UNKNOWN_PHASE_PLAN_KEY warning", () => {
202
+ // Same as v6-good.yaml but with one key intentionally misspelled. Parse
203
+ // should still succeed (warning, not error) and the warning should name the
204
+ // bad key so command authors can act on it.
205
+ const yaml = `schema_version: "6"
206
+ name: typo-feature
207
+ type: feature
208
+ description: "phase_plan with a misspelled key"
209
+ status: in-progress
210
+ created: "2026-05-12"
211
+ command: feature
212
+ phase_plan:
213
+ discover-codebase: active
214
+ prototpe: active # typo — should be "prototype"
215
+ production-build: active
216
+ test-plan: active-light
217
+ uiux-review: active
218
+ code-review-final: active
219
+ deliver: active
220
+ gotchas: as-discovered
221
+ phases:
222
+ discover: { codebase-analysis: { status: pending, gate-passed: false } }
223
+ plan: { brainstorm: { status: pending, gate-passed: false } }
224
+ quality: { code-review-final: { status: pending, gate-passed: false } }
225
+ deliver: { pr-created: false }
226
+ support: { gotchas-recorded: false }
227
+ slice_graph:
228
+ current_slice: a
229
+ slices:
230
+ a: { type: feature-slice, depends_on: [], status: pending, gates: { build-tdd: { status: pending, gate-passed: false } } }
231
+ `;
232
+ const r = parseManifest(yaml);
233
+ if (!r.ok) {
234
+ throw new Error(`expected parse to succeed (warning, not error); got errors: ${r.errors.map((e) => e.code).join(", ")}`);
235
+ }
236
+ const matching = r.warnings.filter((w) => w.code === "W_UNKNOWN_PHASE_PLAN_KEY");
237
+ if (matching.length !== 1) {
238
+ throw new Error(`expected exactly 1 W_UNKNOWN_PHASE_PLAN_KEY warning, got ${matching.length}: ${JSON.stringify(r.warnings)}`);
239
+ }
240
+ if (!matching[0].path.includes("prototpe")) {
241
+ throw new Error(`expected warning path to mention the typo'd key, got ${matching[0].path}`);
242
+ }
243
+ });
244
+ test("v6 happy-path manifest emits no W_UNKNOWN_PHASE_PLAN_KEY warnings", () => {
245
+ const r = parseManifest(loadFixture("v6-good.yaml"));
246
+ if (!r.ok)
247
+ throw new Error("expected v6-good to parse");
248
+ const unknown = r.warnings.filter((w) => w.code === "W_UNKNOWN_PHASE_PLAN_KEY");
249
+ if (unknown.length !== 0) {
250
+ throw new Error(`expected zero W_UNKNOWN_PHASE_PLAN_KEY warnings on v6-good, got: ${JSON.stringify(unknown)}`);
251
+ }
252
+ });
253
+ test("variant: empty-string on non-skeleton is forbidden", () => {
254
+ const yaml = `schema_version: "5"
255
+ name: ok
256
+ type: feature
257
+ description: "x"
258
+ status: in-progress
259
+ created: "2026-05-03"
260
+ command: feature
261
+ phases:
262
+ discover: { codebase-analysis: { status: pending, gate-passed: false } }
263
+ plan: { brainstorm: { status: pending, gate-passed: false } }
264
+ quality: { code-review-final: { status: pending, gate-passed: false } }
265
+ deliver: { pr-created: false }
266
+ support: { gotchas-recorded: false }
267
+ slice_graph:
268
+ current_slice: a
269
+ slices:
270
+ a:
271
+ type: feature-slice
272
+ variant: "" # forbidden — must be absent or null
273
+ depends_on: []
274
+ status: pending
275
+ gates: { build-tdd: { status: pending, gate-passed: false } }
276
+ `;
277
+ expectErrors(parseManifest(yaml), ["E_VARIANT_FORBIDDEN"]);
278
+ });
279
+ // NOTE: transitional tests removed per V5-PLAN §9.2 ("Enforcement is earned,
280
+ // not assumed"). verifyTransition + status-transition enforcement were cut;
281
+ // slice status transitions remain a documented lifecycle convention. If
282
+ // dogfood evidence shows the convention is silently violated, restore both
283
+ // the function and these tests from git history (commit be5ab93's parent).
284
+ console.log("\nMisc: YAML parse errors");
285
+ test("malformed YAML returns E_YAML_PARSE", () => {
286
+ const r = parseManifest("not: : valid: yaml: at all: \n - [unclosed");
287
+ if (r.ok)
288
+ throw new Error("expected error");
289
+ if (!r.errors.some((e) => e.code === "E_YAML_PARSE")) {
290
+ throw new Error(`expected E_YAML_PARSE, got ${r.errors.map((e) => e.code)}`);
291
+ }
292
+ });
293
+ test("empty YAML returns error", () => {
294
+ const r = parseManifest("");
295
+ if (r.ok)
296
+ throw new Error("expected error");
297
+ });
298
+ console.log(`\n=== ${passed} passed, ${failed} failed ===`);
299
+ if (failed > 0) {
300
+ console.log("\nFailures:");
301
+ fails.forEach((f) => console.log(" - " + f));
302
+ process.exit(1);
303
+ }
304
+ process.exit(0);
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Active manifest pointer — `.forge/state/active-manifest.json`.
3
+ *
4
+ * Records which manifest the current session "owns". Used by:
5
+ * - `telemetry.sh` to tag every Skill/Agent invocation with a slice context
6
+ * captured AT INVOCATION TIME (per Codex round-1 #3 — race semantics).
7
+ * - `gate-enforcer.sh` (via `gate-check` CLI) to confirm a gate edit
8
+ * targets the same manifest as the active pointer; otherwise refuses.
9
+ *
10
+ * Write semantics: atomic rename. The pointer is tiny and infrequently
11
+ * written; full `flock(2)` is overkill. `O_EXCL`-style temp + atomic
12
+ * `rename(2)` is sufficient for the single-agent-multiple-tab footgun.
13
+ *
14
+ * Read semantics: missing file → null. Corrupt JSON → throw. The system
15
+ * should never silently degrade enforcement on corruption.
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
18
+ import { isAbsolute, resolve } from "node:path";
19
+ import { execSync } from "node:child_process";
20
+ /** Resolve the project's `.forge/state/` directory. Mirrors telemetry.sh /
21
+ * session-start.sh resolution: prefer `git rev-parse --show-toplevel`,
22
+ * fall back to cwd. */
23
+ export function getStateDir(cwd = process.cwd()) {
24
+ let projectRoot = cwd;
25
+ try {
26
+ const out = execSync("git rev-parse --show-toplevel", {
27
+ cwd,
28
+ stdio: ["ignore", "pipe", "ignore"],
29
+ });
30
+ projectRoot = out.toString().trim() || cwd;
31
+ }
32
+ catch {
33
+ // Not a git repo; fall back to cwd. Same behavior as the bash hooks.
34
+ }
35
+ return resolve(projectRoot, ".forge", "state");
36
+ }
37
+ export function getActivePointerPath(cwd) {
38
+ return resolve(getStateDir(cwd), "active-manifest.json");
39
+ }
40
+ /** Read the active pointer. Returns null if not set; throws on corruption. */
41
+ export function getActiveManifest(cwd) {
42
+ const path = getActivePointerPath(cwd);
43
+ if (!existsSync(path))
44
+ return null;
45
+ let raw;
46
+ try {
47
+ raw = readFileSync(path, "utf-8");
48
+ }
49
+ catch (e) {
50
+ throw new Error(`active-manifest pointer at ${path} cannot be read: ${e.message}`);
51
+ }
52
+ let parsed;
53
+ try {
54
+ parsed = JSON.parse(raw);
55
+ }
56
+ catch (e) {
57
+ throw new Error(`active-manifest pointer at ${path} is corrupt JSON: ${e.message}. ` +
58
+ `Either fix the file by hand or run 'aideas-forge active-manifest clear'.`);
59
+ }
60
+ if (!parsed || typeof parsed !== "object") {
61
+ throw new Error(`active-manifest pointer at ${path} is not a JSON object`);
62
+ }
63
+ const p = parsed;
64
+ if (typeof p.manifest_path !== "string" ||
65
+ typeof p.set_at !== "string" ||
66
+ typeof p.set_by !== "string") {
67
+ throw new Error(`active-manifest pointer at ${path} is missing required fields or has wrong types ` +
68
+ `(manifest_path, set_at, set_by all required, all strings); content: ${raw.slice(0, 200)}`);
69
+ }
70
+ if (!isAbsolute(p.manifest_path)) {
71
+ throw new Error(`active-manifest pointer manifest_path "${p.manifest_path}" must be absolute. ` +
72
+ `Re-run 'aideas-forge active-manifest set <path>' or 'clear'.`);
73
+ }
74
+ // Stale-pointer detection: the pointed-to manifest must still exist as a
75
+ // regular file. Otherwise enforcement would silently skip slice context.
76
+ let stat;
77
+ try {
78
+ stat = statSync(p.manifest_path);
79
+ }
80
+ catch (e) {
81
+ throw new Error(`active-manifest pointer references a manifest that cannot be read: ` +
82
+ `${p.manifest_path} (${e.message}). ` +
83
+ `Run 'aideas-forge active-manifest clear' or set a valid manifest.`);
84
+ }
85
+ if (!stat.isFile()) {
86
+ throw new Error(`active-manifest pointer references "${p.manifest_path}" which is not a regular file ` +
87
+ `(directory or other). Run 'aideas-forge active-manifest clear'.`);
88
+ }
89
+ return {
90
+ manifest_path: p.manifest_path,
91
+ set_at: p.set_at,
92
+ set_by: p.set_by,
93
+ };
94
+ }
95
+ /** Write the active pointer atomically. Verifies the manifest path exists
96
+ * unless `skipExistsCheck` is true. */
97
+ export function setActiveManifest(manifestPath, opts = {}) {
98
+ const absManifest = isAbsolute(manifestPath)
99
+ ? manifestPath
100
+ : resolve(opts.cwd ?? process.cwd(), manifestPath);
101
+ if (!opts.skipExistsCheck) {
102
+ let stat;
103
+ try {
104
+ stat = statSync(absManifest);
105
+ }
106
+ catch (e) {
107
+ throw new Error(`manifest does not exist: ${absManifest} (${e.message})`);
108
+ }
109
+ if (!stat.isFile()) {
110
+ throw new Error(`manifest path is not a regular file: ${absManifest} (got ${stat.isDirectory() ? "directory" : "other"})`);
111
+ }
112
+ }
113
+ const stateDir = getStateDir(opts.cwd);
114
+ mkdirSync(stateDir, { recursive: true });
115
+ const pointer = {
116
+ manifest_path: absManifest,
117
+ set_at: new Date().toISOString(),
118
+ set_by: opts.setBy ?? "manual",
119
+ };
120
+ const finalPath = getActivePointerPath(opts.cwd);
121
+ // Atomic write: write to .tmp, rename. POSIX rename(2) is atomic on the
122
+ // same filesystem; the .tmp suffix avoids torn reads if the writer dies
123
+ // mid-write or another tab races us. Always clean up the tmp file on
124
+ // failure — orphans would confuse later operators.
125
+ const tmpPath = `${finalPath}.${process.pid}.tmp`;
126
+ let renamed = false;
127
+ try {
128
+ writeFileSync(tmpPath, JSON.stringify(pointer, null, 2) + "\n", { mode: 0o644 });
129
+ renameSync(tmpPath, finalPath);
130
+ renamed = true;
131
+ }
132
+ finally {
133
+ if (!renamed) {
134
+ try {
135
+ rmSync(tmpPath, { force: true });
136
+ }
137
+ catch {
138
+ /* best-effort cleanup */
139
+ }
140
+ }
141
+ }
142
+ return pointer;
143
+ }
144
+ /** Delete the active pointer. Idempotent — no error if already absent.
145
+ * Returns true if a pointer existed before this call and was cleared,
146
+ * false if there was nothing to clear. Concurrent-safe: uses force:true
147
+ * so a racing clear does not throw. */
148
+ export function clearActiveManifest(cwd) {
149
+ const path = getActivePointerPath(cwd);
150
+ const existedBefore = existsSync(path);
151
+ rmSync(path, { force: true });
152
+ return existedBefore;
153
+ }
154
+ export async function activeManifestCli(args) {
155
+ const subcommand = args[0];
156
+ switch (subcommand) {
157
+ case "set": {
158
+ // Parse --set-by flag (supports `--set-by foo` and `--set-by=foo bar`).
159
+ let setBy = "manual";
160
+ const positionals = [];
161
+ for (let i = 1; i < args.length; i++) {
162
+ const tok = args[i];
163
+ if (tok === "--set-by") {
164
+ setBy = args[++i] ?? "";
165
+ if (!setBy) {
166
+ console.error("--set-by requires a value");
167
+ return { exitCode: 1 };
168
+ }
169
+ }
170
+ else if (tok.startsWith("--set-by=")) {
171
+ setBy = tok.slice("--set-by=".length);
172
+ if (!setBy) {
173
+ console.error("--set-by requires a value");
174
+ return { exitCode: 1 };
175
+ }
176
+ }
177
+ else if (tok.startsWith("--")) {
178
+ console.error(`unknown flag: ${tok}`);
179
+ return { exitCode: 1 };
180
+ }
181
+ else {
182
+ positionals.push(tok);
183
+ }
184
+ }
185
+ const path = positionals[0];
186
+ if (!path) {
187
+ console.error("Usage: aideas-forge active-manifest set <path-to-manifest.yaml> [--set-by <label>]");
188
+ return { exitCode: 1 };
189
+ }
190
+ if (positionals.length > 1) {
191
+ console.error(`unexpected extra arguments: ${positionals.slice(1).join(" ")}. ` +
192
+ `Use --set-by "your label" for multi-word labels.`);
193
+ return { exitCode: 1 };
194
+ }
195
+ try {
196
+ const ptr = setActiveManifest(path, { setBy });
197
+ console.log(`Active manifest set:\n ${ptr.manifest_path}\n (set_by: ${ptr.set_by}, at ${ptr.set_at})`);
198
+ return { exitCode: 0 };
199
+ }
200
+ catch (e) {
201
+ console.error(`active-manifest set failed: ${e.message}`);
202
+ return { exitCode: 1 };
203
+ }
204
+ }
205
+ case "get": {
206
+ try {
207
+ const ptr = getActiveManifest();
208
+ if (!ptr) {
209
+ console.log("(no active manifest)");
210
+ return { exitCode: 0 };
211
+ }
212
+ console.log(`${ptr.manifest_path}\n set_by: ${ptr.set_by}\n set_at: ${ptr.set_at}`);
213
+ return { exitCode: 0 };
214
+ }
215
+ catch (e) {
216
+ console.error(`active-manifest get failed: ${e.message}`);
217
+ return { exitCode: 2 };
218
+ }
219
+ }
220
+ case "clear": {
221
+ const cleared = clearActiveManifest();
222
+ console.log(cleared ? "Active manifest cleared." : "(no active manifest to clear)");
223
+ return { exitCode: 0 };
224
+ }
225
+ default:
226
+ console.error("Usage: aideas-forge active-manifest <set <path> [set_by] | get | clear>");
227
+ return { exitCode: 1 };
228
+ }
229
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ import { argv, exit } from "node:process";
3
+ import { init } from "./init.js";
4
+ import { update } from "./update.js";
5
+ import { uninstall } from "./uninstall.js";
6
+ import { verifyManifestCli } from "./verify-manifest.js";
7
+ import { acceptDream, listDreams, rejectDream } from "./wiki.js";
8
+ import { wikiUi } from "./wiki-ui.js";
9
+ const USAGE = `
10
+ forge — AI Development Life Cycle
11
+
12
+ Usage:
13
+ forge init Install forge into the current project
14
+ forge update Sync forge assets to the latest version
15
+ forge uninstall Remove forge from the current project
16
+ forge verify-manifest [path] Verify a v6 work-manifest (default: ./manifest.yaml)
17
+ forge wiki status List pending wiki dreams awaiting review
18
+ forge wiki accept <dream_id> Accept a dream (atomic swap into aiwiki/)
19
+ forge wiki reject <dream_id> --reason "..." Reject a dream with reason
20
+ forge wiki ui [--port N] [--no-open] Launch local web UI for dream review
21
+ forge --help Show this message
22
+
23
+ Options (init/update/uninstall):
24
+ --force Overwrite all files (init/update) or include CLAUDE.md/AGENTS.md (uninstall)
25
+ --dry-run Preview changes without applying them
26
+ --purge Remove .forge/ state directory (uninstall only)
27
+
28
+ Options (verify-manifest):
29
+ --json Machine-readable output (for hook consumption)
30
+
31
+ Options (wiki ui):
32
+ --port N Listen on port N (default: 8765; auto-finds free port if busy)
33
+ --no-open Do not auto-open browser
34
+ `.trim();
35
+ const args = argv.slice(2);
36
+ const command = args[0];
37
+ const flags = new Set(args.slice(1));
38
+ switch (command) {
39
+ case "init":
40
+ await init({
41
+ force: flags.has("--force"),
42
+ dryRun: flags.has("--dry-run"),
43
+ });
44
+ break;
45
+ case "update":
46
+ await update({
47
+ force: flags.has("--force"),
48
+ dryRun: flags.has("--dry-run"),
49
+ });
50
+ break;
51
+ case "uninstall":
52
+ await uninstall({
53
+ force: flags.has("--force"),
54
+ purge: flags.has("--purge"),
55
+ dryRun: flags.has("--dry-run"),
56
+ });
57
+ break;
58
+ case "verify-manifest": {
59
+ // First positional arg after `verify-manifest` is the path; defaults to ./manifest.yaml.
60
+ const positional = args.slice(1).find((a) => !a.startsWith("--"));
61
+ const code = await verifyManifestCli({
62
+ path: positional ?? "./manifest.yaml",
63
+ json: flags.has("--json"),
64
+ });
65
+ exit(code);
66
+ break;
67
+ }
68
+ case "wiki": {
69
+ const sub = args[1];
70
+ const root = process.cwd();
71
+ if (sub === "status") {
72
+ const dreams = await listDreams(root);
73
+ if (dreams.length === 0) {
74
+ console.log("No pending dreams.");
75
+ }
76
+ else {
77
+ console.log(`Pending dreams: ${dreams.length}\n`);
78
+ for (const d of dreams) {
79
+ const age = d.age_human;
80
+ const detail = d.trigger_detail ? ` (${d.trigger_detail})` : "";
81
+ console.log(` ${d.dream_id}`);
82
+ console.log(` trigger: ${d.trigger}${detail}`);
83
+ console.log(` created: ${d.created_at} (${age})`);
84
+ console.log(` changes: +${d.new_pages} new, ~${d.changed_pages} changed, -${d.deleted_pages} deleted`);
85
+ console.log(` lint: ${d.lint_status}`);
86
+ console.log("");
87
+ }
88
+ console.log("Review with: forge wiki ui");
89
+ console.log("Accept with: forge wiki accept <dream_id>");
90
+ console.log("Reject with: forge wiki reject <dream_id> --reason \"...\"");
91
+ }
92
+ break;
93
+ }
94
+ if (sub === "accept") {
95
+ const dreamId = args[2];
96
+ if (!dreamId) {
97
+ console.error("Usage: forge wiki accept <dream_id>");
98
+ exit(1);
99
+ }
100
+ try {
101
+ const result = await acceptDream(dreamId, root);
102
+ console.log(`Accepted ${dreamId}`);
103
+ console.log(` ${result.accepted_files.length} files swapped into aiwiki/`);
104
+ console.log(` Originals archived to ${result.archived_to}`);
105
+ }
106
+ catch (err) {
107
+ console.error(`Accept failed: ${err.message}`);
108
+ exit(1);
109
+ }
110
+ break;
111
+ }
112
+ if (sub === "reject") {
113
+ const dreamId = args[2];
114
+ if (!dreamId) {
115
+ console.error("Usage: forge wiki reject <dream_id> --reason \"...\"");
116
+ exit(1);
117
+ }
118
+ const reasonIdx = args.indexOf("--reason");
119
+ const reason = reasonIdx !== -1 ? args[reasonIdx + 1] : "";
120
+ if (!reason) {
121
+ console.error("--reason is required");
122
+ exit(1);
123
+ }
124
+ try {
125
+ await rejectDream(dreamId, reason, root);
126
+ console.log(`Rejected ${dreamId}`);
127
+ console.log(` reason: ${reason}`);
128
+ }
129
+ catch (err) {
130
+ console.error(`Reject failed: ${err.message}`);
131
+ exit(1);
132
+ }
133
+ break;
134
+ }
135
+ if (sub === "ui") {
136
+ const portIdx = args.indexOf("--port");
137
+ const port = portIdx !== -1 ? Number(args[portIdx + 1]) : undefined;
138
+ const code = await wikiUi({
139
+ port: Number.isFinite(port) ? port : undefined,
140
+ noOpen: flags.has("--no-open"),
141
+ });
142
+ exit(code);
143
+ }
144
+ console.error(`Unknown wiki subcommand: ${sub ?? "(none)"}`);
145
+ console.error("Run: forge --help");
146
+ exit(1);
147
+ break;
148
+ }
149
+ case "--help":
150
+ case "-h":
151
+ case undefined:
152
+ console.log(USAGE);
153
+ break;
154
+ default:
155
+ console.error(`Unknown command: ${command}\n`);
156
+ console.log(USAGE);
157
+ exit(1);
158
+ }