cclaw-cli 6.12.0 → 6.13.1
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 +19 -6
- package/dist/artifact-linter/tdd.js +225 -47
- package/dist/artifact-linter.js +10 -1
- package/dist/content/hooks.js +88 -1
- package/dist/content/skills.js +17 -10
- package/dist/content/stages/plan.js +2 -1
- package/dist/content/stages/spec.js +2 -2
- package/dist/content/stages/tdd.js +7 -6
- package/dist/content/start-command.js +6 -3
- package/dist/content/templates.js +10 -4
- package/dist/delegation.d.ts +82 -3
- package/dist/delegation.js +244 -6
- package/dist/flow-state.d.ts +20 -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 +90 -2
- 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 +85 -1
- package/dist/internal/plan-split-waves.js +409 -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 +9 -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, mergeParallelWaveDefinitions, parseImplementationUnitParallelFields, parseImplementationUnits, parseParallelExecutionPlanWaves, parseWavePlanDirectory } 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,118 @@ 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
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* v6.13.1 — build scheduler rows from merged parallel wave definitions + plan units.
|
|
735
|
+
*/
|
|
736
|
+
export function readySliceUnitsFromMergedWaves(mergedWaves, planMarkdown, options) {
|
|
737
|
+
const units = parseImplementationUnits(planMarkdown);
|
|
738
|
+
const metaByUnit = new Map(units.map((u) => {
|
|
739
|
+
const m = parseImplementationUnitParallelFields(u, options);
|
|
740
|
+
return [m.unitId, m];
|
|
741
|
+
}));
|
|
742
|
+
const sliceSet = new Set();
|
|
743
|
+
for (const w of mergedWaves) {
|
|
744
|
+
for (const m of w.members) {
|
|
745
|
+
sliceSet.add(m.sliceId);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const out = [];
|
|
749
|
+
for (const sliceId of [...sliceSet].sort((a, b) => a.localeCompare(b))) {
|
|
750
|
+
const member = mergedWaves.flatMap((w) => w.members).find((x) => x.sliceId === sliceId);
|
|
751
|
+
if (!member)
|
|
752
|
+
continue;
|
|
753
|
+
const meta = metaByUnit.get(member.unitId);
|
|
754
|
+
if (!meta) {
|
|
755
|
+
out.push({
|
|
756
|
+
unitId: member.unitId,
|
|
757
|
+
sliceId,
|
|
758
|
+
dependsOn: [],
|
|
759
|
+
claimedPaths: [],
|
|
760
|
+
parallelizable: true
|
|
761
|
+
});
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
out.push({
|
|
765
|
+
unitId: meta.unitId,
|
|
766
|
+
sliceId,
|
|
767
|
+
dependsOn: meta.dependsOn,
|
|
768
|
+
claimedPaths: meta.claimedPaths,
|
|
769
|
+
parallelizable: meta.parallelizable
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* v6.13.1 — load merged wave plan (Parallel Execution Plan block + wave-plans/) and map to `ReadySliceUnit[]`.
|
|
776
|
+
*/
|
|
777
|
+
export async function loadTddReadySlicePool(planMarkdown, artifactsDir, options) {
|
|
778
|
+
const merged = mergeParallelWaveDefinitions(parseParallelExecutionPlanWaves(planMarkdown), await parseWavePlanDirectory(artifactsDir));
|
|
779
|
+
return readySliceUnitsFromMergedWaves(merged, planMarkdown, options);
|
|
780
|
+
}
|
|
614
781
|
function readMaxParallelOverrideFromEnv() {
|
|
615
782
|
const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS;
|
|
616
783
|
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
@@ -740,8 +907,45 @@ async function writeSubagentTracker(projectRoot, entries) {
|
|
|
740
907
|
}));
|
|
741
908
|
await writeFileSafe(subagentsStatePath(projectRoot), `${JSON.stringify({ active, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
742
909
|
}
|
|
910
|
+
function latestClaimTokenForSpan(entries, spanId) {
|
|
911
|
+
if (!spanId)
|
|
912
|
+
return null;
|
|
913
|
+
let latest = null;
|
|
914
|
+
for (const e of entries) {
|
|
915
|
+
if (e.spanId !== spanId)
|
|
916
|
+
continue;
|
|
917
|
+
if (typeof e.claimToken === "string" && e.claimToken.trim().length > 0) {
|
|
918
|
+
latest = e.claimToken.trim();
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return latest;
|
|
922
|
+
}
|
|
923
|
+
function assertSliceClaimInvariant(flow, stamped, prior) {
|
|
924
|
+
if (stamped.stage !== "tdd" || stamped.agent !== "slice-implementer")
|
|
925
|
+
return;
|
|
926
|
+
if (effectiveWorktreeExecutionMode(flow) !== "worktree-first")
|
|
927
|
+
return;
|
|
928
|
+
if (stamped.status === "scheduled" && typeof stamped.sliceId === "string" && stamped.sliceId.length > 0) {
|
|
929
|
+
const tok = stamped.claimToken?.trim() ?? "";
|
|
930
|
+
if (tok.length === 0) {
|
|
931
|
+
throw new DispatchClaimInvalidError("dispatch_claim_invalid — worktree-first requires --claim-token when scheduling slice-implementer with --slice");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (!TERMINAL_DELEGATION_STATUSES.has(stamped.status))
|
|
935
|
+
return;
|
|
936
|
+
if (stamped.status === "waived" || stamped.status === "stale")
|
|
937
|
+
return;
|
|
938
|
+
const expected = latestClaimTokenForSpan(prior, stamped.spanId);
|
|
939
|
+
if (!expected)
|
|
940
|
+
return;
|
|
941
|
+
const got = stamped.claimToken?.trim() ?? "";
|
|
942
|
+
if (got !== expected) {
|
|
943
|
+
throw new DispatchClaimInvalidError("dispatch_claim_invalid — claimToken must match the scheduled claim for this span");
|
|
944
|
+
}
|
|
945
|
+
}
|
|
743
946
|
export async function appendDelegation(projectRoot, entry) {
|
|
744
|
-
const
|
|
947
|
+
const flowState = await readFlowState(projectRoot);
|
|
948
|
+
const { activeRunId } = flowState;
|
|
745
949
|
await withDirectoryLock(delegationLockPath(projectRoot), async () => {
|
|
746
950
|
const filePath = delegationLogPath(projectRoot);
|
|
747
951
|
const prior = await readDelegationLedger(projectRoot);
|
|
@@ -802,6 +1006,7 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
802
1006
|
if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
|
|
803
1007
|
return;
|
|
804
1008
|
}
|
|
1009
|
+
assertSliceClaimInvariant(flowState, stamped, prior.entries);
|
|
805
1010
|
validateMonotonicTimestamps(stamped, prior.entries);
|
|
806
1011
|
if (stamped.status === "scheduled") {
|
|
807
1012
|
// v6.10.0 (P1+P2): for slice-implementer rows with declared
|
|
@@ -839,6 +1044,39 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
839
1044
|
await writeSubagentTracker(projectRoot, ledger.entries);
|
|
840
1045
|
});
|
|
841
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Scan delegation events for expired `leasedUntil` timestamps and append
|
|
1049
|
+
* best-effort `cclaw_slice_lease_expired` audit rows (one per span/slice key).
|
|
1050
|
+
*/
|
|
1051
|
+
export async function reclaimExpiredDelegationClaims(projectRoot, now = new Date()) {
|
|
1052
|
+
const { events } = await readDelegationEvents(projectRoot);
|
|
1053
|
+
const seen = new Set();
|
|
1054
|
+
let count = 0;
|
|
1055
|
+
const ts = now.toISOString();
|
|
1056
|
+
const filePath = delegationEventsPath(projectRoot);
|
|
1057
|
+
const cutoff = Date.parse(ts);
|
|
1058
|
+
for (const e of events) {
|
|
1059
|
+
if (e.leaseState !== "claimed")
|
|
1060
|
+
continue;
|
|
1061
|
+
if (typeof e.leasedUntil !== "string" || e.leasedUntil.length === 0)
|
|
1062
|
+
continue;
|
|
1063
|
+
if (Date.parse(e.leasedUntil) > cutoff)
|
|
1064
|
+
continue;
|
|
1065
|
+
const key = `${e.spanId ?? ""}|${e.sliceId ?? ""}`;
|
|
1066
|
+
if (seen.has(key))
|
|
1067
|
+
continue;
|
|
1068
|
+
seen.add(key);
|
|
1069
|
+
await fs.appendFile(filePath, `${JSON.stringify({
|
|
1070
|
+
event: "cclaw_slice_lease_expired",
|
|
1071
|
+
spanId: e.spanId,
|
|
1072
|
+
sliceId: e.sliceId,
|
|
1073
|
+
leasedUntil: e.leasedUntil,
|
|
1074
|
+
eventTs: ts
|
|
1075
|
+
})}\n`, { encoding: "utf8", mode: 0o600 });
|
|
1076
|
+
count += 1;
|
|
1077
|
+
}
|
|
1078
|
+
return count;
|
|
1079
|
+
}
|
|
842
1080
|
/**
|
|
843
1081
|
* Aggregate the fulfillment mode cclaw expects for the active harness set.
|
|
844
1082
|
* Priority native > generic-dispatch > role-switch > waiver — the best
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -136,7 +136,27 @@ export interface FlowState {
|
|
|
136
136
|
* sync hasn't visited yet.
|
|
137
137
|
*/
|
|
138
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;
|
|
139
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";
|
|
140
160
|
export interface StageInteractionHint {
|
|
141
161
|
skipQuestions?: boolean;
|
|
142
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
|
@@ -25,7 +25,7 @@ import { LANGUAGE_RULE_PACK_DIR, LEGACY_LANGUAGE_RULE_PACK_FOLDERS } from "./con
|
|
|
25
25
|
import { RESEARCH_PLAYBOOKS } from "./content/research-playbooks.js";
|
|
26
26
|
import { SUBAGENT_CONTEXT_SKILLS } from "./content/subagent-context-skills.js";
|
|
27
27
|
import { CCLAW_AGENTS } from "./content/core-agents.js";
|
|
28
|
-
import { createInitialFlowState } from "./flow-state.js";
|
|
28
|
+
import { createInitialFlowState, effectiveWorktreeExecutionMode } from "./flow-state.js";
|
|
29
29
|
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
30
30
|
import { ManagedResourceSession, setActiveManagedResourceSession } from "./managed-resources.js";
|
|
31
31
|
import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
|
|
@@ -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, formatNextParallelWaveSyncHint, mergeParallelWaveDefinitions, parseParallelExecutionPlanWaves, parseWavePlanDirectory, 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";
|
|
@@ -941,6 +942,67 @@ async function applyTddCutoverIfNeeded(projectRoot) {
|
|
|
941
942
|
obj.tddCutoverSliceId = cutoverSliceId;
|
|
942
943
|
await writeFileSafe(flowStatePath, `${JSON.stringify(obj, null, 2)}\n`, { mode: 0o600 });
|
|
943
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
|
+
}
|
|
944
1006
|
async function cleanLegacyArtifacts(projectRoot) {
|
|
945
1007
|
for (const legacyFolder of DEPRECATED_UTILITY_SKILL_FOLDERS) {
|
|
946
1008
|
await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
|
|
@@ -1073,6 +1135,28 @@ async function assertExpectedHarnessShims(projectRoot, harnesses) {
|
|
|
1073
1135
|
}
|
|
1074
1136
|
}
|
|
1075
1137
|
}
|
|
1138
|
+
async function maybeLogParallelWaveDispatchHint(projectRoot) {
|
|
1139
|
+
const flowPath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
1140
|
+
if (!(await exists(flowPath)))
|
|
1141
|
+
return;
|
|
1142
|
+
try {
|
|
1143
|
+
const state = await readFlowState(projectRoot);
|
|
1144
|
+
if (effectiveWorktreeExecutionMode(state) !== "worktree-first")
|
|
1145
|
+
return;
|
|
1146
|
+
const planPath = runtimePath(projectRoot, "artifacts", "05-plan.md");
|
|
1147
|
+
if (!(await exists(planPath)))
|
|
1148
|
+
return;
|
|
1149
|
+
const planRaw = await fs.readFile(planPath, "utf8");
|
|
1150
|
+
const merged = mergeParallelWaveDefinitions(parseParallelExecutionPlanWaves(planRaw), await parseWavePlanDirectory(runtimePath(projectRoot, "artifacts")));
|
|
1151
|
+
const hint = formatNextParallelWaveSyncHint(merged);
|
|
1152
|
+
if (hint) {
|
|
1153
|
+
process.stdout.write(`cclaw: ${hint}\n`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
catch {
|
|
1157
|
+
// best-effort note only
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1076
1160
|
async function materializeRuntime(projectRoot, config, forceStateReset, operation = "sync") {
|
|
1077
1161
|
await warnStaleInitSentinel(projectRoot, operation);
|
|
1078
1162
|
const sentinelPath = await writeInitSentinel(projectRoot, operation);
|
|
@@ -1093,6 +1177,7 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
|
|
|
1093
1177
|
await writeState(projectRoot, config, forceStateReset);
|
|
1094
1178
|
if (operation === "sync" || operation === "upgrade") {
|
|
1095
1179
|
await applyTddCutoverIfNeeded(projectRoot);
|
|
1180
|
+
await applyPlanLegacyContinuationIfNeeded(projectRoot);
|
|
1096
1181
|
}
|
|
1097
1182
|
try {
|
|
1098
1183
|
await ensureRunSystem(projectRoot, { createIfMissing: false });
|
|
@@ -1112,6 +1197,9 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
|
|
|
1112
1197
|
await assertExpectedHarnessShims(projectRoot, harnesses);
|
|
1113
1198
|
await writeCursorWorkflowRule(projectRoot, harnesses);
|
|
1114
1199
|
await ensureGitignore(projectRoot);
|
|
1200
|
+
if (operation === "sync" || operation === "upgrade") {
|
|
1201
|
+
await maybeLogParallelWaveDispatchHint(projectRoot);
|
|
1202
|
+
}
|
|
1115
1203
|
await managedSession.commit();
|
|
1116
1204
|
await fs.unlink(sentinelPath).catch(() => undefined);
|
|
1117
1205
|
}
|
|
@@ -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
|
+
}>;
|