@warpgogol/forge 5.3.0 → 5.3.1

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 (41) hide show
  1. package/bin/cli.ts +24 -33
  2. package/dist/bin/cli.js +20 -32
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/os/adr/handlers/implement-stamp.d.ts.map +1 -1
  5. package/dist/os/adr/handlers/implement-stamp.js +0 -1
  6. package/dist/os/adr/handlers/implement-stamp.js.map +1 -1
  7. package/dist/os/mission/handlers/archive.d.ts.map +1 -1
  8. package/dist/os/mission/handlers/archive.js +22 -4
  9. package/dist/os/mission/handlers/archive.js.map +1 -1
  10. package/dist/os/rfc/handlers/validate.d.ts.map +1 -1
  11. package/dist/os/rfc/handlers/validate.js +6 -1
  12. package/dist/os/rfc/handlers/validate.js.map +1 -1
  13. package/dist/os/werkstatt/handlers/workspace-deps.d.ts.map +1 -1
  14. package/dist/os/werkstatt/handlers/workspace-deps.js +0 -1
  15. package/dist/os/werkstatt/handlers/workspace-deps.js.map +1 -1
  16. package/dist/src/cli-flags.d.ts +28 -0
  17. package/dist/src/cli-flags.d.ts.map +1 -0
  18. package/dist/src/cli-flags.js +232 -0
  19. package/dist/src/cli-flags.js.map +1 -0
  20. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  21. package/dist/src/onboarding/agents-generate.js +18 -5
  22. package/dist/src/onboarding/agents-generate.js.map +1 -1
  23. package/os/adr/handlers/implement-stamp.ts +0 -1
  24. package/os/mission/handlers/archive.test.ts +38 -1
  25. package/os/mission/handlers/archive.ts +27 -4
  26. package/os/rfc/handlers/validate.test.ts +71 -0
  27. package/os/rfc/handlers/validate.ts +6 -1
  28. package/os/werkstatt/handlers/workspace-deps.ts +0 -1
  29. package/package.json +1 -1
  30. package/profiles/forge-shell.yaml +1 -1
  31. package/profiles/godot-game.yaml +1 -1
  32. package/profiles/knowledge.yaml +1 -1
  33. package/profiles/phaser-game.yaml +1 -1
  34. package/profiles/site-workshop.yaml +1 -1
  35. package/profiles/typescript.yaml +1 -1
  36. package/src/cli-flags.ts +285 -0
  37. package/src/onboarding/agents-generate.ts +20 -5
  38. package/src/tests/agents-generate.test.ts +22 -1
  39. package/src/tests/cli-flags.test.ts +169 -0
  40. package/src/tests/fixtures/agents-generate-business-before.txt +31 -2
  41. package/AGENTS.md +0 -532
