@jamie-tam/forge 6.0.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +77 -59
  2. package/agents/dreamer.md +10 -7
  3. package/agents/gotcha-hunter.md +1 -1
  4. package/agents/prototype-codifier.md +2 -2
  5. package/commands/{forge.md → discover.md} +13 -9
  6. package/commands/dream.md +71 -0
  7. package/commands/feature.md +57 -10
  8. package/commands/{evolve.md → forge-evolve.md} +3 -3
  9. package/commands/greenfield.md +5 -5
  10. package/commands/note.md +64 -0
  11. package/commands/{task-force.md → parallel.md} +15 -15
  12. package/commands/resume.md +2 -2
  13. package/commands/setup.md +18 -17
  14. package/commands/status.md +2 -2
  15. package/commands/wrap.md +130 -0
  16. package/dist/__tests__/hooks.test.js +334 -0
  17. package/dist/__tests__/init.test.js +110 -0
  18. package/dist/__tests__/work-manifest.test.js +48 -14
  19. package/dist/cli.js +0 -0
  20. package/dist/hooks.js +88 -6
  21. package/dist/init.js +39 -1
  22. package/dist/uninstall.js +11 -5
  23. package/dist/work-manifest.js +63 -24
  24. package/hooks/config/gate-requirements.json +1 -1
  25. package/hooks/hooks.json +14 -1
  26. package/hooks/scripts/gate-enforcer.sh +51 -6
  27. package/hooks/scripts/pre-compact.sh +120 -55
  28. package/hooks/scripts/session-start.sh +43 -4
  29. package/hooks/scripts/telemetry.sh +32 -2
  30. package/hooks/templates/CLAUDE.md.template +6 -3
  31. package/package.json +1 -1
  32. package/references/common/phases.md +8 -6
  33. package/references/common/skill-authoring.md +1 -1
  34. package/rules/common/forge-system.md +64 -6
  35. package/rules/common/quality-gates.md +2 -0
  36. package/skills/build-prototype/SKILL.md +4 -4
  37. package/skills/build-tdd/SKILL.md +14 -0
  38. package/skills/concept-slides/SKILL.md +11 -11
  39. package/skills/deliver-deploy/SKILL.md +10 -1
  40. package/skills/harden/SKILL.md +22 -8
  41. package/skills/iterate-prototype/SKILL.md +22 -0
  42. package/skills/quality-test-execution/SKILL.md +26 -1
  43. package/skills/quality-test-plan/SKILL.md +21 -1
  44. package/skills/support-debug/SKILL.md +1 -1
  45. package/skills/support-dream/SKILL.md +8 -7
  46. package/skills/support-gotcha/SKILL.md +3 -3
  47. package/skills/{support-task-force → support-parallel}/SKILL.md +22 -22
  48. package/skills/{support-task-force → support-parallel}/references/dispatch-pattern.md +10 -10
  49. package/skills/{support-task-force → support-parallel}/references/synthesis-template.md +10 -10
  50. package/skills/support-skill-validator/SKILL.md +5 -5
  51. package/skills/support-skill-validator/references/validation-checks.md +1 -1
  52. package/skills/support-system-guide/SKILL.md +4 -3
  53. package/skills/support-wiki-lint/scripts/lint.mjs +52 -0
  54. package/templates/README.md +1 -1
  55. package/templates/aiwiki/CLAUDE.md.template +48 -22
  56. package/templates/aiwiki/schemas/session.md +134 -49
  57. package/templates/manifests/bugfix.yaml +1 -1
  58. package/templates/manifests/feature.yaml +1 -1
  59. package/templates/manifests/greenfield.yaml +1 -1
  60. package/templates/manifests/hotfix.yaml +1 -1
  61. package/templates/manifests/refactor.yaml +1 -1
  62. package/templates/manifests/v5/SCHEMA.md +14 -17
  63. package/templates/manifests/v5/feature.yaml +1 -1
  64. package/templates/manifests/v6/SCHEMA.md +14 -10
  65. package/commands/abort.md +0 -25
  66. package/dist/__tests__/active-manifest.test.js +0 -272
  67. package/dist/__tests__/gate-check.test.js +0 -384
  68. package/dist/active-manifest.js +0 -229
  69. package/dist/gate-check.js +0 -326
@@ -1,229 +0,0 @@
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
- }
@@ -1,326 +0,0 @@
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
- }