@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.
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/agents/architect.md +92 -0
- package/agents/builder.md +122 -0
- package/agents/code-reviewer.md +107 -0
- package/agents/concept-designer.md +207 -0
- package/agents/craft-reviewer.md +132 -0
- package/agents/critic.md +130 -0
- package/agents/doc-writer.md +85 -0
- package/agents/dreamer.md +129 -0
- package/agents/e2e-runner.md +89 -0
- package/agents/gotcha-hunter.md +127 -0
- package/agents/prototype-builder.md +193 -0
- package/agents/prototype-codifier.md +204 -0
- package/agents/prototype-reviewer.md +163 -0
- package/agents/security-reviewer.md +108 -0
- package/agents/spec-reviewer.md +94 -0
- package/agents/tracer.md +98 -0
- package/agents/wireframer.md +109 -0
- package/commands/abort.md +25 -0
- package/commands/bugfix.md +151 -0
- package/commands/evolve.md +118 -0
- package/commands/feature.md +236 -0
- package/commands/forge.md +100 -0
- package/commands/greenfield.md +185 -0
- package/commands/hotfix.md +98 -0
- package/commands/refactor.md +147 -0
- package/commands/resume.md +25 -0
- package/commands/setup.md +201 -0
- package/commands/status.md +27 -0
- package/commands/task-force.md +110 -0
- package/commands/validate.md +12 -0
- package/dist/__tests__/active-manifest.test.js +272 -0
- package/dist/__tests__/copy.test.js +96 -0
- package/dist/__tests__/gate-check.test.js +384 -0
- package/dist/__tests__/wiki.test.js +472 -0
- package/dist/__tests__/work-manifest.test.js +304 -0
- package/dist/active-manifest.js +229 -0
- package/dist/cli.js +158 -0
- package/dist/copy.js +124 -0
- package/dist/gate-check.js +326 -0
- package/dist/hooks.js +60 -0
- package/dist/init.js +140 -0
- package/dist/manifest.js +90 -0
- package/dist/merge.js +77 -0
- package/dist/paths.js +36 -0
- package/dist/uninstall.js +216 -0
- package/dist/update.js +158 -0
- package/dist/verify-manifest.js +65 -0
- package/dist/verify.js +98 -0
- package/dist/wiki-ui.js +310 -0
- package/dist/wiki.js +364 -0
- package/dist/work-manifest.js +798 -0
- package/hooks/config/gate-requirements.json +79 -0
- package/hooks/hooks.json +143 -0
- package/hooks/scripts/analyze-telemetry.sh +114 -0
- package/hooks/scripts/gate-enforcer.sh +164 -0
- package/hooks/scripts/pre-compact.sh +90 -0
- package/hooks/scripts/session-start.sh +81 -0
- package/hooks/scripts/telemetry.sh +41 -0
- package/hooks/scripts/wiki-lint.sh +87 -0
- package/hooks/templates/AGENTS.md.template +48 -0
- package/hooks/templates/CLAUDE.md.template +45 -0
- package/package.json +55 -0
- package/protocols/README.md +40 -0
- package/protocols/codex.md +151 -0
- package/protocols/graphify.md +156 -0
- package/references/common/agent-coordination.md +65 -0
- package/references/common/coding-standards.md +54 -0
- package/references/common/feature-tracking.md +21 -0
- package/references/common/io-protocol.md +36 -0
- package/references/common/phases.md +57 -0
- package/references/common/quality-gates.md +130 -0
- package/references/common/skill-authoring.md +154 -0
- package/references/common/skill-compliance.md +30 -0
- package/references/python/standards.md +44 -0
- package/references/react/standards.md +61 -0
- package/references/typescript/standards.md +42 -0
- package/rules/common/forge-system.md +59 -0
- package/rules/common/git-workflow.md +40 -0
- package/rules/common/guardrails.md +37 -0
- package/rules/common/quality-gates.md +18 -0
- package/rules/common/security.md +50 -0
- package/rules/common/skill-selection.md +78 -0
- package/rules/common/testing.md +58 -0
- package/rules/common/verification.md +39 -0
- package/skills/build-pr-workflow/SKILL.md +301 -0
- package/skills/build-pr-workflow/references/pr-template.md +62 -0
- package/skills/build-pr-workflow/references/subagent-merge.md +47 -0
- package/skills/build-pr-workflow/references/worktree-setup.md +125 -0
- package/skills/build-prototype/SKILL.md +264 -0
- package/skills/build-scaffold/SKILL.md +340 -0
- package/skills/build-tdd/SKILL.md +89 -0
- package/skills/build-wireframe/SKILL.md +110 -0
- package/skills/build-wireframe/assets/baseline-template.html +486 -0
- package/skills/build-wireframe/references/demo-walkthroughs.md +170 -0
- package/skills/build-wireframe/references/gotchas.md +188 -0
- package/skills/build-wireframe/references/legend-lines.md +141 -0
- package/skills/concept-slides/SKILL.md +192 -0
- package/skills/deliver-db-migration/SKILL.md +466 -0
- package/skills/deliver-deploy/SKILL.md +407 -0
- package/skills/deliver-onboarding/SKILL.md +198 -0
- package/skills/deliver-onboarding/references/document-templates.md +393 -0
- package/skills/deliver-onboarding/templates/getting-started.md +122 -0
- package/skills/discover-codebase-analysis/SKILL.md +448 -0
- package/skills/discover-requirements/SKILL.md +418 -0
- package/skills/discover-requirements/templates/prd.md +99 -0
- package/skills/discover-requirements/templates/technical-spec.md +123 -0
- package/skills/discover-requirements/templates/user-stories.md +76 -0
- package/skills/harden/SKILL.md +214 -0
- package/skills/iterate-prototype/SKILL.md +241 -0
- package/skills/plan-architecture/SKILL.md +457 -0
- package/skills/plan-architecture/templates/adr-template.md +52 -0
- package/skills/plan-architecture/templates/api-contract.md +99 -0
- package/skills/plan-architecture/templates/db-schema.md +81 -0
- package/skills/plan-architecture/templates/system-design.md +111 -0
- package/skills/plan-brainstorm/SKILL.md +433 -0
- package/skills/plan-design-system/SKILL.md +279 -0
- package/skills/plan-task-decompose/SKILL.md +454 -0
- package/skills/quality-code-review/SKILL.md +286 -0
- package/skills/quality-security-audit/SKILL.md +292 -0
- package/skills/quality-security-audit/references/audit-report-template.md +89 -0
- package/skills/quality-security-audit/references/owasp-checks.md +178 -0
- package/skills/quality-test-execution/SKILL.md +435 -0
- package/skills/quality-test-plan/SKILL.md +297 -0
- package/skills/quality-test-plan/references/test-type-guide.md +263 -0
- package/skills/quality-test-plan/templates/e2e-test-plan.md +72 -0
- package/skills/quality-test-plan/templates/integration-test-plan.md +74 -0
- package/skills/quality-test-plan/templates/load-test-plan.md +111 -0
- package/skills/quality-test-plan/templates/smoke-test-plan.md +68 -0
- package/skills/quality-test-plan/templates/unit-test-plan.md +56 -0
- package/skills/quality-uiux/SKILL.md +481 -0
- package/skills/support-debug/SKILL.md +464 -0
- package/skills/support-dream/SKILL.md +213 -0
- package/skills/support-gotcha/SKILL.md +249 -0
- package/skills/support-runtime-reachability/SKILL.md +190 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/app.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/handlers/cases.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/app.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/handlers/cases.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/App.tsx +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/components/RingingBanner.tsx +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/hooks/useTwilio.ts +6 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/App.tsx +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/components/MyComp.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/App.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/components/Orphan.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/lib/Service.ts +6 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/main.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/lib/Lonely.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/main.ts +2 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/handler.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/handler.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/main.ts +2 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/lib.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/index.ts +1 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/internal.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.test.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-13-gated-pending-annotation/src/future.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-14-untraceable-annotation/src/decorated.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-15-untraceable-empty/src/lazy.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/lib.py +15 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/main.py +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/parent.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/routes/cases.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/lib/foo.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/other.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/cases.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/users.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/main.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/handlers/cases.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/main.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/lib.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/main.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/lib.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/main.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/lib.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/main.ts +7 -0
- package/skills/support-runtime-reachability/scripts/check.mjs +638 -0
- package/skills/support-runtime-reachability/scripts/check.test.mjs +244 -0
- package/skills/support-skill-validator/SKILL.md +194 -0
- package/skills/support-skill-validator/references/false-positives.md +59 -0
- package/skills/support-skill-validator/references/validation-checks.md +280 -0
- package/skills/support-system-guide/SKILL.md +311 -0
- package/skills/support-task-force/SKILL.md +265 -0
- package/skills/support-task-force/references/dispatch-pattern.md +178 -0
- package/skills/support-task-force/references/synthesis-template.md +126 -0
- package/skills/support-wiki-bootstrap/SKILL.md +37 -0
- package/skills/support-wiki-lint/SKILL.md +196 -0
- package/skills/support-wiki-lint/scripts/lint.mjs +488 -0
- package/skills/support-wiki-lint/scripts/lint.test.mjs +196 -0
- package/templates/README.md +23 -0
- package/templates/aiwiki/CLAUDE.md.template +78 -0
- package/templates/aiwiki/schemas/architecture.md +118 -0
- package/templates/aiwiki/schemas/convention.md +112 -0
- package/templates/aiwiki/schemas/decision.md +144 -0
- package/templates/aiwiki/schemas/gotcha.md +118 -0
- package/templates/aiwiki/schemas/oracle.md +105 -0
- package/templates/aiwiki/schemas/session.md +125 -0
- package/templates/manifests/bugfix.yaml +41 -0
- package/templates/manifests/feature.yaml +69 -0
- package/templates/manifests/greenfield.yaml +61 -0
- package/templates/manifests/hotfix.yaml +45 -0
- package/templates/manifests/refactor.yaml +44 -0
- package/templates/manifests/v5/SCHEMA.md +327 -0
- package/templates/manifests/v5/feature.yaml +77 -0
- package/templates/manifests/v6/SCHEMA.md +199 -0
- package/templates/wiki-html/dream-detail.html +378 -0
- 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
|
+
}
|