@cassiomc1/forgeloop 1.12.0 → 1.13.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 (62) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CLAUDE.md +1 -1
  4. package/CONTRIBUTING.md +90 -0
  5. package/DOCS_INDEX.md +13 -11
  6. package/ENG/c-development-eng.md +112 -0
  7. package/ENG/cpp-development-eng.md +109 -0
  8. package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
  9. package/ENG/go-development-eng.md +103 -0
  10. package/ENG/java-development-eng.md +125 -0
  11. package/ENG/nodejs-backend-development-eng.md +605 -0
  12. package/ENG/php-development-eng.md +104 -0
  13. package/ENG/rust-development-eng.md +422 -0
  14. package/ENG/sql-development-eng.md +108 -0
  15. package/ENG/swift-development-eng.md +111 -0
  16. package/ENG/typescript-development-eng.md +108 -0
  17. package/GUIDE_ROUTER.md +418 -9
  18. package/QUALITY_SCORECARD.md +1 -0
  19. package/README.md +44 -33
  20. package/THIRD_PARTY_NOTICES.md +19 -7
  21. package/completions/_forgeloop +3 -3
  22. package/completions/forgeloop.bash +3 -3
  23. package/completions/forgeloop.fish +7 -0
  24. package/docs/AGENT_PROTOCOL_SUMMARY.md +55 -2
  25. package/docs/CLI_REFERENCE.md +28 -6
  26. package/docs/DOCUMENTATION_GUIDE.md +2 -1
  27. package/docs/GETTING_STARTED.md +59 -0
  28. package/docs/PACKAGE_CONTENTS.md +28 -14
  29. package/docs/RECIPES.md +23 -0
  30. package/docs/RELEASE_CHECKLIST.md +30 -2
  31. package/docs/TROUBLESHOOTING.md +100 -2
  32. package/docs/documentation-manifest.json +652 -0
  33. package/docs/protocol-requirements.json +77 -0
  34. package/package.json +19 -4
  35. package/schemas/routing-input.schema.json +1 -1
  36. package/scripts/CI_VALIDATORS.md +84 -11
  37. package/scripts/generate-agent-protocol-summary.mjs +36 -0
  38. package/src/commands/next.js +19 -7
  39. package/src/commands/task-create.js +84 -25
  40. package/src/commands/task-list.js +22 -2
  41. package/src/config/guides.json +44 -0
  42. package/src/core/build-script.js +151 -0
  43. package/src/core/c-cpp-project.js +143 -0
  44. package/src/core/cli-command-definitions.js +8 -1
  45. package/src/core/command-executors.js +5 -3
  46. package/src/core/command-input.js +140 -102
  47. package/src/core/contract-presets.js +82 -0
  48. package/src/core/error-codes.js +3 -3
  49. package/src/core/filesystem.js +1 -10
  50. package/src/core/go-project.js +206 -0
  51. package/src/core/java-project.js +403 -0
  52. package/src/core/multi-language-project.js +117 -0
  53. package/src/core/next-explanation.js +63 -0
  54. package/src/core/php-project.js +85 -0
  55. package/src/core/project-detection.js +1760 -52
  56. package/src/core/reconcile-closure.js +4 -1
  57. package/src/core/router.js +156 -3
  58. package/src/core/rust-project.js +400 -0
  59. package/src/core/sql-project.js +141 -0
  60. package/src/core/swift-project.js +200 -0
  61. package/src/core/typescript-project.js +349 -0
  62. package/src/core/xml-structure.js +123 -0
