cclaw-cli 6.14.0 → 6.14.2

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.
@@ -17,6 +17,10 @@ import { FlowStateGuardMismatchError, verifyFlowStateGuard } from "../run-persis
17
17
  import { DelegationTimestampError, DispatchCapError, DispatchClaimInvalidError, DispatchDuplicateError, DispatchOverlapError } from "../delegation.js";
18
18
  import { parsePlanSplitWavesArgs, runPlanSplitWaves } from "./plan-split-waves.js";
19
19
  import { runSetWorktreeMode } from "./set-worktree-mode.js";
20
+ import { runSetCheckpointMode } from "./set-checkpoint-mode.js";
21
+ import { runSetIntegrationOverseerMode } from "./set-integration-overseer-mode.js";
22
+ import { runWaveStatusCommand } from "./wave-status.js";
23
+ import { runCohesionContractCommand } from "./cohesion-contract-stub.js";
20
24
  /**
21
25
  * Subcommands that mutate or consult flow-state.json via the CLI runtime.
22
26
  * They all require the sha256 sidecar to match before continuing so a
@@ -30,12 +34,14 @@ const GUARD_ENFORCED_SUBCOMMANDS = new Set([
30
34
  "rewind",
31
35
  "verify-flow-state-diff",
32
36
  "verify-current-state",
33
- "set-worktree-mode"
37
+ "set-worktree-mode",
38
+ "set-checkpoint-mode",
39
+ "set-integration-overseer-mode"
34
40
  ]);
35
41
  export async function runInternalCommand(projectRoot, argv, io) {
36
42
  const [subcommand, ...tokens] = argv;
37
43
  if (!subcommand) {
38
- io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant | plan-split-waves | set-worktree-mode\n");
44
+ io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant | plan-split-waves | set-worktree-mode | set-checkpoint-mode | set-integration-overseer-mode | wave-status | cohesion-contract\n");
39
45
  return 1;
40
46
  }
41
47
  try {
@@ -93,7 +99,19 @@ export async function runInternalCommand(projectRoot, argv, io) {
93
99
  if (subcommand === "set-worktree-mode") {
94
100
  return await runSetWorktreeMode(projectRoot, tokens, io);
95
101
  }
96
- io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant | plan-split-waves | set-worktree-mode\n`);
102
+ if (subcommand === "set-checkpoint-mode") {
103
+ return await runSetCheckpointMode(projectRoot, tokens, io);
104
+ }
105
+ if (subcommand === "set-integration-overseer-mode") {
106
+ return await runSetIntegrationOverseerMode(projectRoot, tokens, io);
107
+ }
108
+ if (subcommand === "wave-status") {
109
+ return await runWaveStatusCommand(projectRoot, tokens, io);
110
+ }
111
+ if (subcommand === "cohesion-contract") {
112
+ return await runCohesionContractCommand(projectRoot, tokens, io);
113
+ }
114
+ io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant | plan-split-waves | set-worktree-mode | set-checkpoint-mode | set-integration-overseer-mode | wave-status | cohesion-contract\n`);
97
115
  return 1;
98
116
  }
99
117
  catch (err) {
@@ -0,0 +1,29 @@
1
+ import type { Writable } from "node:stream";
2
+ interface InternalIo {
3
+ stdout: Writable;
4
+ stderr: Writable;
5
+ }
6
+ export interface CohesionContractArgs {
7
+ stub: boolean;
8
+ force: boolean;
9
+ reason: string | null;
10
+ }
11
+ export declare function parseCohesionContractArgs(tokens: string[]): CohesionContractArgs | null;
12
+ /**
13
+ * v6.14.2 — emit a minimal advisory cohesion contract that satisfies
14
+ * the linter shape check (`cohesion-contract.{md,json}`) so projects
15
+ * with `legacyContinuation: true` and ≥ 2 completed slice-implementer
16
+ * rows can clear the soft `tdd.cohesion_contract_missing` finding
17
+ * without hand-authoring the document.
18
+ *
19
+ * The stub is intentionally bare — `sharedTypes`, `touchpoints`, and
20
+ * `slices` are populated from the active run delegation ledger so
21
+ * downstream tooling can see which slices the contract acknowledges,
22
+ * but the contract carries `status.verdict: "advisory_legacy"` so
23
+ * reviewers know not to treat it as authoritative.
24
+ *
25
+ * Refuses to overwrite an existing contract unless `--force` is
26
+ * passed; the existing file is treated as authored work.
27
+ */
28
+ export declare function runCohesionContractCommand(projectRoot: string, tokens: string[], io: InternalIo): Promise<number>;
29
+ export {};
@@ -0,0 +1,166 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { RUNTIME_ROOT } from "../constants.js";
4
+ import { writeFileSafe } from "../fs-utils.js";
5
+ import { readDelegationLedger } from "../delegation.js";
6
+ export function parseCohesionContractArgs(tokens) {
7
+ const args = { stub: false, force: false, reason: null };
8
+ for (const token of tokens) {
9
+ if (token === "--stub") {
10
+ args.stub = true;
11
+ continue;
12
+ }
13
+ if (token === "--force") {
14
+ args.force = true;
15
+ continue;
16
+ }
17
+ if (token.startsWith("--reason=")) {
18
+ const raw = token.slice("--reason=".length).trim();
19
+ if (raw.length > 0)
20
+ args.reason = raw;
21
+ continue;
22
+ }
23
+ return null;
24
+ }
25
+ if (!args.stub)
26
+ return null;
27
+ return args;
28
+ }
29
+ /**
30
+ * v6.14.2 — emit a minimal advisory cohesion contract that satisfies
31
+ * the linter shape check (`cohesion-contract.{md,json}`) so projects
32
+ * with `legacyContinuation: true` and ≥ 2 completed slice-implementer
33
+ * rows can clear the soft `tdd.cohesion_contract_missing` finding
34
+ * without hand-authoring the document.
35
+ *
36
+ * The stub is intentionally bare — `sharedTypes`, `touchpoints`, and
37
+ * `slices` are populated from the active run delegation ledger so
38
+ * downstream tooling can see which slices the contract acknowledges,
39
+ * but the contract carries `status.verdict: "advisory_legacy"` so
40
+ * reviewers know not to treat it as authoritative.
41
+ *
42
+ * Refuses to overwrite an existing contract unless `--force` is
43
+ * passed; the existing file is treated as authored work.
44
+ */
45
+ export async function runCohesionContractCommand(projectRoot, tokens, io) {
46
+ const parsed = parseCohesionContractArgs(tokens);
47
+ if (!parsed) {
48
+ io.stderr.write("cclaw internal cohesion-contract: usage: --stub [--force] [--reason=\"<short>\"]\n");
49
+ return 1;
50
+ }
51
+ const artifactsDir = path.join(projectRoot, RUNTIME_ROOT, "artifacts");
52
+ const mdPath = path.join(artifactsDir, "cohesion-contract.md");
53
+ const jsonPath = path.join(artifactsDir, "cohesion-contract.json");
54
+ if (!parsed.force) {
55
+ const mdExists = await fileExists(mdPath);
56
+ const jsonExists = await fileExists(jsonPath);
57
+ if (mdExists || jsonExists) {
58
+ io.stderr.write("cclaw internal cohesion-contract: existing cohesion-contract.{md,json} present; pass --force to overwrite.\n");
59
+ return 1;
60
+ }
61
+ }
62
+ const ledger = await readDelegationLedger(projectRoot).catch(() => null);
63
+ const sliceIds = collectSliceIds(ledger?.entries ?? []);
64
+ const reasonNote = parsed.reason
65
+ ? `Reason: ${parsed.reason}`
66
+ : "Reason: legacyContinuation auto-stub.";
67
+ const md = [
68
+ "# Cohesion Contract",
69
+ "",
70
+ "_Advisory stub generated by `cclaw-cli internal cohesion-contract --stub`._",
71
+ "_Status: `advisory_legacy` — populated for legacyContinuation projects so the_",
72
+ "_TDD linter does not block stage-complete on `tdd.cohesion_contract_missing`._",
73
+ "",
74
+ `${reasonNote}`,
75
+ "",
76
+ "## Shared Types & Interfaces",
77
+ "| Symbol | Path | Signature | Owner slice |",
78
+ "|---|---|---|---|",
79
+ "| (none recorded) | (none) | (none) | (n/a) |",
80
+ "",
81
+ "## Naming Conventions",
82
+ "- Stub: per-slice modules continue to follow the existing repo conventions.",
83
+ "",
84
+ "## Invariants",
85
+ "- Stub: no cross-slice invariants asserted; treat each slice as independent until upgraded.",
86
+ "",
87
+ "## Integration Touchpoints",
88
+ "| From slice | To slice | Surface | Integration test name |",
89
+ "|---|---|---|---|",
90
+ "| (none recorded) | (n/a) | (n/a) | (n/a) |",
91
+ "",
92
+ "## Behavior Specifications per Slice",
93
+ sliceIds.length === 0
94
+ ? "- (none recorded)"
95
+ : sliceIds
96
+ .map((sid) => `### Slice ${sid}\n- Behavior: see \`tdd-slices/${sid}.md\` (if present).`)
97
+ .join("\n\n"),
98
+ "",
99
+ "## Status",
100
+ "| Slice | Implemented | Tests pass | Cohesion verified |",
101
+ "|---|---|---|---|",
102
+ sliceIds.length === 0
103
+ ? "| (none) | n/a | n/a | n/a |"
104
+ : sliceIds.map((sid) => `| ${sid} | yes | yes | advisory |`).join("\n"),
105
+ ""
106
+ ].join("\n");
107
+ const jsonStub = {
108
+ version: 1,
109
+ sharedTypes: [],
110
+ touchpoints: [],
111
+ slices: sliceIds.map((sid) => ({
112
+ sliceId: sid,
113
+ description: `Stub entry for ${sid}; advisory under legacyContinuation.`,
114
+ implemented: true,
115
+ testsPass: true,
116
+ cohesionVerified: false
117
+ })),
118
+ status: {
119
+ verdict: "advisory_legacy",
120
+ generatedBy: "cclaw-cli internal cohesion-contract --stub",
121
+ reason: parsed.reason ?? "legacyContinuation auto-stub"
122
+ }
123
+ };
124
+ await writeFileSafe(mdPath, md);
125
+ await writeFileSafe(jsonPath, `${JSON.stringify(jsonStub, null, 2)}\n`);
126
+ io.stdout.write(`cclaw: cohesion-contract stub written (${sliceIds.length} slice(s) referenced). ` +
127
+ "Status: advisory_legacy — review and replace once cross-slice cohesion data is real.\n");
128
+ return 0;
129
+ }
130
+ async function fileExists(filePath) {
131
+ try {
132
+ await fs.access(filePath);
133
+ return true;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
139
+ function collectSliceIds(entries) {
140
+ const set = new Set();
141
+ for (const entry of entries) {
142
+ if (entry.agent !== "slice-implementer")
143
+ continue;
144
+ if (entry.status !== "completed")
145
+ continue;
146
+ if (typeof entry.sliceId !== "string")
147
+ continue;
148
+ if (entry.sliceId.length === 0)
149
+ continue;
150
+ set.add(entry.sliceId);
151
+ }
152
+ return [...set].sort((a, b) => {
153
+ const an = parseSliceNum(a);
154
+ const bn = parseSliceNum(b);
155
+ if (an !== null && bn !== null)
156
+ return an - bn;
157
+ return a.localeCompare(b);
158
+ });
159
+ }
160
+ function parseSliceNum(sliceId) {
161
+ const m = /^S-(\d+)$/u.exec(sliceId);
162
+ if (!m)
163
+ return null;
164
+ const n = Number.parseInt(m[1], 10);
165
+ return Number.isFinite(n) ? n : null;
166
+ }
@@ -0,0 +1,16 @@
1
+ import type { Writable } from "node:stream";
2
+ export interface SetCheckpointModeArgs {
3
+ mode: "per-slice" | "global-red";
4
+ reason: string | null;
5
+ }
6
+ export declare function parseSetCheckpointModeArgs(tokens: string[]): SetCheckpointModeArgs | null;
7
+ /**
8
+ * v6.14.2 — set `flow-state.json::tddCheckpointMode` without advancing
9
+ * the stage DAG. Mirrors `set-worktree-mode`. The `--reason` flag is
10
+ * optional but recommended for the audit trail; it is currently passed
11
+ * through to the writer subsystem string so operators can grep the
12
+ * `.flow-state.guard.json` sidecar.
13
+ */
14
+ export declare function runSetCheckpointMode(projectRoot: string, tokens: string[], io: {
15
+ stderr: Writable;
16
+ }): Promise<number>;
@@ -0,0 +1,72 @@
1
+ import { readFlowState, writeFlowState } from "../runs.js";
2
+ export function parseSetCheckpointModeArgs(tokens) {
3
+ let mode = null;
4
+ let reason = null;
5
+ let positional = null;
6
+ for (const token of tokens) {
7
+ if (token.startsWith("--mode=")) {
8
+ const raw = token.slice("--mode=".length).trim();
9
+ if (raw === "per-slice" || raw === "global-red") {
10
+ mode = raw;
11
+ }
12
+ else {
13
+ return null;
14
+ }
15
+ continue;
16
+ }
17
+ if (token.startsWith("--reason=")) {
18
+ const raw = token.slice("--reason=".length).trim();
19
+ if (raw.length > 0)
20
+ reason = raw;
21
+ continue;
22
+ }
23
+ if (token.startsWith("--")) {
24
+ // unknown flag — let the caller surface usage.
25
+ return null;
26
+ }
27
+ if (positional === null) {
28
+ positional = token.trim();
29
+ continue;
30
+ }
31
+ return null;
32
+ }
33
+ if (mode === null && positional !== null) {
34
+ if (positional === "per-slice" || positional === "global-red") {
35
+ mode = positional;
36
+ }
37
+ else {
38
+ return null;
39
+ }
40
+ }
41
+ if (mode === null)
42
+ return null;
43
+ return { mode, reason };
44
+ }
45
+ /**
46
+ * v6.14.2 — set `flow-state.json::tddCheckpointMode` without advancing
47
+ * the stage DAG. Mirrors `set-worktree-mode`. The `--reason` flag is
48
+ * optional but recommended for the audit trail; it is currently passed
49
+ * through to the writer subsystem string so operators can grep the
50
+ * `.flow-state.guard.json` sidecar.
51
+ */
52
+ export async function runSetCheckpointMode(projectRoot, tokens, io) {
53
+ const parsed = parseSetCheckpointModeArgs(tokens);
54
+ if (!parsed) {
55
+ io.stderr.write("cclaw internal set-checkpoint-mode: usage: <per-slice|global-red> [--reason=\"<short>\"] " +
56
+ "(or --mode=<per-slice|global-red>)\n");
57
+ return 1;
58
+ }
59
+ const state = await readFlowState(projectRoot);
60
+ const writerSubsystem = parsed.reason
61
+ ? `set-checkpoint-mode:${slugifyReason(parsed.reason)}`
62
+ : "set-checkpoint-mode";
63
+ await writeFlowState(projectRoot, { ...state, tddCheckpointMode: parsed.mode }, { writerSubsystem });
64
+ return 0;
65
+ }
66
+ function slugifyReason(reason) {
67
+ return (reason
68
+ .toLowerCase()
69
+ .replace(/[^a-z0-9_-]+/gu, "-")
70
+ .replace(/^-+|-+$/gu, "")
71
+ .slice(0, 60) || "unspecified");
72
+ }
@@ -0,0 +1,14 @@
1
+ import type { Writable } from "node:stream";
2
+ export interface SetIntegrationOverseerModeArgs {
3
+ mode: "conditional" | "always";
4
+ reason: string | null;
5
+ }
6
+ export declare function parseSetIntegrationOverseerModeArgs(tokens: string[]): SetIntegrationOverseerModeArgs | null;
7
+ /**
8
+ * v6.14.2 — set `flow-state.json::integrationOverseerMode` without
9
+ * advancing the stage DAG. Mirrors `set-worktree-mode` and
10
+ * `set-checkpoint-mode`.
11
+ */
12
+ export declare function runSetIntegrationOverseerMode(projectRoot: string, tokens: string[], io: {
13
+ stderr: Writable;
14
+ }): Promise<number>;
@@ -0,0 +1,69 @@
1
+ import { readFlowState, writeFlowState } from "../runs.js";
2
+ export function parseSetIntegrationOverseerModeArgs(tokens) {
3
+ let mode = null;
4
+ let reason = null;
5
+ let positional = null;
6
+ for (const token of tokens) {
7
+ if (token.startsWith("--mode=")) {
8
+ const raw = token.slice("--mode=".length).trim();
9
+ if (raw === "conditional" || raw === "always") {
10
+ mode = raw;
11
+ }
12
+ else {
13
+ return null;
14
+ }
15
+ continue;
16
+ }
17
+ if (token.startsWith("--reason=")) {
18
+ const raw = token.slice("--reason=".length).trim();
19
+ if (raw.length > 0)
20
+ reason = raw;
21
+ continue;
22
+ }
23
+ if (token.startsWith("--")) {
24
+ return null;
25
+ }
26
+ if (positional === null) {
27
+ positional = token.trim();
28
+ continue;
29
+ }
30
+ return null;
31
+ }
32
+ if (mode === null && positional !== null) {
33
+ if (positional === "conditional" || positional === "always") {
34
+ mode = positional;
35
+ }
36
+ else {
37
+ return null;
38
+ }
39
+ }
40
+ if (mode === null)
41
+ return null;
42
+ return { mode, reason };
43
+ }
44
+ /**
45
+ * v6.14.2 — set `flow-state.json::integrationOverseerMode` without
46
+ * advancing the stage DAG. Mirrors `set-worktree-mode` and
47
+ * `set-checkpoint-mode`.
48
+ */
49
+ export async function runSetIntegrationOverseerMode(projectRoot, tokens, io) {
50
+ const parsed = parseSetIntegrationOverseerModeArgs(tokens);
51
+ if (!parsed) {
52
+ io.stderr.write("cclaw internal set-integration-overseer-mode: usage: <conditional|always> [--reason=\"<short>\"] " +
53
+ "(or --mode=<conditional|always>)\n");
54
+ return 1;
55
+ }
56
+ const state = await readFlowState(projectRoot);
57
+ const writerSubsystem = parsed.reason
58
+ ? `set-integration-overseer-mode:${slugifyReason(parsed.reason)}`
59
+ : "set-integration-overseer-mode";
60
+ await writeFlowState(projectRoot, { ...state, integrationOverseerMode: parsed.mode }, { writerSubsystem });
61
+ return 0;
62
+ }
63
+ function slugifyReason(reason) {
64
+ return (reason
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9_-]+/gu, "-")
67
+ .replace(/^-+|-+$/gu, "")
68
+ .slice(0, 60) || "unspecified");
69
+ }
@@ -0,0 +1,51 @@
1
+ import type { Writable } from "node:stream";
2
+ interface InternalIo {
3
+ stdout: Writable;
4
+ stderr: Writable;
5
+ }
6
+ export interface WaveStatusWaveSummary {
7
+ waveId: string;
8
+ members: string[];
9
+ closedMembers: string[];
10
+ openMembers: string[];
11
+ readyMembers: string[];
12
+ blockedMembers: string[];
13
+ status: "closed" | "open" | "partial" | "empty";
14
+ }
15
+ export interface WaveStatusNextDispatch {
16
+ waveId: string | null;
17
+ readyToDispatch: string[];
18
+ pathConflicts: string[];
19
+ mode: "single-slice" | "wave-fanout" | "none";
20
+ }
21
+ export interface WaveStatusReport {
22
+ activeRunId: string;
23
+ currentStage: string;
24
+ tddCutoverSliceId: string | null;
25
+ tddWorktreeCutoverSliceId: string | null;
26
+ legacyContinuation: boolean;
27
+ waves: WaveStatusWaveSummary[];
28
+ nextDispatch: WaveStatusNextDispatch;
29
+ warnings: string[];
30
+ }
31
+ export interface RunWaveStatusOptions {
32
+ /**
33
+ * Override the artifacts directory; useful for fixture tests that
34
+ * place an `05-plan.md` outside `.cclaw/artifacts`. Defaults to
35
+ * `<projectRoot>/.cclaw/artifacts`.
36
+ */
37
+ artifactsDir?: string;
38
+ }
39
+ /**
40
+ * v6.14.2 — deterministic helper for the TDD controller. Reads the
41
+ * managed `<!-- parallel-exec-managed-start -->` block from
42
+ * `<artifacts-dir>/05-plan.md` plus the `wave-plans/` directory and
43
+ * reports waves + the next dispatchable members so the controller does
44
+ * NOT have to page through a 1400-line plan to find the active wave.
45
+ *
46
+ * Always exits 0 unless the plan is malformed (no managed block AND no
47
+ * wave-plans directory), in which case exit 2 with a structured error.
48
+ */
49
+ export declare function runWaveStatus(projectRoot: string, options?: RunWaveStatusOptions): Promise<WaveStatusReport>;
50
+ export declare function runWaveStatusCommand(projectRoot: string, argv: string[], io: InternalIo): Promise<number>;
51
+ export {};