cclaw-cli 7.7.1 → 8.1.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/README.md +210 -134
- package/dist/artifact-frontmatter.d.ts +51 -0
- package/dist/artifact-frontmatter.js +131 -0
- package/dist/artifact-paths.d.ts +7 -27
- package/dist/artifact-paths.js +20 -249
- package/dist/cancel.d.ts +16 -0
- package/dist/cancel.js +66 -0
- package/dist/cli.d.ts +2 -27
- package/dist/cli.js +90 -508
- package/dist/compound.d.ts +26 -0
- package/dist/compound.js +96 -0
- package/dist/config.d.ts +14 -51
- package/dist/config.js +23 -359
- package/dist/constants.d.ts +11 -18
- package/dist/constants.js +19 -106
- package/dist/content/antipatterns.d.ts +1 -0
- package/dist/content/antipatterns.js +109 -0
- package/dist/content/artifact-templates.d.ts +10 -0
- package/dist/content/artifact-templates.js +550 -0
- package/dist/content/cancel-command.d.ts +2 -2
- package/dist/content/cancel-command.js +25 -17
- package/dist/content/core-agents.d.ts +9 -233
- package/dist/content/core-agents.js +39 -768
- package/dist/content/decision-protocol.d.ts +1 -12
- package/dist/content/decision-protocol.js +27 -20
- package/dist/content/examples.d.ts +8 -42
- package/dist/content/examples.js +293 -425
- package/dist/content/idea-command.d.ts +2 -0
- package/dist/content/idea-command.js +38 -0
- package/dist/content/iron-laws.d.ts +4 -138
- package/dist/content/iron-laws.js +18 -197
- package/dist/content/meta-skill.d.ts +1 -3
- package/dist/content/meta-skill.js +57 -134
- package/dist/content/node-hooks.d.ts +12 -8
- package/dist/content/node-hooks.js +188 -838
- package/dist/content/recovery.d.ts +8 -0
- package/dist/content/recovery.js +179 -0
- package/dist/content/reference-patterns.d.ts +4 -13
- package/dist/content/reference-patterns.js +260 -389
- package/dist/content/research-playbooks.d.ts +8 -8
- package/dist/content/research-playbooks.js +108 -121
- package/dist/content/review-loop.d.ts +6 -192
- package/dist/content/review-loop.js +29 -731
- package/dist/content/skills.d.ts +8 -38
- package/dist/content/skills.js +681 -732
- package/dist/content/specialist-prompts/architect.d.ts +1 -0
- package/dist/content/specialist-prompts/architect.js +225 -0
- package/dist/content/specialist-prompts/brainstormer.d.ts +1 -0
- package/dist/content/specialist-prompts/brainstormer.js +168 -0
- package/dist/content/specialist-prompts/index.d.ts +2 -0
- package/dist/content/specialist-prompts/index.js +14 -0
- package/dist/content/specialist-prompts/planner.d.ts +1 -0
- package/dist/content/specialist-prompts/planner.js +182 -0
- package/dist/content/specialist-prompts/reviewer.d.ts +1 -0
- package/dist/content/specialist-prompts/reviewer.js +193 -0
- package/dist/content/specialist-prompts/security-reviewer.d.ts +1 -0
- package/dist/content/specialist-prompts/security-reviewer.js +133 -0
- package/dist/content/specialist-prompts/slice-builder.d.ts +1 -0
- package/dist/content/specialist-prompts/slice-builder.js +232 -0
- package/dist/content/stage-playbooks.d.ts +8 -0
- package/dist/content/stage-playbooks.js +404 -0
- package/dist/content/start-command.d.ts +2 -12
- package/dist/content/start-command.js +221 -207
- package/dist/flow-state.d.ts +21 -178
- package/dist/flow-state.js +67 -170
- package/dist/fs-utils.d.ts +6 -26
- package/dist/fs-utils.js +29 -162
- package/dist/gitignore.d.ts +2 -1
- package/dist/gitignore.js +51 -34
- package/dist/harness-detect.d.ts +10 -0
- package/dist/harness-detect.js +29 -0
- package/dist/install.d.ts +27 -15
- package/dist/install.js +230 -1342
- package/dist/knowledge-store.d.ts +19 -163
- package/dist/knowledge-store.js +56 -590
- package/dist/logger.d.ts +8 -3
- package/dist/logger.js +13 -4
- package/dist/orchestrator-routing.d.ts +29 -0
- package/dist/orchestrator-routing.js +156 -0
- package/dist/run-persistence.d.ts +7 -118
- package/dist/run-persistence.js +29 -845
- package/dist/runtime/run-hook.entry.d.ts +1 -3
- package/dist/runtime/run-hook.entry.js +19 -4
- package/dist/runtime/run-hook.mjs +13 -1024
- package/dist/types.d.ts +25 -261
- package/dist/types.js +8 -36
- package/package.json +6 -3
- package/dist/artifact-linter/brainstorm.d.ts +0 -2
- package/dist/artifact-linter/brainstorm.js +0 -353
- package/dist/artifact-linter/design.d.ts +0 -18
- package/dist/artifact-linter/design.js +0 -444
- package/dist/artifact-linter/findings-dedup.d.ts +0 -56
- package/dist/artifact-linter/findings-dedup.js +0 -232
- package/dist/artifact-linter/plan.d.ts +0 -2
- package/dist/artifact-linter/plan.js +0 -826
- package/dist/artifact-linter/review-army.d.ts +0 -49
- package/dist/artifact-linter/review-army.js +0 -520
- package/dist/artifact-linter/review.d.ts +0 -2
- package/dist/artifact-linter/review.js +0 -113
- package/dist/artifact-linter/scope.d.ts +0 -2
- package/dist/artifact-linter/scope.js +0 -158
- package/dist/artifact-linter/shared.d.ts +0 -637
- package/dist/artifact-linter/shared.js +0 -2163
- package/dist/artifact-linter/ship.d.ts +0 -2
- package/dist/artifact-linter/ship.js +0 -250
- package/dist/artifact-linter/spec.d.ts +0 -2
- package/dist/artifact-linter/spec.js +0 -176
- package/dist/artifact-linter/tdd.d.ts +0 -118
- package/dist/artifact-linter/tdd.js +0 -1404
- package/dist/artifact-linter.d.ts +0 -15
- package/dist/artifact-linter.js +0 -517
- package/dist/codex-feature-flag.d.ts +0 -58
- package/dist/codex-feature-flag.js +0 -193
- package/dist/content/closeout-guidance.d.ts +0 -14
- package/dist/content/closeout-guidance.js +0 -44
- package/dist/content/diff-command.d.ts +0 -1
- package/dist/content/diff-command.js +0 -43
- package/dist/content/harness-doc.d.ts +0 -1
- package/dist/content/harness-doc.js +0 -65
- package/dist/content/hook-events.d.ts +0 -9
- package/dist/content/hook-events.js +0 -23
- package/dist/content/hook-manifest.d.ts +0 -81
- package/dist/content/hook-manifest.js +0 -156
- package/dist/content/hooks.d.ts +0 -11
- package/dist/content/hooks.js +0 -1972
- package/dist/content/idea.d.ts +0 -60
- package/dist/content/idea.js +0 -416
- package/dist/content/language-policy.d.ts +0 -2
- package/dist/content/language-policy.js +0 -13
- package/dist/content/learnings.d.ts +0 -6
- package/dist/content/learnings.js +0 -141
- package/dist/content/observe.d.ts +0 -19
- package/dist/content/observe.js +0 -86
- package/dist/content/opencode-plugin.d.ts +0 -1
- package/dist/content/opencode-plugin.js +0 -635
- package/dist/content/review-prompts.d.ts +0 -1
- package/dist/content/review-prompts.js +0 -104
- package/dist/content/runtime-shared-snippets.d.ts +0 -8
- package/dist/content/runtime-shared-snippets.js +0 -80
- package/dist/content/session-hooks.d.ts +0 -7
- package/dist/content/session-hooks.js +0 -107
- package/dist/content/skills-elicitation.d.ts +0 -1
- package/dist/content/skills-elicitation.js +0 -167
- package/dist/content/stage-command.d.ts +0 -2
- package/dist/content/stage-command.js +0 -17
- package/dist/content/stage-schema.d.ts +0 -117
- package/dist/content/stage-schema.js +0 -955
- package/dist/content/stages/_lint-metadata/index.d.ts +0 -2
- package/dist/content/stages/_lint-metadata/index.js +0 -97
- package/dist/content/stages/brainstorm.d.ts +0 -2
- package/dist/content/stages/brainstorm.js +0 -184
- package/dist/content/stages/design.d.ts +0 -2
- package/dist/content/stages/design.js +0 -288
- package/dist/content/stages/index.d.ts +0 -8
- package/dist/content/stages/index.js +0 -11
- package/dist/content/stages/plan.d.ts +0 -2
- package/dist/content/stages/plan.js +0 -191
- package/dist/content/stages/review.d.ts +0 -2
- package/dist/content/stages/review.js +0 -240
- package/dist/content/stages/schema-types.d.ts +0 -203
- package/dist/content/stages/schema-types.js +0 -1
- package/dist/content/stages/scope.d.ts +0 -2
- package/dist/content/stages/scope.js +0 -254
- package/dist/content/stages/ship.d.ts +0 -2
- package/dist/content/stages/ship.js +0 -159
- package/dist/content/stages/spec.d.ts +0 -2
- package/dist/content/stages/spec.js +0 -170
- package/dist/content/stages/tdd.d.ts +0 -4
- package/dist/content/stages/tdd.js +0 -273
- package/dist/content/state-contracts.d.ts +0 -1
- package/dist/content/state-contracts.js +0 -63
- package/dist/content/status-command.d.ts +0 -4
- package/dist/content/status-command.js +0 -109
- package/dist/content/subagent-context-skills.d.ts +0 -4
- package/dist/content/subagent-context-skills.js +0 -279
- package/dist/content/subagents.d.ts +0 -3
- package/dist/content/subagents.js +0 -997
- package/dist/content/templates.d.ts +0 -26
- package/dist/content/templates.js +0 -1692
- package/dist/content/track-render-context.d.ts +0 -18
- package/dist/content/track-render-context.js +0 -53
- package/dist/content/tree-command.d.ts +0 -1
- package/dist/content/tree-command.js +0 -64
- package/dist/content/utility-skills.d.ts +0 -30
- package/dist/content/utility-skills.js +0 -160
- package/dist/content/view-command.d.ts +0 -2
- package/dist/content/view-command.js +0 -92
- package/dist/delegation.d.ts +0 -649
- package/dist/delegation.js +0 -1539
- package/dist/early-loop.d.ts +0 -70
- package/dist/early-loop.js +0 -302
- package/dist/execution-topology.d.ts +0 -44
- package/dist/execution-topology.js +0 -95
- package/dist/gate-evidence.d.ts +0 -85
- package/dist/gate-evidence.js +0 -631
- package/dist/harness-adapters.d.ts +0 -151
- package/dist/harness-adapters.js +0 -756
- package/dist/harness-selection.d.ts +0 -31
- package/dist/harness-selection.js +0 -214
- package/dist/hook-schema.d.ts +0 -6
- package/dist/hook-schema.js +0 -114
- package/dist/hook-schemas/claude-hooks.v1.json +0 -10
- package/dist/hook-schemas/codex-hooks.v1.json +0 -10
- package/dist/hook-schemas/cursor-hooks.v1.json +0 -13
- package/dist/init-detect.d.ts +0 -2
- package/dist/init-detect.js +0 -50
- package/dist/internal/advance-stage/advance.d.ts +0 -89
- package/dist/internal/advance-stage/advance.js +0 -655
- package/dist/internal/advance-stage/cancel-run.d.ts +0 -8
- package/dist/internal/advance-stage/cancel-run.js +0 -19
- package/dist/internal/advance-stage/flow-state-coercion.d.ts +0 -3
- package/dist/internal/advance-stage/flow-state-coercion.js +0 -81
- package/dist/internal/advance-stage/helpers.d.ts +0 -14
- package/dist/internal/advance-stage/helpers.js +0 -145
- package/dist/internal/advance-stage/hook.d.ts +0 -8
- package/dist/internal/advance-stage/hook.js +0 -40
- package/dist/internal/advance-stage/parsers.d.ts +0 -72
- package/dist/internal/advance-stage/parsers.js +0 -357
- package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +0 -24
- package/dist/internal/advance-stage/proactive-delegation-trace.js +0 -56
- package/dist/internal/advance-stage/review-loop.d.ts +0 -16
- package/dist/internal/advance-stage/review-loop.js +0 -199
- package/dist/internal/advance-stage/rewind.d.ts +0 -14
- package/dist/internal/advance-stage/rewind.js +0 -108
- package/dist/internal/advance-stage/start-flow.d.ts +0 -13
- package/dist/internal/advance-stage/start-flow.js +0 -241
- package/dist/internal/advance-stage/verify.d.ts +0 -21
- package/dist/internal/advance-stage/verify.js +0 -185
- package/dist/internal/advance-stage.d.ts +0 -7
- package/dist/internal/advance-stage.js +0 -138
- package/dist/internal/cohesion-contract-stub.d.ts +0 -24
- package/dist/internal/cohesion-contract-stub.js +0 -148
- package/dist/internal/compound-readiness.d.ts +0 -23
- package/dist/internal/compound-readiness.js +0 -102
- package/dist/internal/detect-public-api-changes.d.ts +0 -5
- package/dist/internal/detect-public-api-changes.js +0 -45
- package/dist/internal/detect-supply-chain-changes.d.ts +0 -6
- package/dist/internal/detect-supply-chain-changes.js +0 -138
- package/dist/internal/early-loop-status.d.ts +0 -7
- package/dist/internal/early-loop-status.js +0 -93
- package/dist/internal/envelope-validate.d.ts +0 -7
- package/dist/internal/envelope-validate.js +0 -66
- package/dist/internal/flow-state-repair.d.ts +0 -20
- package/dist/internal/flow-state-repair.js +0 -104
- package/dist/internal/plan-split-waves.d.ts +0 -190
- package/dist/internal/plan-split-waves.js +0 -764
- package/dist/internal/runtime-integrity.d.ts +0 -7
- package/dist/internal/runtime-integrity.js +0 -268
- package/dist/internal/slice-commit.d.ts +0 -7
- package/dist/internal/slice-commit.js +0 -619
- package/dist/internal/tdd-loop-status.d.ts +0 -14
- package/dist/internal/tdd-loop-status.js +0 -68
- package/dist/internal/tdd-red-evidence.d.ts +0 -7
- package/dist/internal/tdd-red-evidence.js +0 -153
- package/dist/internal/waiver-grant.d.ts +0 -62
- package/dist/internal/waiver-grant.js +0 -294
- package/dist/internal/wave-status.d.ts +0 -74
- package/dist/internal/wave-status.js +0 -506
- package/dist/managed-resources.d.ts +0 -53
- package/dist/managed-resources.js +0 -313
- package/dist/policy.d.ts +0 -10
- package/dist/policy.js +0 -167
- package/dist/retro-gate.d.ts +0 -9
- package/dist/retro-gate.js +0 -47
- package/dist/run-archive.d.ts +0 -61
- package/dist/run-archive.js +0 -391
- package/dist/runs.d.ts +0 -2
- package/dist/runs.js +0 -2
- package/dist/stack-detection.d.ts +0 -116
- package/dist/stack-detection.js +0 -489
- package/dist/streaming/event-stream.d.ts +0 -31
- package/dist/streaming/event-stream.js +0 -114
- package/dist/tdd-cycle.d.ts +0 -107
- package/dist/tdd-cycle.js +0 -289
- package/dist/tdd-verification-evidence.d.ts +0 -17
- package/dist/tdd-verification-evidence.js +0 -122
- package/dist/track-heuristics.d.ts +0 -27
- package/dist/track-heuristics.js +0 -154
- package/dist/util/slice-id.d.ts +0 -58
- package/dist/util/slice-id.js +0 -89
- package/dist/worktree-manager.d.ts +0 -20
- package/dist/worktree-manager.js +0 -108
package/dist/content/hooks.js
DELETED
|
@@ -1,1972 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { RUNTIME_ROOT } from "../constants.js";
|
|
5
|
-
import { DELEGATION_DISPATCH_SURFACES, DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES, DELEGATION_PHASES } from "../delegation.js";
|
|
6
|
-
function resolveCliRuntimeForGeneratedHook() {
|
|
7
|
-
const here = fileURLToPath(import.meta.url);
|
|
8
|
-
// Vitest runs init/sync from src/ and expects helpers to execute the same
|
|
9
|
-
// source runtime, even when a stale dist/ exists in the repository.
|
|
10
|
-
if (process.env.VITEST === "true") {
|
|
11
|
-
const sourceCli = path.resolve(path.dirname(here), "..", "cli.ts");
|
|
12
|
-
const viteNode = path.resolve(path.dirname(here), "..", "..", "node_modules", "vite-node", "vite-node.mjs");
|
|
13
|
-
if (existsSync(sourceCli) && existsSync(viteNode)) {
|
|
14
|
-
return { entrypoint: viteNode, argsPrefix: ["--script", sourceCli] };
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
const candidates = [
|
|
18
|
-
path.resolve(path.dirname(here), "..", "cli.js"),
|
|
19
|
-
path.resolve(path.dirname(here), "..", "..", "dist", "cli.js")
|
|
20
|
-
];
|
|
21
|
-
for (const candidate of candidates) {
|
|
22
|
-
// Synchronous probe runs only during cclaw-cli init/sync generation.
|
|
23
|
-
// The generated hook receives a concrete path and does not need a global bin.
|
|
24
|
-
if (existsSync(candidate))
|
|
25
|
-
return { entrypoint: candidate, argsPrefix: [] };
|
|
26
|
-
}
|
|
27
|
-
return { entrypoint: null, argsPrefix: [] };
|
|
28
|
-
}
|
|
29
|
-
function internalHelperScript(helperName, internalSubcommand, usage, options) {
|
|
30
|
-
const cliRuntime = resolveCliRuntimeForGeneratedHook();
|
|
31
|
-
return `#!/usr/bin/env node
|
|
32
|
-
import fs from "node:fs/promises";
|
|
33
|
-
import path from "node:path";
|
|
34
|
-
import process from "node:process";
|
|
35
|
-
import { spawn } from "node:child_process";
|
|
36
|
-
|
|
37
|
-
const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
38
|
-
const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
|
|
39
|
-
const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
|
|
40
|
-
const HELPER_NAME = ${JSON.stringify(helperName)};
|
|
41
|
-
const INTERNAL_SUBCOMMAND = ${JSON.stringify(internalSubcommand)};
|
|
42
|
-
const USAGE = ${JSON.stringify(usage)};
|
|
43
|
-
const POSITIONAL_ARG_NAME = ${JSON.stringify(options?.positionalArgName ?? null)};
|
|
44
|
-
const POSITIONAL_ARG_REQUIRED = ${JSON.stringify(options?.positionalArgRequired === true)};
|
|
45
|
-
const DEFAULT_QUIET_ENV_VAR = ${JSON.stringify(options?.defaultQuietEnvVar ?? null)};
|
|
46
|
-
|
|
47
|
-
async function detectRoot() {
|
|
48
|
-
const candidates = [
|
|
49
|
-
process.env.CCLAW_PROJECT_ROOT,
|
|
50
|
-
process.env.CLAUDE_PROJECT_DIR,
|
|
51
|
-
process.env.CURSOR_PROJECT_DIR,
|
|
52
|
-
process.env.CURSOR_PROJECT_ROOT,
|
|
53
|
-
process.env.OPENCODE_PROJECT_DIR,
|
|
54
|
-
process.env.OPENCODE_PROJECT_ROOT,
|
|
55
|
-
process.cwd()
|
|
56
|
-
].filter((value) => typeof value === "string" && value.length > 0);
|
|
57
|
-
|
|
58
|
-
for (const candidate of candidates) {
|
|
59
|
-
try {
|
|
60
|
-
const runtimePath = path.join(candidate, RUNTIME_ROOT);
|
|
61
|
-
const stat = await fs.stat(runtimePath);
|
|
62
|
-
if (stat.isDirectory()) return candidate;
|
|
63
|
-
} catch {
|
|
64
|
-
// continue
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return candidates[0] || process.cwd();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function printUsage() {
|
|
71
|
-
process.stderr.write(USAGE + "\\n");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function main() {
|
|
75
|
-
const [, , ...argvTokens] = process.argv;
|
|
76
|
-
if (argvTokens.includes("--help") || argvTokens.includes("-h")) {
|
|
77
|
-
printUsage();
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
let positionalArg = "";
|
|
81
|
-
let flags = argvTokens;
|
|
82
|
-
if (POSITIONAL_ARG_NAME !== null) {
|
|
83
|
-
positionalArg = (argvTokens[0] ?? "").trim();
|
|
84
|
-
flags = argvTokens.slice(1);
|
|
85
|
-
if (POSITIONAL_ARG_REQUIRED && positionalArg.length === 0) {
|
|
86
|
-
printUsage();
|
|
87
|
-
process.exitCode = 1;
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (DEFAULT_QUIET_ENV_VAR !== null) {
|
|
93
|
-
const envRaw = process.env[DEFAULT_QUIET_ENV_VAR];
|
|
94
|
-
if (typeof envRaw !== "string" || envRaw.trim().length === 0) {
|
|
95
|
-
process.env[DEFAULT_QUIET_ENV_VAR] = "1";
|
|
96
|
-
}
|
|
97
|
-
const quietRaw = (process.env[DEFAULT_QUIET_ENV_VAR] ?? "").trim().toLowerCase();
|
|
98
|
-
const quietEnabled = !/^(0|false|no|off)$/u.test(quietRaw);
|
|
99
|
-
const alreadyQuiet = flags.includes("--quiet");
|
|
100
|
-
if (quietEnabled && !alreadyQuiet) {
|
|
101
|
-
flags = [...flags, "--quiet"];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const root = await detectRoot();
|
|
106
|
-
const runtimePath = path.join(root, RUNTIME_ROOT);
|
|
107
|
-
try {
|
|
108
|
-
const stat = await fs.stat(runtimePath);
|
|
109
|
-
if (!stat.isDirectory()) throw new Error("not-dir");
|
|
110
|
-
} catch {
|
|
111
|
-
process.stderr.write("[cclaw] " + HELPER_NAME + ": runtime root not found at " + runtimePath + "\\n");
|
|
112
|
-
process.exitCode = 1;
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const cliEntrypoint = process.env.CCLAW_CLI_JS || CCLAW_CLI_ENTRYPOINT;
|
|
117
|
-
const cliArgsPrefix = process.env.CCLAW_CLI_JS ? [] : CCLAW_CLI_ARGS_PREFIX;
|
|
118
|
-
if (!cliEntrypoint || cliEntrypoint.trim().length === 0) {
|
|
119
|
-
process.stderr.write(
|
|
120
|
-
"[cclaw] " + HELPER_NAME + ": local Node runtime entrypoint is missing. Re-run npx cclaw-cli sync, or set CCLAW_CLI_JS=/absolute/path/to/dist/cli.js for this session.\\n"
|
|
121
|
-
);
|
|
122
|
-
process.exitCode = 1;
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const stat = await fs.stat(cliEntrypoint);
|
|
128
|
-
if (!stat.isFile()) throw new Error("not-file");
|
|
129
|
-
for (const argPath of cliArgsPrefix) {
|
|
130
|
-
if (typeof argPath !== "string" || argPath.startsWith("-")) continue;
|
|
131
|
-
const argStat = await fs.stat(argPath);
|
|
132
|
-
if (!argStat.isFile()) throw new Error("arg-not-file");
|
|
133
|
-
}
|
|
134
|
-
} catch {
|
|
135
|
-
process.stderr.write(
|
|
136
|
-
"[cclaw] " + HELPER_NAME + ": local Node runtime entrypoint not found at " + cliEntrypoint + ". Re-run npx cclaw-cli sync, or set CCLAW_CLI_JS=/absolute/path/to/dist/cli.js for this session.\\n"
|
|
137
|
-
);
|
|
138
|
-
process.exitCode = 1;
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const internalArgs =
|
|
143
|
-
POSITIONAL_ARG_NAME !== null
|
|
144
|
-
? [INTERNAL_SUBCOMMAND, positionalArg, ...flags]
|
|
145
|
-
: [INTERNAL_SUBCOMMAND, ...flags];
|
|
146
|
-
|
|
147
|
-
const child = spawn(process.execPath, [cliEntrypoint, ...cliArgsPrefix, "internal", ...internalArgs], {
|
|
148
|
-
cwd: root,
|
|
149
|
-
env: process.env,
|
|
150
|
-
stdio: "inherit"
|
|
151
|
-
});
|
|
152
|
-
let spawnErrored = false;
|
|
153
|
-
|
|
154
|
-
child.on("error", (error) => {
|
|
155
|
-
spawnErrored = true;
|
|
156
|
-
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
157
|
-
if (code === "ENOENT") {
|
|
158
|
-
process.stderr.write(
|
|
159
|
-
"[cclaw] " + HELPER_NAME + ": node executable not found while invoking local runtime. Re-run npx cclaw-cli sync.\\n"
|
|
160
|
-
);
|
|
161
|
-
} else {
|
|
162
|
-
process.stderr.write(
|
|
163
|
-
"[cclaw] " + HELPER_NAME + ": failed to invoke local Node runtime (" +
|
|
164
|
-
(error instanceof Error ? error.message : String(error)) +
|
|
165
|
-
").\\n"
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
process.exitCode = 1;
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
child.on("close", (code, signal) => {
|
|
172
|
-
if (spawnErrored) {
|
|
173
|
-
process.exitCode = 1;
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (signal) {
|
|
177
|
-
process.exitCode = 1;
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
process.exitCode = typeof code === "number" && code >= 0 ? code : 1;
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
void main();
|
|
185
|
-
`;
|
|
186
|
-
}
|
|
187
|
-
export function startFlowScript() {
|
|
188
|
-
return internalHelperScript("start-flow", "start-flow", "Usage: node " + RUNTIME_ROOT + "/hooks/start-flow.mjs --track=<standard|medium|quick> [--discovery-mode=<lean|guided|deep>] [--class=...] [--prompt=...] [--stack=...] [--reason=...] [--reclassify] [--force-reset]", { defaultQuietEnvVar: "CCLAW_START_FLOW_QUIET" });
|
|
189
|
-
}
|
|
190
|
-
export function cancelRunScript() {
|
|
191
|
-
return internalHelperScript("cancel-run", "cancel-run", "Usage: node " + RUNTIME_ROOT + "/hooks/cancel-run.mjs --reason=<text> [--disposition=<cancelled|abandoned>] [--name=<slug>]");
|
|
192
|
-
}
|
|
193
|
-
export function stageCompleteScript() {
|
|
194
|
-
return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--accept-proactive-waiver=<token>] [--accept-proactive-waiver-reason=\"<why safe>\"] [--skip-questions] [--json]", {
|
|
195
|
-
positionalArgName: "stage",
|
|
196
|
-
positionalArgRequired: true,
|
|
197
|
-
defaultQuietEnvVar: "CCLAW_STAGE_COMPLETE_QUIET"
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
export function delegationRecordScript() {
|
|
201
|
-
return `#!/usr/bin/env node
|
|
202
|
-
import { createHash } from "node:crypto";
|
|
203
|
-
import { spawn } from "node:child_process";
|
|
204
|
-
import fs from "node:fs/promises";
|
|
205
|
-
import path from "node:path";
|
|
206
|
-
import process from "node:process";
|
|
207
|
-
|
|
208
|
-
const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
209
|
-
const VALID_STATUSES = new Set(["scheduled", "launched", "acknowledged", "completed", "failed", "waived", "stale"]);
|
|
210
|
-
const TERMINAL = new Set(["completed", "failed", "waived", "stale"]);
|
|
211
|
-
const VALID_DISPATCH_SURFACES = ${JSON.stringify([...DELEGATION_DISPATCH_SURFACES])};
|
|
212
|
-
const VALID_DISPATCH_SURFACES_SET = new Set(VALID_DISPATCH_SURFACES);
|
|
213
|
-
const SURFACE_PATH_PREFIXES = ${JSON.stringify(DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES)};
|
|
214
|
-
const VALID_DELEGATION_PHASES = ${JSON.stringify([...DELEGATION_PHASES])};
|
|
215
|
-
const VALID_DELEGATION_PHASES_SET = new Set(VALID_DELEGATION_PHASES);
|
|
216
|
-
const LEDGER_SCHEMA_VERSION = 3;
|
|
217
|
-
const FLOW_STATE_GUARD_REL_PATH = RUNTIME_ROOT + "/.flow-state.guard.json";
|
|
218
|
-
|
|
219
|
-
async function verifyFlowStateGuardInline(root) {
|
|
220
|
-
const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
|
|
221
|
-
const guardPath = path.join(root, FLOW_STATE_GUARD_REL_PATH);
|
|
222
|
-
let raw;
|
|
223
|
-
try {
|
|
224
|
-
raw = await fs.readFile(statePath, "utf8");
|
|
225
|
-
} catch {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
let guard;
|
|
229
|
-
try {
|
|
230
|
-
const guardRaw = await fs.readFile(guardPath, "utf8");
|
|
231
|
-
guard = JSON.parse(guardRaw);
|
|
232
|
-
} catch {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
if (!guard || typeof guard !== "object" || typeof guard.sha256 !== "string") return;
|
|
236
|
-
const actual = createHash("sha256").update(raw, "utf8").digest("hex");
|
|
237
|
-
if (actual === guard.sha256) return;
|
|
238
|
-
process.stderr.write(
|
|
239
|
-
"[cclaw] delegation-record: flow-state guard mismatch: " + (guard.runId || "unknown-run") + "\\n" +
|
|
240
|
-
"expected sha: " + guard.sha256 + "\\n" +
|
|
241
|
-
"actual sha: " + actual + "\\n" +
|
|
242
|
-
"last writer: " + (guard.writerSubsystem || "unknown") + "@" + (guard.writtenAt || "unknown") + "\\n" +
|
|
243
|
-
"do not edit flow-state.json by hand. To recover, run:\\n" +
|
|
244
|
-
" cclaw-cli internal flow-state-repair --reason \\"manual_edit_recovery\\"\\n"
|
|
245
|
-
);
|
|
246
|
-
process.exit(2);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function parseArgs(argv) {
|
|
250
|
-
const args = {};
|
|
251
|
-
for (const raw of argv) {
|
|
252
|
-
const valueMatch = /^--([^=]+)=(.*)$/u.exec(raw);
|
|
253
|
-
if (valueMatch) {
|
|
254
|
-
args[valueMatch[1]] = valueMatch[2];
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
const flagMatch = /^--([^=]+)$/u.exec(raw);
|
|
258
|
-
if (flagMatch) args[flagMatch[1]] = true;
|
|
259
|
-
}
|
|
260
|
-
return args;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function exists(filePath) {
|
|
264
|
-
try {
|
|
265
|
-
await fs.access(filePath);
|
|
266
|
-
return true;
|
|
267
|
-
} catch {
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async function detectRoot() {
|
|
273
|
-
const candidates = [
|
|
274
|
-
process.env.CCLAW_PROJECT_ROOT,
|
|
275
|
-
process.env.CLAUDE_PROJECT_DIR,
|
|
276
|
-
process.env.CURSOR_PROJECT_DIR,
|
|
277
|
-
process.env.CURSOR_PROJECT_ROOT,
|
|
278
|
-
process.env.OPENCODE_PROJECT_DIR,
|
|
279
|
-
process.env.OPENCODE_PROJECT_ROOT,
|
|
280
|
-
process.cwd()
|
|
281
|
-
].filter((value) => typeof value === "string" && value.length > 0);
|
|
282
|
-
for (const candidate of candidates) {
|
|
283
|
-
if (await exists(path.join(candidate, RUNTIME_ROOT))) return candidate;
|
|
284
|
-
}
|
|
285
|
-
return candidates[0] || process.cwd();
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async function readRunId(root) {
|
|
289
|
-
try {
|
|
290
|
-
const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "flow-state.json"), "utf8");
|
|
291
|
-
const parsed = JSON.parse(raw);
|
|
292
|
-
return typeof parsed.activeRunId === "string" ? parsed.activeRunId : "unknown-run";
|
|
293
|
-
} catch {
|
|
294
|
-
return "unknown-run";
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Read \`tddGreenMinElapsedMs\` from flow-state.json. Defaults to 4000ms
|
|
299
|
-
// when missing or invalid. Operators set 0 to disable the freshness floor
|
|
300
|
-
// while keeping RED-test-name and passing-assertion checks active.
|
|
301
|
-
async function readTddGreenMinElapsedMsInline(root) {
|
|
302
|
-
try {
|
|
303
|
-
const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "flow-state.json"), "utf8");
|
|
304
|
-
const parsed = JSON.parse(raw);
|
|
305
|
-
if (parsed && typeof parsed.tddGreenMinElapsedMs === "number" && parsed.tddGreenMinElapsedMs >= 0) {
|
|
306
|
-
return Math.floor(parsed.tddGreenMinElapsedMs);
|
|
307
|
-
}
|
|
308
|
-
return 4000;
|
|
309
|
-
} catch {
|
|
310
|
-
return 4000;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Match the RED test name into the GREEN evidenceRef. Returns the
|
|
315
|
-
// basename or stem (without extension) of the most-specific path token
|
|
316
|
-
// in the RED row's first evidenceRef. We deliberately use a substring
|
|
317
|
-
// match, not equality, so callers can include richer text like
|
|
318
|
-
// "REGRESSION: cargo test --test foo => 8 passed; 0 failed".
|
|
319
|
-
function extractRedTestNameInline(redEvidenceRef) {
|
|
320
|
-
if (typeof redEvidenceRef !== "string") return null;
|
|
321
|
-
const trimmed = redEvidenceRef.trim();
|
|
322
|
-
if (trimmed.length === 0) return null;
|
|
323
|
-
// Path-shaped token (foo/bar/baz_test.rs or src/foo.test.ts).
|
|
324
|
-
const pathMatch = /[A-Za-z0-9_./-]+/u.exec(trimmed);
|
|
325
|
-
if (pathMatch) {
|
|
326
|
-
const token = pathMatch[0];
|
|
327
|
-
const slashIdx = token.lastIndexOf("/");
|
|
328
|
-
const base = slashIdx >= 0 ? token.slice(slashIdx + 1) : token;
|
|
329
|
-
const dotIdx = base.indexOf(".");
|
|
330
|
-
const stem = dotIdx > 0 ? base.slice(0, dotIdx) : base;
|
|
331
|
-
if (stem.length >= 4) return stem;
|
|
332
|
-
return base;
|
|
333
|
-
}
|
|
334
|
-
return trimmed;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Match canonical runner pass lines using language-agnostic examples:
|
|
338
|
-
// Node/TS (vitest/jest): "=> N passed; 0 failed" or "Tests: N passed"
|
|
339
|
-
// Python (pytest): "===== N passed in 0.42s ====="
|
|
340
|
-
// Go (go test): "ok pkg 0.123s"
|
|
341
|
-
// Rust (cargo test): "test result: ok. N passed; 0 failed"
|
|
342
|
-
// Java/JVM (maven/surefire): "Tests run: N, Failures: 0, Errors: 0"
|
|
343
|
-
// We accept a generic "passed/failed" shape plus runner-specific patterns.
|
|
344
|
-
const GREEN_PASS_PATTERNS = [
|
|
345
|
-
/=>\\s*\\d+\\s+passed/iu,
|
|
346
|
-
/\\b\\d+\\s+passed[;,]\\s*0\\s+failed\\b/iu,
|
|
347
|
-
/\\btest\\s+result:\\s*ok\\b/iu,
|
|
348
|
-
/\\b\\d+\\s+passed\\s+in\\s+\\d+(?:\\.\\d+)?\\s*s\\b/iu,
|
|
349
|
-
/^ok\\s+\\S+\\s+\\d+(?:\\.\\d+)?s\\b/imu,
|
|
350
|
-
/tests\\s+run\\s*:\\s*\\d+\\s*,\\s*failures\\s*:\\s*0\\s*,\\s*errors\\s*:\\s*0/iu
|
|
351
|
-
];
|
|
352
|
-
|
|
353
|
-
function matchesPassingAssertionInline(value) {
|
|
354
|
-
if (typeof value !== "string") return false;
|
|
355
|
-
return GREEN_PASS_PATTERNS.some((re) => re.test(value));
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async function readDelegationEvents(root) {
|
|
359
|
-
try {
|
|
360
|
-
const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "delegation-events.jsonl"), "utf8");
|
|
361
|
-
return raw
|
|
362
|
-
.split(/\\r?\\n/u)
|
|
363
|
-
.filter((line) => line.trim().length > 0)
|
|
364
|
-
.map((line) => {
|
|
365
|
-
try {
|
|
366
|
-
return JSON.parse(line);
|
|
367
|
-
} catch {
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
})
|
|
371
|
-
.filter((event) => event && typeof event === "object");
|
|
372
|
-
} catch {
|
|
373
|
-
return [];
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async function appendAuditEventInline(root, payload) {
|
|
378
|
-
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
379
|
-
await fs.mkdir(stateDir, { recursive: true });
|
|
380
|
-
await fs.appendFile(
|
|
381
|
-
path.join(stateDir, "delegation-events.jsonl"),
|
|
382
|
-
JSON.stringify(payload) + "\\n",
|
|
383
|
-
{ encoding: "utf8", mode: 0o600 }
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function hasPriorAck(events, args, runId) {
|
|
388
|
-
return events.some((event) =>
|
|
389
|
-
event.runId === runId &&
|
|
390
|
-
event.stage === args.stage &&
|
|
391
|
-
event.agent === args.agent &&
|
|
392
|
-
event.spanId === args["span-id"] &&
|
|
393
|
-
event.event === "acknowledged" &&
|
|
394
|
-
typeof event.ackTs === "string" &&
|
|
395
|
-
event.ackTs.length > 0
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function usage() {
|
|
400
|
-
process.stderr.write([
|
|
401
|
-
"Usage:",
|
|
402
|
-
" node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--supersede=<prevSpanId>] [--allow-parallel] [--paths=<comma-separated>] [--override-cap=<int>] [--reason=<slug>] [--json]",
|
|
403
|
-
" node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--json]",
|
|
404
|
-
" node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\\\"<why>\\\" [--json]",
|
|
405
|
-
" node .cclaw/hooks/delegation-record.mjs --audit-kind=cclaw_integration_overseer_skipped [--audit-reason=\\\"<comma-separated reasons>\\\"] [--slice-ids=\\\"S-1,S-2\\\"] [--json] # non-delegation audit row",
|
|
406
|
-
"",
|
|
407
|
-
"Allowed --dispatch-surface values:",
|
|
408
|
-
" " + VALID_DISPATCH_SURFACES.join(", "),
|
|
409
|
-
"",
|
|
410
|
-
"Per-surface allowed --agent-definition-path prefixes:",
|
|
411
|
-
...VALID_DISPATCH_SURFACES.map((surface) => " " + surface + ": " + (SURFACE_PATH_PREFIXES[surface].length === 0 ? "(any)" : SURFACE_PATH_PREFIXES[surface].join(", "))),
|
|
412
|
-
"",
|
|
413
|
-
"Dispatch dedup:",
|
|
414
|
-
" --supersede=<prevSpanId> close the previous active span on this (stage, agent) as 'stale' before recording the new scheduled row",
|
|
415
|
-
" --allow-parallel record both spans as concurrent; new row is tagged allowParallel: true",
|
|
416
|
-
"",
|
|
417
|
-
"TDD parallel scheduler:",
|
|
418
|
-
" --paths=<a,b,c> repo-relative paths the slice-builder will edit; disjoint sets auto-promote to allowParallel, overlap throws DispatchOverlapError",
|
|
419
|
-
" --override-cap=<int> raise the slice worker fan-out cap once for this dispatch (default cap " + String(5) + ", env CCLAW_MAX_PARALLEL_SLICE_BUILDERS overrides globally)",
|
|
420
|
-
" --reason=<slug> required with --override-cap so cap bypasses are auditable (e.g. red-checkpoint-retry)",
|
|
421
|
-
"",
|
|
422
|
-
"TDD slice phase tagging:",
|
|
423
|
-
" --slice=<id> TDD slice identifier (e.g. S-1) used by the linter to auto-derive the Watched-RED + Vertical Slice Cycle tables.",
|
|
424
|
-
" --phase=<phase> one of " + VALID_DELEGATION_PHASES.join(", ") + ". Pair with --slice to record a TDD slice phase event.",
|
|
425
|
-
" --refactor-rationale=<t> required for deferred refactor paths; must be >=80 chars and mention slice + task context (e.g. S-12 / T-103).",
|
|
426
|
-
" --refactor-outcome=<m> one of inline|deferred. Folds REFACTOR into the phase=green event so a single row can close RED→GREEN→REFACTOR. Pair --refactor-outcome=deferred with --refactor-rationale.",
|
|
427
|
-
" --risk-tier=<t> one of low|medium|high. high triggers integration-overseer in conditional mode.",
|
|
428
|
-
""
|
|
429
|
-
].join("\\n") + "\\n");
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function emitProblems(problems, json, code) {
|
|
433
|
-
const exitCode = typeof code === "number" ? code : 1;
|
|
434
|
-
if (json) {
|
|
435
|
-
process.stdout.write(JSON.stringify({ ok: false, problems, allowedDispatchSurfaces: VALID_DISPATCH_SURFACES }, null, 2) + "\\n");
|
|
436
|
-
} else {
|
|
437
|
-
usage();
|
|
438
|
-
process.stderr.write("[cclaw] delegation-record: " + problems.join("; ") + "\\n");
|
|
439
|
-
}
|
|
440
|
-
process.exitCode = exitCode;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function emitErrorJson(error, details, json) {
|
|
444
|
-
if (json) {
|
|
445
|
-
process.stdout.write(JSON.stringify({ ok: false, error, details }, null, 2) + "\\n");
|
|
446
|
-
} else {
|
|
447
|
-
process.stderr.write("[cclaw] delegation-record: error: " + error + " — " + JSON.stringify(details) + "\\n");
|
|
448
|
-
}
|
|
449
|
-
process.exit(2);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// keep in sync with validateMonotonicTimestamps in src/delegation.ts
|
|
453
|
-
function validateMonotonicTimestampsInline(stamped, prior) {
|
|
454
|
-
const startTs = stamped.startTs;
|
|
455
|
-
if (stamped.launchedTs && startTs && stamped.launchedTs < startTs) {
|
|
456
|
-
return { field: "launchedTs", actual: stamped.launchedTs, bound: startTs };
|
|
457
|
-
}
|
|
458
|
-
if (stamped.ackTs) {
|
|
459
|
-
const ackBound = stamped.launchedTs || startTs;
|
|
460
|
-
if (ackBound && stamped.ackTs < ackBound) {
|
|
461
|
-
return { field: "ackTs", actual: stamped.ackTs, bound: ackBound };
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (stamped.completedTs) {
|
|
465
|
-
const completedBound = stamped.ackTs || stamped.launchedTs || startTs;
|
|
466
|
-
if (completedBound && stamped.completedTs < completedBound) {
|
|
467
|
-
return { field: "completedTs", actual: stamped.completedTs, bound: completedBound };
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
if (!stamped.spanId) return null;
|
|
471
|
-
const priorForSpan = (prior || []).filter((entry) => entry && entry.spanId === stamped.spanId);
|
|
472
|
-
if (priorForSpan.length === 0) return null;
|
|
473
|
-
const tsValues = priorForSpan
|
|
474
|
-
.map((entry) => entry.ts || entry.startTs || "")
|
|
475
|
-
.filter((ts) => ts.length > 0);
|
|
476
|
-
if (tsValues.length === 0) return null;
|
|
477
|
-
let latest = tsValues[0];
|
|
478
|
-
for (let i = 1; i < tsValues.length; i += 1) {
|
|
479
|
-
if (tsValues[i] > latest) latest = tsValues[i];
|
|
480
|
-
}
|
|
481
|
-
const stampedTs = stamped.ts || stamped.startTs || "";
|
|
482
|
-
if (stampedTs && stampedTs < latest) {
|
|
483
|
-
return { field: "ts", actual: stampedTs, bound: latest };
|
|
484
|
-
}
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function normalizeRelPath(value) {
|
|
489
|
-
return String(value || "").replace(/\\\\/gu, "/").replace(/^\\.\\//u, "");
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function dispatchSurfaceMatchesPath(surface, agentDefinitionPath) {
|
|
493
|
-
const allowed = SURFACE_PATH_PREFIXES[surface] || [];
|
|
494
|
-
if (allowed.length === 0) return true;
|
|
495
|
-
const normalized = normalizeRelPath(agentDefinitionPath);
|
|
496
|
-
return allowed.some((prefix) => normalized === prefix.replace(/\\/$/u, "") || normalized.startsWith(prefix));
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
async function pathExists(filePath) {
|
|
500
|
-
try {
|
|
501
|
-
const stat = await fs.stat(filePath);
|
|
502
|
-
return stat.isFile() || stat.isDirectory();
|
|
503
|
-
} catch {
|
|
504
|
-
return false;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function normalizeEvidenceRefs(args) {
|
|
509
|
-
if (Array.isArray(args["evidence-refs"])) {
|
|
510
|
-
return args["evidence-refs"]
|
|
511
|
-
.filter((ref) => typeof ref === "string" && ref.trim().length > 0)
|
|
512
|
-
.map((ref) => ref.trim());
|
|
513
|
-
}
|
|
514
|
-
if (typeof args["evidence-ref"] === "string" && args["evidence-ref"].trim().length > 0) {
|
|
515
|
-
return [args["evidence-ref"].trim()];
|
|
516
|
-
}
|
|
517
|
-
return [];
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function validateDeferredRationaleInline(rationaleRaw, args) {
|
|
521
|
-
const rationale = typeof rationaleRaw === "string" ? rationaleRaw.trim() : "";
|
|
522
|
-
if (rationale.length === 0) {
|
|
523
|
-
return "missing";
|
|
524
|
-
}
|
|
525
|
-
if (rationale.length < 80) {
|
|
526
|
-
return "too-short";
|
|
527
|
-
}
|
|
528
|
-
const lower = rationale.toLowerCase();
|
|
529
|
-
const sliceRaw = typeof args.slice === "string" ? args.slice.trim().toLowerCase() : "";
|
|
530
|
-
const hasSliceMention =
|
|
531
|
-
(sliceRaw.length > 0 && lower.includes(sliceRaw)) ||
|
|
532
|
-
/\\bs-\\d+\\b/iu.test(rationale);
|
|
533
|
-
const hasTaskMention =
|
|
534
|
-
/\\bt-\\d{3}[a-z]?(?:\\.\\d{1,3})?\\b/iu.test(rationale) ||
|
|
535
|
-
/\\btask\\b/iu.test(rationale);
|
|
536
|
-
if (!hasSliceMention || !hasTaskMention) {
|
|
537
|
-
return "missing-context";
|
|
538
|
-
}
|
|
539
|
-
return "ok";
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function buildRow(args, status, runId, now, options) {
|
|
543
|
-
const fulfillmentMode = args["dispatch-surface"] === "role-switch"
|
|
544
|
-
? "role-switch"
|
|
545
|
-
: args["dispatch-surface"] === "cursor-task" || args["dispatch-surface"] === "generic-task"
|
|
546
|
-
? "generic-dispatch"
|
|
547
|
-
: "isolated";
|
|
548
|
-
// Inherit the span's startTs from prior rows so monotonic validation
|
|
549
|
-
// can compare against the original schedule, not the row write time.
|
|
550
|
-
const startTs = (options && options.spanStartTs) || now;
|
|
551
|
-
// claimedPaths from --paths=<comma-separated>. Empty arrays are dropped.
|
|
552
|
-
const claimedPathsRaw = typeof args.paths === "string" ? args.paths : "";
|
|
553
|
-
const claimedPaths = claimedPathsRaw
|
|
554
|
-
.split(",")
|
|
555
|
-
.map((value) => value.trim())
|
|
556
|
-
.filter((value) => value.length > 0);
|
|
557
|
-
// TDD slice tagging via --slice / --phase. Phase must be one of the
|
|
558
|
-
// canonical enum values; the inline validator rejects unknown phases
|
|
559
|
-
// before the row hits the ledger.
|
|
560
|
-
const sliceId =
|
|
561
|
-
typeof args.slice === "string" && args.slice.trim().length > 0
|
|
562
|
-
? args.slice.trim()
|
|
563
|
-
: undefined;
|
|
564
|
-
const phase =
|
|
565
|
-
typeof args.phase === "string" && args.phase.trim().length > 0
|
|
566
|
-
? args.phase.trim()
|
|
567
|
-
: undefined;
|
|
568
|
-
// When --refactor-rationale is supplied it is folded into
|
|
569
|
-
// evidenceRefs[0] so the linter (which reads evidenceRefs only) can
|
|
570
|
-
// surface the rationale without touching new fields. The user may
|
|
571
|
-
// also pass --evidence-ref containing the rationale text.
|
|
572
|
-
let resolvedEvidenceRefs = normalizeEvidenceRefs(args);
|
|
573
|
-
if (
|
|
574
|
-
phase === "refactor-deferred" &&
|
|
575
|
-
typeof args["refactor-rationale"] === "string" &&
|
|
576
|
-
args["refactor-rationale"].trim().length > 0
|
|
577
|
-
) {
|
|
578
|
-
const rationale = args["refactor-rationale"].trim();
|
|
579
|
-
if (!resolvedEvidenceRefs.includes(rationale)) {
|
|
580
|
-
resolvedEvidenceRefs = [rationale, ...resolvedEvidenceRefs];
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
// refactorOutcome folds REFACTOR into a phase=green event. We also
|
|
584
|
-
// accept it on phase=refactor / phase=refactor-deferred for controllers
|
|
585
|
-
// that emit it on the per-phase lifecycle. When mode=deferred and a
|
|
586
|
-
// --refactor-rationale is supplied we mirror the rationale into
|
|
587
|
-
// evidenceRefs[0] so the linter keeps reading evidence (matches the
|
|
588
|
-
// refactor-deferred behavior).
|
|
589
|
-
const refactorOutcomeMode =
|
|
590
|
-
typeof args["refactor-outcome"] === "string"
|
|
591
|
-
? args["refactor-outcome"].trim()
|
|
592
|
-
: "";
|
|
593
|
-
let refactorOutcome;
|
|
594
|
-
if (refactorOutcomeMode === "inline" || refactorOutcomeMode === "deferred") {
|
|
595
|
-
const rationaleRaw =
|
|
596
|
-
typeof args["refactor-rationale"] === "string"
|
|
597
|
-
? args["refactor-rationale"].trim()
|
|
598
|
-
: "";
|
|
599
|
-
refactorOutcome = {
|
|
600
|
-
mode: refactorOutcomeMode,
|
|
601
|
-
...(rationaleRaw.length > 0 ? { rationale: rationaleRaw } : {})
|
|
602
|
-
};
|
|
603
|
-
if (
|
|
604
|
-
refactorOutcomeMode === "deferred" &&
|
|
605
|
-
rationaleRaw.length > 0 &&
|
|
606
|
-
!resolvedEvidenceRefs.includes(rationaleRaw)
|
|
607
|
-
) {
|
|
608
|
-
resolvedEvidenceRefs = [rationaleRaw, ...resolvedEvidenceRefs];
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
const riskTierRaw =
|
|
612
|
-
typeof args["risk-tier"] === "string" ? args["risk-tier"].trim() : "";
|
|
613
|
-
const riskTier =
|
|
614
|
-
riskTierRaw === "low" || riskTierRaw === "medium" || riskTierRaw === "high"
|
|
615
|
-
? riskTierRaw
|
|
616
|
-
: undefined;
|
|
617
|
-
const worktreePath =
|
|
618
|
-
typeof args["worktree-path"] === "string" && args["worktree-path"].trim().length > 0
|
|
619
|
-
? args["worktree-path"].trim()
|
|
620
|
-
: undefined;
|
|
621
|
-
return {
|
|
622
|
-
stage: args.stage,
|
|
623
|
-
agent: args.agent,
|
|
624
|
-
mode: args.mode,
|
|
625
|
-
status,
|
|
626
|
-
spanId: args["span-id"],
|
|
627
|
-
dispatchId: args["dispatch-id"],
|
|
628
|
-
workerRunId: args["worker-run-id"],
|
|
629
|
-
dispatchSurface: args["dispatch-surface"],
|
|
630
|
-
agentDefinitionPath: args["agent-definition-path"],
|
|
631
|
-
fulfillmentMode,
|
|
632
|
-
waiverReason: args["waiver-reason"],
|
|
633
|
-
evidenceRefs: resolvedEvidenceRefs,
|
|
634
|
-
runId,
|
|
635
|
-
startTs,
|
|
636
|
-
ts: now,
|
|
637
|
-
launchedTs: args["launched-ts"] || (status === "launched" ? now : undefined),
|
|
638
|
-
ackTs: args["ack-ts"] || (status === "acknowledged" ? now : undefined),
|
|
639
|
-
completedTs: args["completed-ts"] || (status === "completed" ? now : undefined),
|
|
640
|
-
endTs: TERMINAL.has(status) ? now : undefined,
|
|
641
|
-
schemaVersion: LEDGER_SCHEMA_VERSION,
|
|
642
|
-
allowParallel: args["allow-parallel"] === true ? true : undefined,
|
|
643
|
-
claimedPaths: claimedPaths.length > 0 ? claimedPaths : undefined,
|
|
644
|
-
worktreePath,
|
|
645
|
-
sliceId,
|
|
646
|
-
phase,
|
|
647
|
-
refactorOutcome,
|
|
648
|
-
riskTier
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
async function readDelegationLedgerEntries(root) {
|
|
653
|
-
try {
|
|
654
|
-
const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "delegation-log.json"), "utf8");
|
|
655
|
-
const parsed = JSON.parse(raw);
|
|
656
|
-
if (parsed && Array.isArray(parsed.entries)) return parsed.entries;
|
|
657
|
-
} catch {
|
|
658
|
-
// empty / missing ledger is fine for dedup + monotonicity checks
|
|
659
|
-
}
|
|
660
|
-
return [];
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// keep in sync with findActiveSpanForPair / DispatchDuplicateError in src/delegation.ts
|
|
664
|
-
function findActiveSpanForPairInline(stage, agent, runId, entries) {
|
|
665
|
-
const ACTIVE_STATUSES = new Set(["scheduled", "launched", "acknowledged"]);
|
|
666
|
-
const effectiveTs = (entry) =>
|
|
667
|
-
entry.completedTs || entry.ackTs || entry.launchedTs || entry.endTs || entry.startTs || entry.ts || "";
|
|
668
|
-
const latestBySpan = new Map();
|
|
669
|
-
for (const entry of entries) {
|
|
670
|
-
if (!entry || typeof entry !== "object") continue;
|
|
671
|
-
if (typeof entry.spanId !== "string" || entry.spanId.length === 0) continue;
|
|
672
|
-
// Strict run-scope: entries without a runId are treated as foreign so
|
|
673
|
-
// they cannot keep an old span "active" across runs and trip
|
|
674
|
-
// dispatch_duplicate on a fresh dispatch.
|
|
675
|
-
if (typeof entry.runId !== "string" || entry.runId.length === 0) continue;
|
|
676
|
-
if (entry.runId !== runId) continue;
|
|
677
|
-
if (entry.stage !== stage || entry.agent !== agent) continue;
|
|
678
|
-
const existing = latestBySpan.get(entry.spanId);
|
|
679
|
-
if (!existing || effectiveTs(entry) >= effectiveTs(existing)) {
|
|
680
|
-
latestBySpan.set(entry.spanId, entry);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
for (const entry of latestBySpan.values()) {
|
|
684
|
-
if (ACTIVE_STATUSES.has(entry.status)) return entry;
|
|
685
|
-
}
|
|
686
|
-
return null;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// keep in sync with computeActiveSubagents in src/delegation.ts
|
|
690
|
-
function computeActiveSubagentsInline(entries) {
|
|
691
|
-
const ACTIVE_STATUSES = new Set(["scheduled", "launched", "acknowledged"]);
|
|
692
|
-
const effectiveTs = (entry) =>
|
|
693
|
-
entry.completedTs || entry.ackTs || entry.launchedTs || entry.endTs || entry.startTs || entry.ts || "";
|
|
694
|
-
const latestBySpan = new Map();
|
|
695
|
-
for (const entry of entries) {
|
|
696
|
-
if (!entry || typeof entry !== "object") continue;
|
|
697
|
-
if (typeof entry.spanId !== "string" || entry.spanId.length === 0) continue;
|
|
698
|
-
const existing = latestBySpan.get(entry.spanId);
|
|
699
|
-
if (!existing || effectiveTs(entry) >= effectiveTs(existing)) {
|
|
700
|
-
latestBySpan.set(entry.spanId, entry);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
const active = [];
|
|
704
|
-
for (const entry of latestBySpan.values()) {
|
|
705
|
-
if (ACTIVE_STATUSES.has(entry.status)) active.push(entry);
|
|
706
|
-
}
|
|
707
|
-
return active;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// keep in sync with validateFileOverlap in src/delegation.ts
|
|
711
|
-
function validateFileOverlapInline(stamped, activeEntries) {
|
|
712
|
-
if (stamped.agent !== "slice-builder" || stamped.stage !== "tdd") {
|
|
713
|
-
return { autoParallel: false, conflict: null };
|
|
714
|
-
}
|
|
715
|
-
const newPaths = Array.isArray(stamped.claimedPaths) ? stamped.claimedPaths : [];
|
|
716
|
-
if (newPaths.length === 0) {
|
|
717
|
-
return { autoParallel: false, conflict: null };
|
|
718
|
-
}
|
|
719
|
-
const sameLane = activeEntries.filter(
|
|
720
|
-
(entry) =>
|
|
721
|
-
entry.stage === stamped.stage &&
|
|
722
|
-
entry.agent === stamped.agent &&
|
|
723
|
-
entry.spanId !== stamped.spanId
|
|
724
|
-
);
|
|
725
|
-
if (sameLane.length === 0) {
|
|
726
|
-
return { autoParallel: true, conflict: null };
|
|
727
|
-
}
|
|
728
|
-
for (const existing of sameLane) {
|
|
729
|
-
const existingPaths = Array.isArray(existing.claimedPaths) ? existing.claimedPaths : [];
|
|
730
|
-
if (existingPaths.length === 0) {
|
|
731
|
-
return { autoParallel: false, conflict: null };
|
|
732
|
-
}
|
|
733
|
-
const overlap = newPaths.filter((p) => existingPaths.includes(p));
|
|
734
|
-
if (overlap.length > 0) {
|
|
735
|
-
return {
|
|
736
|
-
autoParallel: false,
|
|
737
|
-
conflict: {
|
|
738
|
-
existingSpanId: existing.spanId || "unknown",
|
|
739
|
-
newSpanId: stamped.spanId || "unknown",
|
|
740
|
-
pair: { stage: stamped.stage, agent: stamped.agent },
|
|
741
|
-
conflictingPaths: overlap
|
|
742
|
-
}
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return { autoParallel: true, conflict: null };
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
const MAX_PARALLEL_SLICE_BUILDERS_INLINE = 5;
|
|
750
|
-
|
|
751
|
-
function readMaxParallelOverrideFromEnvInline() {
|
|
752
|
-
const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_BUILDERS;
|
|
753
|
-
if (typeof raw !== "string" || raw.trim().length === 0) return null;
|
|
754
|
-
const parsed = Number(raw);
|
|
755
|
-
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1) return null;
|
|
756
|
-
return parsed;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// keep in sync with validateFanOutCap in src/delegation.ts
|
|
760
|
-
function validateFanOutCapInline(stamped, activeEntries, override) {
|
|
761
|
-
if (stamped.agent !== "slice-builder" || stamped.stage !== "tdd") return null;
|
|
762
|
-
if (stamped.status !== "scheduled") return null;
|
|
763
|
-
let cap;
|
|
764
|
-
if (override !== null && override !== undefined && Number.isInteger(override) && override >= 1) {
|
|
765
|
-
cap = override;
|
|
766
|
-
} else {
|
|
767
|
-
cap = readMaxParallelOverrideFromEnvInline() || MAX_PARALLEL_SLICE_BUILDERS_INLINE;
|
|
768
|
-
}
|
|
769
|
-
const sameLaneActive = activeEntries.filter(
|
|
770
|
-
(entry) =>
|
|
771
|
-
entry.stage === stamped.stage &&
|
|
772
|
-
entry.agent === stamped.agent &&
|
|
773
|
-
entry.spanId !== stamped.spanId
|
|
774
|
-
);
|
|
775
|
-
if (sameLaneActive.length + 1 > cap) {
|
|
776
|
-
return {
|
|
777
|
-
cap,
|
|
778
|
-
active: sameLaneActive.length,
|
|
779
|
-
pair: { stage: stamped.stage, agent: stamped.agent }
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
return null;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function enforceDispatchDedupInline(stamped, priorEntries, args) {
|
|
786
|
-
if (stamped.status !== "scheduled") return null;
|
|
787
|
-
if (args["allow-parallel"] === true) return null;
|
|
788
|
-
const existing = findActiveSpanForPairInline(
|
|
789
|
-
stamped.stage,
|
|
790
|
-
stamped.agent,
|
|
791
|
-
stamped.runId,
|
|
792
|
-
priorEntries
|
|
793
|
-
);
|
|
794
|
-
if (!existing || existing.spanId === stamped.spanId) return null;
|
|
795
|
-
if (typeof args.supersede === "string" && args.supersede.length > 0) {
|
|
796
|
-
if (args.supersede !== existing.spanId) {
|
|
797
|
-
return {
|
|
798
|
-
kind: "supersede-mismatch",
|
|
799
|
-
details: {
|
|
800
|
-
requested: args.supersede,
|
|
801
|
-
actualActiveSpanId: existing.spanId,
|
|
802
|
-
stage: stamped.stage,
|
|
803
|
-
agent: stamped.agent
|
|
804
|
-
}
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
return { kind: "supersede", existing };
|
|
808
|
-
}
|
|
809
|
-
return {
|
|
810
|
-
kind: "error",
|
|
811
|
-
details: {
|
|
812
|
-
existingSpanId: existing.spanId,
|
|
813
|
-
existingStatus: existing.status,
|
|
814
|
-
newSpanId: stamped.spanId,
|
|
815
|
-
pair: { stage: stamped.stage, agent: stamped.agent },
|
|
816
|
-
hint: "pass --supersede=" + existing.spanId + " to close the previous span as stale, or --allow-parallel to record both as concurrent"
|
|
817
|
-
}
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
async function acquireDelegationLogLock(stateDir) {
|
|
822
|
-
const lockDir = path.join(stateDir, "delegation-log.json.lock");
|
|
823
|
-
const maxWaitMs = 3000;
|
|
824
|
-
const startMs = Date.now();
|
|
825
|
-
let delayMs = 25;
|
|
826
|
-
while (true) {
|
|
827
|
-
try {
|
|
828
|
-
await fs.mkdir(lockDir, { recursive: false });
|
|
829
|
-
return lockDir;
|
|
830
|
-
} catch (err) {
|
|
831
|
-
const code = err && typeof err === "object" && "code" in err ? err.code : "";
|
|
832
|
-
if (code !== "EEXIST") throw err;
|
|
833
|
-
if (Date.now() - startMs >= maxWaitMs) {
|
|
834
|
-
process.stderr.write(
|
|
835
|
-
"[cclaw] delegation-record: timeout waiting for delegation-log.json.lock (max " + maxWaitMs + "ms)\\n"
|
|
836
|
-
);
|
|
837
|
-
process.exit(2);
|
|
838
|
-
}
|
|
839
|
-
const jitter = Math.floor(Math.random() * 25);
|
|
840
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs + jitter));
|
|
841
|
-
delayMs = Math.min(delayMs * 2, 200);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
async function releaseDelegationLogLock(lockDir) {
|
|
847
|
-
try {
|
|
848
|
-
await fs.rm(lockDir, { recursive: true, force: true });
|
|
849
|
-
} catch {
|
|
850
|
-
// best-effort release
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
async function writeDelegationLedgerAtomic(ledgerPath, ledger) {
|
|
855
|
-
const dir = path.dirname(ledgerPath);
|
|
856
|
-
const tmp =
|
|
857
|
-
path.join(dir, ".delegation-log.json." + process.pid + "." + Date.now() + "." + Math.random().toString(16).slice(2) + ".tmp");
|
|
858
|
-
await fs.writeFile(tmp, JSON.stringify(ledger, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
|
|
859
|
-
await fs.rename(tmp, ledgerPath);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
async function persistEntry(root, runId, clean, event, options = {}) {
|
|
863
|
-
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
864
|
-
await fs.mkdir(stateDir, { recursive: true });
|
|
865
|
-
await fs.appendFile(path.join(stateDir, "delegation-events.jsonl"), JSON.stringify(event) + "\\n", { encoding: "utf8", mode: 0o600 });
|
|
866
|
-
|
|
867
|
-
const ledgerPath = path.join(stateDir, "delegation-log.json");
|
|
868
|
-
let ledger = { runId, entries: [], schemaVersion: LEDGER_SCHEMA_VERSION };
|
|
869
|
-
const lockDir = await acquireDelegationLogLock(stateDir);
|
|
870
|
-
try {
|
|
871
|
-
try {
|
|
872
|
-
ledger = JSON.parse(await fs.readFile(ledgerPath, "utf8"));
|
|
873
|
-
if (!Array.isArray(ledger.entries)) ledger.entries = [];
|
|
874
|
-
} catch {
|
|
875
|
-
ledger = { runId, entries: [], schemaVersion: LEDGER_SCHEMA_VERSION };
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// Rerecord semantics: replace any pre-existing row with the same spanId
|
|
879
|
-
// (regardless of its status) so the legacy v1/v2 row is upgraded to v3
|
|
880
|
-
// shape on disk. The append path keeps the historical dedup semantics:
|
|
881
|
-
// an exact (spanId, status, phase) triple is dropped to keep retried hooks
|
|
882
|
-
// idempotent. Including \`phase\` in the dedup key is required because a
|
|
883
|
-
// single TDD slice-builder span legitimately emits FOUR rows with
|
|
884
|
-
// status=completed (one each for phase=red|green|refactor|doc); a
|
|
885
|
-
// dedup on (spanId, status) alone would silently drop GREEN/REFACTOR/DOC
|
|
886
|
-
// and leave the linter reporting tdd_slice_green_missing for slices
|
|
887
|
-
// whose work actually landed.
|
|
888
|
-
if (options.replaceBySpanId) {
|
|
889
|
-
ledger.entries = ledger.entries.filter((entry) => entry.spanId !== clean.spanId);
|
|
890
|
-
ledger.entries.push(clean);
|
|
891
|
-
ledger.runId = runId;
|
|
892
|
-
ledger.schemaVersion = LEDGER_SCHEMA_VERSION;
|
|
893
|
-
await writeDelegationLedgerAtomic(ledgerPath, ledger);
|
|
894
|
-
} else if (!ledger.entries.some((entry) =>
|
|
895
|
-
entry.spanId === clean.spanId &&
|
|
896
|
-
entry.status === clean.status &&
|
|
897
|
-
(entry.phase ?? null) === (clean.phase ?? null)
|
|
898
|
-
)) {
|
|
899
|
-
ledger.entries.push(clean);
|
|
900
|
-
ledger.runId = runId;
|
|
901
|
-
ledger.schemaVersion = LEDGER_SCHEMA_VERSION;
|
|
902
|
-
await writeDelegationLedgerAtomic(ledgerPath, ledger);
|
|
903
|
-
}
|
|
904
|
-
} finally {
|
|
905
|
-
await releaseDelegationLogLock(lockDir);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// keep in sync with computeActiveSubagents in src/delegation.ts
|
|
909
|
-
const ACTIVE_STATUSES = new Set(["scheduled", "launched", "acknowledged"]);
|
|
910
|
-
const effectiveTs = (entry) =>
|
|
911
|
-
entry.completedTs || entry.ackTs || entry.launchedTs || entry.endTs || entry.startTs || entry.ts || "";
|
|
912
|
-
const latestBySpan = new Map();
|
|
913
|
-
for (const entry of ledger.entries) {
|
|
914
|
-
if (!entry || typeof entry !== "object" || typeof entry.spanId !== "string" || entry.spanId.length === 0) continue;
|
|
915
|
-
const existing = latestBySpan.get(entry.spanId);
|
|
916
|
-
if (!existing) {
|
|
917
|
-
latestBySpan.set(entry.spanId, entry);
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
if (effectiveTs(entry) >= effectiveTs(existing)) {
|
|
921
|
-
latestBySpan.set(entry.spanId, entry);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
const active = [];
|
|
925
|
-
for (const entry of latestBySpan.values()) {
|
|
926
|
-
if (ACTIVE_STATUSES.has(entry.status)) active.push(entry);
|
|
927
|
-
}
|
|
928
|
-
active.sort((a, b) => {
|
|
929
|
-
const aKey = a.startTs || a.ts || "";
|
|
930
|
-
const bKey = b.startTs || b.ts || "";
|
|
931
|
-
if (aKey === bKey) return 0;
|
|
932
|
-
return aKey < bKey ? -1 : 1;
|
|
933
|
-
});
|
|
934
|
-
await fs.writeFile(path.join(stateDir, "subagents.json"), JSON.stringify({ active, updatedAt: event.eventTs }, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
async function findLegacyEntry(root, spanId) {
|
|
938
|
-
const ledgerPath = path.join(root, RUNTIME_ROOT, "state", "delegation-log.json");
|
|
939
|
-
let ledger;
|
|
940
|
-
try {
|
|
941
|
-
ledger = JSON.parse(await fs.readFile(ledgerPath, "utf8"));
|
|
942
|
-
} catch {
|
|
943
|
-
return null;
|
|
944
|
-
}
|
|
945
|
-
if (!ledger || !Array.isArray(ledger.entries)) return null;
|
|
946
|
-
return ledger.entries.find((entry) => entry && entry.spanId === spanId) || null;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// Allow-list of non-delegation audit events the controller can emit via
|
|
950
|
-
// the helper. Keep in sync with NON_DELEGATION_AUDIT_EVENTS in
|
|
951
|
-
// src/delegation.ts.
|
|
952
|
-
const VALID_AUDIT_KINDS = new Set([
|
|
953
|
-
"cclaw_integration_overseer_skipped",
|
|
954
|
-
"cclaw_allow_parallel_auto_flip"
|
|
955
|
-
]);
|
|
956
|
-
|
|
957
|
-
async function runAuditEmit(args, json) {
|
|
958
|
-
const kind = String(args["audit-kind"]).trim();
|
|
959
|
-
if (!VALID_AUDIT_KINDS.has(kind)) {
|
|
960
|
-
emitProblems([
|
|
961
|
-
"invalid --audit-kind: " + kind +
|
|
962
|
-
" (allowed: " + [...VALID_AUDIT_KINDS].join(", ") + ")"
|
|
963
|
-
], json, 2);
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
const root = await detectRoot();
|
|
967
|
-
const runId = await readRunId(root);
|
|
968
|
-
const reason = typeof args["audit-reason"] === "string"
|
|
969
|
-
? args["audit-reason"].trim()
|
|
970
|
-
: "";
|
|
971
|
-
const sliceIdsRaw = typeof args["slice-ids"] === "string"
|
|
972
|
-
? args["slice-ids"].trim()
|
|
973
|
-
: "";
|
|
974
|
-
const sliceIds = sliceIdsRaw.length > 0
|
|
975
|
-
? sliceIdsRaw
|
|
976
|
-
.split(",")
|
|
977
|
-
.map((value) => value.trim())
|
|
978
|
-
.filter((value) => value.length > 0)
|
|
979
|
-
: [];
|
|
980
|
-
const ts = new Date().toISOString();
|
|
981
|
-
const payload = {
|
|
982
|
-
event: kind,
|
|
983
|
-
runId,
|
|
984
|
-
ts,
|
|
985
|
-
eventTs: ts,
|
|
986
|
-
...(reason.length > 0 ? { reasons: reason.split(",").map((r) => r.trim()).filter((r) => r.length > 0) } : {}),
|
|
987
|
-
...(sliceIds.length > 0 ? { sliceIds } : {})
|
|
988
|
-
};
|
|
989
|
-
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
990
|
-
try {
|
|
991
|
-
await fs.mkdir(stateDir, { recursive: true });
|
|
992
|
-
await fs.appendFile(
|
|
993
|
-
path.join(stateDir, "delegation-events.jsonl"),
|
|
994
|
-
JSON.stringify(payload) + "\\n",
|
|
995
|
-
{ encoding: "utf8", mode: 0o600 }
|
|
996
|
-
);
|
|
997
|
-
} catch (error) {
|
|
998
|
-
const message = error && typeof error === "object" && "message" in error
|
|
999
|
-
? String(error.message)
|
|
1000
|
-
: String(error);
|
|
1001
|
-
emitErrorJson("audit_emit_failed", { kind, message }, json);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
if (json) {
|
|
1005
|
-
process.stdout.write(JSON.stringify({
|
|
1006
|
-
ok: true,
|
|
1007
|
-
command: "audit-emit",
|
|
1008
|
-
auditKind: kind,
|
|
1009
|
-
runId,
|
|
1010
|
-
sliceIds,
|
|
1011
|
-
ts
|
|
1012
|
-
}, null, 2) + "\\n");
|
|
1013
|
-
} else {
|
|
1014
|
-
process.stdout.write("[cclaw] audit emitted: " + kind + " (run=" + runId + ", ts=" + ts + ")\\n");
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
async function runRerecord(args, json) {
|
|
1019
|
-
const problems = [];
|
|
1020
|
-
for (const key of ["span-id", "dispatch-id", "dispatch-surface", "agent-definition-path"]) {
|
|
1021
|
-
if (!args[key]) problems.push("missing --" + key);
|
|
1022
|
-
}
|
|
1023
|
-
if (args["dispatch-surface"] && !VALID_DISPATCH_SURFACES_SET.has(args["dispatch-surface"])) {
|
|
1024
|
-
problems.push("invalid --dispatch-surface (allowed: " + VALID_DISPATCH_SURFACES.join(", ") + ")");
|
|
1025
|
-
}
|
|
1026
|
-
if (problems.length > 0) {
|
|
1027
|
-
emitProblems(problems, json, 2);
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
const root = await detectRoot();
|
|
1031
|
-
const now = new Date().toISOString();
|
|
1032
|
-
const runId = await readRunId(root);
|
|
1033
|
-
const legacyEntry = await findLegacyEntry(root, args["span-id"]);
|
|
1034
|
-
if (!legacyEntry) {
|
|
1035
|
-
emitProblems(["no legacy ledger entry found for --span-id=" + args["span-id"]], json, 1);
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
1038
|
-
const explicitEvidenceRef =
|
|
1039
|
-
typeof args["evidence-ref"] === "string" && args["evidence-ref"].trim().length > 0
|
|
1040
|
-
? args["evidence-ref"].trim()
|
|
1041
|
-
: "";
|
|
1042
|
-
const legacyEvidenceRefs = Array.isArray(legacyEntry.evidenceRefs)
|
|
1043
|
-
? legacyEntry.evidenceRefs
|
|
1044
|
-
.filter((ref) => typeof ref === "string" && ref.trim().length > 0)
|
|
1045
|
-
.map((ref) => ref.trim())
|
|
1046
|
-
: [];
|
|
1047
|
-
const mergedEvidenceRefs = explicitEvidenceRef.length > 0
|
|
1048
|
-
? [explicitEvidenceRef]
|
|
1049
|
-
: legacyEvidenceRefs;
|
|
1050
|
-
if (args["dispatch-surface"] !== "role-switch") {
|
|
1051
|
-
if (!dispatchSurfaceMatchesPath(args["dispatch-surface"], args["agent-definition-path"])) {
|
|
1052
|
-
const allowedPrefixes = SURFACE_PATH_PREFIXES[args["dispatch-surface"]];
|
|
1053
|
-
emitProblems([
|
|
1054
|
-
"--agent-definition-path does not lie under any allowed prefix for --dispatch-surface=" + args["dispatch-surface"] + " (expected one of: " + (allowedPrefixes.join(", ") || "(any)") + ")"
|
|
1055
|
-
], json, 2);
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
const exists = await pathExists(path.join(root, args["agent-definition-path"]));
|
|
1059
|
-
if (!exists) {
|
|
1060
|
-
emitProblems(["--agent-definition-path does not exist on disk: " + args["agent-definition-path"]], json, 2);
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
const merged = {
|
|
1065
|
-
stage: legacyEntry.stage,
|
|
1066
|
-
agent: legacyEntry.agent,
|
|
1067
|
-
mode: legacyEntry.mode || "mandatory",
|
|
1068
|
-
"span-id": args["span-id"],
|
|
1069
|
-
"dispatch-id": args["dispatch-id"],
|
|
1070
|
-
"worker-run-id": args["worker-run-id"] || legacyEntry.workerRunId,
|
|
1071
|
-
"dispatch-surface": args["dispatch-surface"],
|
|
1072
|
-
"agent-definition-path": args["agent-definition-path"],
|
|
1073
|
-
"ack-ts": args["ack-ts"] || legacyEntry.ackTs || now,
|
|
1074
|
-
"completed-ts": args["completed-ts"] || legacyEntry.completedTs || now,
|
|
1075
|
-
"launched-ts": args["launched-ts"] || legacyEntry.launchedTs || now,
|
|
1076
|
-
"evidence-ref": explicitEvidenceRef.length > 0 ? explicitEvidenceRef : undefined,
|
|
1077
|
-
"evidence-refs": mergedEvidenceRefs
|
|
1078
|
-
};
|
|
1079
|
-
const status = "completed";
|
|
1080
|
-
const clean = Object.fromEntries(Object.entries(buildRow(merged, status, runId, now)).filter(([, value]) => value !== undefined));
|
|
1081
|
-
clean.fulfillmentMode = clean.dispatchSurface === "role-switch" ? "role-switch" : (clean.dispatchSurface === "cursor-task" || clean.dispatchSurface === "generic-task" ? "generic-dispatch" : "isolated");
|
|
1082
|
-
const event = { ...clean, event: status, eventTs: now, rerecord: true };
|
|
1083
|
-
await persistEntry(root, runId, clean, event, { replaceBySpanId: true });
|
|
1084
|
-
process.stdout.write(JSON.stringify({ ok: true, event, rerecord: true }, null, 2) + "\\n");
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
const LIFECYCLE_PHASES = ["scheduled", "launched", "acknowledged", "completed"];
|
|
1088
|
-
|
|
1089
|
-
function mergeSpanTemplate(spanEvents) {
|
|
1090
|
-
const base = {};
|
|
1091
|
-
const keys = [
|
|
1092
|
-
"stage",
|
|
1093
|
-
"agent",
|
|
1094
|
-
"mode",
|
|
1095
|
-
"runId",
|
|
1096
|
-
"dispatchId",
|
|
1097
|
-
"dispatchSurface",
|
|
1098
|
-
"agentDefinitionPath",
|
|
1099
|
-
"workerRunId",
|
|
1100
|
-
"fulfillmentMode",
|
|
1101
|
-
"schemaVersion",
|
|
1102
|
-
"parentSpanId",
|
|
1103
|
-
"evidenceRefs",
|
|
1104
|
-
"waiverReason"
|
|
1105
|
-
];
|
|
1106
|
-
for (const e of spanEvents) {
|
|
1107
|
-
if (!e || typeof e !== "object") continue;
|
|
1108
|
-
for (const k of keys) {
|
|
1109
|
-
if (base[k] === undefined && e[k] !== undefined) {
|
|
1110
|
-
base[k] = e[k];
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
return base;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
function repairFulfillmentMode(base) {
|
|
1118
|
-
if (base.fulfillmentMode) return base.fulfillmentMode;
|
|
1119
|
-
if (base.dispatchSurface === "role-switch") return "role-switch";
|
|
1120
|
-
if (base.dispatchSurface === "cursor-task" || base.dispatchSurface === "generic-task") {
|
|
1121
|
-
return "generic-dispatch";
|
|
1122
|
-
}
|
|
1123
|
-
return "isolated";
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
async function runRepair(args, json) {
|
|
1127
|
-
const problems = [];
|
|
1128
|
-
if (!args["span-id"]) problems.push("repair mode requires --span-id");
|
|
1129
|
-
if (!args["repair-reason"] || String(args["repair-reason"]).trim().length === 0) {
|
|
1130
|
-
problems.push("repair mode requires --repair-reason=<text>");
|
|
1131
|
-
}
|
|
1132
|
-
if (problems.length > 0) {
|
|
1133
|
-
emitProblems(problems, json, 2);
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
const spanId = args["span-id"];
|
|
1137
|
-
const repairedReason = String(args["repair-reason"]).trim();
|
|
1138
|
-
const root = await detectRoot();
|
|
1139
|
-
const events = await readDelegationEvents(root);
|
|
1140
|
-
const spanEvents = events.filter(
|
|
1141
|
-
(e) => e && e.spanId === spanId && typeof e.event === "string" && LIFECYCLE_PHASES.includes(e.event)
|
|
1142
|
-
);
|
|
1143
|
-
if (spanEvents.length === 0) {
|
|
1144
|
-
emitProblems(
|
|
1145
|
-
["repair refused: no lifecycle delegation-events.jsonl rows found for --span-id=" + spanId],
|
|
1146
|
-
json,
|
|
1147
|
-
2
|
|
1148
|
-
);
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
const present = new Set(spanEvents.map((e) => e.event));
|
|
1152
|
-
const base = mergeSpanTemplate(spanEvents);
|
|
1153
|
-
if (!base.stage || !base.agent || !base.mode) {
|
|
1154
|
-
emitProblems(["repair refused: span events missing stage/agent/mode to clone"], json, 2);
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
const runId =
|
|
1158
|
-
typeof base.runId === "string" && base.runId.length > 0 ? base.runId : await readRunId(root);
|
|
1159
|
-
const fulfillmentMode = repairFulfillmentMode(base);
|
|
1160
|
-
const schemaVersion =
|
|
1161
|
-
typeof base.schemaVersion === "number" && base.schemaVersion > 0
|
|
1162
|
-
? base.schemaVersion
|
|
1163
|
-
: LEDGER_SCHEMA_VERSION;
|
|
1164
|
-
const evidenceRefs = Array.isArray(base.evidenceRefs)
|
|
1165
|
-
? base.evidenceRefs.filter((r) => typeof r === "string" && r.trim().length > 0)
|
|
1166
|
-
: [];
|
|
1167
|
-
const now = new Date().toISOString();
|
|
1168
|
-
const appended = [];
|
|
1169
|
-
|
|
1170
|
-
for (const status of LIFECYCLE_PHASES) {
|
|
1171
|
-
if (present.has(status)) continue;
|
|
1172
|
-
if (status === "completed" && base.dispatchSurface !== "role-switch") {
|
|
1173
|
-
if (!base.dispatchId || !base.dispatchSurface || !base.agentDefinitionPath) {
|
|
1174
|
-
emitProblems(
|
|
1175
|
-
[
|
|
1176
|
-
"repair refused: cannot synthesize completed row without dispatchId, dispatchSurface, and agentDefinitionPath on span " +
|
|
1177
|
-
spanId
|
|
1178
|
-
],
|
|
1179
|
-
json,
|
|
1180
|
-
2
|
|
1181
|
-
);
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
if (status === "completed" && base.dispatchSurface === "role-switch" && evidenceRefs.length === 0) {
|
|
1186
|
-
emitProblems(
|
|
1187
|
-
["repair refused: role-switch completed synthesis requires evidenceRefs on span " + spanId],
|
|
1188
|
-
json,
|
|
1189
|
-
2
|
|
1190
|
-
);
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
const launchedTs =
|
|
1194
|
-
status === "launched" || status === "acknowledged" || status === "completed" ? now : undefined;
|
|
1195
|
-
const ackTs = status === "acknowledged" || status === "completed" ? now : undefined;
|
|
1196
|
-
const completedTs = status === "completed" ? now : undefined;
|
|
1197
|
-
const endTs = status === "completed" ? now : undefined;
|
|
1198
|
-
const row = {
|
|
1199
|
-
stage: base.stage,
|
|
1200
|
-
agent: base.agent,
|
|
1201
|
-
mode: base.mode,
|
|
1202
|
-
status,
|
|
1203
|
-
spanId,
|
|
1204
|
-
dispatchId: base.dispatchId,
|
|
1205
|
-
workerRunId: base.workerRunId,
|
|
1206
|
-
dispatchSurface: base.dispatchSurface,
|
|
1207
|
-
agentDefinitionPath: base.agentDefinitionPath,
|
|
1208
|
-
fulfillmentMode,
|
|
1209
|
-
evidenceRefs,
|
|
1210
|
-
runId,
|
|
1211
|
-
startTs: now,
|
|
1212
|
-
ts: now,
|
|
1213
|
-
launchedTs,
|
|
1214
|
-
ackTs,
|
|
1215
|
-
completedTs,
|
|
1216
|
-
endTs,
|
|
1217
|
-
schemaVersion
|
|
1218
|
-
};
|
|
1219
|
-
const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
|
|
1220
|
-
const event = { ...clean, event: status, eventTs: now, repairedAt: now, repairedReason };
|
|
1221
|
-
await persistEntry(root, runId, clean, event);
|
|
1222
|
-
present.add(status);
|
|
1223
|
-
appended.push(status);
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
if (json) {
|
|
1227
|
-
process.stdout.write(
|
|
1228
|
-
JSON.stringify({ ok: true, repair: true, spanId, appended, repairedAt: now, repairedReason }, null, 2) + "\\n"
|
|
1229
|
-
);
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
async function runSliceCommitIfNeeded(root, row, runId) {
|
|
1234
|
-
if (
|
|
1235
|
-
row.stage !== "tdd" ||
|
|
1236
|
-
row.agent !== "slice-builder" ||
|
|
1237
|
-
row.status !== "completed" ||
|
|
1238
|
-
row.phase !== "doc"
|
|
1239
|
-
) {
|
|
1240
|
-
return { ok: true, skipped: true };
|
|
1241
|
-
}
|
|
1242
|
-
const sliceId = typeof row.sliceId === "string" ? row.sliceId.trim() : "";
|
|
1243
|
-
const spanId = typeof row.spanId === "string" ? row.spanId.trim() : "";
|
|
1244
|
-
if (sliceId.length === 0 || spanId.length === 0) {
|
|
1245
|
-
return { ok: true, skipped: true };
|
|
1246
|
-
}
|
|
1247
|
-
const helperPath = path.join(root, RUNTIME_ROOT, "hooks", "slice-commit.mjs");
|
|
1248
|
-
if (!(await exists(helperPath))) {
|
|
1249
|
-
return { ok: true, skipped: true };
|
|
1250
|
-
}
|
|
1251
|
-
const helperArgs = [
|
|
1252
|
-
helperPath,
|
|
1253
|
-
"--json",
|
|
1254
|
-
"--quiet",
|
|
1255
|
-
"--slice=" + sliceId,
|
|
1256
|
-
"--span-id=" + spanId,
|
|
1257
|
-
"--run-id=" + runId
|
|
1258
|
-
];
|
|
1259
|
-
let explicitWorktreePath =
|
|
1260
|
-
typeof row.worktreePath === "string" && row.worktreePath.trim().length > 0
|
|
1261
|
-
? row.worktreePath.trim()
|
|
1262
|
-
: "";
|
|
1263
|
-
if (explicitWorktreePath.length === 0) {
|
|
1264
|
-
const priorLedger = await readDelegationLedgerEntries(root);
|
|
1265
|
-
const priorSpanPath = priorLedger
|
|
1266
|
-
.filter((entry) => entry && entry.spanId === spanId && entry.runId === runId)
|
|
1267
|
-
.map((entry) =>
|
|
1268
|
-
entry && typeof entry.worktreePath === "string" ? entry.worktreePath.trim() : "")
|
|
1269
|
-
.find((value) => value.length > 0);
|
|
1270
|
-
if (priorSpanPath) {
|
|
1271
|
-
explicitWorktreePath = priorSpanPath;
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
if (explicitWorktreePath.length > 0) {
|
|
1275
|
-
helperArgs.push("--worktree-path=" + explicitWorktreePath);
|
|
1276
|
-
}
|
|
1277
|
-
if (typeof row.taskId === "string" && row.taskId.trim().length > 0) {
|
|
1278
|
-
helperArgs.push("--task-id=" + row.taskId.trim());
|
|
1279
|
-
}
|
|
1280
|
-
if (Array.isArray(row.claimedPaths) && row.claimedPaths.length > 0) {
|
|
1281
|
-
helperArgs.push("--claimed-paths=" + row.claimedPaths.join(","));
|
|
1282
|
-
}
|
|
1283
|
-
if (Array.isArray(row.evidenceRefs) && row.evidenceRefs.length > 0) {
|
|
1284
|
-
const title = String(row.evidenceRefs[0] || "").trim();
|
|
1285
|
-
if (title.length > 0) {
|
|
1286
|
-
helperArgs.push("--title=" + title.slice(0, 120));
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
return await new Promise((resolve) => {
|
|
1291
|
-
const child = spawn(process.execPath, helperArgs, {
|
|
1292
|
-
cwd: root,
|
|
1293
|
-
env: process.env,
|
|
1294
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1295
|
-
});
|
|
1296
|
-
let out = "";
|
|
1297
|
-
let err = "";
|
|
1298
|
-
child.stdout.on("data", (chunk) => {
|
|
1299
|
-
out += String(chunk ?? "");
|
|
1300
|
-
});
|
|
1301
|
-
child.stderr.on("data", (chunk) => {
|
|
1302
|
-
err += String(chunk ?? "");
|
|
1303
|
-
});
|
|
1304
|
-
child.on("error", (error) => {
|
|
1305
|
-
resolve({
|
|
1306
|
-
ok: false,
|
|
1307
|
-
errorCode: "slice_commit_failed",
|
|
1308
|
-
details: {
|
|
1309
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1310
|
-
}
|
|
1311
|
-
});
|
|
1312
|
-
});
|
|
1313
|
-
child.on("close", (code) => {
|
|
1314
|
-
let payload = null;
|
|
1315
|
-
const trimmed = out.trim();
|
|
1316
|
-
if (trimmed.length > 0) {
|
|
1317
|
-
try {
|
|
1318
|
-
payload = JSON.parse(trimmed);
|
|
1319
|
-
} catch {
|
|
1320
|
-
payload = null;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
if (code === 0) {
|
|
1324
|
-
resolve({ ok: true, payload });
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
const payloadCode =
|
|
1328
|
-
payload && typeof payload === "object" && typeof payload.errorCode === "string"
|
|
1329
|
-
? payload.errorCode
|
|
1330
|
-
: "slice_commit_failed";
|
|
1331
|
-
resolve({
|
|
1332
|
-
ok: false,
|
|
1333
|
-
errorCode: payloadCode,
|
|
1334
|
-
details:
|
|
1335
|
-
payload && typeof payload === "object"
|
|
1336
|
-
? payload
|
|
1337
|
-
: {
|
|
1338
|
-
stderr: err.trim(),
|
|
1339
|
-
stdout: out.trim()
|
|
1340
|
-
}
|
|
1341
|
-
});
|
|
1342
|
-
});
|
|
1343
|
-
});
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
async function runSliceWorktreePrepareIfNeeded(root, row, runId) {
|
|
1347
|
-
if (
|
|
1348
|
-
row.stage !== "tdd" ||
|
|
1349
|
-
row.agent !== "slice-builder" ||
|
|
1350
|
-
row.status !== "scheduled"
|
|
1351
|
-
) {
|
|
1352
|
-
return { ok: true, skipped: true };
|
|
1353
|
-
}
|
|
1354
|
-
const sliceId = typeof row.sliceId === "string" ? row.sliceId.trim() : "";
|
|
1355
|
-
const spanId = typeof row.spanId === "string" ? row.spanId.trim() : "";
|
|
1356
|
-
if (sliceId.length === 0 || spanId.length === 0) {
|
|
1357
|
-
return { ok: true, skipped: true };
|
|
1358
|
-
}
|
|
1359
|
-
const helperPath = path.join(root, RUNTIME_ROOT, "hooks", "slice-commit.mjs");
|
|
1360
|
-
if (!(await exists(helperPath))) {
|
|
1361
|
-
return { ok: true, skipped: true };
|
|
1362
|
-
}
|
|
1363
|
-
const helperArgs = [
|
|
1364
|
-
helperPath,
|
|
1365
|
-
"--json",
|
|
1366
|
-
"--quiet",
|
|
1367
|
-
"--prepare-worktree",
|
|
1368
|
-
"--slice=" + sliceId,
|
|
1369
|
-
"--span-id=" + spanId,
|
|
1370
|
-
"--run-id=" + runId
|
|
1371
|
-
];
|
|
1372
|
-
if (Array.isArray(row.claimedPaths) && row.claimedPaths.length > 0) {
|
|
1373
|
-
helperArgs.push("--claimed-paths=" + row.claimedPaths.join(","));
|
|
1374
|
-
}
|
|
1375
|
-
return await new Promise((resolve) => {
|
|
1376
|
-
const child = spawn(process.execPath, helperArgs, {
|
|
1377
|
-
cwd: root,
|
|
1378
|
-
env: process.env,
|
|
1379
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1380
|
-
});
|
|
1381
|
-
let out = "";
|
|
1382
|
-
let err = "";
|
|
1383
|
-
child.stdout.on("data", (chunk) => {
|
|
1384
|
-
out += String(chunk ?? "");
|
|
1385
|
-
});
|
|
1386
|
-
child.stderr.on("data", (chunk) => {
|
|
1387
|
-
err += String(chunk ?? "");
|
|
1388
|
-
});
|
|
1389
|
-
child.on("error", (error) => {
|
|
1390
|
-
resolve({
|
|
1391
|
-
ok: false,
|
|
1392
|
-
errorCode: "worktree_prepare_failed",
|
|
1393
|
-
details: {
|
|
1394
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1395
|
-
}
|
|
1396
|
-
});
|
|
1397
|
-
});
|
|
1398
|
-
child.on("close", (code) => {
|
|
1399
|
-
let payload = null;
|
|
1400
|
-
const trimmed = out.trim();
|
|
1401
|
-
if (trimmed.length > 0) {
|
|
1402
|
-
try {
|
|
1403
|
-
payload = JSON.parse(trimmed);
|
|
1404
|
-
} catch {
|
|
1405
|
-
payload = null;
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
if (code === 0) {
|
|
1409
|
-
resolve({ ok: true, payload });
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
const payloadCode =
|
|
1413
|
-
payload && typeof payload === "object" && typeof payload.errorCode === "string"
|
|
1414
|
-
? payload.errorCode
|
|
1415
|
-
: "worktree_prepare_failed";
|
|
1416
|
-
resolve({
|
|
1417
|
-
ok: false,
|
|
1418
|
-
errorCode: payloadCode,
|
|
1419
|
-
details:
|
|
1420
|
-
payload && typeof payload === "object"
|
|
1421
|
-
? payload
|
|
1422
|
-
: {
|
|
1423
|
-
stderr: err.trim(),
|
|
1424
|
-
stdout: out.trim()
|
|
1425
|
-
}
|
|
1426
|
-
});
|
|
1427
|
-
});
|
|
1428
|
-
});
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
async function main() {
|
|
1432
|
-
const args = parseArgs(process.argv.slice(2));
|
|
1433
|
-
const json = args.json !== undefined;
|
|
1434
|
-
|
|
1435
|
-
const guardRoot = await detectRoot();
|
|
1436
|
-
await verifyFlowStateGuardInline(guardRoot);
|
|
1437
|
-
|
|
1438
|
-
if (args.repair) {
|
|
1439
|
-
await runRepair(args, json);
|
|
1440
|
-
return;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
if (args.rerecord) {
|
|
1444
|
-
await runRerecord(args, json);
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
// Audit-only emit path. When the controller wants to record a
|
|
1449
|
-
// non-delegation audit row (e.g. \`cclaw_integration_overseer_skipped\`
|
|
1450
|
-
// when the wave heuristic chose to skip the overseer dispatch), pass
|
|
1451
|
-
// --audit-kind=<event-name> [--audit-reason=<text>] [--slice-ids=<csv>]
|
|
1452
|
-
// and the helper appends a single line to delegation-events.jsonl
|
|
1453
|
-
// without touching the lifecycle ledger. The kind must be in the
|
|
1454
|
-
// canonical allow-list so a typo cannot inject an unrecognized event.
|
|
1455
|
-
if (typeof args["audit-kind"] === "string" && args["audit-kind"].trim().length > 0) {
|
|
1456
|
-
await runAuditEmit(args, json);
|
|
1457
|
-
return;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
const problems = [];
|
|
1461
|
-
if (!args.stage) problems.push("missing --stage");
|
|
1462
|
-
if (!args.agent) problems.push("missing --agent");
|
|
1463
|
-
if (args.mode !== "mandatory" && args.mode !== "proactive") problems.push("--mode must be mandatory or proactive");
|
|
1464
|
-
if (!VALID_STATUSES.has(args.status)) problems.push("invalid --status");
|
|
1465
|
-
if (!args["span-id"]) problems.push("missing --span-id");
|
|
1466
|
-
if (args.status === "waived" && !args["waiver-reason"]) problems.push("waived status requires --waiver-reason");
|
|
1467
|
-
|
|
1468
|
-
// Strict --dispatch-surface enum validation: any provided surface must be
|
|
1469
|
-
// in the canonical allow-list. Do this BEFORE we use the value to gate
|
|
1470
|
-
// completed/role-switch fields.
|
|
1471
|
-
if (args["dispatch-surface"] !== undefined && !VALID_DISPATCH_SURFACES_SET.has(args["dispatch-surface"])) {
|
|
1472
|
-
problems.push("invalid --dispatch-surface (allowed: " + VALID_DISPATCH_SURFACES.join(", ") + ")");
|
|
1473
|
-
emitProblems(problems, json, 2);
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
// TDD slice phase tagging validation. --phase is strictly enum-bound;
|
|
1478
|
-
// --slice must be a non-empty string when provided;
|
|
1479
|
-
// --phase=refactor-deferred requires either an explicit
|
|
1480
|
-
// --refactor-rationale or an --evidence-ref with rationale text so the
|
|
1481
|
-
// linter has something to render.
|
|
1482
|
-
if (args.phase !== undefined && !VALID_DELEGATION_PHASES_SET.has(args.phase)) {
|
|
1483
|
-
problems.push("invalid --phase (allowed: " + VALID_DELEGATION_PHASES.join(", ") + ")");
|
|
1484
|
-
emitProblems(problems, json, 2);
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
if (args.slice !== undefined && (typeof args.slice !== "string" || args.slice.trim().length === 0)) {
|
|
1488
|
-
problems.push("--slice requires a non-empty value");
|
|
1489
|
-
emitProblems(problems, json, 2);
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
// 7.6.0 — phase-event status validation.
|
|
1493
|
-
// \`--phase=<phase>\` carries phase-level granularity (RED/GREEN/REFACTOR/DOC
|
|
1494
|
-
// outcomes). It is only meaningful on terminal statuses
|
|
1495
|
-
// (\`completed\` or \`failed\`). The dispatch-level ack (no phase) keeps
|
|
1496
|
-
// \`--status=acknowledged\`. Refuse acknowledged/launched/scheduled/waived/stale
|
|
1497
|
-
// rows that carry a phase so phantom-open slices cannot be recorded.
|
|
1498
|
-
if (
|
|
1499
|
-
typeof args.phase === "string" &&
|
|
1500
|
-
args.phase.length > 0 &&
|
|
1501
|
-
args.status !== "completed" &&
|
|
1502
|
-
args.status !== "failed"
|
|
1503
|
-
) {
|
|
1504
|
-
const sliceFlag = typeof args.slice === "string" && args.slice.length > 0
|
|
1505
|
-
? "--slice=" + args.slice + " "
|
|
1506
|
-
: "";
|
|
1507
|
-
const spanFlag = typeof args["span-id"] === "string" && args["span-id"].length > 0
|
|
1508
|
-
? "--span-id=" + args["span-id"] + " "
|
|
1509
|
-
: "";
|
|
1510
|
-
const correctedCommandHint =
|
|
1511
|
-
"node .cclaw/hooks/delegation-record.mjs --stage=" + (args.stage || "<stage>") +
|
|
1512
|
-
" --agent=" + (args.agent || "<agent>") +
|
|
1513
|
-
" --mode=" + (args.mode || "mandatory") +
|
|
1514
|
-
" --status=completed --phase=" + args.phase +
|
|
1515
|
-
" " + sliceFlag + spanFlag +
|
|
1516
|
-
'--evidence-ref="<phase outcome>"';
|
|
1517
|
-
emitErrorJson(
|
|
1518
|
-
"phase_event_requires_completed_or_failed_status",
|
|
1519
|
-
{
|
|
1520
|
-
phase: args.phase,
|
|
1521
|
-
status: args.status,
|
|
1522
|
-
spanId: args["span-id"] || "unknown",
|
|
1523
|
-
correctedCommandHint
|
|
1524
|
-
},
|
|
1525
|
-
json
|
|
1526
|
-
);
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
if (args.phase === "refactor-deferred") {
|
|
1530
|
-
const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
|
|
1531
|
-
if (rationaleQuality !== "ok") {
|
|
1532
|
-
if (rationaleQuality === "missing") {
|
|
1533
|
-
problems.push("--phase=refactor-deferred requires --refactor-rationale=<text>");
|
|
1534
|
-
} else if (rationaleQuality === "too-short") {
|
|
1535
|
-
problems.push("--refactor-rationale for deferred refactor must be at least 80 characters");
|
|
1536
|
-
} else {
|
|
1537
|
-
problems.push("--refactor-rationale for deferred refactor must mention slice/task context (e.g. S-12 and T-103)");
|
|
1538
|
-
}
|
|
1539
|
-
emitProblems(problems, json, 2);
|
|
1540
|
-
return;
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
// --refactor-outcome must be one of inline|deferred. When mode=deferred
|
|
1545
|
-
// a rationale is required (either --refactor-rationale or --evidence-ref
|
|
1546
|
-
// carrying the rationale text). --risk-tier must be one of low|medium|high
|
|
1547
|
-
// if provided.
|
|
1548
|
-
if (
|
|
1549
|
-
args["refactor-outcome"] !== undefined &&
|
|
1550
|
-
args["refactor-outcome"] !== "inline" &&
|
|
1551
|
-
args["refactor-outcome"] !== "deferred"
|
|
1552
|
-
) {
|
|
1553
|
-
problems.push("invalid --refactor-outcome (allowed: inline, deferred)");
|
|
1554
|
-
emitProblems(problems, json, 2);
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
if (args["refactor-outcome"] === "deferred") {
|
|
1558
|
-
const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
|
|
1559
|
-
if (rationaleQuality !== "ok") {
|
|
1560
|
-
if (rationaleQuality === "missing") {
|
|
1561
|
-
problems.push("--refactor-outcome=deferred requires --refactor-rationale=<text>");
|
|
1562
|
-
} else if (rationaleQuality === "too-short") {
|
|
1563
|
-
problems.push("--refactor-rationale for deferred refactor must be at least 80 characters");
|
|
1564
|
-
} else {
|
|
1565
|
-
problems.push("--refactor-rationale for deferred refactor must mention slice/task context (e.g. S-12 and T-103)");
|
|
1566
|
-
}
|
|
1567
|
-
emitProblems(problems, json, 2);
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
if (
|
|
1572
|
-
args["risk-tier"] !== undefined &&
|
|
1573
|
-
args["risk-tier"] !== "low" &&
|
|
1574
|
-
args["risk-tier"] !== "medium" &&
|
|
1575
|
-
args["risk-tier"] !== "high"
|
|
1576
|
-
) {
|
|
1577
|
-
problems.push("invalid --risk-tier (allowed: low, medium, high)");
|
|
1578
|
-
emitProblems(problems, json, 2);
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
if (args["override-cap"] !== undefined) {
|
|
1582
|
-
const overrideRaw = String(args["override-cap"]).trim();
|
|
1583
|
-
const overrideNum = Number(overrideRaw);
|
|
1584
|
-
if (!Number.isInteger(overrideNum) || overrideNum < 1) {
|
|
1585
|
-
problems.push("--override-cap must be an integer >= 1");
|
|
1586
|
-
emitProblems(problems, json, 2);
|
|
1587
|
-
return;
|
|
1588
|
-
}
|
|
1589
|
-
const reasonRaw = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
1590
|
-
if (reasonRaw.length === 0) {
|
|
1591
|
-
problems.push("--override-cap requires --reason=<slug>");
|
|
1592
|
-
emitProblems(problems, json, 2);
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
if (args.status === "completed" && args["dispatch-surface"] !== "role-switch") {
|
|
1598
|
-
for (const key of ["dispatch-id", "dispatch-surface", "agent-definition-path"]) {
|
|
1599
|
-
if (!args[key]) problems.push("completed isolated/generic status requires --" + key);
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
if (args.status === "completed" && args["dispatch-surface"] === "role-switch" && !args["evidence-ref"]) {
|
|
1603
|
-
problems.push("completed role-switch status requires --evidence-ref");
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
// Validate --agent-definition-path against the surface and on-disk
|
|
1607
|
-
// existence whenever both are provided.
|
|
1608
|
-
if (args["dispatch-surface"] && args["agent-definition-path"] && args["dispatch-surface"] !== "role-switch" && args["dispatch-surface"] !== "manual") {
|
|
1609
|
-
if (!dispatchSurfaceMatchesPath(args["dispatch-surface"], args["agent-definition-path"])) {
|
|
1610
|
-
const allowedPrefixes = SURFACE_PATH_PREFIXES[args["dispatch-surface"]];
|
|
1611
|
-
problems.push("--agent-definition-path does not lie under any allowed prefix for --dispatch-surface=" + args["dispatch-surface"] + " (expected one of: " + (allowedPrefixes.join(", ") || "(any)") + ")");
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
if (problems.length > 0) {
|
|
1616
|
-
emitProblems(problems, json, 2);
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
const root = await detectRoot();
|
|
1621
|
-
const now = new Date().toISOString();
|
|
1622
|
-
const runId = await readRunId(root);
|
|
1623
|
-
|
|
1624
|
-
// For completed isolated/generic rows, --agent-definition-path must
|
|
1625
|
-
// resolve to an existing file or directory inside the project. This
|
|
1626
|
-
// catches typos and stale generated agent paths before they enter the
|
|
1627
|
-
// ledger. Skipped for role-switch (no agent file is generated) and
|
|
1628
|
-
// manual (intentionally free-form).
|
|
1629
|
-
if (
|
|
1630
|
-
args.status === "completed" &&
|
|
1631
|
-
args["dispatch-surface"] &&
|
|
1632
|
-
args["dispatch-surface"] !== "role-switch" &&
|
|
1633
|
-
args["dispatch-surface"] !== "manual" &&
|
|
1634
|
-
args["agent-definition-path"]
|
|
1635
|
-
) {
|
|
1636
|
-
const exists = await pathExists(path.join(root, args["agent-definition-path"]));
|
|
1637
|
-
if (!exists) {
|
|
1638
|
-
emitProblems(["--agent-definition-path does not exist on disk: " + args["agent-definition-path"]], json, 2);
|
|
1639
|
-
return;
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
// Completed isolated/generic rows require explicit --ack-ts OR a prior
|
|
1644
|
-
// acknowledged event for the same span. fulfillmentMode=isolated cannot
|
|
1645
|
-
// be claimed without an ACK timestamp anchor.
|
|
1646
|
-
if (args.status === "completed" && args["dispatch-surface"] !== "role-switch" && !args["ack-ts"]) {
|
|
1647
|
-
const priorEvents = await readDelegationEvents(root);
|
|
1648
|
-
if (!hasPriorAck(priorEvents, args, runId)) {
|
|
1649
|
-
const ackProblem = "completed isolated/generic status requires prior acknowledged event for same span or --ack-ts";
|
|
1650
|
-
emitProblems([ackProblem], json, 2);
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
const status = args.status;
|
|
1656
|
-
const priorLedger = await readDelegationLedgerEntries(root);
|
|
1657
|
-
const priorForSpan = priorLedger.filter((e) => e && e.spanId === args["span-id"]);
|
|
1658
|
-
const inheritedWorktreePath = priorForSpan
|
|
1659
|
-
.map((entry) =>
|
|
1660
|
-
entry && typeof entry.worktreePath === "string" ? entry.worktreePath.trim() : "")
|
|
1661
|
-
.find((value) => value.length > 0);
|
|
1662
|
-
if (
|
|
1663
|
-
inheritedWorktreePath &&
|
|
1664
|
-
(typeof args["worktree-path"] !== "string" || args["worktree-path"].trim().length === 0)
|
|
1665
|
-
) {
|
|
1666
|
-
args["worktree-path"] = inheritedWorktreePath;
|
|
1667
|
-
}
|
|
1668
|
-
const inheritedStartTs = priorForSpan
|
|
1669
|
-
.map((e) => e.startTs)
|
|
1670
|
-
.filter((ts) => typeof ts === "string" && ts.length > 0)
|
|
1671
|
-
.sort()[0];
|
|
1672
|
-
// When no prior row exists, fall back to the earliest user-supplied
|
|
1673
|
-
// event timestamp so the monotonic validator never sees the row write
|
|
1674
|
-
// time overshoot the real event timestamps.
|
|
1675
|
-
const lifecycleCandidates = [
|
|
1676
|
-
inheritedStartTs,
|
|
1677
|
-
args["launched-ts"],
|
|
1678
|
-
args["ack-ts"],
|
|
1679
|
-
args["completed-ts"],
|
|
1680
|
-
now
|
|
1681
|
-
].filter((value) => typeof value === "string" && value.length > 0);
|
|
1682
|
-
const spanStartTs = inheritedStartTs ||
|
|
1683
|
-
lifecycleCandidates.reduce((min, candidate) => (candidate < min ? candidate : min), now);
|
|
1684
|
-
const row = buildRow(args, status, runId, now, { spanStartTs });
|
|
1685
|
-
const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
|
|
1686
|
-
const event = { ...clean, event: status, eventTs: now };
|
|
1687
|
-
let autoParallelAuditEvent = null;
|
|
1688
|
-
|
|
1689
|
-
const violation = validateMonotonicTimestampsInline(clean, priorLedger);
|
|
1690
|
-
if (violation) {
|
|
1691
|
-
emitErrorJson("delegation_timestamp_non_monotonic", violation, json);
|
|
1692
|
-
return;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
// File-overlap scheduler + fan-out cap. Run before the dispatch
|
|
1696
|
-
// dedup so disjoint claimedPaths can auto-promote to allowParallel,
|
|
1697
|
-
// emit an audit event for the flip, and bypass the duplicate guard.
|
|
1698
|
-
if (status === "scheduled") {
|
|
1699
|
-
const sameRunPrior = priorLedger.filter((entry) => entry.runId === runId);
|
|
1700
|
-
const activeForRun = computeActiveSubagentsInline(sameRunPrior);
|
|
1701
|
-
const overlap = validateFileOverlapInline(clean, activeForRun);
|
|
1702
|
-
if (overlap.conflict) {
|
|
1703
|
-
emitErrorJson("dispatch_overlap", overlap.conflict, json);
|
|
1704
|
-
return;
|
|
1705
|
-
}
|
|
1706
|
-
if (overlap.autoParallel && clean.allowParallel !== true) {
|
|
1707
|
-
clean.allowParallel = true;
|
|
1708
|
-
args["allow-parallel"] = true;
|
|
1709
|
-
event.allowParallel = true;
|
|
1710
|
-
autoParallelAuditEvent = {
|
|
1711
|
-
event: "cclaw_allow_parallel_auto_flip",
|
|
1712
|
-
runId,
|
|
1713
|
-
ts: now,
|
|
1714
|
-
eventTs: now,
|
|
1715
|
-
stage: clean.stage,
|
|
1716
|
-
agent: clean.agent,
|
|
1717
|
-
spanId: clean.spanId,
|
|
1718
|
-
sliceId: clean.sliceId,
|
|
1719
|
-
reason: "disjoint-claimed-paths-auto-flip",
|
|
1720
|
-
claimedPaths: Array.isArray(clean.claimedPaths) ? clean.claimedPaths : []
|
|
1721
|
-
};
|
|
1722
|
-
}
|
|
1723
|
-
const overrideRaw = typeof args["override-cap"] === "string" ? args["override-cap"] : null;
|
|
1724
|
-
const override = overrideRaw !== null ? Number(overrideRaw) : null;
|
|
1725
|
-
const capViolation = validateFanOutCapInline(clean, activeForRun, override);
|
|
1726
|
-
if (capViolation) {
|
|
1727
|
-
emitErrorJson("dispatch_cap", capViolation, json);
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
const preparedWorktree = await runSliceWorktreePrepareIfNeeded(root, clean, runId);
|
|
1731
|
-
if (!preparedWorktree.ok) {
|
|
1732
|
-
emitErrorJson(
|
|
1733
|
-
preparedWorktree.errorCode || "worktree_prepare_failed",
|
|
1734
|
-
preparedWorktree.details || {},
|
|
1735
|
-
json
|
|
1736
|
-
);
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
if (
|
|
1740
|
-
preparedWorktree.payload &&
|
|
1741
|
-
typeof preparedWorktree.payload === "object" &&
|
|
1742
|
-
typeof preparedWorktree.payload.worktreePath === "string" &&
|
|
1743
|
-
preparedWorktree.payload.worktreePath.trim().length > 0
|
|
1744
|
-
) {
|
|
1745
|
-
clean.worktreePath = preparedWorktree.payload.worktreePath.trim();
|
|
1746
|
-
event.worktreePath = clean.worktreePath;
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
const dedupViolation = enforceDispatchDedupInline(clean, priorLedger, args);
|
|
1750
|
-
if (dedupViolation) {
|
|
1751
|
-
if (dedupViolation.kind === "supersede") {
|
|
1752
|
-
const stalenessTs = new Date(new Date(now).getTime() - 1).toISOString();
|
|
1753
|
-
const staleRow = {
|
|
1754
|
-
stage: dedupViolation.existing.stage,
|
|
1755
|
-
agent: dedupViolation.existing.agent,
|
|
1756
|
-
mode: dedupViolation.existing.mode,
|
|
1757
|
-
status: "stale",
|
|
1758
|
-
spanId: dedupViolation.existing.spanId,
|
|
1759
|
-
runId,
|
|
1760
|
-
startTs: dedupViolation.existing.startTs || stalenessTs,
|
|
1761
|
-
ts: stalenessTs,
|
|
1762
|
-
endTs: stalenessTs,
|
|
1763
|
-
supersededBy: clean.spanId,
|
|
1764
|
-
schemaVersion: LEDGER_SCHEMA_VERSION
|
|
1765
|
-
};
|
|
1766
|
-
const staleEvent = { ...staleRow, event: "stale", eventTs: stalenessTs };
|
|
1767
|
-
await persistEntry(root, runId, staleRow, staleEvent);
|
|
1768
|
-
} else if (dedupViolation.kind === "error") {
|
|
1769
|
-
emitErrorJson("dispatch_duplicate", dedupViolation.details, json);
|
|
1770
|
-
return;
|
|
1771
|
-
} else if (dedupViolation.kind === "supersede-mismatch") {
|
|
1772
|
-
emitErrorJson("dispatch_supersede_mismatch", dedupViolation.details, json);
|
|
1773
|
-
return;
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
// GREEN evidence freshness contract for \`slice-builder --phase green
|
|
1778
|
-
// --status=completed\`. Three checks:
|
|
1779
|
-
// 1. green_evidence_red_test_mismatch — evidenceRefs[0] must contain
|
|
1780
|
-
// the basename/stem of the RED span's first evidenceRef.
|
|
1781
|
-
// 2. green_evidence_passing_assertion_missing — evidenceRefs[0]
|
|
1782
|
-
// must carry a recognized passing-assertion line ("=> N passed;
|
|
1783
|
-
// 0 failed" or runner-specific equivalents).
|
|
1784
|
-
// 3. green_evidence_too_fresh — completedTs minus ackTs must be
|
|
1785
|
-
// >= flow-state.json::tddGreenMinElapsedMs (default 4000ms).
|
|
1786
|
-
// Escape hatch for legitimate observational GREENs (cross-slice
|
|
1787
|
-
// handoff, no-op verification): --green-mode=observational.
|
|
1788
|
-
if (
|
|
1789
|
-
clean.stage === "tdd" &&
|
|
1790
|
-
clean.agent === "slice-builder" &&
|
|
1791
|
-
clean.phase === "green" &&
|
|
1792
|
-
clean.status === "completed"
|
|
1793
|
-
) {
|
|
1794
|
-
const isObservational =
|
|
1795
|
-
typeof args["green-mode"] === "string" &&
|
|
1796
|
-
args["green-mode"].trim().toLowerCase() === "observational";
|
|
1797
|
-
const greenEvidenceFirst =
|
|
1798
|
-
Array.isArray(clean.evidenceRefs) && clean.evidenceRefs.length > 0
|
|
1799
|
-
? String(clean.evidenceRefs[0])
|
|
1800
|
-
: "";
|
|
1801
|
-
|
|
1802
|
-
// Locate the matching RED row's first evidenceRef in the events log.
|
|
1803
|
-
const priorEvents = await readDelegationEvents(root);
|
|
1804
|
-
let redEvidenceRef = null;
|
|
1805
|
-
for (let i = priorEvents.length - 1; i >= 0; i -= 1) {
|
|
1806
|
-
const ev = priorEvents[i];
|
|
1807
|
-
if (!ev) continue;
|
|
1808
|
-
if (ev.runId !== runId) continue;
|
|
1809
|
-
if (ev.stage !== "tdd") continue;
|
|
1810
|
-
if (ev.sliceId !== clean.sliceId) continue;
|
|
1811
|
-
if (ev.phase !== "red") continue;
|
|
1812
|
-
if (Array.isArray(ev.evidenceRefs) && ev.evidenceRefs.length > 0) {
|
|
1813
|
-
redEvidenceRef = String(ev.evidenceRefs[0] || "");
|
|
1814
|
-
break;
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
// The freshness contract only fires when there's a matching RED row
|
|
1819
|
-
// for this slice in the active run. Without RED context we have
|
|
1820
|
-
// nothing to verify GREEN against (legacy ledger imports, RED
|
|
1821
|
-
// happened outside cclaw harness, or test fixtures that bypass
|
|
1822
|
-
// RED). Once a RED row is present, the contract becomes
|
|
1823
|
-
// mandatory unless explicitly waived via --green-mode=observational.
|
|
1824
|
-
const hasRedContext = redEvidenceRef !== null;
|
|
1825
|
-
const escapeFastGreen = isObservational;
|
|
1826
|
-
|
|
1827
|
-
if (hasRedContext && !escapeFastGreen) {
|
|
1828
|
-
// Check 1: RED test name match.
|
|
1829
|
-
const stem = extractRedTestNameInline(redEvidenceRef);
|
|
1830
|
-
if (stem && greenEvidenceFirst.length > 0 && !greenEvidenceFirst.toLowerCase().includes(stem.toLowerCase())) {
|
|
1831
|
-
emitErrorJson(
|
|
1832
|
-
"green_evidence_red_test_mismatch",
|
|
1833
|
-
{
|
|
1834
|
-
sliceId: clean.sliceId,
|
|
1835
|
-
redEvidenceFirst: redEvidenceRef,
|
|
1836
|
-
greenEvidenceFirst,
|
|
1837
|
-
expectedSubstring: stem,
|
|
1838
|
-
remediation:
|
|
1839
|
-
"evidenceRefs[0] on the GREEN row must reference the same test the RED row cited. Re-run the matching RED test, capture its passing output, and pass it as --evidence-ref."
|
|
1840
|
-
},
|
|
1841
|
-
json
|
|
1842
|
-
);
|
|
1843
|
-
return;
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
// Check 2: passing-assertion line.
|
|
1847
|
-
if (greenEvidenceFirst.length > 0 && !matchesPassingAssertionInline(greenEvidenceFirst)) {
|
|
1848
|
-
emitErrorJson(
|
|
1849
|
-
"green_evidence_passing_assertion_missing",
|
|
1850
|
-
{
|
|
1851
|
-
sliceId: clean.sliceId,
|
|
1852
|
-
greenEvidenceFirst,
|
|
1853
|
-
remediation:
|
|
1854
|
-
"evidenceRefs[0] on the GREEN row must contain a passing-assertion line (language-agnostic examples: Node/Vitest \\"=> N passed; 0 failed\\", Python/Pytest \\"N passed in 0.42s\\", Go \\"ok pkg 0.12s\\", Rust \\"test result: ok\\", Java/Maven \\"Tests run: N, Failures: 0, Errors: 0\\"). Re-run the test and paste a fresh runner line."
|
|
1855
|
-
},
|
|
1856
|
-
json
|
|
1857
|
-
);
|
|
1858
|
-
return;
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
// Check 3: fast-green floor. ackTs is required upstream; we use
|
|
1862
|
-
// the persisted ackTs from prior events when not provided on this
|
|
1863
|
-
// row.
|
|
1864
|
-
const minMs = await readTddGreenMinElapsedMsInline(root);
|
|
1865
|
-
if (minMs > 0 && clean.completedTs) {
|
|
1866
|
-
let ackTs = clean.ackTs;
|
|
1867
|
-
if (!ackTs) {
|
|
1868
|
-
for (let i = priorEvents.length - 1; i >= 0; i -= 1) {
|
|
1869
|
-
const ev = priorEvents[i];
|
|
1870
|
-
if (!ev) continue;
|
|
1871
|
-
if (ev.spanId !== clean.spanId) continue;
|
|
1872
|
-
if (typeof ev.ackTs === "string" && ev.ackTs.length > 0) {
|
|
1873
|
-
ackTs = ev.ackTs;
|
|
1874
|
-
break;
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
if (ackTs) {
|
|
1879
|
-
const completedMs = Date.parse(clean.completedTs);
|
|
1880
|
-
const ackMs = Date.parse(ackTs);
|
|
1881
|
-
if (Number.isFinite(completedMs) && Number.isFinite(ackMs)) {
|
|
1882
|
-
const elapsed = completedMs - ackMs;
|
|
1883
|
-
if (elapsed < minMs) {
|
|
1884
|
-
emitErrorJson(
|
|
1885
|
-
"green_evidence_too_fresh",
|
|
1886
|
-
{
|
|
1887
|
-
sliceId: clean.sliceId,
|
|
1888
|
-
ackTs,
|
|
1889
|
-
completedTs: clean.completedTs,
|
|
1890
|
-
elapsedMs: elapsed,
|
|
1891
|
-
minMs,
|
|
1892
|
-
remediation:
|
|
1893
|
-
"GREEN completedTs - ackTs is below the freshness floor. Either run the verification test for real and re-record, or pass --green-mode=observational for legitimate no-op verification spans."
|
|
1894
|
-
},
|
|
1895
|
-
json
|
|
1896
|
-
);
|
|
1897
|
-
return;
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
const sliceCommitResult = await runSliceCommitIfNeeded(root, clean, runId);
|
|
1906
|
-
if (!sliceCommitResult.ok) {
|
|
1907
|
-
emitErrorJson(
|
|
1908
|
-
sliceCommitResult.errorCode || "slice_commit_failed",
|
|
1909
|
-
sliceCommitResult.details || {},
|
|
1910
|
-
json
|
|
1911
|
-
);
|
|
1912
|
-
return;
|
|
1913
|
-
}
|
|
1914
|
-
if (
|
|
1915
|
-
sliceCommitResult.payload &&
|
|
1916
|
-
typeof sliceCommitResult.payload === "object" &&
|
|
1917
|
-
typeof sliceCommitResult.payload.commitSha === "string"
|
|
1918
|
-
) {
|
|
1919
|
-
event.sliceCommitSha = sliceCommitResult.payload.commitSha;
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
await persistEntry(root, runId, clean, event);
|
|
1923
|
-
if (autoParallelAuditEvent) {
|
|
1924
|
-
await appendAuditEventInline(root, autoParallelAuditEvent);
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
process.stdout.write(JSON.stringify({ ok: true, event }, null, 2) + "\\n");
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
void main();
|
|
1931
|
-
`;
|
|
1932
|
-
}
|
|
1933
|
-
export function sliceCommitScript() {
|
|
1934
|
-
return internalHelperScript("slice-commit", "slice-commit", "Usage: node " + RUNTIME_ROOT + "/hooks/slice-commit.mjs --slice=<S-N> --span-id=<span-id> [--task-id=<T-id>] [--title=<text>] [--run-id=<run-id>] [--worktree-path=<abs-or-rel-path>] [--prepare-worktree] [--claimed-paths=<path1,path2,...>] [--claimed-path=<path> ...] [--json] [--quiet]");
|
|
1935
|
-
}
|
|
1936
|
-
export function runHookCmdScript() {
|
|
1937
|
-
return `: << 'CMDBLOCK'
|
|
1938
|
-
@echo off
|
|
1939
|
-
REM Cross-platform wrapper for cclaw Node hook runtime.
|
|
1940
|
-
REM Windows executes this batch block; Unix shells treat it as a heredoc comment.
|
|
1941
|
-
if "%~1"=="" (
|
|
1942
|
-
echo [cclaw] run-hook.cmd: missing hook name >&2
|
|
1943
|
-
exit /b 1
|
|
1944
|
-
)
|
|
1945
|
-
set "HOOK_DIR=%~dp0"
|
|
1946
|
-
set "RUNTIME=%HOOK_DIR%run-hook.mjs"
|
|
1947
|
-
where node >nul 2>nul
|
|
1948
|
-
if %ERRORLEVEL% neq 0 (
|
|
1949
|
-
REM Best-effort: missing node should not block harness execution loops.
|
|
1950
|
-
echo [cclaw] run-hook.cmd: node not found; cclaw hook skipped. Run npx cclaw-cli sync. >&2
|
|
1951
|
-
exit /b 0
|
|
1952
|
-
)
|
|
1953
|
-
node "%RUNTIME%" %*
|
|
1954
|
-
exit /b %ERRORLEVEL%
|
|
1955
|
-
CMDBLOCK
|
|
1956
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
1957
|
-
if [ "$#" -lt 1 ]; then
|
|
1958
|
-
echo "[cclaw] run-hook.cmd: missing hook name" >&2
|
|
1959
|
-
exit 1
|
|
1960
|
-
fi
|
|
1961
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
1962
|
-
echo "[cclaw] run-hook.cmd: node not found; cclaw hook skipped. Run npx cclaw-cli sync." >&2
|
|
1963
|
-
exit 0
|
|
1964
|
-
fi
|
|
1965
|
-
exec node "\${SCRIPT_DIR}/run-hook.mjs" "$@"
|
|
1966
|
-
`;
|
|
1967
|
-
}
|
|
1968
|
-
export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
|
|
1969
|
-
export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
|
|
1970
|
-
export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
|
|
1971
|
-
export { nodeHookRuntimeScript } from "./node-hooks.js";
|
|
1972
|
-
export { opencodePluginJs } from "./opencode-plugin.js";
|