@@ -516,7 +516,7 @@ workspace:
516
516
  "hooks": {
517
517
  "pre_user_prompt": [
518
518
  {
519
- "command": "node $ROOT_WORKSPACE_PATH/hooks/pre-user-prompt-wrapper.mjs",
519
+ "command": "node \"${ROOT_WORKSPACE_PATH:-$PWD}/hooks/pre-user-prompt-wrapper.mjs\"",
520
520
  "show_output": true
521
521
  }
522
522
  ]
@@ -453,7 +453,7 @@ workspace:
453
453
  "hooks": {
454
454
  "pre_user_prompt": [
455
455
  {
456
- "command": "node $ROOT_WORKSPACE_PATH/hooks/pre-user-prompt-wrapper.mjs",
456
+ "command": "node \"${ROOT_WORKSPACE_PATH:-$PWD}/hooks/pre-user-prompt-wrapper.mjs\"",
457
457
  "show_output": true
458
458
  }
459
459
  ]
@@ -0,0 +1,285 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Argv-to-flags resolution for the standalone forge CLI — a standalone
4
+ port of the kernel's RFC-0260 semantics (resolveCommandFlags for schema-carrying
5
+ commands, the heuristic parser for schema-less ones). Ensures a declared string
6
+ flag never reaches a handler as `true`, which crashed `forge rfc.validate --id`
7
+ with `TypeError: targetId.toLowerCase is not a function`.</purpose>
8
+ <non-goals>
9
+ <item>Do not import from @warpgogol/* — forge is autonomous (FORGE-AUTONOMY-01); keep this a self-contained port.</item>
10
+ <item>Do not call console.* or process.exit — bin/cli.ts renders diagnostics and decides the exit code.</item>
11
+ </non-goals>
12
+ </MODULE_CONTRACT>
13
+ <CHANGE_SUMMARY>
14
+ <item>Standalone port of kernel argv resolution for the forge CLI: strict KERNEL-FLAG-01/02/03 + KERNEL-ARG-01 diagnostics for schema-carrying commands, legacy heuristic parse (with KERNEL-ARG-01) for schema-less commands; repeated single-value string flags diagnose instead of silently promoting to an array that crashes handlers.</item>
15
+ </CHANGE_SUMMARY>
16
+ */
17
+
18
+ import type {
19
+ Diagnostic,
20
+ ForgeCommandDefinition,
21
+ ForgeFlagSpec,
22
+ ForgeFlagValue,
23
+ } from "./types.ts";
24
+
25
+ /**
26
+ * Flags every CLI command accepts regardless of its own declared schema —
27
+ * mirrors the kernel's KERNEL_UNIVERSAL_FLAGS so both entry points accept the
28
+ * same universal switches.
29
+ */
30
+ export const CLI_UNIVERSAL_FLAGS: Record<string, ForgeFlagSpec> = {
31
+ site: { kind: "string", description: "Target a specific site by name." },
32
+ all: { kind: "boolean", description: "Run against every discovered app." },
33
+ json: { kind: "boolean", description: "Emit machine-readable JSON instead of pretty output." },
34
+ quiet: { kind: "boolean", description: "Suppress non-essential log output." },
35
+ verbose: { kind: "boolean", description: "Emit additional diagnostic log output." },
36
+ root: { kind: "string", description: "Override the resolved workspace root." },
37
+ "dry-run": { kind: "boolean", description: "Report intended mutations without writing them." },
38
+ force: { kind: "boolean", description: "Bypass a normally-blocking safety check." },
39
+ help: { kind: "boolean", description: "Print command help instead of executing." },
40
+ };
41
+
42
+ /**
43
+ * Boolean switches for the legacy heuristic path (schema-less commands).
44
+ * Mirrors the kernel's KERNEL_BOOLEAN_FLAGS: a listed flag never consumes the
45
+ * following token as its value.
46
+ */
47
+ const CLI_BOOLEAN_FLAGS = new Set<string>([
48
+ "all",
49
+ "all-apps",
50
+ "approve-all",
51
+ "dry-run",
52
+ "force",
53
+ "help",
54
+ "inplace",
55
+ "json",
56
+ "mini",
57
+ "packages",
58
+ "page",
59
+ "prod",
60
+ "quiet",
61
+ "regen",
62
+ "regenerate",
63
+ "report-only",
64
+ "show-versions",
65
+ "strict",
66
+ "verbose",
67
+ "version",
68
+ "write",
69
+ ]);
70
+
71
+ function addFlagValue(
72
+ target: Record<string, ForgeFlagValue>,
73
+ key: string,
74
+ value: string | boolean,
75
+ ): void {
76
+ const existing = target[key];
77
+
78
+ if (existing === undefined) {
79
+ target[key] = value;
80
+ return;
81
+ }
82
+
83
+ if (Array.isArray(existing)) {
84
+ if (typeof value === "string") {
85
+ target[key] = [...existing, value];
86
+ }
87
+ return;
88
+ }
89
+
90
+ if (typeof existing === "string") {
91
+ if (typeof value === "string") {
92
+ target[key] = [existing, value];
93
+ return;
94
+ }
95
+
96
+ target[key] = existing;
97
+ return;
98
+ }
99
+
100
+ target[key] = value;
101
+ }
102
+
103
+ function positionalDiagnostic(entry: string, commandName?: string): Diagnostic {
104
+ const scope = commandName ? ` for command "${commandName}"` : "";
105
+ return {
106
+ ruleId: "KERNEL-ARG-01",
107
+ severity: "error",
108
+ message: `Unexpected positional argument "${entry}"${scope}. All arguments must be passed as flags.`,
109
+ fixHint: `Convert "${entry}" to a flag, e.g. --id ${entry}.`,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Legacy heuristic parse for commands without a `flags` schema — mirrors the
115
+ * kernel's parseKernelArgv: unknown flags are accepted, boolean-ness comes
116
+ * from CLI_BOOLEAN_FLAGS, and positional tokens produce KERNEL-ARG-01.
117
+ */
118
+ export function parseCliArgs(argv: string[]): {
119
+ flags: Record<string, ForgeFlagValue>;
120
+ diagnostics: Diagnostic[];
121
+ } {
122
+ const flags: Record<string, ForgeFlagValue> = {};
123
+ const diagnostics: Diagnostic[] = [];
124
+ let passthrough = false;
125
+
126
+ for (let index = 0; index < argv.length; index += 1) {
127
+ const entry = argv[index]!;
128
+
129
+ if (passthrough) {
130
+ diagnostics.push(positionalDiagnostic(entry));
131
+ continue;
132
+ }
133
+
134
+ if (entry === "--") {
135
+ passthrough = true;
136
+ continue;
137
+ }
138
+
139
+ if (!entry.startsWith("--")) {
140
+ diagnostics.push(positionalDiagnostic(entry));
141
+ continue;
142
+ }
143
+
144
+ const withoutPrefix = entry.slice(2);
145
+ const [flagName, inlineValue] = withoutPrefix.split("=", 2);
146
+ if (!flagName) continue;
147
+
148
+ if (inlineValue !== undefined) {
149
+ addFlagValue(flags, flagName, inlineValue);
150
+ continue;
151
+ }
152
+
153
+ if (CLI_BOOLEAN_FLAGS.has(flagName)) {
154
+ addFlagValue(flags, flagName, true);
155
+ continue;
156
+ }
157
+
158
+ const next = argv[index + 1];
159
+ if (typeof next === "string" && !next.startsWith("--")) {
160
+ addFlagValue(flags, flagName, next);
161
+ index += 1;
162
+ continue;
163
+ }
164
+
165
+ addFlagValue(flags, flagName, true);
166
+ }
167
+
168
+ return { flags, diagnostics };
169
+ }
170
+
171
+ function resolveStrict(
172
+ rawArgv: string[],
173
+ command: ForgeCommandDefinition,
174
+ ): { flags: Record<string, ForgeFlagValue>; diagnostics: Diagnostic[] } {
175
+ const schema: Record<string, ForgeFlagSpec> = {
176
+ ...CLI_UNIVERSAL_FLAGS,
177
+ ...(command.flags ?? {}),
178
+ };
179
+ const validFlagNames = Object.keys(schema).sort();
180
+ const flags: Record<string, ForgeFlagValue> = {};
181
+ const diagnostics: Diagnostic[] = [];
182
+ let passthrough = false;
183
+
184
+ for (let index = 0; index < rawArgv.length; index += 1) {
185
+ const entry = rawArgv[index]!;
186
+
187
+ if (passthrough) {
188
+ diagnostics.push(positionalDiagnostic(entry, command.name));
189
+ continue;
190
+ }
191
+
192
+ if (entry === "--") {
193
+ passthrough = true;
194
+ continue;
195
+ }
196
+
197
+ if (!entry.startsWith("--")) {
198
+ diagnostics.push(positionalDiagnostic(entry, command.name));
199
+ continue;
200
+ }
201
+
202
+ const withoutPrefix = entry.slice(2);
203
+ const [flagName, inlineValue] = withoutPrefix.split("=", 2);
204
+ if (!flagName) continue;
205
+
206
+ const spec = schema[flagName];
207
+ if (!spec) {
208
+ diagnostics.push({
209
+ ruleId: "KERNEL-FLAG-01",
210
+ severity: "error",
211
+ message: `Unknown flag "--${flagName}" for command "${command.name}". Valid flags: ${validFlagNames.join(", ")}`,
212
+ fixHint: `Remove --${flagName}, fix the typo, or add it to the flags schema for ${command.name}.`,
213
+ });
214
+ continue;
215
+ }
216
+
217
+ if (inlineValue !== undefined) {
218
+ addFlagValue(
219
+ flags,
220
+ flagName,
221
+ spec.kind === "boolean" ? inlineValue !== "false" : inlineValue,
222
+ );
223
+ continue;
224
+ }
225
+
226
+ if (spec.kind === "boolean") {
227
+ addFlagValue(flags, flagName, true);
228
+ continue;
229
+ }
230
+
231
+ const next = rawArgv[index + 1];
232
+ if (typeof next === "string" && !next.startsWith("--")) {
233
+ addFlagValue(flags, flagName, next);
234
+ index += 1;
235
+ continue;
236
+ }
237
+
238
+ diagnostics.push({
239
+ ruleId: "KERNEL-FLAG-02",
240
+ severity: "error",
241
+ message: `Flag "--${flagName}" for command "${command.name}" requires a value but none was given.`,
242
+ fixHint: `Pass --${flagName} <value> or --${flagName}=<value>.`,
243
+ });
244
+ }
245
+
246
+ for (const [flagName, spec] of Object.entries(schema)) {
247
+ const value = flags[flagName];
248
+ if (spec.required && value === undefined) {
249
+ diagnostics.push({
250
+ ruleId: "KERNEL-FLAG-03",
251
+ severity: "error",
252
+ message: `Required flag "--${flagName}" is missing for command "${command.name}".`,
253
+ fixHint: `Pass --${flagName} <value>.`,
254
+ });
255
+ } else if (value === undefined && spec.default !== undefined) {
256
+ flags[flagName] = spec.default;
257
+ } else if (spec.kind === "string" && Array.isArray(value)) {
258
+ diagnostics.push({
259
+ ruleId: "KERNEL-FLAG-02",
260
+ severity: "error",
261
+ message: `Flag "--${flagName}" for command "${command.name}" accepts a single value but was given multiple.`,
262
+ fixHint: `Pass --${flagName} once, or declare the flag as kind "string[]" if multiple values are intended.`,
263
+ });
264
+ }
265
+ }
266
+
267
+ return { flags, diagnostics };
268
+ }
269
+
270
+ /**
271
+ * Resolve raw argv against a command's declared flag schema (merged with
272
+ * CLI_UNIVERSAL_FLAGS) — the standalone-CLI counterpart of the kernel's
273
+ * resolveCommandFlags. Schema-carrying commands (`command.flags` set,
274
+ * including `{}`) resolve strictly; schema-less commands keep the legacy
275
+ * heuristic parse. Either way, positional tokens produce KERNEL-ARG-01.
276
+ */
277
+ export function resolveCliFlags(
278
+ argv: string[],
279
+ command: ForgeCommandDefinition,
280
+ ): { flags: Record<string, ForgeFlagValue>; diagnostics: Diagnostic[] } {
281
+ if (command.flags) {
282
+ return resolveStrict(argv, command);
283
+ }
284
+ return parseCliArgs(argv);
285
+ }
@@ -141,10 +141,22 @@ function readRegister(workspaceRoot: string): BehavioralRegister {
141
141
  return "business";
142
142
  }
143
143
 
144
+ /** resolveForgeRoot throws when neither layout exists — routing-table
145
+ * extraction is best-effort, so an unresolvable root yields an empty table. */
146
+ function safeResolveForgeRoot(workspaceRoot: string): string | undefined {
147
+ try {
148
+ return resolveForgeRoot(workspaceRoot);
149
+ } catch {
150
+ return undefined;
151
+ }
152
+ }
153
+
144
154
  function extractTriggerPhrases(
145
- workspaceRoot: string,
155
+ forgeRoot: string | undefined,
146
156
  ): Array<{ name: string; triggerPhrases: string[] }> {
147
- const forgeRoot = path.join(workspaceRoot, "packages", "forge");
157
+ // Consumer packages have no packages/forge — the root must come from the
158
+ // runtime context (or the installed package), never a hardcoded monorepo path.
159
+ if (!forgeRoot) return [];
148
160
  const result: Array<{ name: string; triggerPhrases: string[] }> = [];
149
161
  for (const skill of FORGE_SKILLS) {
150
162
  if (skill.category !== "fo") continue;
@@ -187,10 +199,10 @@ function generateTriggersTable(
187
199
  }
188
200
 
189
201
  function generateBehavioralLayer(
190
- workspaceRoot: string,
191
202
  register: BehavioralRegister,
203
+ forgeRoot: string | undefined,
192
204
  ): string {
193
- const triggerPhrases = extractTriggerPhrases(workspaceRoot);
205
+ const triggerPhrases = extractTriggerPhrases(forgeRoot);
194
206
 
195
207
  let template: string;
196
208
  try {
@@ -355,7 +367,10 @@ export async function runAgentsGenerate(
355
367
  }
356
368
 
357
369
  // Behavioral layer section (RFC-0548)
358
- const behavioralLayer = generateBehavioralLayer(workspaceRoot, register);
370
+ const behavioralLayer = generateBehavioralLayer(
371
+ register,
372
+ context.forgeRoot ?? safeResolveForgeRoot(workspaceRoot),
373
+ );
359
374
  dynamicLines.push(behavioralLayer);
360
375
  dynamicLines.push("");
361
376
 
@@ -10,7 +10,7 @@ routing table, conditional extended layer, markers, and idempotency (RFC-0548, R
10
10
  */
11
11
 
12
12
  import { test, expect, beforeEach, afterEach } from "vitest";
13
- import { mkdtemp, rm, writeFile, mkdir, readFile } from "node:fs/promises";
13
+ import { mkdtemp, rm, writeFile, mkdir, readFile, symlink } from "node:fs/promises";
14
14
  import { existsSync } from "node:fs";
15
15
  import { tmpdir } from "node:os";
16
16
  import { join } from "node:path";
@@ -108,6 +108,27 @@ test("agents-generate includes intent-to-skill routing table", async () => {
108
108
  expect(agentsMd).toContain("fo-");
109
109
  });
110
110
 
111
+ test("agents-generate populates routing table rows in consumer layout (no packages/forge)", async () => {
112
+ await makeForgeYaml(tempDir);
113
+ // Consumer install: the package lives under node_modules — skills ship inside
114
+ // it. A symlinked node_modules/@warpgogol/forge mimics the npm layout.
115
+ const consumerPkg = join(tempDir, "node_modules", "@warpgogol", "forge");
116
+ await mkdir(join(consumerPkg, ".."), { recursive: true });
117
+ await symlink(FORGE_ROOT, consumerPkg, "junction");
118
+
119
+ // forgeRoot undefined → extraction must resolve via the consumer layout.
120
+ const result = await runAgentsGenerate(
121
+ { argv: [], flags: {} },
122
+ { ...makeContext(tempDir), forgeRoot: undefined },
123
+ );
124
+ expect(result.exitCode).toBe(0);
125
+
126
+ const agentsMd = await readFile(join(tempDir, "AGENTS.md"), "utf8");
127
+ // A quoted trigger phrase appears only in routing-table rows — the Skills
128
+ // table contains bare skill names, so this probes extraction end-to-end.
129
+ expect(agentsMd).toContain('"show RFC and ADR status"');
130
+ });
131
+
111
132
  test("agents-generate includes fixed policy text sections", async () => {
112
133
  await makeForgeYaml(tempDir);
113
134
  const result = await runAgentsGenerate({ argv: [], flags: {} }, makeContext(tempDir));
@@ -0,0 +1,169 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for src/cli-flags.ts — schema-driven argv-to-flags
4
+ resolution for the standalone forge CLI (strict KERNEL-FLAG-01/02/03 and
5
+ KERNEL-ARG-01 diagnostics, legacy heuristic path for schema-less commands).</purpose>
6
+ <non-goals>
7
+ <item>Do not test bin/cli.ts dispatch — these tests cover pure functions only.</item>
8
+ </non-goals>
9
+ </MODULE_CONTRACT>
10
+ <CHANGE_SUMMARY>
11
+ <item>Initial cli-flags resolution tests — covers the `forge rfc.validate --id` crash regression (value-less declared string flag must produce KERNEL-FLAG-02, not a boolean).</item>
12
+ </CHANGE_SUMMARY>
13
+ */
14
+
15
+ import { describe, expect, test } from "vitest";
16
+ import { parseCliArgs, resolveCliFlags } from "../cli-flags.ts";
17
+ import type { ForgeCommandDefinition, ForgeFlagSpec } from "../types.ts";
18
+
19
+ function command(flags?: Record<string, ForgeFlagSpec>): ForgeCommandDefinition {
20
+ return {
21
+ name: "test.command",
22
+ description: "test command",
23
+ scope: "workspace",
24
+ flags,
25
+ execute: () => {},
26
+ };
27
+ }
28
+
29
+ const errorRuleIds = (diagnostics: { ruleId: string; severity: string }[]) =>
30
+ diagnostics.filter((d) => d.severity === "error").map((d) => d.ruleId);
31
+
32
+ describe("resolveCliFlags — strict (schema-carrying) path", () => {
33
+ const rfcValidate = command({
34
+ id: { kind: "string", description: "Target a single RFC by id." },
35
+ });
36
+
37
+ test("value-less declared string flag errors with KERNEL-FLAG-02 instead of becoming `true`", () => {
38
+ const { flags, diagnostics } = resolveCliFlags(["--id"], rfcValidate);
39
+ expect(flags["id"]).toBeUndefined();
40
+ expect(errorRuleIds(diagnostics)).toEqual(["KERNEL-FLAG-02"]);
41
+ expect(diagnostics[0]!.message).toContain('Flag "--id"');
42
+ expect(diagnostics[0]!.message).toContain('"test.command"');
43
+ });
44
+
45
+ test("value-less declared string flag before another flag still errors", () => {
46
+ const { flags, diagnostics } = resolveCliFlags(["--id", "--json"], rfcValidate);
47
+ expect(flags["id"]).toBeUndefined();
48
+ expect(flags["json"]).toBe(true);
49
+ expect(errorRuleIds(diagnostics)).toEqual(["KERNEL-FLAG-02"]);
50
+ });
51
+
52
+ test("--id <value> and --id=<value> resolve to the string", () => {
53
+ for (const argv of [["--id", "RFC-0609"], ["--id=RFC-0609"]]) {
54
+ const { flags, diagnostics } = resolveCliFlags(argv, rfcValidate);
55
+ expect(flags["id"]).toBe("RFC-0609");
56
+ expect(errorRuleIds(diagnostics)).toEqual([]);
57
+ }
58
+ });
59
+
60
+ test("repeated single-value string flag errors instead of promoting to an array", () => {
61
+ const { flags, diagnostics } = resolveCliFlags(["--id", "RFC-1", "--id", "RFC-2"], rfcValidate);
62
+ expect(errorRuleIds(diagnostics)).toEqual(["KERNEL-FLAG-02"]);
63
+ expect(diagnostics[0]!.message).toContain("single value");
64
+ expect(flags["id"]).toEqual(["RFC-1", "RFC-2"]);
65
+ });
66
+
67
+ test("unknown flag errors with KERNEL-FLAG-01 listing valid flags", () => {
68
+ const { diagnostics } = resolveCliFlags(["--idd"], rfcValidate);
69
+ expect(errorRuleIds(diagnostics)).toEqual(["KERNEL-FLAG-01"]);
70
+ expect(diagnostics[0]!.message).toContain("id");
71
+ });
72
+
73
+ test("unknown flag followed by a bare token also flags the token as positional", () => {
74
+ const { diagnostics } = resolveCliFlags(["--idd", "RFC-1"], rfcValidate);
75
+ expect(errorRuleIds(diagnostics)).toEqual(["KERNEL-FLAG-01", "KERNEL-ARG-01"]);
76
+ });
77
+
78
+ test("required flag missing errors with KERNEL-FLAG-03", () => {
79
+ const cmd = command({
80
+ name: { kind: "string", required: true, description: "Required name." },
81
+ });
82
+ const { diagnostics } = resolveCliFlags([], cmd);
83
+ expect(errorRuleIds(diagnostics)).toEqual(["KERNEL-FLAG-03"]);
84
+ });
85
+
86
+ test("declared default applies when flag absent", () => {
87
+ const cmd = command({
88
+ mode: { kind: "string", default: "all", description: "Mode." },
89
+ });
90
+ const { flags, diagnostics } = resolveCliFlags([], cmd);
91
+ expect(flags["mode"]).toBe("all");
92
+ expect(errorRuleIds(diagnostics)).toEqual([]);
93
+ });
94
+
95
+ test("positional token errors with KERNEL-ARG-01", () => {
96
+ const { diagnostics } = resolveCliFlags(["RFC-0609"], rfcValidate);
97
+ expect(errorRuleIds(diagnostics)).toEqual(["KERNEL-ARG-01"]);
98
+ });
99
+
100
+ test("tokens after -- are positional errors", () => {
101
+ const { diagnostics } = resolveCliFlags(["--id", "RFC-1", "--", "extra"], rfcValidate);
102
+ expect(errorRuleIds(diagnostics)).toEqual(["KERNEL-ARG-01"]);
103
+ });
104
+
105
+ test("universal flags accepted on commands with empty schema", () => {
106
+ const { flags, diagnostics } = resolveCliFlags(
107
+ ["--json", "--dry-run", "--help", "--site", "x"],
108
+ command({}),
109
+ );
110
+ expect(flags["json"]).toBe(true);
111
+ expect(flags["dry-run"]).toBe(true);
112
+ expect(flags["help"]).toBe(true);
113
+ expect(flags["site"]).toBe("x");
114
+ expect(errorRuleIds(diagnostics)).toEqual([]);
115
+ });
116
+
117
+ test("universal boolean does not swallow the next flag's token", () => {
118
+ const cmd = command({ fix: { kind: "boolean", description: "Fix." } });
119
+ const { flags, diagnostics } = resolveCliFlags(["--json", "--fix"], cmd);
120
+ expect(flags["json"]).toBe(true);
121
+ expect(flags["fix"]).toBe(true);
122
+ expect(errorRuleIds(diagnostics)).toEqual([]);
123
+ });
124
+
125
+ test("--flag=false on a declared boolean resolves to false", () => {
126
+ const cmd = command({ fix: { kind: "boolean", description: "Fix." } });
127
+ const { flags, diagnostics } = resolveCliFlags(["--fix=false"], cmd);
128
+ expect(flags["fix"]).toBe(false);
129
+ expect(errorRuleIds(diagnostics)).toEqual([]);
130
+ });
131
+
132
+ test("string[] flag collects repeated values", () => {
133
+ const cmd = command({
134
+ files: { kind: "string[]", description: "Files." },
135
+ });
136
+ const { flags, diagnostics } = resolveCliFlags(["--files", "a", "--files", "b"], cmd);
137
+ expect(flags["files"]).toEqual(["a", "b"]);
138
+ expect(errorRuleIds(diagnostics)).toEqual([]);
139
+ });
140
+ });
141
+
142
+ describe("resolveCliFlags — legacy (schema-less) path", () => {
143
+ const schemaLess = command(undefined);
144
+
145
+ test("unknown flags still parse heuristically", () => {
146
+ const { flags, diagnostics } = resolveCliFlags(["--anything", "v"], schemaLess);
147
+ expect(flags["anything"]).toBe("v");
148
+ expect(errorRuleIds(diagnostics)).toEqual([]);
149
+ });
150
+
151
+ test("known boolean switches do not consume the next token", () => {
152
+ const { flags } = resolveCliFlags(["--json", "v"], schemaLess);
153
+ expect(flags["json"]).toBe(true);
154
+ });
155
+
156
+ test("positional token errors with KERNEL-ARG-01", () => {
157
+ const { diagnostics } = resolveCliFlags(["positional"], schemaLess);
158
+ expect(errorRuleIds(diagnostics)).toEqual(["KERNEL-ARG-01"]);
159
+ });
160
+ });
161
+
162
+ describe("parseCliArgs", () => {
163
+ test("--key=value and --key value forms both resolve", () => {
164
+ const { flags } = parseCliArgs(["--a=1", "--b", "2", "--json"]);
165
+ expect(flags["a"]).toBe("1");
166
+ expect(flags["b"]).toBe("2");
167
+ expect(flags["json"]).toBe(true);
168
+ });
169
+ });
@@ -103,8 +103,37 @@ This section defines the agent's core behavioral contract. It is generated from
103
103
 
104
104
  When the operator expresses an intent in natural language, the agent routes to the matching skill. The routing table is generated from `triggerPhrases` fields in skill frontmatter.
105
105
 
106
- | Operator says something like | Skill |
107
- | ---------------------------- | ----- |
106
+ | Operator says something like | Skill |
107
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
108
+ | "write tests for this code", "add unit tests", "add property-based tests", "write test coverage for this function" | `fo-add-tests` |
109
+ | "analyze codebase architecture", "find deepening opportunities", "review code structure quality" | `fo-architecture` |
110
+ | "manage Compass headers", "annotate source files with Compass", "audit Compass module contracts" | `fo-compass-annotate` |
111
+ | "design summit", "multi-persona review", "party mode" | `fo-design-summit` |
112
+ | "audit documentation sync", "update docs after code changes", "check AGENTS.md and README are in sync" | `fo-doc-audit` |
113
+ | "explore this idea", "let me think about this", "what are the options for", "what if we" | `fo-explore` |
114
+ | "extract DNA invariant", "discover architectural invariants", "formalize implicit architectural rules" | `fo-extract-dna` |
115
+ | "fix issues from review", "apply review findings and fix", "iterative fix workflow for code" | `fo-fix` |
116
+ | "create a handoff document", "compact conversation for next agent", "prepare handoff for another agent" | `fo-handoff` |
117
+ | "harvest portable patterns from code", "extract reusable code pattern to forge", "port this pattern to forge" | `fo-harvest` |
118
+ | "I have an idea for a change", "analyze this idea and route it", "decompose this feature idea", "does this need an RFC", "should I write an ADR for this" | `fo-idea` |
119
+ | "audit this RFC", "check RFC for ecosystem fit", "review RFC against DNA and forward-only rules" | `fo-idea-audit` |
120
+ | "record this architectural decision", "create an ADR", "document a local technical decision" | `fo-idea-create-adr` |
121
+ | "draft an RFC", "create a full RFC", "scaffold an RFC from template" | `fo-idea-create-rfc` |
122
+ | "enhance this RFC", "fix audit findings in RFC", "improve RFC based on audit" | `fo-idea-enhance` |
123
+ | "I just want to see the plan", "plan this feature without implementing", "run idea to plan pipeline" | `fo-idea-i-just-want-to-see-the-plan` |
124
+ | "I just want to see the result", "run the full pipeline automatically", "implement this end-to-end without pauses" | `fo-idea-i-just-want-to-see-the-result` |
125
+ | "implement this RFC", "execute the implementation plan", "realize this RFC end-to-end" | `fo-idea-implement` |
126
+ | "plan the implementation for this RFC", "create implementation plan", "map affected artifacts and draft steps" | `fo-idea-plan` |
127
+ | "show RFC and ADR status", "what is the status of this RFC", "list RFCs by status" | `fo-idea-status` |
128
+ | "distill knowledge", "compact skill knowledge", "knowledge lifecycle", "promote fix patterns" | `fo-knowledge-distill` |
129
+ | "sync memory from codex", "import external agent knowledge", "recall sessions from other tools", "sync memory from claude code" | `fo-memory-sync` |
130
+ | "I found a bug", "file a GitHub issue for this bug", "report a bug conversationally", "interactive QA session" | `fo-qa` |
131
+ | "review this code diff", "check code against Forge standards", "fitness check on code changes" | `fo-review` |
132
+ | "session retrospective", "triage session discoveries", "Завершаем эту сессию", "End session", "/session-end" | `fo-session-retro` |
133
+ | "save this session", "enhance session transcript with annotations", "save session with quality checks" | `fo-session-save` |
134
+ | "ingest external specification", "vendor spec package into docs", "author spec skeleton" | `fo-spec-ingest` |
135
+ | "commit after request", "auto-commit changes", "step commit" | `fo-step-commit` |
136
+ | "triage this issue", "categorize incoming bug report", "triage external pull request", "process feature request" | `fo-triage` |
108
137
 
109
138
  The agent uses judgment to calibrate routing — minor edits (typo fixes, small CSS changes) do not require skill invocation, while significant changes (new features, architectural decisions) do.
110
139