@@ -0,0 +1,151 @@
1
+ function quotedValue(text, start) {
2
+ const quote = text[start];
3
+ let value = "";
4
+ let escaped = false;
5
+ for (let index = start + 1; index < text.length; index += 1) {
6
+ const character = text[index];
7
+ if (escaped) {
8
+ value += character;
9
+ escaped = false;
10
+ } else if (character === "\\") {
11
+ escaped = true;
12
+ } else if (character === quote) {
13
+ return { value, next: index + 1 };
14
+ } else {
15
+ value += character;
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function bracketEnd(text, start) {
22
+ const marker = text.slice(start).match(/^\[(=*)\[/u);
23
+ if (!marker) return null;
24
+ const closing = `]${marker[1]}]`;
25
+ const end = text.indexOf(closing, start + marker[0].length);
26
+ return end < 0 ? null : { next: end + closing.length };
27
+ }
28
+
29
+ function maskRange(text, start, end) {
30
+ return text.slice(start, end).replace(/[^\n\r]/gu, " ");
31
+ }
32
+
33
+ function skipHashComment(text, start) {
34
+ const bracket = bracketEnd(text, start + 1);
35
+ if (text[start + 1] === "[" && bracket) return bracket.next;
36
+ const lineEnd = text.slice(start).search(/[\n\r]/u);
37
+ return lineEnd < 0 ? text.length : start + lineEnd;
38
+ }
39
+
40
+ function skipBuildToken(text, start) {
41
+ if (text[start] === "#") return { next: skipHashComment(text, start) };
42
+ if (text[start] === "\"" || text[start] === "'") {
43
+ const quoted = quotedValue(text, start);
44
+ return quoted ? { next: quoted.next } : null;
45
+ }
46
+ const bracket = bracketEnd(text, start);
47
+ return bracket ? { next: bracket.next } : { next: start + 1 };
48
+ }
49
+
50
+ export function maskBuildScript(text) {
51
+ if (typeof text !== "string") return null;
52
+ let result = "";
53
+ for (let index = 0; index < text.length;) {
54
+ const step = skipBuildToken(text, index);
55
+ if (!step) return null;
56
+ result += step.next === index + 1
57
+ ? text[index]
58
+ : maskRange(text, index, step.next);
59
+ index = step.next;
60
+ }
61
+ return result;
62
+ }
63
+
64
+ function callEnd(text, open) {
65
+ let depth = 1;
66
+ for (let index = open + 1; index < text.length; index += 1) {
67
+ const character = text[index];
68
+ const step = skipBuildToken(text, index);
69
+ if (!step) return null;
70
+ if (step.next !== index + 1) {
71
+ index = step.next - 1;
72
+ continue;
73
+ }
74
+ if (character === "(") depth += 1;
75
+ else if (character === ")") {
76
+ depth -= 1;
77
+ if (depth === 0) return index;
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+
83
+ export function extractBuildCalls(text, names, { caseInsensitive = false } = {}) {
84
+ if (typeof text !== "string" || !Array.isArray(names)) return null;
85
+ const wanted = new Set(names.map((name) => caseInsensitive ? name.toLowerCase() : name));
86
+ const calls = [];
87
+ for (let index = 0; index < text.length;) {
88
+ const step = skipBuildToken(text, index);
89
+ if (!step) return null;
90
+ if (step.next !== index + 1) {
91
+ index = step.next;
92
+ continue;
93
+ }
94
+ const name = text.slice(index).match(/^[A-Za-z_][A-Za-z0-9_]*/u)?.[0];
95
+ if (!name || !wanted.has(caseInsensitive ? name.toLowerCase() : name)) {
96
+ index += name?.length ?? 1;
97
+ continue;
98
+ }
99
+ let open = index + name.length;
100
+ while (/\s/u.test(text[open] ?? "")) open += 1;
101
+ if (text[open] !== "(") {
102
+ index += name.length;
103
+ continue;
104
+ }
105
+ const close = callEnd(text, open);
106
+ if (close === null) return null;
107
+ calls.push({ name, body: text.slice(open + 1, close) });
108
+ index = close + 1;
109
+ }
110
+ return calls;
111
+ }
112
+
113
+ function splitBuildArguments(text) {
114
+ const parts = [];
115
+ let start = 0;
116
+ const depths = { "(": 0, "[": 0, "{": 0 };
117
+ for (let index = 0; index < text.length;) {
118
+ const step = skipBuildToken(text, index);
119
+ if (!step) return null;
120
+ if (step.next !== index + 1) {
121
+ index = step.next;
122
+ continue;
123
+ }
124
+ const character = text[index];
125
+ if (character === "(" || character === "[" || character === "{") depths[character] += 1;
126
+ if (character === ")" || character === "]" || character === "}") {
127
+ const opening = character === ")" ? "(" : character === "]" ? "[" : "{";
128
+ depths[opening] -= 1;
129
+ if (depths[opening] < 0) return null;
130
+ }
131
+ if (character === "," && Object.values(depths).every((depth) => depth === 0)) {
132
+ parts.push(text.slice(start, index).trim());
133
+ start = index + 1;
134
+ }
135
+ index += 1;
136
+ }
137
+ if (Object.values(depths).some((depth) => depth !== 0)) return null;
138
+ parts.push(text.slice(start).trim());
139
+ return parts.filter(Boolean);
140
+ }
141
+
142
+ export function quotedBuildArguments(text) {
143
+ const parts = splitBuildArguments(text);
144
+ if (!parts) return null;
145
+ return parts.flatMap((part) => {
146
+ const value = part.length >= 2 && (part[0] === "\"" || part[0] === "'")
147
+ ? quotedValue(part, 0)
148
+ : null;
149
+ return value && value.next === part.length ? [value.value] : [];
150
+ });
151
+ }
@@ -0,0 +1,143 @@
1
+ import { createLanguageProject, C_SOURCE_EXTENSIONS, CPP_SOURCE_EXTENSIONS, pathBelongsToRoot, portablePath } from "./multi-language-project.js";
2
+ import { extractBuildCalls, maskBuildScript, quotedBuildArguments } from "./build-script.js";
3
+
4
+ function invalidNative() {
5
+ return {
6
+ valid: false,
7
+ c: false,
8
+ cpp: false,
9
+ make: false,
10
+ };
11
+ }
12
+
13
+ function nativeLanguageTokens(text) {
14
+ const tokens = text.toUpperCase().match(/\b(?:CXX|CPP|C)\b/gu) ?? [];
15
+ return {
16
+ c: tokens.includes("C"),
17
+ cpp: tokens.some((language) => language === "CXX" || language === "CPP"),
18
+ };
19
+ }
20
+
21
+ function nativeLanguagesFromCMake(text) {
22
+ const result = { c: false, cpp: false };
23
+ const calls = extractBuildCalls(text, ["project", "enable_language"], { caseInsensitive: true });
24
+ if (!calls) return result;
25
+ for (const call of calls) {
26
+ const body = maskBuildScript(call.body);
27
+ if (body === null) continue;
28
+ if (call.name.toLowerCase() === "enable_language") {
29
+ const languages = nativeLanguageTokens(body);
30
+ result.c ||= languages.c;
31
+ result.cpp ||= languages.cpp;
32
+ continue;
33
+ }
34
+ const explicit = body.match(/\blanguages?\b([\s\S]*)/iu)?.[1]
35
+ ?? body.match(/^\s*[^\s]+\s+((?:C|CXX|CPP)\b[\s\S]*)/iu)?.[1]
36
+ ?? "";
37
+ const languages = nativeLanguageTokens(explicit);
38
+ result.c ||= languages.c;
39
+ result.cpp ||= languages.cpp;
40
+ }
41
+ return result;
42
+ }
43
+
44
+ export function parseCMake(text) {
45
+ if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidNative();
46
+ const source = maskBuildScript(text);
47
+ if (source === null) return invalidNative();
48
+ const languages = nativeLanguagesFromCMake(text);
49
+ const hasProject = /\bproject\s*\(/iu.test(source);
50
+ const hasLanguage = /\benable_language\s*\(/iu.test(source);
51
+ return { valid: hasProject || hasLanguage, ...languages, make: false };
52
+ }
53
+
54
+ export function parseMeson(text) {
55
+ if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidNative();
56
+ const calls = extractBuildCalls(text, ["project", "add_languages"]);
57
+ if (!calls || calls.length === 0) return invalidNative();
58
+ const languageValues = calls.flatMap((call) => {
59
+ const values = quotedBuildArguments(call.body) ?? [];
60
+ return call.name === "project" ? values.slice(1) : values;
61
+ });
62
+ return {
63
+ valid: true,
64
+ c: languageValues.some((value) => value.toLowerCase() === "c"),
65
+ cpp: languageValues.some((value) => ["cpp", "cxx"].includes(value.toLowerCase())),
66
+ make: false,
67
+ };
68
+ }
69
+
70
+ export function parseBazelNative(text) {
71
+ if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidNative();
72
+ const source = maskBuildScript(text);
73
+ const hasNativeRule = source !== null && /\b(?:cc_|c_)(?:library|binary|test|shared_library|static_library)\s*\(/u.test(source);
74
+ return { valid: hasNativeRule, c: false, cpp: false, make: false };
75
+ }
76
+
77
+ export function parseMakefile(text) {
78
+ if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidNative();
79
+ return { valid: true, c: false, cpp: false, make: true };
80
+ }
81
+
82
+ function sourceEvidence(sourceFiles, targetRoot, projectRoot, projectRoots) {
83
+ const c = sourceFiles.some((file) => C_SOURCE_EXTENSIONS.has(file.extension)
84
+ && pathBelongsToRoot(portablePath(targetRoot, file.path), projectRoot, projectRoots));
85
+ const cpp = sourceFiles.some((file) => CPP_SOURCE_EXTENSIONS.has(file.extension)
86
+ && pathBelongsToRoot(portablePath(targetRoot, file.path), projectRoot, projectRoots));
87
+ return { c, cpp };
88
+ }
89
+
90
+ function parseNativeManifest(manifestName, manifestText) {
91
+ const normalizedName = manifestName.toLowerCase();
92
+ if (normalizedName.includes("cmake")) return parseCMake(manifestText);
93
+ if (normalizedName.includes("meson")) return parseMeson(manifestText);
94
+ if (["makefile", "gnumakefile"].includes(normalizedName)) return parseMakefile(manifestText);
95
+ return parseBazelNative(manifestText);
96
+ }
97
+
98
+ function nativeFrameworks(parsed, source) {
99
+ return {
100
+ c: parsed.c || (parsed.valid && source.c),
101
+ cpp: parsed.cpp || (parsed.valid && source.cpp),
102
+ };
103
+ }
104
+
105
+ function nativePrimarySignals({ manifestRelative, projectRoot, parsed, source, c, cpp }) {
106
+ if (!c && !cpp) return [];
107
+ return [
108
+ ...(c && parsed.c ? [`${manifestRelative}:c`] : []),
109
+ ...(cpp && parsed.cpp ? [`${manifestRelative}:cpp`] : []),
110
+ ...(c && source.c && !parsed.c ? [`${projectRoot}/source:c`] : []),
111
+ ...(cpp && source.cpp && !parsed.cpp ? [`${projectRoot}/source:cpp`] : []),
112
+ ];
113
+ }
114
+
115
+ export function inspectNativeProject({ projectRoot, manifestRelative, manifestText, manifestName, targetRoot, sourceFiles, projectRoots }) {
116
+ const parsed = parseNativeManifest(manifestName, manifestText);
117
+ const source = sourceEvidence(sourceFiles, targetRoot, projectRoot, projectRoots);
118
+ const { c, cpp } = nativeFrameworks(parsed, source);
119
+ const primary = c || cpp;
120
+ const frameworks = [];
121
+ if (c) frameworks.push("c");
122
+ if (cpp) frameworks.push("cpp");
123
+ const primarySignals = nativePrimarySignals({ manifestRelative, projectRoot, parsed, source, c, cpp });
124
+ const supportingSignals = parsed.make ? [`${manifestRelative}:make`] : [];
125
+ return createLanguageProject({
126
+ kind: "native",
127
+ root: projectRoot,
128
+ manifest: manifestRelative,
129
+ frameworks,
130
+ primary,
131
+ primarySignals,
132
+ supportingSignals,
133
+ internal: { ...parsed, valid: parsed.valid },
134
+ });
135
+ }
136
+
137
+ export function nativeFrameworkForSource(fileName) {
138
+ const rawExtension = fileName.match(/\.[^.]+$/u)?.[0] ?? "";
139
+ const extension = rawExtension === ".C" ? ".C" : rawExtension.toLowerCase();
140
+ if (C_SOURCE_EXTENSIONS.has(extension)) return "c";
141
+ if (CPP_SOURCE_EXTENSIONS.has(extension)) return "cpp";
142
+ return null;
143
+ }
@@ -324,6 +324,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
324
324
  ...CLI_COMMON_OPTIONS,
325
325
  ...CLI_TASK_OPTION,
326
326
  "--compact": Object.freeze({ targetKey: "compact", parseType: "boolean", takesValue: false, description: "emit a bounded next-action projection" }),
327
+ "--explain": Object.freeze({ targetKey: "explain", parseType: "boolean", takesValue: false, description: "include bounded read-only blocker and recovery explanation" }),
327
328
  "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
328
329
  }),
329
330
  writes: [],
@@ -991,7 +992,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
991
992
  writes: [".forgeloop/task-state/<taskKey>/executions/exec-<id>.json", ".forgeloop/task-state/<taskKey>/work-state.json", ".forgeloop/task-state/<taskKey>/events.ndjson"],
992
993
  removes: [],
993
994
  mayExecuteExternalProcess: true,
994
- description: "Refreshes the work-state checkpoint of an EXECUTING task whose objective is already satisfied in the current repository, after contract-bound executed evidence, so canonical completion can proceed.",
995
+ description: "Refreshes the work-state checkpoint of an EXECUTING, VERIFYING, or REVIEWING task whose objective is already satisfied in the current repository, after contract-bound executed evidence, so canonical completion can proceed.",
995
996
  }),
996
997
  "validate-receipt": Object.freeze({
997
998
  name: "validate-receipt",
@@ -1038,6 +1039,8 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
1038
1039
  ...CLI_TASK_OPTION,
1039
1040
  "--claim": Object.freeze({ targetKey: "claims", parseType: "string", takesValue: true, repeatable: true, valueName: "path", missingValueMessage: "--claim requires a path", description: "scoped file path or directory prefix claimed for mutation" }),
1040
1041
  "--contract-file": Object.freeze({ targetKey: "contractFile", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--contract-file requires a path", description: "path to initial contract file" }),
1042
+ "--preset": Object.freeze({ targetKey: "preset", parseType: "string", takesValue: true, valueName: "name", missingValueMessage: "--preset requires a name", description: "bounded contract preset: documentation, bug, feature, or release" }),
1043
+ "--preview": Object.freeze({ targetKey: "preview", parseType: "boolean", takesValue: false, description: "preview the contract without creating lifecycle state" }),
1041
1044
  "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
1042
1045
  }),
1043
1046
  writes: [".forgeloop/task-state/<taskKey>/task.json", ".forgeloop/task-state/<taskKey>/contract.json"],
@@ -1051,6 +1054,10 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
1051
1054
  mutation: "READ_ONLY",
1052
1055
  options: Object.freeze({
1053
1056
  ...CLI_COMMON_OPTIONS,
1057
+ "--phase": Object.freeze({ targetKey: "phase", parseType: "string", takesValue: true, valueName: "phase", missingValueMessage: "--phase requires a phase", description: "only return tasks in this lifecycle phase" }),
1058
+ "--active": Object.freeze({ targetKey: "active", parseType: "boolean", takesValue: false, description: "only return healthy tasks with active ownership and a non-terminal phase" }),
1059
+ "--limit": Object.freeze({ targetKey: "limit", parseType: "non-negative-integer", takesValue: true, valueName: "number", missingValueMessage: "--limit requires a non-negative integer", description: "maximum tasks to return" }),
1060
+ "--offset": Object.freeze({ targetKey: "offset", parseType: "non-negative-integer", takesValue: true, valueName: "number", missingValueMessage: "--offset requires a non-negative integer", description: "number of sorted tasks to skip" }),
1054
1061
  "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
1055
1062
  }),
1056
1063
  writes: [],
@@ -170,7 +170,7 @@ export const COMMAND_EXECUTORS = {
170
170
  exitCode: 0,
171
171
  }),
172
172
  next: async ({ target, packageRoot, options, authorityContext, runtimeContext }) => ({
173
- result: await runNext({ target, packageRoot, taskId: options.taskId, authorityContext, runtimeContext, compact: options.compact }),
173
+ result: await runNext({ target, packageRoot, taskId: options.taskId, authorityContext, runtimeContext, compact: options.compact, explain: options.explain }),
174
174
  exitCode: 0,
175
175
  }),
176
176
  continuity: async ({ target, packageRoot, options }) => ({
@@ -600,11 +600,13 @@ export const COMMAND_EXECUTORS = {
600
600
  taskId: options.taskId,
601
601
  claims: options.claims,
602
602
  contractFile: options.contractFile,
603
+ preset: options.preset,
604
+ preview: options.preview,
603
605
  }),
604
606
  exitCode: 0,
605
607
  }),
606
- "task-list": async ({ target, packageRoot }) => ({
607
- result: await runTaskList({ target, packageRoot }),
608
+ "task-list": async ({ target, packageRoot, options }) => ({
609
+ result: await runTaskList({ target, packageRoot, phase: options.phase, active: options.active, limit: options.limit, offset: options.offset }),
608
610
  exitCode: 0,
609
611
  }),
610
612
  "task-show": async ({ target, packageRoot, options }) => ({
@@ -14,6 +14,133 @@ function validateSearchCommandInput(command, options, help) {
14
14
  }
15
15
  }
16
16
 
17
+ function validatePolicyInput(command, options) {
18
+ if (command === "policy" && !options.policy) throw inputError("policy requires a name");
19
+ if (command !== "policy" && options.policy) throw inputError(`Policy name is not valid for ${command}`);
20
+ }
21
+
22
+ function validateTaskCreationInput(command, options) {
23
+ if (command === "bundle" && !options.taskId) throw inputError("bundle requires --task");
24
+ if (command === "task-create" && !options.taskId) throw inputError("task-create requires --task");
25
+ if (command !== "task-create" && (options.preset || options.preview)) {
26
+ throw inputError(`preset/preview options are not valid for ${command}`);
27
+ }
28
+ if (command === "task-create" && options.preview && !options.preset && !options.contractFile) {
29
+ throw inputError("task-create --preview requires --preset or --contract-file");
30
+ }
31
+ if (command === "task-create" && options.preset && options.contractFile) {
32
+ throw inputError("task-create accepts either --preset or --contract-file, not both");
33
+ }
34
+ const hasTaskListFilter = Boolean(
35
+ options.phase
36
+ || options.active
37
+ || (options.limit !== undefined && options.limit !== null)
38
+ || (options.offset !== undefined && options.offset !== null && options.offset !== 0),
39
+ );
40
+ if (command !== "task-list" && hasTaskListFilter) {
41
+ throw inputError(`task-list filters are not valid for ${command}`);
42
+ }
43
+ }
44
+
45
+ function validateExecutionProfileInput(command, options) {
46
+ if (options.executionProfile !== null && options.executionProfile !== undefined) {
47
+ if (command !== "route") throw inputError(`executionProfile is not valid for ${command}`);
48
+ if (!EXECUTION_PROFILE_REQUESTS.includes(options.executionProfile)) {
49
+ throw inputError(`route --execution-profile must be one of ${EXECUTION_PROFILE_REQUESTS.join(", ")}`);
50
+ }
51
+ }
52
+ }
53
+
54
+ function validateUsageInput(command, options, help) {
55
+ if (command === "usage-record" && !help) {
56
+ if (!options.taskId) throw inputError("usage-record requires --task");
57
+ if ((options.usageSource ?? "ACTOR_REPORTED") !== "ACTOR_REPORTED") {
58
+ throw inputError("usage-record accepts only --source ACTOR_REPORTED");
59
+ }
60
+ }
61
+ if (command === "efficiency" && !help && !options.taskId) {
62
+ throw inputError("efficiency requires --task");
63
+ }
64
+ }
65
+
66
+ function validateQualityInput(command, options, help) {
67
+ if (["quality-baseline", "quality-verify", "quality-status"].includes(command) && !help && !options.taskId) {
68
+ throw inputError(`${command} requires --task`);
69
+ }
70
+ if (["quality-baseline", "quality-verify"].includes(command) && !help
71
+ && options.timeoutMs !== null && options.timeoutMs !== undefined
72
+ && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 0 || options.timeoutMs > 300000)) {
73
+ throw inputError(`${command} --timeout-ms must be between 0 and 300000`);
74
+ }
75
+ }
76
+
77
+ function validateProfileAndUsageInput(command, options, help) {
78
+ validateExecutionProfileInput(command, options);
79
+ validateUsageInput(command, options, help);
80
+ validateQualityInput(command, options, help);
81
+ if (command !== "quality-baseline" && options.replace === true) throw inputError("--replace is only valid for quality-baseline");
82
+ if (command !== "usage-record" && options.usageSource !== undefined && options.usageSource !== "ACTOR_REPORTED") {
83
+ throw inputError(`usageSource is not valid for ${command}`);
84
+ }
85
+ }
86
+
87
+ function validateOutputInput(command, options) {
88
+ if (options.compact === true && !["next", "task-show"].includes(command)) {
89
+ throw inputError(`compact output is not valid for ${command}`);
90
+ }
91
+ if (command !== "next" && options.explain === true) throw inputError(`explain output is not valid for ${command}`);
92
+ }
93
+
94
+ function validateTaskSelectorsInput(command, options, help) {
95
+ const taskCommands = ["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "handoff-accept", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"];
96
+ if (taskCommands.includes(command) && !options.taskId) throw inputError(`${command} requires --task`);
97
+ if (command === "handoff-show" && !help && !options.handoffId) throw inputError("handoff-show requires --id");
98
+ if (command === "handoff-accept" && !help) {
99
+ if (!options.handoffId) throw inputError("handoff-accept requires --handoff");
100
+ if (!options.consumerId) throw inputError("handoff-accept requires --consumer-id");
101
+ }
102
+ if (command === "responsibility-set" && !help && !options.responsibilityLabel) {
103
+ throw inputError("responsibility-set requires --label");
104
+ }
105
+ if (command === "verify-scope" && !help) {
106
+ const mode = String(options.verificationScopeMode ?? "AUTO").toUpperCase();
107
+ if (!["AUTO", "CHANGED", "CLAIMED", "FULL"].includes(mode)) throw inputError("verify-scope --mode must be AUTO, CHANGED, CLAIMED, or FULL");
108
+ }
109
+ if (command === "attestation-verify-range" && !help) {
110
+ if (!options.baseRevision) throw inputError("attestation-verify-range requires --base");
111
+ if (!options.headRevision) throw inputError("attestation-verify-range requires --head");
112
+ }
113
+ }
114
+
115
+ function validateCheckInput(command, options, help) {
116
+ if (command === "record-check" && !help) {
117
+ if (!options.checkId) throw inputError("record-check requires --id");
118
+ if (!options.checkRequirement) throw inputError("record-check requires --requirement");
119
+ if (!options.checkStatus) throw inputError("record-check requires --status");
120
+ if (!options.checkEvidenceKind) throw inputError("record-check requires --evidence-kind");
121
+ if (!options.checkCommand && !options.checkResult) throw inputError("record-check requires --command or --result");
122
+ }
123
+ if (command === "run-check" && !help) {
124
+ if (!options.checkId) throw inputError("run-check requires --id");
125
+ if (!options.checkRequirement) throw inputError("run-check requires --requirement");
126
+ if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
127
+ || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
128
+ throw inputError("run-check accepts only --id, --requirement, --details, --timeout-ms, --scope-ref, and -- <argv>");
129
+ }
130
+ if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) throw inputError("run-check requires -- followed by an exact command argv");
131
+ }
132
+ if (command === "reconcile-closure" && !help) {
133
+ if (!options.taskId) throw inputError("reconcile-closure requires --task");
134
+ if (!options.checkId) throw inputError("reconcile-closure requires --id");
135
+ if (!options.checkRequirement) throw inputError("reconcile-closure requires --requirement");
136
+ if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
137
+ || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
138
+ throw inputError("reconcile-closure accepts only --id, --requirement, --details, and -- <argv>");
139
+ }
140
+ if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) throw inputError("reconcile-closure requires -- followed by an exact command argv");
141
+ }
142
+ }
143
+
17
144
  /**
18
145
  * Transport-neutral option defaults shared by the CLI parser and the
19
146
  * programmatic command runtime so every executor observes the same
@@ -25,6 +152,7 @@ export function defaultCommandInputValues() {
25
152
  dryRun: false,
26
153
  json: false,
27
154
  compact: false,
155
+ explain: false,
28
156
  strict: false,
29
157
  fix: false,
30
158
  adopt: [],
@@ -64,6 +192,12 @@ export function defaultCommandInputValues() {
64
192
  to: null,
65
193
  file: null,
66
194
  contractFile: null,
195
+ preset: null,
196
+ preview: false,
197
+ phase: null,
198
+ active: false,
199
+ limit: null,
200
+ offset: 0,
67
201
  routeFile: null,
68
202
  stateFile: null,
69
203
  receiptFile: null,
@@ -128,110 +262,14 @@ export function validateForgeLoopCommandInput({ command, input, help = false } =
128
262
  const options = input ?? {};
129
263
  if (!command) return;
130
264
 
131
- if (command === "policy" && !options.policy) {
132
- throw inputError("policy requires a name");
133
- }
134
- if (command !== "policy" && options.policy) {
135
- throw inputError(`Policy name is not valid for ${command}`);
136
- }
137
- if (command === "bundle" && !options.taskId) {
138
- throw inputError("bundle requires --task");
139
- }
140
- if (command === "task-create" && !options.taskId) {
141
- throw inputError("task-create requires --task");
142
- }
265
+ validatePolicyInput(command, options);
266
+ validateTaskCreationInput(command, options);
143
267
  validateSearchCommandInput(command, options, help);
144
- if (options.executionProfile !== null && options.executionProfile !== undefined) {
145
- if (command !== "route") throw inputError(`executionProfile is not valid for ${command}`);
146
- if (!EXECUTION_PROFILE_REQUESTS.includes(options.executionProfile)) {
147
- throw inputError(`route --execution-profile must be one of ${EXECUTION_PROFILE_REQUESTS.join(", ")}`);
148
- }
149
- }
150
- if (command === "usage-record" && !help) {
151
- if (!options.taskId) throw inputError("usage-record requires --task");
152
- if ((options.usageSource ?? "ACTOR_REPORTED") !== "ACTOR_REPORTED") {
153
- throw inputError("usage-record accepts only --source ACTOR_REPORTED");
154
- }
155
- }
156
- if (command === "efficiency" && !help && !options.taskId) {
157
- throw inputError("efficiency requires --task");
158
- }
159
- if (["quality-baseline", "quality-verify", "quality-status"].includes(command) && !help && !options.taskId) {
160
- throw inputError(`${command} requires --task`);
161
- }
162
- if (["quality-baseline", "quality-verify"].includes(command) && !help
163
- && options.timeoutMs !== null && options.timeoutMs !== undefined
164
- && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 0 || options.timeoutMs > 300000)) {
165
- throw inputError(`${command} --timeout-ms must be between 0 and 300000`);
166
- }
167
- if (command !== "quality-baseline" && options.replace === true) {
168
- throw inputError(`--replace is only valid for quality-baseline`);
169
- }
170
- if (command !== "usage-record" && options.usageSource !== undefined && options.usageSource !== "ACTOR_REPORTED") {
171
- throw inputError(`usageSource is not valid for ${command}`);
172
- }
173
- if (options.compact === true && !["next", "task-show"].includes(command)) {
174
- throw inputError(`compact output is not valid for ${command}`);
175
- }
176
- if (["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "handoff-accept", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"].includes(command)
177
- && !options.taskId) {
178
- throw inputError(`${command} requires --task`);
179
- }
180
- if (command === "handoff-show" && !help && !options.handoffId) {
181
- throw inputError("handoff-show requires --id");
182
- }
183
- if (command === "handoff-accept" && !help) {
184
- if (!options.handoffId) {
185
- throw inputError("handoff-accept requires --handoff");
186
- }
187
- if (!options.consumerId) {
188
- throw inputError("handoff-accept requires --consumer-id");
189
- }
190
- }
191
- if (command === "responsibility-set" && !help && !options.responsibilityLabel) {
192
- throw inputError("responsibility-set requires --label");
193
- }
194
- if (command === "verify-scope" && !help) {
195
- const mode = String(options.verificationScopeMode ?? "AUTO").toUpperCase();
196
- if (!["AUTO", "CHANGED", "CLAIMED", "FULL"].includes(mode)) {
197
- throw inputError("verify-scope --mode must be AUTO, CHANGED, CLAIMED, or FULL");
198
- }
199
- }
200
- if (command === "attestation-verify-range" && !help) {
201
- if (!options.baseRevision) throw inputError("attestation-verify-range requires --base");
202
- if (!options.headRevision) throw inputError("attestation-verify-range requires --head");
203
- }
268
+ validateProfileAndUsageInput(command, options, help);
269
+ validateOutputInput(command, options);
270
+ validateTaskSelectorsInput(command, options, help);
204
271
 
205
272
  validateContinuityOptions(command, options);
206
273
 
207
- if (command === "record-check" && !help) {
208
- if (!options.checkId) throw inputError("record-check requires --id");
209
- if (!options.checkRequirement) throw inputError("record-check requires --requirement");
210
- if (!options.checkStatus) throw inputError("record-check requires --status");
211
- if (!options.checkEvidenceKind) throw inputError("record-check requires --evidence-kind");
212
- if (!options.checkCommand && !options.checkResult) throw inputError("record-check requires --command or --result");
213
- }
214
- if (command === "run-check" && !help) {
215
- if (!options.checkId) throw inputError("run-check requires --id");
216
- if (!options.checkRequirement) throw inputError("run-check requires --requirement");
217
- if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
218
- || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
219
- throw inputError("run-check accepts only --id, --requirement, --details, --timeout-ms, --scope-ref, and -- <argv>");
220
- }
221
- if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
222
- throw inputError("run-check requires -- followed by an exact command argv");
223
- }
224
- }
225
- if (command === "reconcile-closure" && !help) {
226
- if (!options.taskId) throw inputError("reconcile-closure requires --task");
227
- if (!options.checkId) throw inputError("reconcile-closure requires --id");
228
- if (!options.checkRequirement) throw inputError("reconcile-closure requires --requirement");
229
- if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
230
- || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
231
- throw inputError("reconcile-closure accepts only --id, --requirement, --details, and -- <argv>");
232
- }
233
- if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
234
- throw inputError("reconcile-closure requires -- followed by an exact command argv");
235
- }
236
- }
274
+ validateCheckInput(command, options, help);
237
275
  }