@cassiomc1/forgeloop 0.1.14 → 0.1.16

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 (61) hide show
  1. package/.forgeloop/forgeloop.gitignore +1 -0
  2. package/DOCS_INDEX.md +60 -0
  3. package/EXECUTION_STATE.md +9 -0
  4. package/LOOP_ENGINEERING.md +44 -6
  5. package/LOOP_SYSTEM_DESIGN.md +8 -0
  6. package/PROTOCOL_INTEGRATION.md +41 -0
  7. package/QUALITY_SCORECARD.md +2 -0
  8. package/README.md +183 -643
  9. package/TERMINOLOGY.md +4 -0
  10. package/THREAT_MODEL.md +14 -0
  11. package/docs/assets/forgeloop-flow.svg +1 -0
  12. package/docs/forgeloop-flow.mmd +51 -0
  13. package/package.json +16 -3
  14. package/schemas/check.schema.json +2 -0
  15. package/schemas/continuity.schema.json +57 -0
  16. package/schemas/execution.schema.json +63 -0
  17. package/scripts/CI_VALIDATORS.md +32 -0
  18. package/src/cli.js +351 -196
  19. package/src/commands/clear-continuity.js +9 -0
  20. package/src/commands/continuity.js +25 -0
  21. package/src/commands/doctor.js +17 -2
  22. package/src/commands/reconcile-continuity.js +23 -0
  23. package/src/commands/record-continuity.js +63 -0
  24. package/src/commands/run-check.js +83 -0
  25. package/src/commands/status.js +14 -1
  26. package/src/commands/update.js +12 -13
  27. package/src/commands/validate-protocol.js +37 -0
  28. package/src/core/artifacts.js +9 -0
  29. package/src/core/bundles.js +78 -0
  30. package/src/core/checks.js +16 -0
  31. package/src/core/command-resolution.js +295 -0
  32. package/src/core/command-tokenizer.js +122 -0
  33. package/src/core/completion-artifacts.js +229 -47
  34. package/src/core/completion.js +19 -1
  35. package/src/core/conformance.js +8 -2
  36. package/src/core/continuity-cli-options.js +58 -0
  37. package/src/core/continuity-conformance.js +46 -0
  38. package/src/core/continuity-observability.js +20 -0
  39. package/src/core/continuity-reconciliation.js +224 -0
  40. package/src/core/continuity.js +245 -0
  41. package/src/core/evidence-readiness.js +26 -1
  42. package/src/core/execution.js +185 -0
  43. package/src/core/inspect.js +9 -1
  44. package/src/core/installation-authority.js +178 -0
  45. package/src/core/json-safety.js +17 -13
  46. package/src/core/next-action-artifacts.js +118 -0
  47. package/src/core/next-action-continuity.js +65 -0
  48. package/src/core/next-action-model.js +127 -0
  49. package/src/core/next-action-phases.js +12 -0
  50. package/src/core/next-action.js +22 -249
  51. package/src/core/npm-classifier.js +343 -0
  52. package/src/core/package-manager-classifiers.js +37 -0
  53. package/src/core/preflight-consistency.js +221 -0
  54. package/src/core/preflight-loaders.js +112 -0
  55. package/src/core/preflight-model.js +83 -0
  56. package/src/core/preflight.js +34 -444
  57. package/src/core/protocol.js +7 -0
  58. package/src/core/schema-validation.js +16 -1
  59. package/src/core/templates.js +3 -0
  60. package/src/core/verification-capability.js +31 -495
  61. package/src/core/verification-constants.js +61 -0
