cclaw-cli 6.11.0 → 6.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-linter/plan.js +60 -2
- package/dist/artifact-linter/shared.d.ts +9 -0
- package/dist/artifact-linter/spec.js +14 -0
- package/dist/artifact-linter/tdd.d.ts +29 -0
- package/dist/artifact-linter/tdd.js +398 -11
- package/dist/artifact-linter.js +10 -1
- package/dist/content/core-agents.d.ts +2 -2
- package/dist/content/core-agents.js +3 -3
- package/dist/content/examples.js +4 -4
- package/dist/content/hooks.js +48 -1
- package/dist/content/skills.d.ts +10 -0
- package/dist/content/skills.js +64 -2
- package/dist/content/stage-schema.js +13 -4
- package/dist/content/stages/plan.js +2 -1
- package/dist/content/stages/schema-types.d.ts +1 -1
- package/dist/content/stages/spec.js +2 -2
- package/dist/content/stages/tdd.js +8 -7
- package/dist/content/templates.js +10 -4
- package/dist/delegation.d.ts +73 -3
- package/dist/delegation.js +196 -6
- package/dist/flow-state.d.ts +35 -0
- package/dist/flow-state.js +7 -0
- package/dist/gate-evidence.d.ts +5 -0
- package/dist/gate-evidence.js +58 -1
- package/dist/install.js +173 -1
- package/dist/integration-fanin.d.ts +44 -0
- package/dist/integration-fanin.js +180 -0
- package/dist/internal/advance-stage/advance.js +16 -1
- package/dist/internal/advance-stage/start-flow.js +3 -1
- package/dist/internal/advance-stage.js +13 -4
- package/dist/internal/plan-split-waves.d.ts +39 -1
- package/dist/internal/plan-split-waves.js +190 -6
- package/dist/internal/set-worktree-mode.d.ts +10 -0
- package/dist/internal/set-worktree-mode.js +28 -0
- package/dist/managed-resources.js +2 -0
- package/dist/run-persistence.js +22 -0
- package/dist/worktree-manager.d.ts +50 -0
- package/dist/worktree-manager.js +136 -0
- package/dist/worktree-types.d.ts +36 -0
- package/dist/worktree-types.js +6 -0
- package/package.json +1 -1
package/dist/delegation.js
CHANGED
|
@@ -8,6 +8,8 @@ import { exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
|
|
|
8
8
|
import { HARNESS_ADAPTERS } from "./harness-adapters.js";
|
|
9
9
|
import { readFlowState } from "./runs.js";
|
|
10
10
|
import { mandatoryAgentsFor, stageSchema } from "./content/stage-schema.js";
|
|
11
|
+
import { effectiveWorktreeExecutionMode } from "./flow-state.js";
|
|
12
|
+
import { compareCanonicalUnitIds } from "./internal/plan-split-waves.js";
|
|
11
13
|
const execFileAsync = promisify(execFile);
|
|
12
14
|
const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived", "stale"]);
|
|
13
15
|
export const DELEGATION_DISPATCH_SURFACES = [
|
|
@@ -43,7 +45,8 @@ export const DELEGATION_PHASES = [
|
|
|
43
45
|
"green",
|
|
44
46
|
"refactor",
|
|
45
47
|
"refactor-deferred",
|
|
46
|
-
"doc"
|
|
48
|
+
"doc",
|
|
49
|
+
"resolve-conflict"
|
|
47
50
|
];
|
|
48
51
|
export const DELEGATION_LEDGER_SCHEMA_VERSION = 3;
|
|
49
52
|
function delegationLogPath(projectRoot) {
|
|
@@ -238,7 +241,23 @@ function isDelegationEntry(value) {
|
|
|
238
241
|
(typeof o.sliceId === "string" && o.sliceId.length > 0)) &&
|
|
239
242
|
(o.phase === undefined ||
|
|
240
243
|
(typeof o.phase === "string" &&
|
|
241
|
-
DELEGATION_PHASES.includes(o.phase)))
|
|
244
|
+
DELEGATION_PHASES.includes(o.phase))) &&
|
|
245
|
+
(o.claimToken === undefined || typeof o.claimToken === "string") &&
|
|
246
|
+
(o.ownerLaneId === undefined || typeof o.ownerLaneId === "string") &&
|
|
247
|
+
(o.leasedUntil === undefined || typeof o.leasedUntil === "string") &&
|
|
248
|
+
(o.leaseState === undefined ||
|
|
249
|
+
o.leaseState === "claimed" ||
|
|
250
|
+
o.leaseState === "expired" ||
|
|
251
|
+
o.leaseState === "released" ||
|
|
252
|
+
o.leaseState === "reclaimed") &&
|
|
253
|
+
(o.dependsOn === undefined ||
|
|
254
|
+
(Array.isArray(o.dependsOn) && o.dependsOn.every((item) => typeof item === "string"))) &&
|
|
255
|
+
(o.integrationState === undefined ||
|
|
256
|
+
o.integrationState === "pending" ||
|
|
257
|
+
o.integrationState === "applied" ||
|
|
258
|
+
o.integrationState === "conflict" ||
|
|
259
|
+
o.integrationState === "resolved" ||
|
|
260
|
+
o.integrationState === "abandoned"));
|
|
242
261
|
}
|
|
243
262
|
function isDelegationDispatchSurface(value) {
|
|
244
263
|
return typeof value === "string" && DELEGATION_DISPATCH_SURFACES.includes(value);
|
|
@@ -348,7 +367,12 @@ export async function readDelegationLedger(projectRoot) {
|
|
|
348
367
|
const NON_DELEGATION_AUDIT_EVENTS = new Set([
|
|
349
368
|
"mandatory_delegations_skipped_by_track",
|
|
350
369
|
"artifact_validation_demoted_by_track",
|
|
351
|
-
"expansion_strategist_skipped_by_track"
|
|
370
|
+
"expansion_strategist_skipped_by_track",
|
|
371
|
+
"cclaw_slice_lease_expired",
|
|
372
|
+
"cclaw_fanin_applied",
|
|
373
|
+
"cclaw_fanin_conflict",
|
|
374
|
+
"cclaw_fanin_resolved",
|
|
375
|
+
"cclaw_fanin_abandoned"
|
|
352
376
|
]);
|
|
353
377
|
function isAuditEventLine(parsed) {
|
|
354
378
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
@@ -356,13 +380,47 @@ function isAuditEventLine(parsed) {
|
|
|
356
380
|
const evt = parsed.event;
|
|
357
381
|
return typeof evt === "string" && NON_DELEGATION_AUDIT_EVENTS.has(evt);
|
|
358
382
|
}
|
|
383
|
+
const FAN_IN_AUDIT_EVENT_KINDS = new Set([
|
|
384
|
+
"cclaw_fanin_applied",
|
|
385
|
+
"cclaw_fanin_conflict",
|
|
386
|
+
"cclaw_fanin_resolved",
|
|
387
|
+
"cclaw_fanin_abandoned"
|
|
388
|
+
]);
|
|
389
|
+
function isFanInAuditRecord(value) {
|
|
390
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
391
|
+
return false;
|
|
392
|
+
const o = value;
|
|
393
|
+
const evt = o.event;
|
|
394
|
+
if (typeof evt !== "string" || !FAN_IN_AUDIT_EVENT_KINDS.has(evt)) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
return typeof o.ts === "string" && o.ts.length > 0;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Append a deterministic fan-in audit row (not a delegation lifecycle event).
|
|
401
|
+
*/
|
|
402
|
+
export async function recordCclawFanInAudit(projectRoot, params) {
|
|
403
|
+
const filePath = delegationEventsPath(projectRoot);
|
|
404
|
+
const payload = {
|
|
405
|
+
event: params.kind,
|
|
406
|
+
runId: params.runId,
|
|
407
|
+
laneId: params.laneId,
|
|
408
|
+
sliceIds: params.sliceIds,
|
|
409
|
+
integrationBranch: params.integrationBranch,
|
|
410
|
+
details: params.details,
|
|
411
|
+
ts: new Date().toISOString()
|
|
412
|
+
};
|
|
413
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
414
|
+
await fs.appendFile(filePath, `${JSON.stringify(payload)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
415
|
+
}
|
|
359
416
|
export async function readDelegationEvents(projectRoot) {
|
|
360
417
|
const filePath = delegationEventsPath(projectRoot);
|
|
361
418
|
if (!(await exists(filePath))) {
|
|
362
|
-
return { events: [], corruptLines: [] };
|
|
419
|
+
return { events: [], corruptLines: [], fanInAudits: [] };
|
|
363
420
|
}
|
|
364
421
|
const events = [];
|
|
365
422
|
const corruptLines = [];
|
|
423
|
+
const fanInAudits = [];
|
|
366
424
|
const text = await fs.readFile(filePath, "utf8").catch(() => "");
|
|
367
425
|
const lines = text.split(/\r?\n/gu);
|
|
368
426
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -374,6 +432,9 @@ export async function readDelegationEvents(projectRoot) {
|
|
|
374
432
|
if (isDelegationEvent(parsed)) {
|
|
375
433
|
events.push(parsed);
|
|
376
434
|
}
|
|
435
|
+
else if (isFanInAuditRecord(parsed)) {
|
|
436
|
+
fanInAudits.push(parsed);
|
|
437
|
+
}
|
|
377
438
|
else if (isAuditEventLine(parsed)) {
|
|
378
439
|
// Wave 24 audit-only row (e.g. mandatory_delegations_skipped_by_track).
|
|
379
440
|
// Not a delegation lifecycle event but valid audit content.
|
|
@@ -387,7 +448,7 @@ export async function readDelegationEvents(projectRoot) {
|
|
|
387
448
|
corruptLines.push(index + 1);
|
|
388
449
|
}
|
|
389
450
|
}
|
|
390
|
-
return { events, corruptLines };
|
|
451
|
+
return { events, corruptLines, fanInAudits };
|
|
391
452
|
}
|
|
392
453
|
async function appendDelegationEvent(projectRoot, event) {
|
|
393
454
|
const filePath = delegationEventsPath(projectRoot);
|
|
@@ -605,12 +666,70 @@ export class DispatchCapError extends Error {
|
|
|
605
666
|
this.pair = params.pair;
|
|
606
667
|
}
|
|
607
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* v6.13.0 — claim / lease contract violation for worktree-first TDD rows.
|
|
671
|
+
*/
|
|
672
|
+
export class DispatchClaimInvalidError extends Error {
|
|
673
|
+
constructor(message) {
|
|
674
|
+
super(message);
|
|
675
|
+
this.name = "DispatchClaimInvalidError";
|
|
676
|
+
}
|
|
677
|
+
}
|
|
608
678
|
/**
|
|
609
679
|
* v6.10.0 (P2) — default cap on active `slice-implementer` spans in a
|
|
610
680
|
* single TDD run. Aligned with evanflow's parallel cap. Override via
|
|
611
681
|
* `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<int>` (validated `>=1`).
|
|
612
682
|
*/
|
|
613
683
|
export const MAX_PARALLEL_SLICE_IMPLEMENTERS = 5;
|
|
684
|
+
/**
|
|
685
|
+
* Return up to `cap` slice units whose dependsOn are satisfied, avoiding
|
|
686
|
+
* `claimedPaths` intersections with already-selected units and active holders.
|
|
687
|
+
*/
|
|
688
|
+
export function selectReadySlices(units, opts) {
|
|
689
|
+
const pool = opts.legacyContinuation ? units.filter((u) => u.parallelizable) : units;
|
|
690
|
+
const ordered = [...pool].sort((a, b) => compareCanonicalUnitIds(a.unitId, b.unitId));
|
|
691
|
+
const selected = [];
|
|
692
|
+
const blockedPaths = new Set();
|
|
693
|
+
for (const holder of opts.activePathHolders) {
|
|
694
|
+
for (const p of holder.paths) {
|
|
695
|
+
blockedPaths.add(p);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
for (const u of ordered) {
|
|
699
|
+
if (opts.completedUnitIds.has(u.unitId))
|
|
700
|
+
continue;
|
|
701
|
+
if (!u.dependsOn.every((d) => opts.completedUnitIds.has(d)))
|
|
702
|
+
continue;
|
|
703
|
+
let clash = false;
|
|
704
|
+
for (const p of u.claimedPaths) {
|
|
705
|
+
if (blockedPaths.has(p)) {
|
|
706
|
+
clash = true;
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (clash)
|
|
711
|
+
continue;
|
|
712
|
+
for (const v of selected) {
|
|
713
|
+
for (const pu of u.claimedPaths) {
|
|
714
|
+
if (v.claimedPaths.includes(pu)) {
|
|
715
|
+
clash = true;
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (clash)
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
if (clash)
|
|
723
|
+
continue;
|
|
724
|
+
selected.push(u);
|
|
725
|
+
for (const p of u.claimedPaths) {
|
|
726
|
+
blockedPaths.add(p);
|
|
727
|
+
}
|
|
728
|
+
if (selected.length >= opts.cap)
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
return selected;
|
|
732
|
+
}
|
|
614
733
|
function readMaxParallelOverrideFromEnv() {
|
|
615
734
|
const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS;
|
|
616
735
|
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
@@ -740,8 +859,45 @@ async function writeSubagentTracker(projectRoot, entries) {
|
|
|
740
859
|
}));
|
|
741
860
|
await writeFileSafe(subagentsStatePath(projectRoot), `${JSON.stringify({ active, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
742
861
|
}
|
|
862
|
+
function latestClaimTokenForSpan(entries, spanId) {
|
|
863
|
+
if (!spanId)
|
|
864
|
+
return null;
|
|
865
|
+
let latest = null;
|
|
866
|
+
for (const e of entries) {
|
|
867
|
+
if (e.spanId !== spanId)
|
|
868
|
+
continue;
|
|
869
|
+
if (typeof e.claimToken === "string" && e.claimToken.trim().length > 0) {
|
|
870
|
+
latest = e.claimToken.trim();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return latest;
|
|
874
|
+
}
|
|
875
|
+
function assertSliceClaimInvariant(flow, stamped, prior) {
|
|
876
|
+
if (stamped.stage !== "tdd" || stamped.agent !== "slice-implementer")
|
|
877
|
+
return;
|
|
878
|
+
if (effectiveWorktreeExecutionMode(flow) !== "worktree-first")
|
|
879
|
+
return;
|
|
880
|
+
if (stamped.status === "scheduled" && typeof stamped.sliceId === "string" && stamped.sliceId.length > 0) {
|
|
881
|
+
const tok = stamped.claimToken?.trim() ?? "";
|
|
882
|
+
if (tok.length === 0) {
|
|
883
|
+
throw new DispatchClaimInvalidError("dispatch_claim_invalid — worktree-first requires --claim-token when scheduling slice-implementer with --slice");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (!TERMINAL_DELEGATION_STATUSES.has(stamped.status))
|
|
887
|
+
return;
|
|
888
|
+
if (stamped.status === "waived" || stamped.status === "stale")
|
|
889
|
+
return;
|
|
890
|
+
const expected = latestClaimTokenForSpan(prior, stamped.spanId);
|
|
891
|
+
if (!expected)
|
|
892
|
+
return;
|
|
893
|
+
const got = stamped.claimToken?.trim() ?? "";
|
|
894
|
+
if (got !== expected) {
|
|
895
|
+
throw new DispatchClaimInvalidError("dispatch_claim_invalid — claimToken must match the scheduled claim for this span");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
743
898
|
export async function appendDelegation(projectRoot, entry) {
|
|
744
|
-
const
|
|
899
|
+
const flowState = await readFlowState(projectRoot);
|
|
900
|
+
const { activeRunId } = flowState;
|
|
745
901
|
await withDirectoryLock(delegationLockPath(projectRoot), async () => {
|
|
746
902
|
const filePath = delegationLogPath(projectRoot);
|
|
747
903
|
const prior = await readDelegationLedger(projectRoot);
|
|
@@ -802,6 +958,7 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
802
958
|
if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
|
|
803
959
|
return;
|
|
804
960
|
}
|
|
961
|
+
assertSliceClaimInvariant(flowState, stamped, prior.entries);
|
|
805
962
|
validateMonotonicTimestamps(stamped, prior.entries);
|
|
806
963
|
if (stamped.status === "scheduled") {
|
|
807
964
|
// v6.10.0 (P1+P2): for slice-implementer rows with declared
|
|
@@ -839,6 +996,39 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
839
996
|
await writeSubagentTracker(projectRoot, ledger.entries);
|
|
840
997
|
});
|
|
841
998
|
}
|
|
999
|
+
/**
|
|
1000
|
+
* Scan delegation events for expired `leasedUntil` timestamps and append
|
|
1001
|
+
* best-effort `cclaw_slice_lease_expired` audit rows (one per span/slice key).
|
|
1002
|
+
*/
|
|
1003
|
+
export async function reclaimExpiredDelegationClaims(projectRoot, now = new Date()) {
|
|
1004
|
+
const { events } = await readDelegationEvents(projectRoot);
|
|
1005
|
+
const seen = new Set();
|
|
1006
|
+
let count = 0;
|
|
1007
|
+
const ts = now.toISOString();
|
|
1008
|
+
const filePath = delegationEventsPath(projectRoot);
|
|
1009
|
+
const cutoff = Date.parse(ts);
|
|
1010
|
+
for (const e of events) {
|
|
1011
|
+
if (e.leaseState !== "claimed")
|
|
1012
|
+
continue;
|
|
1013
|
+
if (typeof e.leasedUntil !== "string" || e.leasedUntil.length === 0)
|
|
1014
|
+
continue;
|
|
1015
|
+
if (Date.parse(e.leasedUntil) > cutoff)
|
|
1016
|
+
continue;
|
|
1017
|
+
const key = `${e.spanId ?? ""}|${e.sliceId ?? ""}`;
|
|
1018
|
+
if (seen.has(key))
|
|
1019
|
+
continue;
|
|
1020
|
+
seen.add(key);
|
|
1021
|
+
await fs.appendFile(filePath, `${JSON.stringify({
|
|
1022
|
+
event: "cclaw_slice_lease_expired",
|
|
1023
|
+
spanId: e.spanId,
|
|
1024
|
+
sliceId: e.sliceId,
|
|
1025
|
+
leasedUntil: e.leasedUntil,
|
|
1026
|
+
eventTs: ts
|
|
1027
|
+
})}\n`, { encoding: "utf8", mode: 0o600 });
|
|
1028
|
+
count += 1;
|
|
1029
|
+
}
|
|
1030
|
+
return count;
|
|
1031
|
+
}
|
|
842
1032
|
/**
|
|
843
1033
|
* Aggregate the fulfillment mode cclaw expects for the active harness set.
|
|
844
1034
|
* Priority native > generic-dispatch > role-switch > waiver — the best
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -121,7 +121,42 @@ export interface FlowState {
|
|
|
121
121
|
completedStageMeta?: Partial<Record<FlowStage, {
|
|
122
122
|
completedAt: string;
|
|
123
123
|
}>>;
|
|
124
|
+
/**
|
|
125
|
+
* v6.12.0 — TDD migration cutover marker. When `cclaw-cli sync` detects an
|
|
126
|
+
* existing `06-tdd.md` with legacy per-slice tables but no auto-render
|
|
127
|
+
* markers, it inserts the markers and records the highest legacy slice id
|
|
128
|
+
* here (e.g. `"S-10"`). The TDD linter uses this value to:
|
|
129
|
+
* - exempt slices `<= cutoverSliceId` from new mandatory rules (legacy
|
|
130
|
+
* slices keep their markdown tables);
|
|
131
|
+
* - emit `tdd_legacy_section_writes_after_cutover` advisory when a slice
|
|
132
|
+
* id `> cutoverSliceId` appears in legacy per-slice sections of
|
|
133
|
+
* `06-tdd.md` (post-cutover prose belongs in `tdd-slices/S-<id>.md`).
|
|
134
|
+
*
|
|
135
|
+
* Optional + best-effort: omitted on fresh installs and on legacy files
|
|
136
|
+
* sync hasn't visited yet.
|
|
137
|
+
*/
|
|
138
|
+
tddCutoverSliceId?: string;
|
|
139
|
+
/**
|
|
140
|
+
* v6.13.0 — when `worktree-first` (default for newly initialized runs),
|
|
141
|
+
* slice-implementer work happens in isolated git worktrees with explicit
|
|
142
|
+
* claims/leases and deterministic fan-in integration.
|
|
143
|
+
*
|
|
144
|
+
* Omitted on legacy `flow-state.json` files: treated as `single-tree` via
|
|
145
|
+
* `effectiveWorktreeExecutionMode`.
|
|
146
|
+
*/
|
|
147
|
+
worktreeExecutionMode?: "single-tree" | "worktree-first";
|
|
148
|
+
/**
|
|
149
|
+
* v6.13.0 — set by `cclaw-cli sync` when the plan predates parallel-metadata
|
|
150
|
+
* fields. Relaxes some plan linters for existing implementation units and
|
|
151
|
+
* defaults scheduler parallelism to opt-in only for those units.
|
|
152
|
+
*/
|
|
153
|
+
legacyContinuation?: boolean;
|
|
124
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Effective worktree mode: legacy state files without the field keep
|
|
157
|
+
* single-tree scheduling to avoid breaking existing runs on upgrade.
|
|
158
|
+
*/
|
|
159
|
+
export declare function effectiveWorktreeExecutionMode(state: FlowState): "single-tree" | "worktree-first";
|
|
125
160
|
export interface StageInteractionHint {
|
|
126
161
|
skipQuestions?: boolean;
|
|
127
162
|
sourceStage?: FlowStage;
|
package/dist/flow-state.js
CHANGED
|
@@ -44,6 +44,13 @@ export function createInitialCloseoutState() {
|
|
|
44
44
|
compoundPromoted: 0
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Effective worktree mode: legacy state files without the field keep
|
|
49
|
+
* single-tree scheduling to avoid breaking existing runs on upgrade.
|
|
50
|
+
*/
|
|
51
|
+
export function effectiveWorktreeExecutionMode(state) {
|
|
52
|
+
return state.worktreeExecutionMode ?? "single-tree";
|
|
53
|
+
}
|
|
47
54
|
export function isFlowTrack(value) {
|
|
48
55
|
return typeof value === "string" && FLOW_TRACKS.includes(value);
|
|
49
56
|
}
|
package/dist/gate-evidence.d.ts
CHANGED
|
@@ -66,6 +66,11 @@ export interface VerifyCurrentStageGateEvidenceOptions {
|
|
|
66
66
|
extraStageFlags?: string[];
|
|
67
67
|
}
|
|
68
68
|
export declare function verifyCurrentStageGateEvidence(projectRoot: string, flowState: FlowState, options?: VerifyCurrentStageGateEvidenceOptions): Promise<GateEvidenceCheckResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Validates that every lane-backed slice which reached REFACTOR has a matching
|
|
71
|
+
* `cclaw_fanin_applied` audit row for the active run (v6.13.0 worktree-first).
|
|
72
|
+
*/
|
|
73
|
+
export declare function verifyTddWorktreeFanInClosure(projectRoot: string, flowState: FlowState): Promise<string[]>;
|
|
69
74
|
export declare function verifyCompletedStagesGateClosure(flowState: FlowState): CompletedStagesClosureResult;
|
|
70
75
|
export interface GateReconciliationResult {
|
|
71
76
|
stage: FlowStage;
|
package/dist/gate-evidence.js
CHANGED
|
@@ -5,7 +5,8 @@ import { ELICITATION_STAGES, evaluateQaLogFloor } from "./artifact-linter/shared
|
|
|
5
5
|
import { resolveArtifactPath } from "./artifact-paths.js";
|
|
6
6
|
import { RUNTIME_ROOT } from "./constants.js";
|
|
7
7
|
import { stageSchema } from "./content/stage-schema.js";
|
|
8
|
-
import { readDelegationLedger } from "./delegation.js";
|
|
8
|
+
import { readDelegationLedger, readDelegationEvents } from "./delegation.js";
|
|
9
|
+
import { effectiveWorktreeExecutionMode } from "./flow-state.js";
|
|
9
10
|
import { exists } from "./fs-utils.js";
|
|
10
11
|
import { computeEarlyLoopStatus, isEarlyLoopStage, normalizeEarlyLoopMaxIterations } from "./early-loop.js";
|
|
11
12
|
import { detectPublicApiChanges } from "./internal/detect-public-api-changes.js";
|
|
@@ -487,6 +488,62 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState, opt
|
|
|
487
488
|
qaLogFloor
|
|
488
489
|
};
|
|
489
490
|
}
|
|
491
|
+
function sliceHasTerminalRefactor(events, runId, sliceId) {
|
|
492
|
+
for (const e of events) {
|
|
493
|
+
if (e.runId !== runId || e.stage !== "tdd" || e.sliceId !== sliceId)
|
|
494
|
+
continue;
|
|
495
|
+
if (e.agent !== "slice-implementer" || e.status !== "completed")
|
|
496
|
+
continue;
|
|
497
|
+
if (e.phase === "refactor" || e.phase === "refactor-deferred")
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
function closedSlicesWithLane(events, runId) {
|
|
503
|
+
const withLane = new Set();
|
|
504
|
+
for (const e of events) {
|
|
505
|
+
if (e.runId !== runId || e.stage !== "tdd")
|
|
506
|
+
continue;
|
|
507
|
+
if (e.agent !== "slice-implementer" || e.status !== "completed" || e.phase !== "green")
|
|
508
|
+
continue;
|
|
509
|
+
if (!e.ownerLaneId?.trim() || !e.sliceId)
|
|
510
|
+
continue;
|
|
511
|
+
withLane.add(e.sliceId);
|
|
512
|
+
}
|
|
513
|
+
const out = new Set();
|
|
514
|
+
for (const sid of withLane) {
|
|
515
|
+
if (sliceHasTerminalRefactor(events, runId, sid))
|
|
516
|
+
out.add(sid);
|
|
517
|
+
}
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Validates that every lane-backed slice which reached REFACTOR has a matching
|
|
522
|
+
* `cclaw_fanin_applied` audit row for the active run (v6.13.0 worktree-first).
|
|
523
|
+
*/
|
|
524
|
+
export async function verifyTddWorktreeFanInClosure(projectRoot, flowState) {
|
|
525
|
+
if (effectiveWorktreeExecutionMode(flowState) !== "worktree-first")
|
|
526
|
+
return [];
|
|
527
|
+
const runId = flowState.activeRunId;
|
|
528
|
+
const { events, fanInAudits } = await readDelegationEvents(projectRoot);
|
|
529
|
+
const needing = closedSlicesWithLane(events, runId);
|
|
530
|
+
if (needing.size === 0)
|
|
531
|
+
return [];
|
|
532
|
+
const applied = new Set();
|
|
533
|
+
for (const a of fanInAudits) {
|
|
534
|
+
if (a.runId !== runId || a.event !== "cclaw_fanin_applied")
|
|
535
|
+
continue;
|
|
536
|
+
for (const s of a.sliceIds ?? [])
|
|
537
|
+
applied.add(s);
|
|
538
|
+
}
|
|
539
|
+
const issues = [];
|
|
540
|
+
for (const sid of needing) {
|
|
541
|
+
if (!applied.has(sid)) {
|
|
542
|
+
issues.push(`tdd worktree fan-in closure: slice ${sid} completed on a lane but cclaw_fanin_applied is missing for run ${runId}.`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return issues;
|
|
546
|
+
}
|
|
490
547
|
export function verifyCompletedStagesGateClosure(flowState) {
|
|
491
548
|
const issues = [];
|
|
492
549
|
const openStages = [];
|
package/dist/install.js
CHANGED
|
@@ -33,7 +33,8 @@ import { HARNESS_ADAPTERS, harnessShimFileNames, harnessShimSkillNames, syncHarn
|
|
|
33
33
|
import { validateHookDocument } from "./hook-schema.js";
|
|
34
34
|
import { detectHarnesses } from "./init-detect.js";
|
|
35
35
|
import { classifyCodexHooksFlag, codexConfigPath, readCodexConfig } from "./codex-feature-flag.js";
|
|
36
|
-
import { CorruptFlowStateError, ensureRunSystem } from "./runs.js";
|
|
36
|
+
import { CorruptFlowStateError, ensureRunSystem, readFlowState, writeFlowState } from "./runs.js";
|
|
37
|
+
import { PLAN_SPLIT_DEFAULT_WAVE_SIZE, buildParallelExecutionPlanSection, planArtifactLacksV613ParallelMetadata, upsertParallelExecutionPlanSection } from "./internal/plan-split-waves.js";
|
|
37
38
|
import { FLOW_STAGES } from "./types.js";
|
|
38
39
|
const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
|
|
39
40
|
const CURSOR_RULE_REL_PATH = ".cursor/rules/cclaw-workflow.mdc";
|
|
@@ -835,6 +836,173 @@ async function writeState(projectRoot, config, forceReset = false) {
|
|
|
835
836
|
const state = createInitialFlowState({ track: "standard" });
|
|
836
837
|
await writeFileSafe(statePath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
837
838
|
}
|
|
839
|
+
/**
|
|
840
|
+
* v6.12.0 — TDD auto-cutover sync. When sync detects a legacy `06-tdd.md`
|
|
841
|
+
* (no auto-render markers) carrying observable slice activity (e.g. `S-N`
|
|
842
|
+
* referenced ≥3 times in slice-section bodies), insert the v6.11.0 marker
|
|
843
|
+
* skeleton + a one-line cutover banner and stamp the highest legacy slice
|
|
844
|
+
* id into `flow-state.json::tddCutoverSliceId`. Idempotent: re-running sync
|
|
845
|
+
* is byte-stable once markers are present.
|
|
846
|
+
*
|
|
847
|
+
* Design notes:
|
|
848
|
+
* - Best-effort: read failures, missing flow-state, or unparseable JSON
|
|
849
|
+
* all short-circuit silently. We never throw inside sync for migration
|
|
850
|
+
* bookkeeping.
|
|
851
|
+
* - We use `writeFlowState({ allowReset: true })` so we don't trip
|
|
852
|
+
* `validateFlowTransition` (the only field we mutate is the new
|
|
853
|
+
* additive `tddCutoverSliceId`; transition rules don't apply).
|
|
854
|
+
* - The banner mirrors the language in the `## Per-Slice Ritual`
|
|
855
|
+
* skill section so a reader of `06-tdd.md` who hasn't seen v6.12.0
|
|
856
|
+
* understands the contract change.
|
|
857
|
+
*/
|
|
858
|
+
async function applyTddCutoverIfNeeded(projectRoot) {
|
|
859
|
+
const tddArtifactPath = runtimePath(projectRoot, "artifacts", "06-tdd.md");
|
|
860
|
+
let existing;
|
|
861
|
+
try {
|
|
862
|
+
existing = await fs.readFile(tddArtifactPath, "utf8");
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (existing.includes("<!-- auto-start: tdd-slice-summary -->")) {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const sliceMatches = [...existing.matchAll(/\bS-(\d+)\b/gu)];
|
|
871
|
+
if (sliceMatches.length < 3) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
let maxSliceNum = 0;
|
|
875
|
+
for (const match of sliceMatches) {
|
|
876
|
+
const n = Number.parseInt(match[1], 10);
|
|
877
|
+
if (Number.isFinite(n) && n > maxSliceNum) {
|
|
878
|
+
maxSliceNum = n;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (maxSliceNum <= 0) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const cutoverSliceId = `S-${maxSliceNum}`;
|
|
885
|
+
const banner = [
|
|
886
|
+
`<!-- v6.12.0 cutover: slices S-1..${cutoverSliceId} use legacy per-slice tables.`,
|
|
887
|
+
` New slices (S-${maxSliceNum + 1}+) use phase events + tdd-slices/<id>.md.`,
|
|
888
|
+
" Controller MUST NOT add new rows to legacy sections. -->"
|
|
889
|
+
].join("\n");
|
|
890
|
+
const slicesIndexBlock = [
|
|
891
|
+
"<!-- auto-start: slices-index -->",
|
|
892
|
+
"## Slices Index",
|
|
893
|
+
"",
|
|
894
|
+
"_Auto-rendered from `tdd-slices/S-*.md` once slice-documenter or controller writes per-slice files. Do not edit by hand._",
|
|
895
|
+
"<!-- auto-end: slices-index -->"
|
|
896
|
+
].join("\n");
|
|
897
|
+
const summaryBlock = [
|
|
898
|
+
"<!-- auto-start: tdd-slice-summary -->",
|
|
899
|
+
"<!-- auto-end: tdd-slice-summary -->"
|
|
900
|
+
].join("\n");
|
|
901
|
+
let nextRaw;
|
|
902
|
+
if (existing.startsWith("---\n")) {
|
|
903
|
+
const fmEnd = existing.indexOf("\n---", 4);
|
|
904
|
+
if (fmEnd >= 0) {
|
|
905
|
+
const fmClose = fmEnd + 4;
|
|
906
|
+
const head = existing.slice(0, fmClose);
|
|
907
|
+
const tail = existing.slice(fmClose);
|
|
908
|
+
nextRaw = `${head}\n\n${banner}\n\n${slicesIndexBlock}\n\n${summaryBlock}\n${tail}`;
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
nextRaw = `${banner}\n\n${slicesIndexBlock}\n\n${summaryBlock}\n\n${existing}`;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
nextRaw = `${banner}\n\n${slicesIndexBlock}\n\n${summaryBlock}\n\n${existing}`;
|
|
916
|
+
}
|
|
917
|
+
await writeFileSafe(tddArtifactPath, nextRaw);
|
|
918
|
+
const slicesDir = runtimePath(projectRoot, "artifacts", "tdd-slices");
|
|
919
|
+
await ensureDir(slicesDir);
|
|
920
|
+
const flowStatePath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
921
|
+
let flowStateRaw;
|
|
922
|
+
try {
|
|
923
|
+
flowStateRaw = await fs.readFile(flowStatePath, "utf8");
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
let parsed;
|
|
929
|
+
try {
|
|
930
|
+
parsed = JSON.parse(flowStateRaw);
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const obj = parsed;
|
|
939
|
+
if (typeof obj.tddCutoverSliceId === "string" && obj.tddCutoverSliceId.length > 0) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
obj.tddCutoverSliceId = cutoverSliceId;
|
|
943
|
+
await writeFileSafe(flowStatePath, `${JSON.stringify(obj, null, 2)}\n`, { mode: 0o600 });
|
|
944
|
+
}
|
|
945
|
+
const V613_LEGACY_PLAN_BANNER = "<!-- legacy-continuation: predates v6.13 parallel metadata. New units MAY add dependsOn/claimedPaths/parallelizable; existing units treated as best-effort serial. -->";
|
|
946
|
+
/**
|
|
947
|
+
* v6.13.0 — when `05-plan.md` lacks parallel-metadata bullets on any
|
|
948
|
+
* implementation unit, stamp `flow-state.json::legacyContinuation`, insert
|
|
949
|
+
* a banner + managed Parallel Execution Plan stub, and keep behavior idempotent.
|
|
950
|
+
*/
|
|
951
|
+
async function applyPlanLegacyContinuationIfNeeded(projectRoot) {
|
|
952
|
+
const planArtifactPath = runtimePath(projectRoot, "artifacts", "05-plan.md");
|
|
953
|
+
let existingPlan;
|
|
954
|
+
try {
|
|
955
|
+
existingPlan = await fs.readFile(planArtifactPath, "utf8");
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (!planArtifactLacksV613ParallelMetadata(existingPlan)) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
let nextPlan = existingPlan;
|
|
964
|
+
if (!nextPlan.includes("legacy-continuation: predates v6.13")) {
|
|
965
|
+
if (nextPlan.startsWith("---\n")) {
|
|
966
|
+
const fmEnd = nextPlan.indexOf("\n---", 4);
|
|
967
|
+
if (fmEnd >= 0) {
|
|
968
|
+
const fmClose = fmEnd + 4;
|
|
969
|
+
const head = nextPlan.slice(0, fmClose);
|
|
970
|
+
const tail = nextPlan.slice(fmClose);
|
|
971
|
+
nextPlan = `${head}\n\n${V613_LEGACY_PLAN_BANNER}\n${tail}`;
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
nextPlan = `${V613_LEGACY_PLAN_BANNER}\n\n${nextPlan}`;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
nextPlan = `${V613_LEGACY_PLAN_BANNER}\n\n${nextPlan}`;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const parallelStub = buildParallelExecutionPlanSection([], PLAN_SPLIT_DEFAULT_WAVE_SIZE);
|
|
982
|
+
if (!nextPlan.includes("<!-- parallel-exec-managed-start -->")) {
|
|
983
|
+
nextPlan = upsertParallelExecutionPlanSection(nextPlan, parallelStub);
|
|
984
|
+
}
|
|
985
|
+
if (nextPlan !== existingPlan) {
|
|
986
|
+
await writeFileSafe(planArtifactPath, nextPlan);
|
|
987
|
+
}
|
|
988
|
+
const flowStatePath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
989
|
+
if (!(await exists(flowStatePath))) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
try {
|
|
993
|
+
const state = await readFlowState(projectRoot);
|
|
994
|
+
if (state.legacyContinuation === true) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
await writeFlowState(projectRoot, { ...state, legacyContinuation: true }, {
|
|
998
|
+
allowReset: true,
|
|
999
|
+
writerSubsystem: "plan-legacy-continuation-sync"
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
// Best-effort: corrupt/missing state is handled elsewhere on sync.
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
838
1006
|
async function cleanLegacyArtifacts(projectRoot) {
|
|
839
1007
|
for (const legacyFolder of DEPRECATED_UTILITY_SKILL_FOLDERS) {
|
|
840
1008
|
await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
|
|
@@ -985,6 +1153,10 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
|
|
|
985
1153
|
writeRulebook(projectRoot)
|
|
986
1154
|
]);
|
|
987
1155
|
await writeState(projectRoot, config, forceStateReset);
|
|
1156
|
+
if (operation === "sync" || operation === "upgrade") {
|
|
1157
|
+
await applyTddCutoverIfNeeded(projectRoot);
|
|
1158
|
+
await applyPlanLegacyContinuationIfNeeded(projectRoot);
|
|
1159
|
+
}
|
|
988
1160
|
try {
|
|
989
1161
|
await ensureRunSystem(projectRoot, { createIfMissing: false });
|
|
990
1162
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type FlowState } from "./flow-state.js";
|
|
2
|
+
import type { WorktreeLaneId } from "./worktree-types.js";
|
|
3
|
+
export type FanInEventKind = "applied" | "conflict" | "resolved" | "abandoned";
|
|
4
|
+
export interface FanInLaneOptions {
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
/** Lane directory under `.cclaw/worktrees/<laneId>`. */
|
|
7
|
+
laneId: WorktreeLaneId;
|
|
8
|
+
/** Integration branch to receive the patch (must already exist locally). */
|
|
9
|
+
integrationBranch: string;
|
|
10
|
+
/**
|
|
11
|
+
* Baseline ref for `git diff` in the lane (fork point vs integration).
|
|
12
|
+
* When omitted, computed as `git merge-base <integration> HEAD` in the lane.
|
|
13
|
+
*/
|
|
14
|
+
baseRef?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface FanInLaneResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
event: FanInEventKind;
|
|
19
|
+
details: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build a unified diff from `baseRef..HEAD` in the lane worktree and apply it
|
|
23
|
+
* to the integration branch in the main repo using three-way merge.
|
|
24
|
+
* On conflict, the integration branch working tree is reset and, when possible,
|
|
25
|
+
* git HEAD is restored to the branch that was checked out before fan-in.
|
|
26
|
+
*/
|
|
27
|
+
export declare function fanInLane(options: FanInLaneOptions): Promise<FanInLaneResult>;
|
|
28
|
+
export interface ResolverDispatchHint {
|
|
29
|
+
sliceId: string;
|
|
30
|
+
command: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns the canonical CLI hint for resolving fan-in conflicts for a slice.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildResolveConflictDispatchHint(sliceId: string): ResolverDispatchHint;
|
|
36
|
+
/**
|
|
37
|
+
* Merge every lane that recorded a completed GREEN `ownerLaneId` for the
|
|
38
|
+
* active run, then emit `cclaw_fanin_*` audit rows. Does nothing in
|
|
39
|
+
* `single-tree` mode or when git is unavailable.
|
|
40
|
+
*/
|
|
41
|
+
export declare function runTddDeterministicFanInBeforeAdvance(projectRoot: string, flowState: FlowState): Promise<{
|
|
42
|
+
ok: boolean;
|
|
43
|
+
issues: string[];
|
|
44
|
+
}>;
|