cclaw-cli 6.7.0 → 6.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-linter/design.js +1 -1
- package/dist/artifact-linter/shared.js +2 -1
- package/dist/artifact-linter/tdd.d.ts +11 -0
- package/dist/artifact-linter/tdd.js +174 -7
- package/dist/content/harness-doc.js +1 -1
- package/dist/content/hooks.js +209 -6
- package/dist/content/iron-laws.js +6 -2
- package/dist/content/node-hooks.js +15 -1308
- package/dist/content/skills-elicitation.js +2 -2
- package/dist/content/skills.js +2 -0
- package/dist/content/stages/brainstorm.js +2 -2
- package/dist/content/stages/design.js +2 -2
- package/dist/content/stages/scope.js +2 -2
- package/dist/content/stages/tdd.js +1 -0
- package/dist/content/subagents.js +11 -1
- package/dist/delegation.d.ts +105 -0
- package/dist/delegation.js +229 -6
- package/dist/early-loop.js +15 -1
- package/dist/gate-evidence.js +15 -23
- package/dist/harness-adapters.js +4 -2
- package/dist/install.js +37 -221
- package/dist/internal/advance-stage.js +9 -0
- package/dist/internal/detect-supply-chain-changes.d.ts +6 -0
- package/dist/internal/detect-supply-chain-changes.js +138 -0
- package/dist/internal/flow-state-repair.d.ts +7 -0
- package/dist/internal/flow-state-repair.js +57 -18
- package/dist/run-persistence.d.ts +2 -0
- package/dist/run-persistence.js +62 -3
- package/dist/runtime/run-hook.mjs +44 -8729
- package/package.json +1 -1
package/dist/install.js
CHANGED
|
@@ -38,8 +38,6 @@ import { FLOW_STAGES } from "./types.js";
|
|
|
38
38
|
const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
|
|
39
39
|
const CURSOR_RULE_REL_PATH = ".cursor/rules/cclaw-workflow.mdc";
|
|
40
40
|
const CURSOR_GUIDELINES_REL_PATH = ".cursor/rules/cclaw-guidelines.mdc";
|
|
41
|
-
const GIT_HOOK_MANAGED_MARKER = "cclaw-managed-git-hook";
|
|
42
|
-
const GIT_HOOK_RUNTIME_REL_DIR = `${RUNTIME_ROOT}/hooks/git`;
|
|
43
41
|
const INIT_SENTINEL_FILE = ".init-in-progress";
|
|
44
42
|
const execFileAsync = promisify(execFile);
|
|
45
43
|
function runtimePath(projectRoot, ...segments) {
|
|
@@ -145,13 +143,17 @@ const DEPRECATED_COMMAND_FILES = [
|
|
|
145
143
|
const DEPRECATED_SKILL_FILES = [
|
|
146
144
|
["flow-finish", "SKILL.md"],
|
|
147
145
|
["flow-ops", "SKILL.md"],
|
|
148
|
-
["tdd-cycle-log", "SKILL.md"],
|
|
149
146
|
["flow-retro", "SKILL.md"],
|
|
150
147
|
["flow-compound", "SKILL.md"],
|
|
151
148
|
["flow-archive", "SKILL.md"],
|
|
152
149
|
["flow-rewind", "SKILL.md"],
|
|
153
150
|
["using-git-worktrees", "SKILL.md"]
|
|
154
151
|
];
|
|
152
|
+
// Skill folders whose entire directory should be removed on sync so the
|
|
153
|
+
// abandoned tree doesn't linger in user projects.
|
|
154
|
+
const DEPRECATED_SKILL_FOLDERS_FULL = [
|
|
155
|
+
"tdd-cycle-log"
|
|
156
|
+
];
|
|
155
157
|
const DEPRECATED_STATE_FILES = [
|
|
156
158
|
"checkpoint.json",
|
|
157
159
|
"flow-state.snapshot.json",
|
|
@@ -161,7 +163,10 @@ const DEPRECATED_STATE_FILES = [
|
|
|
161
163
|
"harness-gaps.json",
|
|
162
164
|
"context-mode.json",
|
|
163
165
|
"session-digest.md",
|
|
164
|
-
"context-warnings.jsonl"
|
|
166
|
+
"context-warnings.jsonl",
|
|
167
|
+
// Runtime Honesty 6.9.0 removed the per-run TDD cycle JSONL: gate evidence
|
|
168
|
+
// now reads cycle phase progression directly from the artifact table.
|
|
169
|
+
"tdd-cycle-log.jsonl"
|
|
165
170
|
];
|
|
166
171
|
const DEPRECATED_HOOK_FILES = [
|
|
167
172
|
"observe.sh",
|
|
@@ -193,225 +198,33 @@ async function resolveGitHooksDir(projectRoot) {
|
|
|
193
198
|
return null;
|
|
194
199
|
}
|
|
195
200
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const HOOK_NAME = ${JSON.stringify(hookName)};
|
|
205
|
-
const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
206
|
-
|
|
207
|
-
function runGit(args, cwd) {
|
|
208
|
-
const result = spawnSync("git", args, {
|
|
209
|
-
cwd,
|
|
210
|
-
encoding: "utf8",
|
|
211
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
212
|
-
});
|
|
213
|
-
return {
|
|
214
|
-
status: typeof result.status === "number" ? result.status : 1,
|
|
215
|
-
stdout: typeof result.stdout === "string" ? result.stdout : ""
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function resolveRepoRoot() {
|
|
220
|
-
const result = runGit(["rev-parse", "--show-toplevel"], process.cwd());
|
|
221
|
-
if (result.status === 0) {
|
|
222
|
-
const root = result.stdout.trim();
|
|
223
|
-
if (root.length > 0) return root;
|
|
224
|
-
}
|
|
225
|
-
return process.cwd();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function isZeroSha(value) {
|
|
229
|
-
return /^0{40,64}$/u.test(value);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function readStdin() {
|
|
233
|
-
try {
|
|
234
|
-
return fs.readFileSync(0, "utf8");
|
|
235
|
-
} catch {
|
|
236
|
-
return "";
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function uniqueLines(chunks) {
|
|
241
|
-
return [...new Set(chunks
|
|
242
|
-
.join("\n")
|
|
243
|
-
.split(/\r?\n/gu)
|
|
244
|
-
.map((line) => line.trim())
|
|
245
|
-
.filter((line) => line.length > 0))].join("\n");
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function diffNames(root, range) {
|
|
249
|
-
const result = runGit(["diff", "--name-only", range], root);
|
|
250
|
-
return result.status === 0 ? result.stdout : "";
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function changedFilesFromUnpushedCommits(root, localSha = "HEAD") {
|
|
254
|
-
const revList = runGit(["rev-list", "--reverse", localSha, "--not", "--remotes"], root);
|
|
255
|
-
if (revList.status !== 0 || revList.stdout.trim().length === 0) {
|
|
256
|
-
return "";
|
|
257
|
-
}
|
|
258
|
-
const chunks = [];
|
|
259
|
-
for (const commit of revList.stdout.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean)) {
|
|
260
|
-
const diffTree = runGit(["diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commit], root);
|
|
261
|
-
if (diffTree.status === 0) chunks.push(diffTree.stdout);
|
|
262
|
-
}
|
|
263
|
-
return uniqueLines(chunks);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function changedFilesFromPrePushStdin(root, stdin) {
|
|
267
|
-
const chunks = [];
|
|
268
|
-
for (const rawLine of stdin.split(/\r?\n/gu)) {
|
|
269
|
-
const parts = rawLine.trim().split(/\s+/u);
|
|
270
|
-
if (parts.length < 4) continue;
|
|
271
|
-
const [localRef, localSha, remoteRef, remoteSha] = parts;
|
|
272
|
-
void localRef;
|
|
273
|
-
void remoteRef;
|
|
274
|
-
if (!localSha || isZeroSha(localSha)) continue;
|
|
275
|
-
if (remoteSha && !isZeroSha(remoteSha)) {
|
|
276
|
-
chunks.push(diffNames(root, remoteSha + ".." + localSha));
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
const upstream = runGit(["rev-parse", "--verify", "--quiet", "@{upstream}"], root);
|
|
280
|
-
if (upstream.status === 0 && upstream.stdout.trim().length > 0) {
|
|
281
|
-
chunks.push(diffNames(root, upstream.stdout.trim() + ".." + localSha));
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
chunks.push(changedFilesFromUnpushedCommits(root, localSha));
|
|
285
|
-
}
|
|
286
|
-
return uniqueLines(chunks);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function resolveChangedFiles(root) {
|
|
290
|
-
if (HOOK_NAME === "pre-commit") {
|
|
291
|
-
const result = runGit(["diff", "--cached", "--name-only"], root);
|
|
292
|
-
return result.status === 0 ? result.stdout : "";
|
|
293
|
-
}
|
|
294
|
-
const stdinChanged = changedFilesFromPrePushStdin(root, readStdin());
|
|
295
|
-
if (stdinChanged.length > 0) {
|
|
296
|
-
return stdinChanged;
|
|
297
|
-
}
|
|
298
|
-
const upstreamResult = runGit(["diff", "--name-only", "@{upstream}..HEAD"], root);
|
|
299
|
-
if (upstreamResult.status === 0) {
|
|
300
|
-
return upstreamResult.stdout;
|
|
301
|
-
}
|
|
302
|
-
const unpushed = changedFilesFromUnpushedCommits(root);
|
|
303
|
-
if (unpushed.length > 0) {
|
|
304
|
-
return unpushed;
|
|
305
|
-
}
|
|
306
|
-
const fallback = runGit(["diff", "--name-only", "HEAD~1...HEAD"], root);
|
|
307
|
-
return fallback.status === 0 ? fallback.stdout : "";
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const root = resolveRepoRoot();
|
|
311
|
-
const runtimeHook = path.join(root, RUNTIME_ROOT, "hooks", "run-hook.mjs");
|
|
312
|
-
if (!fs.existsSync(runtimeHook)) {
|
|
313
|
-
// cclaw git relay is installed but the runtime entrypoint is missing —
|
|
314
|
-
// warn visibly (without blocking the commit) so the drift is noticed.
|
|
315
|
-
process.stderr.write(
|
|
316
|
-
"[cclaw] " + HOOK_NAME + ": " + runtimeHook + " not found; run \`cclaw sync\` to reinstall\\n"
|
|
317
|
-
);
|
|
318
|
-
process.exit(0);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const changedFiles = resolveChangedFiles(root)
|
|
322
|
-
.split(/\\r?\\n/gu)
|
|
323
|
-
.map((line) => line.trim())
|
|
324
|
-
.filter((line) => line.length > 0);
|
|
325
|
-
if (changedFiles.length === 0) {
|
|
326
|
-
process.exit(0);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const payload = JSON.stringify({
|
|
330
|
-
tool_name: "Write",
|
|
331
|
-
tool_input: {
|
|
332
|
-
path: changedFiles.join("\\n"),
|
|
333
|
-
paths: changedFiles
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
const result = spawnSync(process.execPath, [runtimeHook, "prompt-guard"], {
|
|
338
|
-
cwd: root,
|
|
339
|
-
env: process.env,
|
|
340
|
-
input: payload,
|
|
341
|
-
encoding: "utf8",
|
|
342
|
-
stdio: ["pipe", "ignore", "inherit"]
|
|
343
|
-
});
|
|
344
|
-
process.exit(typeof result.status === "number" ? result.status : 1);
|
|
345
|
-
`;
|
|
346
|
-
}
|
|
347
|
-
function managedGitRelayHook(hookName) {
|
|
348
|
-
return `#!/usr/bin/env node
|
|
349
|
-
// ${GIT_HOOK_MANAGED_MARKER}: relay ${hookName}
|
|
350
|
-
import fs from "node:fs";
|
|
351
|
-
import path from "node:path";
|
|
352
|
-
import process from "node:process";
|
|
353
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
354
|
-
|
|
355
|
-
const RUNTIME_REL_DIR = ${JSON.stringify(GIT_HOOK_RUNTIME_REL_DIR)};
|
|
356
|
-
const HOOK_NAME = ${JSON.stringify(hookName)};
|
|
357
|
-
|
|
358
|
-
function resolveRepoRoot() {
|
|
359
|
-
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
360
|
-
cwd: process.cwd(),
|
|
361
|
-
encoding: "utf8",
|
|
362
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
363
|
-
});
|
|
364
|
-
if (typeof result.status === "number" && result.status === 0) {
|
|
365
|
-
const root = (result.stdout || "").trim();
|
|
366
|
-
if (root.length > 0) return root;
|
|
367
|
-
}
|
|
368
|
-
return process.cwd();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const root = resolveRepoRoot();
|
|
372
|
-
const runtimeHook = path.join(root, RUNTIME_REL_DIR, HOOK_NAME + ".mjs");
|
|
373
|
-
if (!fs.existsSync(runtimeHook)) {
|
|
374
|
-
process.exit(0);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const child = spawn(process.execPath, [runtimeHook, ...process.argv.slice(2)], {
|
|
378
|
-
cwd: root,
|
|
379
|
-
env: process.env,
|
|
380
|
-
stdio: "inherit"
|
|
381
|
-
});
|
|
382
|
-
child.on("error", () => process.exit(1));
|
|
383
|
-
child.on("close", (code, signal) => {
|
|
384
|
-
process.exit(signal ? 1 : typeof code === "number" ? code : 1);
|
|
385
|
-
});
|
|
386
|
-
`;
|
|
387
|
-
}
|
|
388
|
-
async function removeManagedGitHookRelays(projectRoot) {
|
|
201
|
+
// Legacy cleanup: prior versions installed Node-based git pre-commit/pre-push relays
|
|
202
|
+
// under .git/hooks/* and a runtime tree at .cclaw/hooks/git/. Runtime Honesty 6.9.0
|
|
203
|
+
// removed managed git hooks entirely; the cleanup below stays so existing installs
|
|
204
|
+
// shed the leftover files on next sync/uninstall.
|
|
205
|
+
const LEGACY_GIT_HOOK_MANAGED_MARKER = "cclaw-managed-git-hook";
|
|
206
|
+
const LEGACY_GIT_HOOK_RUNTIME_REL_DIR = `${RUNTIME_ROOT}/hooks/git`;
|
|
207
|
+
async function cleanupLegacyManagedGitHookRelays(projectRoot) {
|
|
389
208
|
const hooksDir = await resolveGitHooksDir(projectRoot);
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
content
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
continue;
|
|
209
|
+
if (hooksDir) {
|
|
210
|
+
for (const hookName of ["pre-commit", "pre-push"]) {
|
|
211
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
212
|
+
if (!(await exists(hookPath)))
|
|
213
|
+
continue;
|
|
214
|
+
let content = "";
|
|
215
|
+
try {
|
|
216
|
+
content = await fs.readFile(hookPath, "utf8");
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
content = "";
|
|
220
|
+
}
|
|
221
|
+
if (!content.includes(LEGACY_GIT_HOOK_MANAGED_MARKER))
|
|
222
|
+
continue;
|
|
223
|
+
await fs.rm(hookPath, { force: true });
|
|
406
224
|
}
|
|
407
|
-
await fs.rm(hookPath, { force: true });
|
|
408
225
|
}
|
|
409
|
-
}
|
|
410
|
-
async function syncManagedGitHooks(projectRoot, config) {
|
|
411
|
-
void config;
|
|
412
|
-
await removeManagedGitHookRelays(projectRoot);
|
|
413
226
|
try {
|
|
414
|
-
await fs.rm(path.join(projectRoot,
|
|
227
|
+
await fs.rm(path.join(projectRoot, LEGACY_GIT_HOOK_RUNTIME_REL_DIR), { recursive: true, force: true });
|
|
415
228
|
}
|
|
416
229
|
catch {
|
|
417
230
|
// best-effort cleanup
|
|
@@ -1021,6 +834,9 @@ async function cleanLegacyArtifacts(projectRoot) {
|
|
|
1021
834
|
for (const legacyFolder of DEPRECATED_STAGE_SKILL_FOLDERS) {
|
|
1022
835
|
await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
|
|
1023
836
|
}
|
|
837
|
+
for (const legacyFolder of DEPRECATED_SKILL_FOLDERS_FULL) {
|
|
838
|
+
await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
|
|
839
|
+
}
|
|
1024
840
|
for (const legacyAgentFile of DEPRECATED_AGENT_FILES) {
|
|
1025
841
|
await removeBestEffort(runtimePath(projectRoot, "agents", legacyAgentFile));
|
|
1026
842
|
}
|
|
@@ -1173,7 +989,7 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
|
|
|
1173
989
|
await ensureKnowledgeStore(projectRoot);
|
|
1174
990
|
await writeHooks(projectRoot, config);
|
|
1175
991
|
await syncDisabledHarnessArtifacts(projectRoot, harnesses);
|
|
1176
|
-
await
|
|
992
|
+
await cleanupLegacyManagedGitHookRelays(projectRoot);
|
|
1177
993
|
await syncHarnessShims(projectRoot, harnesses);
|
|
1178
994
|
await assertExpectedHarnessShims(projectRoot, harnesses);
|
|
1179
995
|
await writeCursorWorkflowRule(projectRoot, harnesses);
|
|
@@ -1401,7 +1217,7 @@ export async function uninstallCclaw(projectRoot) {
|
|
|
1401
1217
|
}
|
|
1402
1218
|
await removeCclawFromAgentsMd(projectRoot);
|
|
1403
1219
|
await removeGitignorePatterns(projectRoot);
|
|
1404
|
-
await
|
|
1220
|
+
await cleanupLegacyManagedGitHookRelays(projectRoot);
|
|
1405
1221
|
const hookFiles = [
|
|
1406
1222
|
".claude/hooks/hooks.json",
|
|
1407
1223
|
".cursor/hooks.json",
|
|
@@ -14,6 +14,7 @@ import { parseAdvanceStageArgs, parseCancelRunArgs, parseHookArgs, parseRewindAr
|
|
|
14
14
|
import { parseFlowStateRepairArgs, runFlowStateRepair } from "./flow-state-repair.js";
|
|
15
15
|
import { parseWaiverGrantArgs, runWaiverGrant } from "./waiver-grant.js";
|
|
16
16
|
import { FlowStateGuardMismatchError, verifyFlowStateGuard } from "../run-persistence.js";
|
|
17
|
+
import { DelegationTimestampError, DispatchDuplicateError } from "../delegation.js";
|
|
17
18
|
/**
|
|
18
19
|
* Subcommands that mutate or consult flow-state.json via the CLI runtime.
|
|
19
20
|
* They all require the sha256 sidecar to match before continuing so a
|
|
@@ -91,6 +92,14 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
91
92
|
io.stderr.write(`cclaw internal ${subcommand}: ${err.message}\n`);
|
|
92
93
|
return 2;
|
|
93
94
|
}
|
|
95
|
+
if (err instanceof DelegationTimestampError) {
|
|
96
|
+
io.stderr.write(`error: delegation_timestamp_non_monotonic — ${err.field}: ${err.actual} < ${err.priorBound}\n`);
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
if (err instanceof DispatchDuplicateError) {
|
|
100
|
+
io.stderr.write(`error: dispatch_duplicate — ${err.message}\n`);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
94
103
|
io.stderr.write(`cclaw internal ${subcommand} failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
95
104
|
return 1;
|
|
96
105
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const WORKFLOW_PATH = /(^|\/)\.github\/workflows\//u;
|
|
5
|
+
const CURSOR_CONFIG_PATH = /(^|\/)\.cursor\//u;
|
|
6
|
+
const PACKAGE_JSON_PATH = /(^|\/)package\.json$/u;
|
|
7
|
+
const SUPPLY_CHAIN_DEP_KEYS = [
|
|
8
|
+
"dependencies",
|
|
9
|
+
"devDependencies",
|
|
10
|
+
"peerDependencies",
|
|
11
|
+
"optionalDependencies"
|
|
12
|
+
];
|
|
13
|
+
async function resolveDiffBase(projectRoot) {
|
|
14
|
+
try {
|
|
15
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD~1"], {
|
|
16
|
+
cwd: projectRoot
|
|
17
|
+
});
|
|
18
|
+
const base = stdout.trim();
|
|
19
|
+
return base.length > 0 ? base : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function readFileAtRev(projectRoot, rev, filePath) {
|
|
26
|
+
try {
|
|
27
|
+
const { stdout } = await execFileAsync("git", ["show", `${rev}:${filePath}`], {
|
|
28
|
+
cwd: projectRoot,
|
|
29
|
+
maxBuffer: 32 * 1024 * 1024
|
|
30
|
+
});
|
|
31
|
+
return stdout;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function dependencyMapsDiffer(before, after) {
|
|
38
|
+
const beforeKeys = before ? Object.keys(before).sort() : [];
|
|
39
|
+
const afterKeys = after ? Object.keys(after).sort() : [];
|
|
40
|
+
if (beforeKeys.length !== afterKeys.length)
|
|
41
|
+
return true;
|
|
42
|
+
for (let i = 0; i < beforeKeys.length; i += 1) {
|
|
43
|
+
if (beforeKeys[i] !== afterKeys[i])
|
|
44
|
+
return true;
|
|
45
|
+
const k = beforeKeys[i];
|
|
46
|
+
if (before[k] !== after[k]) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
async function packageJsonHasDependencyDiff(projectRoot, base, filePath) {
|
|
53
|
+
const beforeRaw = await readFileAtRev(projectRoot, base, filePath);
|
|
54
|
+
const afterRaw = await readFileAtRev(projectRoot, "HEAD", filePath);
|
|
55
|
+
// If either side is missing or unparseable, treat as changed (be conservative).
|
|
56
|
+
if (beforeRaw === null || afterRaw === null)
|
|
57
|
+
return true;
|
|
58
|
+
let beforeJson;
|
|
59
|
+
let afterJson;
|
|
60
|
+
try {
|
|
61
|
+
beforeJson = JSON.parse(beforeRaw);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
afterJson = JSON.parse(afterRaw);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const beforeObj = beforeJson !== null && typeof beforeJson === "object"
|
|
73
|
+
? beforeJson
|
|
74
|
+
: {};
|
|
75
|
+
const afterObj = afterJson !== null && typeof afterJson === "object"
|
|
76
|
+
? afterJson
|
|
77
|
+
: {};
|
|
78
|
+
for (const key of SUPPLY_CHAIN_DEP_KEYS) {
|
|
79
|
+
const beforeMap = (beforeObj[key] !== null && typeof beforeObj[key] === "object")
|
|
80
|
+
? beforeObj[key]
|
|
81
|
+
: undefined;
|
|
82
|
+
const afterMap = (afterObj[key] !== null && typeof afterObj[key] === "object")
|
|
83
|
+
? afterObj[key]
|
|
84
|
+
: undefined;
|
|
85
|
+
if (dependencyMapsDiffer(beforeMap, afterMap)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
export async function detectSupplyChainChanges(projectRoot) {
|
|
92
|
+
const base = await resolveDiffBase(projectRoot);
|
|
93
|
+
if (!base) {
|
|
94
|
+
return { triggered: false, changedFiles: [], reasons: [] };
|
|
95
|
+
}
|
|
96
|
+
let changed = [];
|
|
97
|
+
try {
|
|
98
|
+
const range = `${base}..HEAD`;
|
|
99
|
+
const { stdout } = await execFileAsync("git", ["diff", "--name-only", range], {
|
|
100
|
+
cwd: projectRoot
|
|
101
|
+
});
|
|
102
|
+
changed = stdout
|
|
103
|
+
.split(/\r?\n/gu)
|
|
104
|
+
.map((line) => line.trim())
|
|
105
|
+
.filter((line) => line.length > 0);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { triggered: false, changedFiles: [], reasons: [] };
|
|
109
|
+
}
|
|
110
|
+
const matchedFiles = [];
|
|
111
|
+
const reasons = [];
|
|
112
|
+
for (const filePath of changed) {
|
|
113
|
+
if (WORKFLOW_PATH.test(filePath)) {
|
|
114
|
+
matchedFiles.push(filePath);
|
|
115
|
+
reasons.push(`.github/workflows changed: ${filePath}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (CURSOR_CONFIG_PATH.test(filePath)) {
|
|
119
|
+
matchedFiles.push(filePath);
|
|
120
|
+
reasons.push(`.cursor config changed: ${filePath}`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (PACKAGE_JSON_PATH.test(filePath)) {
|
|
124
|
+
// Only flag when supply-chain dependency keys differ.
|
|
125
|
+
const depDiffers = await packageJsonHasDependencyDiff(projectRoot, base, filePath);
|
|
126
|
+
if (depDiffers) {
|
|
127
|
+
matchedFiles.push(filePath);
|
|
128
|
+
reasons.push(`${filePath} dependencies/devDependencies/peerDependencies/optionalDependencies changed`);
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
triggered: matchedFiles.length > 0,
|
|
135
|
+
changedFiles: matchedFiles,
|
|
136
|
+
reasons
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -7,6 +7,13 @@ export interface FlowStateRepairArgs {
|
|
|
7
7
|
reason: string;
|
|
8
8
|
json: boolean;
|
|
9
9
|
quiet: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* v6.9.0 — when true, normalize `state/early-loop.json` to the canonical
|
|
12
|
+
* shape derived from `early-loop-log.jsonl`. Lets operators recover from
|
|
13
|
+
* legacy hand-written `early-loop.json` files that drifted from the
|
|
14
|
+
* source-of-truth log.
|
|
15
|
+
*/
|
|
16
|
+
earlyLoop: boolean;
|
|
10
17
|
}
|
|
11
18
|
export declare function parseFlowStateRepairArgs(tokens: string[]): FlowStateRepairArgs;
|
|
12
19
|
export declare function runFlowStateRepair(projectRoot: string, args: FlowStateRepairArgs, io: InternalIo): Promise<number>;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
4
|
+
import { clampEarlyLoopStatusForWrite, computeEarlyLoopStatus, isEarlyLoopStage } from "../early-loop.js";
|
|
5
|
+
import { writeFileSafe } from "../fs-utils.js";
|
|
3
6
|
import { repairFlowStateGuard } from "../run-persistence.js";
|
|
7
|
+
import { readFlowState } from "../runs.js";
|
|
4
8
|
export function parseFlowStateRepairArgs(tokens) {
|
|
5
9
|
let reason;
|
|
6
10
|
let json = false;
|
|
7
11
|
let quiet = false;
|
|
12
|
+
let earlyLoop = false;
|
|
8
13
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
9
14
|
const token = tokens[i];
|
|
10
15
|
const nextToken = tokens[i + 1];
|
|
@@ -16,6 +21,10 @@ export function parseFlowStateRepairArgs(tokens) {
|
|
|
16
21
|
quiet = true;
|
|
17
22
|
continue;
|
|
18
23
|
}
|
|
24
|
+
if (token === "--early-loop") {
|
|
25
|
+
earlyLoop = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
19
28
|
if (token === "--reason") {
|
|
20
29
|
if (!nextToken || nextToken.startsWith("--")) {
|
|
21
30
|
throw new Error("--reason requires a short slug value.");
|
|
@@ -33,33 +42,63 @@ export function parseFlowStateRepairArgs(tokens) {
|
|
|
33
42
|
if (!reason || reason.length === 0) {
|
|
34
43
|
throw new Error("internal flow-state-repair requires --reason=<slug> (e.g. --reason=manual_edit_recovery).");
|
|
35
44
|
}
|
|
36
|
-
return { reason, json, quiet };
|
|
45
|
+
return { reason, json, quiet, earlyLoop };
|
|
46
|
+
}
|
|
47
|
+
async function repairEarlyLoopFile(projectRoot, io) {
|
|
48
|
+
const flow = await readFlowState(projectRoot).catch(() => null);
|
|
49
|
+
if (!flow) {
|
|
50
|
+
return { performed: false, skipped: "flow-state-unreadable" };
|
|
51
|
+
}
|
|
52
|
+
const stage = flow.currentStage;
|
|
53
|
+
if (!isEarlyLoopStage(stage)) {
|
|
54
|
+
return { performed: false, skipped: `current-stage-${stage}-not-early-loop` };
|
|
55
|
+
}
|
|
56
|
+
const runId = flow.activeRunId.trim();
|
|
57
|
+
if (runId.length === 0) {
|
|
58
|
+
io.stderr.write("cclaw internal flow-state-repair --early-loop: active run has no runId; cannot derive canonical early-loop.json.\n");
|
|
59
|
+
return { performed: false, skipped: "missing-active-runId" };
|
|
60
|
+
}
|
|
61
|
+
const stateDir = path.join(projectRoot, RUNTIME_ROOT, "state");
|
|
62
|
+
const logPath = path.join(stateDir, "early-loop-log.jsonl");
|
|
63
|
+
const status = await computeEarlyLoopStatus(stage, runId, logPath);
|
|
64
|
+
const persisted = clampEarlyLoopStatusForWrite(status);
|
|
65
|
+
const finalStatus = persisted.status;
|
|
66
|
+
const target = path.join(stateDir, "early-loop.json");
|
|
67
|
+
await writeFileSafe(target, `${JSON.stringify(finalStatus, null, 2)}\n`);
|
|
68
|
+
return {
|
|
69
|
+
performed: true,
|
|
70
|
+
stage,
|
|
71
|
+
runId,
|
|
72
|
+
iteration: finalStatus.iteration,
|
|
73
|
+
openConcernCount: finalStatus.openConcerns.length
|
|
74
|
+
};
|
|
37
75
|
}
|
|
38
76
|
export async function runFlowStateRepair(projectRoot, args, io) {
|
|
39
77
|
const result = await repairFlowStateGuard(projectRoot, args.reason);
|
|
40
78
|
const logRel = path.relative(projectRoot, result.repairLogPath).replace(/\\/gu, "/");
|
|
41
79
|
const guardRel = path.relative(projectRoot, result.guardPath).replace(/\\/gu, "/");
|
|
80
|
+
let earlyLoopOutcome = null;
|
|
81
|
+
if (args.earlyLoop) {
|
|
82
|
+
earlyLoopOutcome = await repairEarlyLoopFile(projectRoot, io);
|
|
83
|
+
}
|
|
84
|
+
void fs;
|
|
85
|
+
const payload = {
|
|
86
|
+
ok: true,
|
|
87
|
+
command: "flow-state-repair",
|
|
88
|
+
reason: args.reason,
|
|
89
|
+
sidecar: result.sidecar,
|
|
90
|
+
guardPath: guardRel,
|
|
91
|
+
repairLogPath: logRel,
|
|
92
|
+
completedStageMetaBackfilled: result.completedStageMetaBackfilled,
|
|
93
|
+
earlyLoop: earlyLoopOutcome,
|
|
94
|
+
runtimeRoot: RUNTIME_ROOT
|
|
95
|
+
};
|
|
42
96
|
if (args.json) {
|
|
43
|
-
io.stdout.write(`${JSON.stringify(
|
|
44
|
-
ok: true,
|
|
45
|
-
command: "flow-state-repair",
|
|
46
|
-
reason: args.reason,
|
|
47
|
-
sidecar: result.sidecar,
|
|
48
|
-
guardPath: guardRel,
|
|
49
|
-
repairLogPath: logRel,
|
|
50
|
-
runtimeRoot: RUNTIME_ROOT
|
|
51
|
-
})}\n`);
|
|
97
|
+
io.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
52
98
|
return 0;
|
|
53
99
|
}
|
|
54
100
|
if (!args.quiet) {
|
|
55
|
-
io.stdout.write(`${JSON.stringify(
|
|
56
|
-
ok: true,
|
|
57
|
-
command: "flow-state-repair",
|
|
58
|
-
reason: args.reason,
|
|
59
|
-
sidecar: result.sidecar,
|
|
60
|
-
guardPath: guardRel,
|
|
61
|
-
repairLogPath: logRel
|
|
62
|
-
}, null, 2)}\n`);
|
|
101
|
+
io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
63
102
|
}
|
|
64
103
|
return 0;
|
|
65
104
|
}
|
|
@@ -92,6 +92,8 @@ export interface FlowStateRepairResult {
|
|
|
92
92
|
sidecar: FlowStateGuardSidecar;
|
|
93
93
|
repairLogPath: string;
|
|
94
94
|
guardPath: string;
|
|
95
|
+
/** Stages that were retro-backfilled into completedStageMeta during repair. */
|
|
96
|
+
completedStageMetaBackfilled: FlowStage[];
|
|
95
97
|
}
|
|
96
98
|
/**
|
|
97
99
|
* Recompute the write-guard sidecar from the current on-disk flow-state
|