@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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke tests for src/copy.ts (copyTreeDir, copyFlatDir, copyRules).
|
|
3
|
+
*
|
|
4
|
+
* Regression: copyTreeDir crashed with ENOTDIR on reinstall when the source
|
|
5
|
+
* tree mixed top-level files (e.g. templates/README.md) with directories.
|
|
6
|
+
* treeChanged() recursed into a file as if it were a directory.
|
|
7
|
+
*
|
|
8
|
+
* Run with: npm run build && node dist/__tests__/copy.test.js
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { copyTreeDir } from "../copy.js";
|
|
14
|
+
let passed = 0;
|
|
15
|
+
let failed = 0;
|
|
16
|
+
const fails = [];
|
|
17
|
+
function test(name, fn) {
|
|
18
|
+
try {
|
|
19
|
+
fn();
|
|
20
|
+
passed++;
|
|
21
|
+
console.log(` PASS ${name}`);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
failed++;
|
|
25
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
26
|
+
fails.push(`${name}: ${msg}`);
|
|
27
|
+
console.log(` FAIL ${name}\n ${msg}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function mkTempRoot() {
|
|
31
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "forge-copy-test-"));
|
|
32
|
+
}
|
|
33
|
+
function makeMixedSource(root) {
|
|
34
|
+
const src = path.join(root, "src");
|
|
35
|
+
fs.mkdirSync(src, { recursive: true });
|
|
36
|
+
fs.writeFileSync(path.join(src, "README.md"), "top-level file content\n");
|
|
37
|
+
fs.mkdirSync(path.join(src, "subdir"), { recursive: true });
|
|
38
|
+
fs.writeFileSync(path.join(src, "subdir", "nested.md"), "nested content\n");
|
|
39
|
+
return src;
|
|
40
|
+
}
|
|
41
|
+
console.log("\n=== copy.ts smoke tests ===\n");
|
|
42
|
+
test("copyTreeDir installs mixed file+dir source on first install", () => {
|
|
43
|
+
const root = mkTempRoot();
|
|
44
|
+
const src = makeMixedSource(root);
|
|
45
|
+
const dest = path.join(root, "dest");
|
|
46
|
+
const result = copyTreeDir(src, dest, { force: false });
|
|
47
|
+
if (!fs.existsSync(path.join(dest, "README.md"))) {
|
|
48
|
+
throw new Error("README.md not copied");
|
|
49
|
+
}
|
|
50
|
+
if (!fs.existsSync(path.join(dest, "subdir", "nested.md"))) {
|
|
51
|
+
throw new Error("subdir/nested.md not copied");
|
|
52
|
+
}
|
|
53
|
+
if (result.added.length !== 2) {
|
|
54
|
+
throw new Error(`expected 2 added entries, got ${result.added.length}: ${result.added.join(",")}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
test("copyTreeDir reinstall with force does NOT crash on mixed source (regression)", () => {
|
|
58
|
+
const root = mkTempRoot();
|
|
59
|
+
const src = makeMixedSource(root);
|
|
60
|
+
const dest = path.join(root, "dest");
|
|
61
|
+
// First install — establishes dest.
|
|
62
|
+
copyTreeDir(src, dest, { force: false });
|
|
63
|
+
// Mutate the top-level file so reinstall has something to detect.
|
|
64
|
+
fs.writeFileSync(path.join(src, "README.md"), "top-level file CHANGED\n");
|
|
65
|
+
// Second install with force: this is the path that previously crashed
|
|
66
|
+
// with ENOTDIR because treeChanged() called readdirSync on README.md.
|
|
67
|
+
const result = copyTreeDir(src, dest, { force: true });
|
|
68
|
+
if (!result.changed.includes("README.md") &&
|
|
69
|
+
!result.unchanged.includes("README.md")) {
|
|
70
|
+
throw new Error(`README.md missing from result on reinstall: changed=${result.changed.join(",")} unchanged=${result.unchanged.join(",")}`);
|
|
71
|
+
}
|
|
72
|
+
if (!result.changed.includes("README.md")) {
|
|
73
|
+
throw new Error("README.md was modified but copyTreeDir reported it as unchanged");
|
|
74
|
+
}
|
|
75
|
+
const destContent = fs.readFileSync(path.join(dest, "README.md"), "utf-8");
|
|
76
|
+
if (destContent !== "top-level file CHANGED\n") {
|
|
77
|
+
throw new Error(`README.md content not propagated on reinstall: got ${JSON.stringify(destContent)}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
test("copyTreeDir reinstall reports unchanged file as unchanged (not changed)", () => {
|
|
81
|
+
const root = mkTempRoot();
|
|
82
|
+
const src = makeMixedSource(root);
|
|
83
|
+
const dest = path.join(root, "dest");
|
|
84
|
+
copyTreeDir(src, dest, { force: false });
|
|
85
|
+
const result = copyTreeDir(src, dest, { force: true });
|
|
86
|
+
if (!result.unchanged.includes("README.md")) {
|
|
87
|
+
throw new Error(`README.md should be unchanged on identical reinstall, got: changed=${result.changed.join(",")} unchanged=${result.unchanged.join(",")}`);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
console.log(`\n=== ${passed} passed, ${failed} failed ===`);
|
|
91
|
+
if (failed > 0) {
|
|
92
|
+
console.log("\nFailures:");
|
|
93
|
+
fails.forEach((f) => console.log(" - " + f));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
process.exit(0);
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test for src/gate-check.ts.
|
|
3
|
+
* Calls gateCheck() programmatically (avoids stdin plumbing in tests).
|
|
4
|
+
*/
|
|
5
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
import { gateCheck } from "../gate-check.js";
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
const FIXTURES = join(__dirname, "..", "..", "src", "__fixtures__", "work-manifest");
|
|
14
|
+
let passed = 0;
|
|
15
|
+
let failed = 0;
|
|
16
|
+
const fails = [];
|
|
17
|
+
function test(name, fn) {
|
|
18
|
+
try {
|
|
19
|
+
fn();
|
|
20
|
+
passed++;
|
|
21
|
+
console.log(` PASS ${name}`);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
failed++;
|
|
25
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
26
|
+
fails.push(`${name}: ${msg}`);
|
|
27
|
+
console.log(` FAIL ${name}\n ${msg}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function withTmpFile(seed, fn) {
|
|
31
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "forge-gatecheck-"));
|
|
32
|
+
const path = join(tmpDir, "manifest.yaml");
|
|
33
|
+
writeFileSync(path, seed);
|
|
34
|
+
try {
|
|
35
|
+
fn(path);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const goodV5 = readFileSync(join(FIXTURES, "v5-good.yaml"), "utf-8");
|
|
42
|
+
console.log("\n=== gate-check.ts smoke tests ===\n");
|
|
43
|
+
test("Edit that flips a slice gate from false → true emits transition", () => {
|
|
44
|
+
withTmpFile(goodV5, (path) => {
|
|
45
|
+
const result = gateCheck({
|
|
46
|
+
tool_name: "Edit",
|
|
47
|
+
tool_input: {
|
|
48
|
+
file_path: path,
|
|
49
|
+
old_string: "skeleton-runs: { status: pending, gate-passed: false }",
|
|
50
|
+
new_string: "skeleton-runs: { status: complete, gate-passed: true }",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (!result.ok)
|
|
54
|
+
throw new Error(`expected ok, got: ${result.error}`);
|
|
55
|
+
if (result.transitions.length !== 1) {
|
|
56
|
+
throw new Error(`expected 1 transition, got ${result.transitions.length}`);
|
|
57
|
+
}
|
|
58
|
+
const t = result.transitions[0];
|
|
59
|
+
if (t.gate !== "skeleton-runs")
|
|
60
|
+
throw new Error(`expected gate=skeleton-runs, got ${t.gate}`);
|
|
61
|
+
if (t.slice_id !== "skeleton")
|
|
62
|
+
throw new Error(`expected slice=skeleton, got ${t.slice_id}`);
|
|
63
|
+
if (t.scope !== "slice")
|
|
64
|
+
throw new Error(`expected scope=slice, got ${t.scope}`);
|
|
65
|
+
if (t.transition !== "false-to-true")
|
|
66
|
+
throw new Error(`expected false-to-true, got ${t.transition}`);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
test("Edit that flips a manifest-level (phase) gate emits scope=manifest", () => {
|
|
70
|
+
withTmpFile(goodV5, (path) => {
|
|
71
|
+
const result = gateCheck({
|
|
72
|
+
tool_name: "Edit",
|
|
73
|
+
tool_input: {
|
|
74
|
+
file_path: path,
|
|
75
|
+
old_string: "test-plan: { status: pending, gate-passed: false }",
|
|
76
|
+
new_string: "test-plan: { status: complete, gate-passed: true }",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (!result.ok)
|
|
80
|
+
throw new Error(`expected ok, got: ${result.error}`);
|
|
81
|
+
if (result.transitions.length !== 1) {
|
|
82
|
+
throw new Error(`expected 1 transition, got ${result.transitions.length}`);
|
|
83
|
+
}
|
|
84
|
+
const t = result.transitions[0];
|
|
85
|
+
if (t.gate !== "test-plan")
|
|
86
|
+
throw new Error(`gate mismatch: ${t.gate}`);
|
|
87
|
+
if (t.scope !== "manifest")
|
|
88
|
+
throw new Error(`expected scope=manifest, got ${t.scope}`);
|
|
89
|
+
if (t.slice_id !== undefined)
|
|
90
|
+
throw new Error(`phase gate must not have slice_id`);
|
|
91
|
+
if (t.path !== "phases.quality.test-plan")
|
|
92
|
+
throw new Error(`path mismatch: ${t.path}`);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
test("ambiguous Edit (old_string matches multiple times) fails closed", () => {
|
|
96
|
+
withTmpFile(goodV5, (path) => {
|
|
97
|
+
const result = gateCheck({
|
|
98
|
+
tool_name: "Edit",
|
|
99
|
+
tool_input: {
|
|
100
|
+
file_path: path,
|
|
101
|
+
// build-tdd appears in 3 slices' gate blocks
|
|
102
|
+
old_string: "build-tdd: { status: pending, gate-passed: false }",
|
|
103
|
+
new_string: "build-tdd: { status: complete, gate-passed: true }",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (result.ok)
|
|
107
|
+
throw new Error("expected fail-closed on ambiguous edit");
|
|
108
|
+
if (!result.error.includes("ambiguous")) {
|
|
109
|
+
throw new Error(`expected 'ambiguous' in error, got: ${result.error}`);
|
|
110
|
+
}
|
|
111
|
+
if (result.exitCode !== 1)
|
|
112
|
+
throw new Error(`expected exitCode 1, got ${result.exitCode}`);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
test("Edit with non-matching old_string fails closed", () => {
|
|
116
|
+
withTmpFile(goodV5, (path) => {
|
|
117
|
+
const result = gateCheck({
|
|
118
|
+
tool_name: "Edit",
|
|
119
|
+
tool_input: {
|
|
120
|
+
file_path: path,
|
|
121
|
+
old_string: "this string is not in the file",
|
|
122
|
+
new_string: "replacement",
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
if (result.ok)
|
|
126
|
+
throw new Error("expected fail-closed on no match");
|
|
127
|
+
if (!result.error.includes("not found")) {
|
|
128
|
+
throw new Error(`expected 'not found' in error, got: ${result.error}`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
test("Edit that doesn't change any gate-passed value emits empty transitions", () => {
|
|
133
|
+
withTmpFile(goodV5, (path) => {
|
|
134
|
+
const result = gateCheck({
|
|
135
|
+
tool_name: "Edit",
|
|
136
|
+
tool_input: {
|
|
137
|
+
file_path: path,
|
|
138
|
+
old_string: 'description: "End-to-end happy-path manifest covering skeleton + 3 slices"',
|
|
139
|
+
new_string: 'description: "Edited description, no gate impact"',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (!result.ok)
|
|
143
|
+
throw new Error(`expected ok, got: ${result.error}`);
|
|
144
|
+
if (result.transitions.length !== 0) {
|
|
145
|
+
throw new Error(`expected 0 transitions, got ${result.transitions.length}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
test("Write that introduces fully-passed gates from scratch emits transitions", () => {
|
|
150
|
+
withTmpFile("", (path) => {
|
|
151
|
+
// Write from empty/non-existent baseline: only delete actually existing
|
|
152
|
+
rmSync(path);
|
|
153
|
+
const newContent = goodV5.replace(/gate-passed: false/g, "gate-passed: true");
|
|
154
|
+
const result = gateCheck({
|
|
155
|
+
tool_name: "Write",
|
|
156
|
+
tool_input: { file_path: path, content: newContent },
|
|
157
|
+
});
|
|
158
|
+
if (!result.ok)
|
|
159
|
+
throw new Error(`expected ok, got: ${result.error}`);
|
|
160
|
+
if (result.transitions.length === 0)
|
|
161
|
+
throw new Error("expected transitions on Write");
|
|
162
|
+
for (const t of result.transitions) {
|
|
163
|
+
if (t.transition !== "false-to-true") {
|
|
164
|
+
throw new Error(`unexpected direction: ${t.transition}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
test("non-Edit, non-Write tool returns ok with no transitions (no-op)", () => {
|
|
170
|
+
const result = gateCheck({ tool_name: "Bash", tool_input: { file_path: "anything" } });
|
|
171
|
+
if (!result.ok)
|
|
172
|
+
throw new Error(`expected ok no-op, got: ${result.error}`);
|
|
173
|
+
if (result.transitions.length !== 0)
|
|
174
|
+
throw new Error("expected no transitions on non-edit tool");
|
|
175
|
+
});
|
|
176
|
+
test("malformed YAML in proposed content returns exit 2", () => {
|
|
177
|
+
withTmpFile(goodV5, (path) => {
|
|
178
|
+
const result = gateCheck({
|
|
179
|
+
tool_name: "Write",
|
|
180
|
+
tool_input: { file_path: path, content: "foo: [unclosed\nbar" },
|
|
181
|
+
});
|
|
182
|
+
if (result.ok)
|
|
183
|
+
throw new Error("expected error on malformed YAML");
|
|
184
|
+
if (result.exitCode !== 2)
|
|
185
|
+
throw new Error(`expected exitCode 2, got ${result.exitCode}`);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
test("v4 manifest: phase gate transitions still detected (no slice_graph)", () => {
|
|
189
|
+
const v4 = readFileSync(join(FIXTURES, "v4-legacy.yaml"), "utf-8");
|
|
190
|
+
withTmpFile(v4, (path) => {
|
|
191
|
+
const result = gateCheck({
|
|
192
|
+
tool_name: "Edit",
|
|
193
|
+
tool_input: {
|
|
194
|
+
file_path: path,
|
|
195
|
+
old_string: "code-review-final: { status: pending, gate-passed: false }",
|
|
196
|
+
new_string: "code-review-final: { status: complete, gate-passed: true }",
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
if (!result.ok)
|
|
200
|
+
throw new Error(`expected ok, got: ${result.error}`);
|
|
201
|
+
const t = result.transitions.find((x) => x.gate === "code-review-final");
|
|
202
|
+
if (!t)
|
|
203
|
+
throw new Error("expected code-review-final transition");
|
|
204
|
+
if (t.scope !== "manifest")
|
|
205
|
+
throw new Error(`expected scope=manifest, got ${t.scope}`);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
test("Edit reverting true → false emits true-to-false transition", () => {
|
|
209
|
+
// Seed with a gate already passed, then revert it.
|
|
210
|
+
const seed = goodV5.replace("skeleton-runs: { status: pending, gate-passed: false }", "skeleton-runs: { status: complete, gate-passed: true }");
|
|
211
|
+
withTmpFile(seed, (path) => {
|
|
212
|
+
const result = gateCheck({
|
|
213
|
+
tool_name: "Edit",
|
|
214
|
+
tool_input: {
|
|
215
|
+
file_path: path,
|
|
216
|
+
old_string: "skeleton-runs: { status: complete, gate-passed: true }",
|
|
217
|
+
new_string: "skeleton-runs: { status: pending, gate-passed: false }",
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
if (!result.ok)
|
|
221
|
+
throw new Error(`expected ok, got: ${result.error}`);
|
|
222
|
+
if (result.transitions.length !== 1) {
|
|
223
|
+
throw new Error(`expected 1 transition, got ${result.transitions.length}`);
|
|
224
|
+
}
|
|
225
|
+
if (result.transitions[0].transition !== "true-to-false") {
|
|
226
|
+
throw new Error(`expected true-to-false, got ${result.transitions[0].transition}`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
test("null payload is rejected (defends stdin null/garbage)", () => {
|
|
231
|
+
// Cast to bypass the typed signature; this models what JSON.parse("null") returns.
|
|
232
|
+
const result = gateCheck(null);
|
|
233
|
+
if (result.ok)
|
|
234
|
+
throw new Error("expected error on null payload");
|
|
235
|
+
if (!result.error.includes("must be a JSON object")) {
|
|
236
|
+
throw new Error(`expected JSON-object error, got: ${result.error}`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
test("array payload is rejected", () => {
|
|
240
|
+
const result = gateCheck([]);
|
|
241
|
+
if (result.ok)
|
|
242
|
+
throw new Error("expected error on array payload");
|
|
243
|
+
if (!result.error.includes("must be a JSON object")) {
|
|
244
|
+
throw new Error(`expected JSON-object error, got: ${result.error}`);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
test("YAML date-scalar root rejected (js-yaml parses as Date object)", () => {
|
|
248
|
+
withTmpFile(goodV5, (path) => {
|
|
249
|
+
const result = gateCheck({
|
|
250
|
+
tool_name: "Write",
|
|
251
|
+
tool_input: { file_path: path, content: "2026-05-04\n" }, // bare ISO date
|
|
252
|
+
});
|
|
253
|
+
if (result.ok)
|
|
254
|
+
throw new Error("expected error on date-scalar root");
|
|
255
|
+
if (!result.error.includes("not a YAML mapping")) {
|
|
256
|
+
throw new Error(`expected mapping error, got: ${result.error}`);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
test("YAML array root is rejected as not-a-mapping", () => {
|
|
261
|
+
withTmpFile("- one\n- two\n- three\n", (path) => {
|
|
262
|
+
const result = gateCheck({
|
|
263
|
+
tool_name: "Write",
|
|
264
|
+
tool_input: { file_path: path, content: "- a\n- b\n" },
|
|
265
|
+
});
|
|
266
|
+
if (result.ok)
|
|
267
|
+
throw new Error("expected error on YAML array root");
|
|
268
|
+
if (!result.error.includes("not a YAML mapping")) {
|
|
269
|
+
throw new Error(`expected mapping error, got: ${result.error}`);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
test("string-typed gate-passed fails closed (no Boolean coercion)", () => {
|
|
274
|
+
// Seed uniquely-occurring skeleton-runs gate with a string value.
|
|
275
|
+
const seed = goodV5.replace("skeleton-runs: { status: pending, gate-passed: false }", 'skeleton-runs: { status: pending, gate-passed: "false" }');
|
|
276
|
+
withTmpFile(seed, (path) => {
|
|
277
|
+
// Any unambiguous edit triggers the manifest walk; the walk catches the
|
|
278
|
+
// non-boolean gate-passed regardless of whether the edit touches it.
|
|
279
|
+
const result = gateCheck({
|
|
280
|
+
tool_name: "Edit",
|
|
281
|
+
tool_input: {
|
|
282
|
+
file_path: path,
|
|
283
|
+
old_string: 'description: "End-to-end happy-path manifest covering skeleton + 3 slices"',
|
|
284
|
+
new_string: 'description: "Edited"',
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
if (result.ok)
|
|
288
|
+
throw new Error("expected fail-closed on string-typed gate-passed");
|
|
289
|
+
if (!result.error.includes("must be a boolean")) {
|
|
290
|
+
throw new Error(`expected boolean-error, got: ${result.error}`);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
test("number-typed gate-passed fails closed", () => {
|
|
295
|
+
const seed = goodV5.replace("skeleton-runs: { status: pending, gate-passed: false }", "skeleton-runs: { status: pending, gate-passed: 0 }");
|
|
296
|
+
withTmpFile(seed, (path) => {
|
|
297
|
+
const result = gateCheck({
|
|
298
|
+
tool_name: "Edit",
|
|
299
|
+
tool_input: {
|
|
300
|
+
file_path: path,
|
|
301
|
+
old_string: 'description: "End-to-end happy-path manifest covering skeleton + 3 slices"',
|
|
302
|
+
new_string: 'description: "Edited"',
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
if (result.ok)
|
|
306
|
+
throw new Error("expected fail-closed on number gate-passed");
|
|
307
|
+
if (!result.error.includes("must be a boolean")) {
|
|
308
|
+
throw new Error(`expected boolean-error, got: ${result.error}`);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
test("transitions are sorted by yaml-path (deterministic output)", () => {
|
|
313
|
+
withTmpFile(goodV5, (path) => {
|
|
314
|
+
// Flip three slice gates simultaneously across different slices.
|
|
315
|
+
const oldBlock = ` auth-login:
|
|
316
|
+
type: feature-slice
|
|
317
|
+
depends_on: [skeleton]
|
|
318
|
+
status: pending
|
|
319
|
+
gates:
|
|
320
|
+
build-tdd: { status: pending, gate-passed: false }
|
|
321
|
+
wiki-lint: { status: pending, gate-passed: false }
|
|
322
|
+
runtime-reach: { status: pending, gate-passed: false }
|
|
323
|
+
code-review: { status: pending, gate-passed: false }`;
|
|
324
|
+
const newBlock = ` auth-login:
|
|
325
|
+
type: feature-slice
|
|
326
|
+
depends_on: [skeleton]
|
|
327
|
+
status: pending
|
|
328
|
+
gates:
|
|
329
|
+
build-tdd: { status: complete, gate-passed: true }
|
|
330
|
+
wiki-lint: { status: complete, gate-passed: true }
|
|
331
|
+
runtime-reach: { status: complete, gate-passed: true }
|
|
332
|
+
code-review: { status: complete, gate-passed: true }`;
|
|
333
|
+
const result = gateCheck({
|
|
334
|
+
tool_name: "Edit",
|
|
335
|
+
tool_input: { file_path: path, old_string: oldBlock, new_string: newBlock },
|
|
336
|
+
});
|
|
337
|
+
if (!result.ok)
|
|
338
|
+
throw new Error(`expected ok, got: ${result.error}`);
|
|
339
|
+
const paths = result.transitions.map((t) => t.path);
|
|
340
|
+
const sorted = [...paths].sort();
|
|
341
|
+
if (JSON.stringify(paths) !== JSON.stringify(sorted)) {
|
|
342
|
+
throw new Error(`paths not sorted: ${paths.join(",")}`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
test("Edit with multiple gate transitions in one new_string emits all", () => {
|
|
347
|
+
withTmpFile(goodV5, (path) => {
|
|
348
|
+
// Replace the entire skeleton.gates block, flipping all three gates at once.
|
|
349
|
+
const oldBlock = ` gates:
|
|
350
|
+
skeleton-runs: { status: pending, gate-passed: false }
|
|
351
|
+
wiki-lint: { status: pending, gate-passed: false }
|
|
352
|
+
runtime-reach: { status: pending, gate-passed: false }`;
|
|
353
|
+
const newBlock = ` gates:
|
|
354
|
+
skeleton-runs: { status: complete, gate-passed: true }
|
|
355
|
+
wiki-lint: { status: complete, gate-passed: true }
|
|
356
|
+
runtime-reach: { status: complete, gate-passed: true }`;
|
|
357
|
+
const result = gateCheck({
|
|
358
|
+
tool_name: "Edit",
|
|
359
|
+
tool_input: { file_path: path, old_string: oldBlock, new_string: newBlock },
|
|
360
|
+
});
|
|
361
|
+
if (!result.ok)
|
|
362
|
+
throw new Error(`expected ok, got: ${result.error}`);
|
|
363
|
+
if (result.transitions.length !== 3) {
|
|
364
|
+
throw new Error(`expected 3 transitions, got ${result.transitions.length}`);
|
|
365
|
+
}
|
|
366
|
+
const names = result.transitions.map((t) => t.gate).sort();
|
|
367
|
+
if (JSON.stringify(names) !== JSON.stringify(["runtime-reach", "skeleton-runs", "wiki-lint"])) {
|
|
368
|
+
throw new Error(`gate names mismatch: ${names.join(",")}`);
|
|
369
|
+
}
|
|
370
|
+
for (const t of result.transitions) {
|
|
371
|
+
if (t.slice_id !== "skeleton")
|
|
372
|
+
throw new Error(`expected slice=skeleton, got ${t.slice_id}`);
|
|
373
|
+
if (t.scope !== "slice")
|
|
374
|
+
throw new Error(`expected scope=slice, got ${t.scope}`);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
console.log(`\n=== ${passed} passed, ${failed} failed ===`);
|
|
379
|
+
if (failed > 0) {
|
|
380
|
+
console.log("\nFailures:");
|
|
381
|
+
fails.forEach((f) => console.log(" - " + f));
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
process.exit(0);
|