@@ -0,0 +1,295 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ extractToolFromArgs,
6
+ normalizeExecutableName,
7
+ splitCommandPipeline,
8
+ tokenizeCommand,
9
+ } from "./command-tokenizer.js";
10
+ import {
11
+ classifyNpmInvocation,
12
+ getNpmLifecycleCandidates,
13
+ getNpmScriptName,
14
+ parseNpmInvocation,
15
+ parseNpmInvocationArgs,
16
+ } from "./npm-classifier.js";
17
+ import {
18
+ classifyPnpmInvocation,
19
+ classifyYarnInvocation,
20
+ } from "./package-manager-classifiers.js";
21
+
22
+ function classifySingleCommand(commandInput) {
23
+ const tokens = Array.isArray(commandInput) ? [...commandInput] : tokenizeCommand(commandInput);
24
+ if (tokens.length === 0) return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
25
+
26
+ let i = 0;
27
+ while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i++;
28
+ if (i >= tokens.length) return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
29
+
30
+ const binaryToken = tokens[i];
31
+ const binary = normalizeExecutableName(binaryToken);
32
+ const rest = tokens.slice(i + 1);
33
+
34
+ if (binary === "call") return classifyCommandResolution(rest);
35
+
36
+ if (binaryToken.includes("node_modules/.bin/") || binaryToken.startsWith("./node_modules/")) {
37
+ return { resolutionMode: "LOCAL_PACKAGE_BINARY", mayInstall: false, installer: null, tool: binary };
38
+ }
39
+
40
+ if (binary === "npx") {
41
+ if (rest.some((arg) => arg === "--no-install" || arg === "--no")) {
42
+ const nonInstallArgs = rest.filter((arg) => arg !== "--no-install" && arg !== "--no");
43
+ return {
44
+ resolutionMode: "NON_INSTALLING_RESOLUTION",
45
+ mayInstall: false,
46
+ installer: "npx",
47
+ tool: extractToolFromArgs(nonInstallArgs),
48
+ };
49
+ }
50
+ return {
51
+ resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
52
+ mayInstall: true,
53
+ installer: "npx",
54
+ tool: extractToolFromArgs(rest),
55
+ };
56
+ }
57
+
58
+ if (binary === "pnpx" || binary === "bunx" || binary === "uvx") {
59
+ return {
60
+ resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
61
+ mayInstall: true,
62
+ installer: binary,
63
+ tool: extractToolFromArgs(rest),
64
+ };
65
+ }
66
+
67
+ if (binary === "pnpm") return classifyPnpmInvocation(rest);
68
+ if (binary === "yarn") return classifyYarnInvocation(rest);
69
+
70
+ if (binary === "bun") {
71
+ if (rest[0] === "x") {
72
+ return {
73
+ resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
74
+ mayInstall: true,
75
+ installer: "bun x",
76
+ tool: extractToolFromArgs(rest.slice(1)),
77
+ };
78
+ }
79
+ if (["add", "install", "i"].includes(rest[0])) {
80
+ return {
81
+ resolutionMode: "EXPLICIT_INSTALLATION",
82
+ mayInstall: true,
83
+ installer: "bun",
84
+ tool: extractToolFromArgs(rest.slice(1)),
85
+ };
86
+ }
87
+ return { resolutionMode: "LOCAL_PACKAGE_BINARY", mayInstall: false, installer: null, tool: null };
88
+ }
89
+
90
+ if (binary === "uv") {
91
+ if (rest[0] === "tool" && rest[1] === "run") {
92
+ return {
93
+ resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
94
+ mayInstall: true,
95
+ installer: "uv tool run",
96
+ tool: extractToolFromArgs(rest.slice(2)),
97
+ };
98
+ }
99
+ if (rest[0] === "pip" && rest[1] === "install") {
100
+ return {
101
+ resolutionMode: "EXPLICIT_INSTALLATION",
102
+ mayInstall: true,
103
+ installer: "uv pip install",
104
+ tool: extractToolFromArgs(rest.slice(2)),
105
+ };
106
+ }
107
+ if (rest[0] === "add") {
108
+ return {
109
+ resolutionMode: "EXPLICIT_INSTALLATION",
110
+ mayInstall: true,
111
+ installer: "uv add",
112
+ tool: extractToolFromArgs(rest.slice(1)),
113
+ };
114
+ }
115
+ return { resolutionMode: "LOCAL_EXECUTABLE", mayInstall: false, installer: null, tool: null };
116
+ }
117
+
118
+ if (binary === "pipx") {
119
+ if (rest[0] === "run") {
120
+ return {
121
+ resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
122
+ mayInstall: true,
123
+ installer: "pipx run",
124
+ tool: extractToolFromArgs(rest.slice(1)),
125
+ };
126
+ }
127
+ if (rest[0] === "install") {
128
+ return {
129
+ resolutionMode: "EXPLICIT_INSTALLATION",
130
+ mayInstall: true,
131
+ installer: "pipx install",
132
+ tool: extractToolFromArgs(rest.slice(1)),
133
+ };
134
+ }
135
+ return { resolutionMode: "LOCAL_EXECUTABLE", mayInstall: false, installer: null, tool: null };
136
+ }
137
+
138
+ if (binary === "npm") return classifyNpmInvocation(parseNpmInvocationArgs(rest));
139
+
140
+ if (binary === "pip" || binary === "pip3") {
141
+ if (rest[0] === "install") {
142
+ return {
143
+ resolutionMode: "EXPLICIT_INSTALLATION",
144
+ mayInstall: true,
145
+ installer: binary,
146
+ tool: extractToolFromArgs(rest.slice(1)),
147
+ };
148
+ }
149
+ }
150
+ if (binary === "python" || binary === "python3") {
151
+ if (rest[0] === "-m" && rest[1] === "pip" && rest[2] === "install") {
152
+ return {
153
+ resolutionMode: "EXPLICIT_INSTALLATION",
154
+ mayInstall: true,
155
+ installer: `${binary} -m pip install`,
156
+ tool: extractToolFromArgs(rest.slice(3)),
157
+ };
158
+ }
159
+ }
160
+
161
+ if (binary === "cargo") {
162
+ if (rest[0] === "install" || rest[0] === "binstall") {
163
+ return {
164
+ resolutionMode: "EXPLICIT_INSTALLATION",
165
+ mayInstall: true,
166
+ installer: binary,
167
+ tool: extractToolFromArgs(rest.slice(1)),
168
+ };
169
+ }
170
+ }
171
+
172
+ if (["brew", "apt", "apt-get", "apk", "dnf", "pacman"].includes(binary)) {
173
+ if (["install", "add", "-S"].includes(rest[0])) {
174
+ return {
175
+ resolutionMode: "EXPLICIT_INSTALLATION",
176
+ mayInstall: true,
177
+ installer: binary,
178
+ tool: extractToolFromArgs(rest.slice(1)),
179
+ };
180
+ }
181
+ }
182
+
183
+ return { resolutionMode: "LOCAL_EXECUTABLE", mayInstall: false, installer: null, tool: null };
184
+ }
185
+
186
+ export function classifyCommandResolution(commandInput) {
187
+ if (Array.isArray(commandInput)) {
188
+ if (commandInput.length === 0 || commandInput.some((item) => typeof item !== "string" || item.trim() === "")) {
189
+ return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
190
+ }
191
+ const binary = normalizeExecutableName(commandInput[0]);
192
+ if (["sh", "bash", "zsh", "dash", "ksh"].includes(binary)) {
193
+ const shellFlagIndex = commandInput.findIndex((item, index) => index > 0 && /^-.*c/.test(item));
194
+ const shellCommand = shellFlagIndex >= 0 ? commandInput[shellFlagIndex + 1] : null;
195
+ if (shellCommand) return classifyCommandResolution(shellCommand);
196
+ }
197
+ if (binary === "cmd") {
198
+ const shellFlagIndex = commandInput.findIndex((item, index) => index > 0 && /^\/c$/iu.test(item));
199
+ const shellCommand = shellFlagIndex >= 0 ? commandInput.slice(shellFlagIndex + 1).join(" ") : null;
200
+ if (shellCommand) return classifyCommandResolution(shellCommand);
201
+ }
202
+ return classifySingleCommand(commandInput);
203
+ }
204
+
205
+ if (typeof commandInput !== "string" || commandInput.trim() === "") {
206
+ return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
207
+ }
208
+
209
+ const subcommands = splitCommandPipeline(commandInput);
210
+ for (const subcommand of subcommands) {
211
+ const result = classifySingleCommand(subcommand);
212
+ if (result.mayInstall) return result;
213
+ }
214
+ return classifySingleCommand(subcommands[0] || commandInput);
215
+ }
216
+
217
+ export const MAX_NPM_SCRIPT_DEPTH = 16;
218
+
219
+ async function resolveNpmScriptRisk({ scriptName, packageJson, visited = new Set(), depth = 0 } = {}) {
220
+ if (depth > MAX_NPM_SCRIPT_DEPTH) {
221
+ return { resolutionMode: "UNKNOWN", mayInstall: true, installer: "npm-script", tool: null, reason: "MAX_SCRIPT_DEPTH" };
222
+ }
223
+ if (visited.has(scriptName)) {
224
+ return { resolutionMode: "UNKNOWN", mayInstall: true, installer: "npm-script", tool: null, reason: "SCRIPT_CYCLE" };
225
+ }
226
+
227
+ const nextVisited = new Set(visited);
228
+ nextVisited.add(scriptName);
229
+ const candidates = getNpmLifecycleCandidates({ scriptName, scripts: packageJson?.scripts ?? {} });
230
+ for (const candidate of candidates) {
231
+ const scriptBody = packageJson?.scripts?.[candidate];
232
+ if (typeof scriptBody !== "string" || scriptBody.trim() === "") continue;
233
+
234
+ const direct = classifyCommandResolution(scriptBody);
235
+ if (direct.mayInstall) {
236
+ return {
237
+ resolutionMode: direct.resolutionMode,
238
+ mayInstall: true,
239
+ installer: direct.installer ?? "npm-script",
240
+ tool: direct.tool,
241
+ dispatch: { kind: "npm-script", scriptName: candidate },
242
+ };
243
+ }
244
+
245
+ for (const subcommand of splitCommandPipeline(scriptBody)) {
246
+ const nestedScriptName = getNpmScriptName(subcommand);
247
+ if (!nestedScriptName) continue;
248
+ const nested = await resolveNpmScriptRisk({
249
+ scriptName: nestedScriptName,
250
+ packageJson,
251
+ visited: nextVisited,
252
+ depth: depth + 1,
253
+ });
254
+ if (nested?.mayInstall) return nested;
255
+ }
256
+ }
257
+ return null;
258
+ }
259
+
260
+ async function readPackageJsonIfPresent(cwd) {
261
+ if (!cwd || typeof cwd !== "string") return null;
262
+ try {
263
+ const raw = await fs.readFile(path.resolve(cwd, "package.json"), "utf8");
264
+ return JSON.parse(raw);
265
+ } catch {
266
+ return null;
267
+ }
268
+ }
269
+
270
+ export async function resolveExecutionResolution({ argv, cwd } = {}) {
271
+ const direct = classifyCommandResolution(argv);
272
+ if (direct.mayInstall && direct.resolutionMode !== "UNKNOWN") return direct;
273
+
274
+ const npmInvocation = parseNpmInvocation(argv);
275
+ const scriptName = getNpmScriptName(argv);
276
+ if (scriptName && (npmInvocation.workspace || npmInvocation.workspaces)) {
277
+ return {
278
+ resolutionMode: "UNKNOWN",
279
+ mayInstall: true,
280
+ installer: "npm-workspace",
281
+ tool: null,
282
+ reason: "NPM_WORKSPACE_SCRIPT_UNRESOLVED",
283
+ dispatch: { kind: "npm-workspace-script", scriptName },
284
+ };
285
+ }
286
+
287
+ if (direct.mayInstall) return direct;
288
+ if (!scriptName) return direct;
289
+
290
+ const packageJson = await readPackageJsonIfPresent(cwd);
291
+ if (!packageJson || typeof packageJson.scripts !== "object" || packageJson.scripts === null) return direct;
292
+
293
+ const nestedRisk = await resolveNpmScriptRisk({ scriptName, packageJson });
294
+ return nestedRisk?.mayInstall ? nestedRisk : direct;
295
+ }
@@ -0,0 +1,122 @@
1
+ export function tokenizeCommand(commandString) {
2
+ const tokens = [];
3
+ let current = "";
4
+ let inSingleQuote = false;
5
+ let inDoubleQuote = false;
6
+
7
+ for (let i = 0; i < commandString.length; i++) {
8
+ const char = commandString[i];
9
+
10
+ if (char === "'" && !inDoubleQuote) {
11
+ inSingleQuote = !inSingleQuote;
12
+ } else if (char === '"' && !inSingleQuote) {
13
+ inDoubleQuote = !inDoubleQuote;
14
+ } else if (/\s/.test(char) && !inSingleQuote && !inDoubleQuote) {
15
+ if (current.length > 0) {
16
+ tokens.push(current);
17
+ current = "";
18
+ }
19
+ } else {
20
+ current += char;
21
+ }
22
+ }
23
+
24
+ if (current.length > 0) tokens.push(current);
25
+ return tokens;
26
+ }
27
+
28
+ export function splitCommandPipeline(commandString) {
29
+ const parts = [];
30
+ let current = "";
31
+ let inSingleQuote = false;
32
+ let inDoubleQuote = false;
33
+
34
+ for (let i = 0; i < commandString.length; i++) {
35
+ const char = commandString[i];
36
+ const next = commandString[i + 1];
37
+
38
+ if (char === "'" && !inDoubleQuote) {
39
+ inSingleQuote = !inSingleQuote;
40
+ current += char;
41
+ } else if (char === '"' && !inSingleQuote) {
42
+ inDoubleQuote = !inDoubleQuote;
43
+ current += char;
44
+ } else if (!inSingleQuote && !inDoubleQuote) {
45
+ if ((char === "&" && next === "&") || (char === "|" && next === "|")) {
46
+ if (current.trim().length > 0) parts.push(current.trim());
47
+ current = "";
48
+ i++;
49
+ } else if (char === ";" || char === "|") {
50
+ if (current.trim().length > 0) parts.push(current.trim());
51
+ current = "";
52
+ } else {
53
+ current += char;
54
+ }
55
+ } else {
56
+ current += char;
57
+ }
58
+ }
59
+
60
+ if (current.trim().length > 0) parts.push(current.trim());
61
+ return parts.length > 0 ? parts : [commandString];
62
+ }
63
+
64
+ export function extractToolFromArgs(args) {
65
+ if (!Array.isArray(args) || args.length === 0) return null;
66
+ for (let idx = 0; idx < args.length; idx++) {
67
+ const arg = args[idx];
68
+ if (arg === "-p" || arg === "--package") {
69
+ if (args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
70
+ }
71
+ if (arg.startsWith("--package=")) return arg.split("=")[1];
72
+ if (!arg.startsWith("-")) return arg;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ export function extractNpmExecTool(args) {
78
+ if (!Array.isArray(args) || args.length === 0) return null;
79
+ for (let idx = 0; idx < args.length; idx++) {
80
+ const arg = args[idx];
81
+ if (arg === "-p" || arg === "--package") {
82
+ if (args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
83
+ }
84
+ if (arg.startsWith("--package=")) return arg.slice("--package=".length);
85
+ }
86
+ let afterDoubleDash = false;
87
+ for (let idx = 0; idx < args.length; idx++) {
88
+ const arg = args[idx];
89
+ if (arg === "--") {
90
+ afterDoubleDash = true;
91
+ if (args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
92
+ continue;
93
+ }
94
+ if (!afterDoubleDash && !arg.startsWith("-")) return arg;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ export function normalizeExecutableName(binaryToken) {
100
+ const base = binaryToken.split(/[\\/]/u).pop() ?? binaryToken;
101
+ return base.toLowerCase().replace(/\.(?:cmd|bat|exe)$/u, "");
102
+ }
103
+
104
+ export function unwrapCommandArgv(argv) {
105
+ if (!Array.isArray(argv) || argv.length === 0) return null;
106
+ let i = 0;
107
+ while (i < argv.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(argv[i])) i++;
108
+ if (i >= argv.length) return null;
109
+ const binary = normalizeExecutableName(argv[i]);
110
+ if (["sh", "bash", "zsh", "dash", "ksh"].includes(binary)) {
111
+ const shellFlagIndex = argv.findIndex((item, index) => index > i && /^-.*c/.test(item));
112
+ const shellCommand = shellFlagIndex >= 0 ? argv[shellFlagIndex + 1] : null;
113
+ if (shellCommand) return tokenizeCommand(shellCommand);
114
+ }
115
+ if (binary === "cmd") {
116
+ const shellFlagIndex = argv.findIndex((item, index) => index > i && /^\/c$/iu.test(item));
117
+ const shellCommand = shellFlagIndex >= 0 ? argv.slice(shellFlagIndex + 1).join(" ") : null;
118
+ if (shellCommand) return tokenizeCommand(shellCommand);
119
+ }
120
+ if (binary === "call") return argv.slice(i + 1);
121
+ return argv.slice(i);
122
+ }