@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
package/dist/copy.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
function fileChanged(srcPath, destPath) {
|
|
4
|
+
if (!existsSync(destPath))
|
|
5
|
+
return true;
|
|
6
|
+
return readFileSync(srcPath, "utf-8") !== readFileSync(destPath, "utf-8");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Copy a directory of .md files (flat) to a target directory.
|
|
10
|
+
*/
|
|
11
|
+
export function copyFlatDir(src, dest, opts) {
|
|
12
|
+
const result = { changed: [], unchanged: [], added: [], skipped: [] };
|
|
13
|
+
if (!existsSync(src))
|
|
14
|
+
return result;
|
|
15
|
+
mkdirSync(dest, { recursive: true });
|
|
16
|
+
for (const entry of readdirSync(src)) {
|
|
17
|
+
if (!entry.endsWith(".md"))
|
|
18
|
+
continue;
|
|
19
|
+
const srcPath = join(src, entry);
|
|
20
|
+
const destPath = join(dest, entry);
|
|
21
|
+
if (existsSync(destPath) && !opts.force) {
|
|
22
|
+
result.skipped.push(entry);
|
|
23
|
+
}
|
|
24
|
+
else if (!existsSync(destPath)) {
|
|
25
|
+
cpSync(srcPath, destPath);
|
|
26
|
+
result.added.push(entry);
|
|
27
|
+
}
|
|
28
|
+
else if (fileChanged(srcPath, destPath)) {
|
|
29
|
+
cpSync(srcPath, destPath);
|
|
30
|
+
result.changed.push(entry);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
result.unchanged.push(entry);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Copy a directory tree (skills have subdirectories with templates/).
|
|
40
|
+
*/
|
|
41
|
+
export function copyTreeDir(src, dest, opts) {
|
|
42
|
+
const result = { changed: [], unchanged: [], added: [], skipped: [] };
|
|
43
|
+
if (!existsSync(src))
|
|
44
|
+
return result;
|
|
45
|
+
mkdirSync(dest, { recursive: true });
|
|
46
|
+
for (const entry of readdirSync(src)) {
|
|
47
|
+
const srcPath = join(src, entry);
|
|
48
|
+
const destPath = join(dest, entry);
|
|
49
|
+
if (existsSync(destPath) && !opts.force) {
|
|
50
|
+
result.skipped.push(entry);
|
|
51
|
+
}
|
|
52
|
+
else if (!existsSync(destPath)) {
|
|
53
|
+
cpSync(srcPath, destPath, { recursive: true });
|
|
54
|
+
result.added.push(entry);
|
|
55
|
+
}
|
|
56
|
+
else if (statSync(srcPath).isDirectory()
|
|
57
|
+
? treeChanged(srcPath, destPath)
|
|
58
|
+
: fileChanged(srcPath, destPath)) {
|
|
59
|
+
cpSync(srcPath, destPath, { recursive: true });
|
|
60
|
+
result.changed.push(entry);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
result.unchanged.push(entry);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if any file in a directory tree differs.
|
|
70
|
+
* Only walks the source tree — does not detect extra files in destination.
|
|
71
|
+
* Stale destination-only files are a /validate concern, not an install concern.
|
|
72
|
+
*/
|
|
73
|
+
function treeChanged(srcDir, destDir) {
|
|
74
|
+
for (const entry of readdirSync(srcDir)) {
|
|
75
|
+
const srcPath = join(srcDir, entry);
|
|
76
|
+
const destPath = join(destDir, entry);
|
|
77
|
+
if (statSync(srcPath).isDirectory()) {
|
|
78
|
+
if (treeChanged(srcPath, destPath))
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
else if (fileChanged(srcPath, destPath)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Copy rules directory, preserving the common/ and language-specific/ structure.
|
|
89
|
+
*/
|
|
90
|
+
export function copyRules(src, dest, opts) {
|
|
91
|
+
const result = { changed: [], unchanged: [], added: [], skipped: [] };
|
|
92
|
+
if (!existsSync(src))
|
|
93
|
+
return result;
|
|
94
|
+
mkdirSync(dest, { recursive: true });
|
|
95
|
+
for (const subdir of readdirSync(src)) {
|
|
96
|
+
const srcSub = join(src, subdir);
|
|
97
|
+
if (!statSync(srcSub).isDirectory())
|
|
98
|
+
continue;
|
|
99
|
+
const destSub = join(dest, subdir);
|
|
100
|
+
mkdirSync(destSub, { recursive: true });
|
|
101
|
+
for (const file of readdirSync(srcSub)) {
|
|
102
|
+
if (!file.endsWith(".md"))
|
|
103
|
+
continue;
|
|
104
|
+
const srcFile = join(srcSub, file);
|
|
105
|
+
const destFile = join(destSub, file);
|
|
106
|
+
const label = `${subdir}/${file}`;
|
|
107
|
+
if (existsSync(destFile) && !opts.force) {
|
|
108
|
+
result.skipped.push(label);
|
|
109
|
+
}
|
|
110
|
+
else if (!existsSync(destFile)) {
|
|
111
|
+
cpSync(srcFile, destFile);
|
|
112
|
+
result.added.push(label);
|
|
113
|
+
}
|
|
114
|
+
else if (fileChanged(srcFile, destFile)) {
|
|
115
|
+
cpSync(srcFile, destFile);
|
|
116
|
+
result.changed.push(label);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
result.unchanged.push(label);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate-check CLI — diffs an in-flight manifest edit and reports gate
|
|
3
|
+
* transitions for downstream enforcement (gate-enforcer.sh).
|
|
4
|
+
*
|
|
5
|
+
* Replaces hand-rolled YAML grepping in bash. Reads a Claude Code hook
|
|
6
|
+
* payload from stdin, applies the proposed Edit/Write to the on-disk
|
|
7
|
+
* manifest, and emits the list of gates whose `gate-passed` field is
|
|
8
|
+
* transitioning. Each transition includes its scope (slice vs manifest)
|
|
9
|
+
* and the containing slice ID where applicable.
|
|
10
|
+
*
|
|
11
|
+
* Per Codex Phase 2 design review: the bash hook MUST NOT hand-roll
|
|
12
|
+
* YAML path tracking. This is the helper.
|
|
13
|
+
*
|
|
14
|
+
* Spec:
|
|
15
|
+
* - Input (stdin, JSON): { tool_name: "Edit"|"Write", tool_input: {...} }
|
|
16
|
+
* - Edit input: { file_path, old_string, new_string }
|
|
17
|
+
* - Write input: { file_path, content }
|
|
18
|
+
* - Ambiguous Edit (old_string matches 0 or >1 times): fail-closed exit 1.
|
|
19
|
+
* - YAML parse error in old or new: emitted as error; exit 2.
|
|
20
|
+
*
|
|
21
|
+
* Output (JSON when --json):
|
|
22
|
+
* { ok: true, transitions: [{gate, scope, slice_id?, transition, path}, ...] }
|
|
23
|
+
* { ok: false, error: "..." }
|
|
24
|
+
*
|
|
25
|
+
* Exit codes:
|
|
26
|
+
* 0 — transitions extracted (list may be empty)
|
|
27
|
+
* 1 — bad payload, ambiguous edit, or unable to compute new content
|
|
28
|
+
* 2 — internal/file/YAML error
|
|
29
|
+
*/
|
|
30
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
31
|
+
import { load as yamlLoad } from "js-yaml";
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Public API
|
|
34
|
+
// ============================================================================
|
|
35
|
+
export function gateCheck(payload) {
|
|
36
|
+
// Defend against malformed payloads: stdin can deliver `null`, an array,
|
|
37
|
+
// or a primitive (`JSON.parse("42") === 42`). Reject before dereferencing.
|
|
38
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: "payload must be a JSON object (got null, array, or primitive)",
|
|
42
|
+
exitCode: 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const tool = payload.tool_name;
|
|
46
|
+
const input = payload.tool_input ?? {};
|
|
47
|
+
if (tool !== "Edit" && tool !== "Write") {
|
|
48
|
+
return {
|
|
49
|
+
ok: true,
|
|
50
|
+
transitions: [],
|
|
51
|
+
}; // not a manifest edit; no-op
|
|
52
|
+
}
|
|
53
|
+
const filePath = input.file_path;
|
|
54
|
+
if (!filePath) {
|
|
55
|
+
return { ok: false, error: "tool_input.file_path is required", exitCode: 1 };
|
|
56
|
+
}
|
|
57
|
+
// Read current content (may not exist yet for first Write).
|
|
58
|
+
let oldContent = "";
|
|
59
|
+
if (existsSync(filePath)) {
|
|
60
|
+
try {
|
|
61
|
+
oldContent = readFileSync(filePath, "utf-8");
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: `cannot read current file ${filePath}: ${e.message}`,
|
|
67
|
+
exitCode: 2,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Compute proposed new content.
|
|
72
|
+
let newContent;
|
|
73
|
+
if (tool === "Write") {
|
|
74
|
+
if (typeof input.content !== "string") {
|
|
75
|
+
return { ok: false, error: "Write requires tool_input.content (string)", exitCode: 1 };
|
|
76
|
+
}
|
|
77
|
+
newContent = input.content;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Edit
|
|
81
|
+
if (typeof input.old_string !== "string" || typeof input.new_string !== "string") {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: "Edit requires tool_input.old_string and tool_input.new_string (strings)",
|
|
85
|
+
exitCode: 1,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (input.old_string === "") {
|
|
89
|
+
return { ok: false, error: "Edit old_string must not be empty", exitCode: 1 };
|
|
90
|
+
}
|
|
91
|
+
const occurrences = countOccurrences(oldContent, input.old_string);
|
|
92
|
+
if (occurrences === 0) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: `Edit old_string not found in ${filePath}; cannot compute proposed content`,
|
|
96
|
+
exitCode: 1,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (occurrences > 1) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: `Edit old_string is ambiguous (${occurrences} occurrences in ${filePath}); broaden the match before retrying`,
|
|
103
|
+
exitCode: 1,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
newContent = oldContent.replace(input.old_string, input.new_string);
|
|
107
|
+
}
|
|
108
|
+
// Parse both as YAML. Use raw js-yaml; gate-check only needs the
|
|
109
|
+
// structural shape, not v5 invariants (those are verify-manifest's job).
|
|
110
|
+
let oldParsed;
|
|
111
|
+
let newParsed;
|
|
112
|
+
try {
|
|
113
|
+
oldParsed = oldContent ? yamlLoad(oldContent) : {};
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: `current file ${filePath} is not valid YAML: ${e.message}`,
|
|
119
|
+
exitCode: 2,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
newParsed = yamlLoad(newContent);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: `proposed content is not valid YAML: ${e.message}`,
|
|
129
|
+
exitCode: 2,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (newParsed === null || newParsed === undefined) {
|
|
133
|
+
return { ok: false, error: "proposed content is empty", exitCode: 1 };
|
|
134
|
+
}
|
|
135
|
+
if (!isPlainObject(newParsed)) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
error: "proposed content is not a YAML mapping (got array, scalar, or non-plain object like Date)",
|
|
139
|
+
exitCode: 1,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Walk both manifests for gates; diff.
|
|
143
|
+
let oldGates;
|
|
144
|
+
let newGates;
|
|
145
|
+
try {
|
|
146
|
+
oldGates = collectGates(oldParsed);
|
|
147
|
+
newGates = collectGates(newParsed);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
// collectGates throws on non-boolean gate-passed (per Codex round-2 #1):
|
|
151
|
+
// strings/numbers/arrays must fail closed, not silently coerce to truthy.
|
|
152
|
+
return { ok: false, error: e.message, exitCode: 1 };
|
|
153
|
+
}
|
|
154
|
+
const transitions = [];
|
|
155
|
+
// Iterate paths in sorted order so output is deterministic across runs.
|
|
156
|
+
const allPaths = [...new Set([...oldGates.keys(), ...newGates.keys()])].sort();
|
|
157
|
+
for (const yamlPath of allPaths) {
|
|
158
|
+
const oldVal = oldGates.get(yamlPath);
|
|
159
|
+
const newVal = newGates.get(yamlPath);
|
|
160
|
+
const oldPassed = oldVal?.gatePassed ?? false;
|
|
161
|
+
const newPassed = newVal?.gatePassed ?? false;
|
|
162
|
+
if (oldPassed === newPassed)
|
|
163
|
+
continue;
|
|
164
|
+
const ref = newVal ?? oldVal;
|
|
165
|
+
// Scope is position-derived (structural truth, per Codex round-2 #4).
|
|
166
|
+
// The bash hook reads gate-requirements.json for skill/agent lookups;
|
|
167
|
+
// gate-check does not — that avoided a precedence inversion where a
|
|
168
|
+
// mis-configured `scope: manifest` on a slice gate could emit a
|
|
169
|
+
// contradictory `{scope: "manifest", slice_id: "..."}`.
|
|
170
|
+
const scope = ref.sliceId ? "slice" : "manifest";
|
|
171
|
+
transitions.push({
|
|
172
|
+
gate: ref.gate,
|
|
173
|
+
scope,
|
|
174
|
+
...(ref.sliceId ? { slice_id: ref.sliceId } : {}),
|
|
175
|
+
transition: oldPassed ? "true-to-false" : "false-to-true",
|
|
176
|
+
path: yamlPath,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, transitions };
|
|
180
|
+
}
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// Helpers
|
|
183
|
+
// ============================================================================
|
|
184
|
+
function countOccurrences(haystack, needle) {
|
|
185
|
+
if (needle === "")
|
|
186
|
+
return 0;
|
|
187
|
+
let count = 0;
|
|
188
|
+
let idx = 0;
|
|
189
|
+
while (true) {
|
|
190
|
+
const found = haystack.indexOf(needle, idx);
|
|
191
|
+
if (found === -1)
|
|
192
|
+
return count;
|
|
193
|
+
count++;
|
|
194
|
+
idx = found + needle.length;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Walk a parsed manifest tree and collect every gate location. Returns a
|
|
198
|
+
* map keyed by full YAML path → { gate, sliceId, gatePassed }. */
|
|
199
|
+
function collectGates(manifest) {
|
|
200
|
+
const out = new Map();
|
|
201
|
+
if (!manifest || typeof manifest !== "object")
|
|
202
|
+
return out;
|
|
203
|
+
const m = manifest;
|
|
204
|
+
// Phase gates: phases.<phase>.<gate-name> = { status, gate-passed }
|
|
205
|
+
const phases = m.phases;
|
|
206
|
+
if (phases && typeof phases === "object" && !Array.isArray(phases)) {
|
|
207
|
+
for (const [phaseName, phaseBlock] of Object.entries(phases)) {
|
|
208
|
+
if (!phaseBlock || typeof phaseBlock !== "object" || Array.isArray(phaseBlock))
|
|
209
|
+
continue;
|
|
210
|
+
for (const [gateName, gateVal] of Object.entries(phaseBlock)) {
|
|
211
|
+
if (isGateObject(gateVal)) {
|
|
212
|
+
const ymlPath = `phases.${phaseName}.${gateName}`;
|
|
213
|
+
out.set(ymlPath, {
|
|
214
|
+
gate: gateName,
|
|
215
|
+
gatePassed: extractGatePassed(gateVal, ymlPath),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Slice gates: slice_graph.slices.<id>.gates.<gate-name> = { status, gate-passed }
|
|
222
|
+
const sg = m.slice_graph;
|
|
223
|
+
if (sg && typeof sg === "object" && !Array.isArray(sg)) {
|
|
224
|
+
const slices = sg.slices;
|
|
225
|
+
if (slices && typeof slices === "object" && !Array.isArray(slices)) {
|
|
226
|
+
for (const [sliceId, slice] of Object.entries(slices)) {
|
|
227
|
+
if (!slice || typeof slice !== "object" || Array.isArray(slice))
|
|
228
|
+
continue;
|
|
229
|
+
const gates = slice.gates;
|
|
230
|
+
if (!gates || typeof gates !== "object" || Array.isArray(gates))
|
|
231
|
+
continue;
|
|
232
|
+
for (const [gateName, gateVal] of Object.entries(gates)) {
|
|
233
|
+
if (isGateObject(gateVal)) {
|
|
234
|
+
const ymlPath = `slice_graph.slices.${sliceId}.gates.${gateName}`;
|
|
235
|
+
out.set(ymlPath, {
|
|
236
|
+
gate: gateName,
|
|
237
|
+
sliceId,
|
|
238
|
+
gatePassed: extractGatePassed(gateVal, ymlPath),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/** Strict boolean extraction — fail closed on non-boolean gate-passed.
|
|
248
|
+
* Per Codex round-2 #1: `Boolean("false")` is truthy, which would suppress
|
|
249
|
+
* expected false→true transitions. Strings, numbers, arrays must throw. */
|
|
250
|
+
function extractGatePassed(gateVal, yamlPath) {
|
|
251
|
+
const raw = gateVal["gate-passed"];
|
|
252
|
+
if (raw === true)
|
|
253
|
+
return true;
|
|
254
|
+
if (raw === false)
|
|
255
|
+
return false;
|
|
256
|
+
throw new Error(`gate-passed at ${yamlPath} must be a boolean (got ${typeof raw}: ${JSON.stringify(raw)})`);
|
|
257
|
+
}
|
|
258
|
+
/** A plain object (not array, not Date, not RegExp, not null). js-yaml will
|
|
259
|
+
* happily return a Date for a bare YAML timestamp at the document root —
|
|
260
|
+
* that passes `typeof === "object"` and would slip through naive checks. */
|
|
261
|
+
function isPlainObject(v) {
|
|
262
|
+
return Object.prototype.toString.call(v) === "[object Object]";
|
|
263
|
+
}
|
|
264
|
+
function isGateObject(v) {
|
|
265
|
+
return Boolean(v &&
|
|
266
|
+
typeof v === "object" &&
|
|
267
|
+
!Array.isArray(v) &&
|
|
268
|
+
"gate-passed" in v);
|
|
269
|
+
}
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// CLI entry point — `aideas-forge gate-check [--json]`
|
|
272
|
+
// ============================================================================
|
|
273
|
+
export async function gateCheckCli(args) {
|
|
274
|
+
const json = args.includes("--json");
|
|
275
|
+
// Read JSON payload from stdin.
|
|
276
|
+
let stdin;
|
|
277
|
+
try {
|
|
278
|
+
stdin = await readStdin();
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
return emitError(json, `failed to read stdin: ${e.message}`, 2);
|
|
282
|
+
}
|
|
283
|
+
let payload;
|
|
284
|
+
try {
|
|
285
|
+
payload = JSON.parse(stdin);
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
return emitError(json, `stdin is not valid JSON: ${e.message}`, 1);
|
|
289
|
+
}
|
|
290
|
+
const result = gateCheck(payload);
|
|
291
|
+
if (!result.ok) {
|
|
292
|
+
return emitError(json, result.error, result.exitCode);
|
|
293
|
+
}
|
|
294
|
+
if (json) {
|
|
295
|
+
process.stdout.write(JSON.stringify({ ok: true, transitions: result.transitions }) + "\n");
|
|
296
|
+
}
|
|
297
|
+
else if (result.transitions.length === 0) {
|
|
298
|
+
console.log("No gate transitions detected.");
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
console.log(`${result.transitions.length} gate transition(s):`);
|
|
302
|
+
for (const t of result.transitions) {
|
|
303
|
+
const where = t.slice_id ? ` (slice: ${t.slice_id})` : "";
|
|
304
|
+
console.log(` ${t.transition.padEnd(13)} ${t.gate}${where} [${t.scope}] @ ${t.path}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
function emitError(json, message, exitCode) {
|
|
310
|
+
if (json) {
|
|
311
|
+
process.stdout.write(JSON.stringify({ ok: false, error: message }) + "\n");
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
console.error(message);
|
|
315
|
+
}
|
|
316
|
+
return exitCode;
|
|
317
|
+
}
|
|
318
|
+
function readStdin() {
|
|
319
|
+
return new Promise((res, rej) => {
|
|
320
|
+
let data = "";
|
|
321
|
+
process.stdin.setEncoding("utf-8");
|
|
322
|
+
process.stdin.on("data", (chunk) => (data += chunk));
|
|
323
|
+
process.stdin.on("end", () => res(data));
|
|
324
|
+
process.stdin.on("error", rej);
|
|
325
|
+
});
|
|
326
|
+
}
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { SOURCE } from "./paths.js";
|
|
4
|
+
/**
|
|
5
|
+
* Install forge hooks into the project's .claude/settings.local.json.
|
|
6
|
+
* Copies hook scripts to .claude/hooks/ and merges hook definitions
|
|
7
|
+
* into existing settings without clobbering user hooks.
|
|
8
|
+
*/
|
|
9
|
+
export function installHooks(projectRoot, opts) {
|
|
10
|
+
const claudeDir = resolve(projectRoot, ".claude");
|
|
11
|
+
const settingsPath = join(claudeDir, "settings.local.json");
|
|
12
|
+
const hooksDir = join(claudeDir, "hooks");
|
|
13
|
+
const scriptsDir = join(hooksDir, "scripts");
|
|
14
|
+
const configDir = join(hooksDir, "config");
|
|
15
|
+
// Copy hook scripts
|
|
16
|
+
const srcScripts = join(SOURCE.hooks, "scripts");
|
|
17
|
+
const srcConfig = join(SOURCE.hooks, "config");
|
|
18
|
+
if (existsSync(srcScripts)) {
|
|
19
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
20
|
+
cpSync(srcScripts, scriptsDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
if (existsSync(srcConfig)) {
|
|
23
|
+
mkdirSync(configDir, { recursive: true });
|
|
24
|
+
cpSync(srcConfig, configDir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
// Read forge hook definitions
|
|
27
|
+
const srcHooksJson = join(SOURCE.hooks, "hooks.json");
|
|
28
|
+
if (!existsSync(srcHooksJson)) {
|
|
29
|
+
return { installed: false, message: "No hooks.json found in package" };
|
|
30
|
+
}
|
|
31
|
+
const forgeHooks = JSON.parse(readFileSync(srcHooksJson, "utf-8")).hooks;
|
|
32
|
+
// Read existing settings
|
|
33
|
+
let settings = {};
|
|
34
|
+
if (existsSync(settingsPath)) {
|
|
35
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
36
|
+
}
|
|
37
|
+
// Merge forge hooks with existing hooks
|
|
38
|
+
const existingHooks = (settings.hooks ?? {});
|
|
39
|
+
settings.hooks = mergeHooks(existingHooks, forgeHooks);
|
|
40
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
41
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
42
|
+
return { installed: true, message: "Hooks merged" };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Merge forge hooks into existing hooks by event type.
|
|
46
|
+
* For each event forge defines, remove any existing entry that exactly
|
|
47
|
+
* matches a forge entry (old or new), then append the new forge entries.
|
|
48
|
+
* User-defined entries that don't match any forge entry are preserved.
|
|
49
|
+
*/
|
|
50
|
+
function mergeHooks(existing, forge) {
|
|
51
|
+
const merged = { ...existing };
|
|
52
|
+
for (const [event, forgeEntries] of Object.entries(forge)) {
|
|
53
|
+
const existingEntries = merged[event] ?? [];
|
|
54
|
+
const forgeStrings = new Set(forgeEntries.map((e) => JSON.stringify(e)));
|
|
55
|
+
// Keep only entries that are NOT identical to a forge entry
|
|
56
|
+
const userEntries = existingEntries.filter((entry) => !forgeStrings.has(JSON.stringify(entry)));
|
|
57
|
+
merged[event] = [...userEntries, ...forgeEntries];
|
|
58
|
+
}
|
|
59
|
+
return merged;
|
|
60
|
+
}
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { cwd } from "node:process";
|
|
4
|
+
import { SOURCE, targets } from "./paths.js";
|
|
5
|
+
import { copyFlatDir, copyRules, copyTreeDir } from "./copy.js";
|
|
6
|
+
import { installHooks } from "./hooks.js";
|
|
7
|
+
import { verify } from "./verify.js";
|
|
8
|
+
import { collectHashes, writeManifest } from "./manifest.js";
|
|
9
|
+
import { hasForgeMarkers, mergeForgeBlock } from "./merge.js";
|
|
10
|
+
function summarize(label, r) {
|
|
11
|
+
const parts = [];
|
|
12
|
+
if (r.added.length)
|
|
13
|
+
parts.push(`${r.added.length} new`);
|
|
14
|
+
if (r.changed.length)
|
|
15
|
+
parts.push(`${r.changed.length} updated`);
|
|
16
|
+
if (r.unchanged.length)
|
|
17
|
+
parts.push(`${r.unchanged.length} unchanged`);
|
|
18
|
+
if (r.skipped.length)
|
|
19
|
+
parts.push(`${r.skipped.length} skipped`);
|
|
20
|
+
console.log(` ${label.padEnd(12)} ${parts.join(", ")}`);
|
|
21
|
+
for (const f of r.added)
|
|
22
|
+
console.log(` + ${f}`);
|
|
23
|
+
for (const f of r.changed)
|
|
24
|
+
console.log(` ~ ${f}`);
|
|
25
|
+
}
|
|
26
|
+
export async function init(opts) {
|
|
27
|
+
const projectRoot = cwd();
|
|
28
|
+
const t = targets(projectRoot);
|
|
29
|
+
if (opts.dryRun) {
|
|
30
|
+
console.log("forge init --dry-run\n");
|
|
31
|
+
console.log("Would install to:", t.claude);
|
|
32
|
+
console.log("Directories: agents, commands, skills, rules, references, protocols, templates, hooks");
|
|
33
|
+
console.log("Generates: CLAUDE.md, AGENTS.md");
|
|
34
|
+
console.log("\nNo files were changed.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log("forge init\n");
|
|
38
|
+
// Check if already initialized (skip check during update)
|
|
39
|
+
if (!opts.mergeMarkers && existsSync(t.forgeJson) && !opts.force) {
|
|
40
|
+
const meta = JSON.parse(readFileSync(t.forgeJson, "utf-8"));
|
|
41
|
+
console.log(`Already initialized (v${meta.version}). Use --force to re-install, or 'forge update' to sync.`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Validate templates upfront (fail before any filesystem writes)
|
|
45
|
+
if (opts.mergeMarkers) {
|
|
46
|
+
for (const src of [SOURCE.claudeMdTemplate, SOURCE.agentsMdTemplate]) {
|
|
47
|
+
if (existsSync(src) && !hasForgeMarkers(readFileSync(src, "utf-8"))) {
|
|
48
|
+
console.error(`Template ${src} is missing valid FORGE markers.`);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
mkdirSync(t.claude, { recursive: true });
|
|
55
|
+
// Copy assets
|
|
56
|
+
const agents = copyFlatDir(SOURCE.agents, t.agents, opts);
|
|
57
|
+
summarize("agents:", agents);
|
|
58
|
+
const commands = copyFlatDir(SOURCE.commands, t.commands, opts);
|
|
59
|
+
summarize("commands:", commands);
|
|
60
|
+
const skills = copyTreeDir(SOURCE.skills, t.skills, opts);
|
|
61
|
+
summarize("skills:", skills);
|
|
62
|
+
const rules = copyRules(SOURCE.rules, t.rules, opts);
|
|
63
|
+
summarize("rules:", rules);
|
|
64
|
+
const protocols = copyFlatDir(SOURCE.protocols, t.protocols, opts);
|
|
65
|
+
summarize("protocols:", protocols);
|
|
66
|
+
const references = copyRules(SOURCE.references, t.references, opts);
|
|
67
|
+
summarize("references:", references);
|
|
68
|
+
const templates = copyTreeDir(SOURCE.templates, t.templates, opts);
|
|
69
|
+
summarize("templates:", templates);
|
|
70
|
+
// Install hooks (merges with existing)
|
|
71
|
+
const hooks = installHooks(projectRoot, opts);
|
|
72
|
+
console.log(` hooks: ${hooks.message}`);
|
|
73
|
+
// Generate project docs from templates (marker-merge during update)
|
|
74
|
+
for (const [label, src, dest] of [
|
|
75
|
+
["CLAUDE.md", SOURCE.claudeMdTemplate, t.claudeMd],
|
|
76
|
+
["AGENTS.md", SOURCE.agentsMdTemplate, t.agentsMd],
|
|
77
|
+
]) {
|
|
78
|
+
if (!existsSync(src))
|
|
79
|
+
continue;
|
|
80
|
+
const templateContent = readFileSync(src, "utf-8");
|
|
81
|
+
if (!existsSync(dest)) {
|
|
82
|
+
writeFileSync(dest, templateContent);
|
|
83
|
+
console.log(` ${label}: generated from template`);
|
|
84
|
+
}
|
|
85
|
+
else if (opts.mergeMarkers) {
|
|
86
|
+
const existingContent = readFileSync(dest, "utf-8");
|
|
87
|
+
const merged = mergeForgeBlock(existingContent, templateContent);
|
|
88
|
+
if (merged !== null) {
|
|
89
|
+
if (merged !== existingContent) {
|
|
90
|
+
writeFileSync(dest, merged);
|
|
91
|
+
console.log(` ${label}: framework sections updated (user content preserved)`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log(` ${label}: unchanged`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log(` ${label}: preserved (no forge markers — add markers to enable merge)`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (opts.force) {
|
|
102
|
+
writeFileSync(dest, templateContent);
|
|
103
|
+
console.log(` ${label}: generated from template`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.log(` ${label}: exists (skipped)`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Structural verification
|
|
110
|
+
const pkg = JSON.parse(readFileSync(resolve(SOURCE.agents, "..", "package.json"), "utf-8"));
|
|
111
|
+
console.log("\nVerifying installation...");
|
|
112
|
+
const check = verify(projectRoot);
|
|
113
|
+
if (check.failed.length === 0) {
|
|
114
|
+
console.log(` ${check.passed.length} checks passed`);
|
|
115
|
+
// Build file manifest with checksums
|
|
116
|
+
const files = {};
|
|
117
|
+
for (const dir of [t.agents, t.commands, t.skills, t.rules, t.protocols, t.references, t.templates]) {
|
|
118
|
+
if (existsSync(dir)) {
|
|
119
|
+
Object.assign(files, collectHashes(dir, t.claude));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Write manifest
|
|
123
|
+
mkdirSync(t.forgeState, { recursive: true });
|
|
124
|
+
const manifest = {
|
|
125
|
+
version: pkg.version,
|
|
126
|
+
installedAt: new Date().toISOString(),
|
|
127
|
+
files,
|
|
128
|
+
};
|
|
129
|
+
writeManifest(t.forgeJson, manifest);
|
|
130
|
+
console.log(`\nForge v${pkg.version} installed (${Object.keys(files).length} files tracked).`);
|
|
131
|
+
console.log("Run /setup to configure your project profile, then /validate for consistency check.");
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.log(` ${check.passed.length} passed, ${check.failed.length} FAILED:`);
|
|
135
|
+
for (const f of check.failed)
|
|
136
|
+
console.log(` ✗ ${f}`);
|
|
137
|
+
console.log(`\nInstallation incomplete. Fix the above and re-run.`);
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
}
|
|
140
|
+
}
|