cclaw-cli 6.14.3 → 7.0.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 +0 -2
- package/dist/artifact-linter/brainstorm.js +1 -1
- package/dist/artifact-linter/design.js +2 -2
- package/dist/artifact-linter/findings-dedup.js +1 -1
- package/dist/artifact-linter/plan.js +6 -6
- package/dist/artifact-linter/review-army.d.ts +1 -1
- package/dist/artifact-linter/review-army.js +1 -1
- package/dist/artifact-linter/scope.js +6 -6
- package/dist/artifact-linter/shared.d.ts +37 -73
- package/dist/artifact-linter/shared.js +30 -37
- package/dist/artifact-linter/spec.js +1 -1
- package/dist/artifact-linter/tdd.d.ts +20 -33
- package/dist/artifact-linter/tdd.js +89 -617
- package/dist/artifact-linter.js +11 -32
- package/dist/cli.js +1 -1
- package/dist/config.js +1 -1
- package/dist/constants.js +1 -1
- package/dist/content/core-agents.d.ts +8 -26
- package/dist/content/core-agents.js +48 -94
- package/dist/content/examples.d.ts +1 -1
- package/dist/content/examples.js +4 -4
- package/dist/content/hooks.js +62 -149
- package/dist/content/idea.js +2 -2
- package/dist/content/iron-laws.js +1 -1
- package/dist/content/node-hooks.js +2 -2
- package/dist/content/skills-elicitation.js +2 -2
- package/dist/content/skills.d.ts +4 -6
- package/dist/content/skills.js +14 -53
- package/dist/content/stage-schema.d.ts +3 -3
- package/dist/content/stage-schema.js +8 -46
- package/dist/content/stages/brainstorm.js +5 -5
- package/dist/content/stages/plan.js +2 -2
- package/dist/content/stages/review.js +1 -1
- package/dist/content/stages/schema-types.d.ts +1 -1
- package/dist/content/stages/scope.js +1 -1
- package/dist/content/stages/spec.js +2 -2
- package/dist/content/stages/tdd.js +43 -108
- package/dist/content/start-command.js +3 -3
- package/dist/content/subagent-context-skills.js +5 -3
- package/dist/content/subagents.js +13 -74
- package/dist/content/templates.d.ts +6 -6
- package/dist/content/templates.js +23 -24
- package/dist/content/utility-skills.d.ts +1 -1
- package/dist/content/utility-skills.js +1 -1
- package/dist/delegation.d.ts +79 -139
- package/dist/delegation.js +83 -215
- package/dist/early-loop.js +1 -1
- package/dist/flow-state.d.ts +24 -129
- package/dist/flow-state.js +5 -30
- package/dist/gate-evidence.d.ts +2 -7
- package/dist/gate-evidence.js +2 -59
- package/dist/harness-adapters.d.ts +1 -1
- package/dist/harness-adapters.js +11 -10
- package/dist/install.js +24 -459
- package/dist/internal/advance-stage/advance.d.ts +5 -5
- package/dist/internal/advance-stage/advance.js +9 -24
- package/dist/internal/advance-stage/parsers.d.ts +1 -1
- package/dist/internal/advance-stage/review-loop.d.ts +1 -1
- package/dist/internal/advance-stage/review-loop.js +3 -3
- package/dist/internal/advance-stage/start-flow.js +1 -3
- package/dist/internal/advance-stage.js +4 -23
- package/dist/internal/cohesion-contract-stub.d.ts +8 -13
- package/dist/internal/cohesion-contract-stub.js +18 -24
- package/dist/internal/flow-state-repair.d.ts +1 -1
- package/dist/internal/plan-split-waves.d.ts +44 -7
- package/dist/internal/plan-split-waves.js +113 -12
- package/dist/internal/wave-status.d.ts +3 -6
- package/dist/internal/wave-status.js +5 -27
- package/dist/policy.js +1 -1
- package/dist/run-persistence.js +10 -44
- package/dist/runtime/run-hook.mjs +3 -3
- package/dist/track-heuristics.js +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/integration-fanin.d.ts +0 -44
- package/dist/integration-fanin.js +0 -180
- package/dist/internal/set-checkpoint-mode.d.ts +0 -16
- package/dist/internal/set-checkpoint-mode.js +0 -72
- package/dist/internal/set-integration-overseer-mode.d.ts +0 -14
- package/dist/internal/set-integration-overseer-mode.js +0 -69
- package/dist/internal/set-worktree-mode.d.ts +0 -10
- package/dist/internal/set-worktree-mode.js +0 -28
- package/dist/worktree-manager.d.ts +0 -50
- package/dist/worktree-manager.js +0 -136
- package/dist/worktree-types.d.ts +0 -36
- package/dist/worktree-types.js +0 -6
package/dist/delegation.js
CHANGED
|
@@ -8,7 +8,6 @@ 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
11
|
import { compareCanonicalUnitIds, mergeParallelWaveDefinitions, parseImplementationUnitParallelFields, parseImplementationUnits, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "./internal/plan-split-waves.js";
|
|
13
12
|
const execFileAsync = promisify(execFile);
|
|
14
13
|
const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived", "stale"]);
|
|
@@ -21,6 +20,10 @@ export const DELEGATION_DISPATCH_SURFACES = [
|
|
|
21
20
|
"role-switch",
|
|
22
21
|
"manual"
|
|
23
22
|
];
|
|
23
|
+
/** Agents that declare `claimedPaths` for parallel/disjoint scheduling and fan-out caps. */
|
|
24
|
+
export function isParallelTddSliceWorker(agent) {
|
|
25
|
+
return agent === "slice-builder";
|
|
26
|
+
}
|
|
24
27
|
/**
|
|
25
28
|
* Per-surface allowed agent-definition path prefixes. Used by the generated
|
|
26
29
|
* `.cclaw/hooks/delegation-record.mjs` helper to reject mismatched
|
|
@@ -237,27 +240,10 @@ function isDelegationEntry(value) {
|
|
|
237
240
|
(o.supersededBy === undefined || typeof o.supersededBy === "string") &&
|
|
238
241
|
(o.claimedPaths === undefined ||
|
|
239
242
|
(Array.isArray(o.claimedPaths) && o.claimedPaths.every((item) => typeof item === "string"))) &&
|
|
240
|
-
(o.sliceId === undefined ||
|
|
241
|
-
(typeof o.sliceId === "string" && o.sliceId.length > 0)) &&
|
|
243
|
+
(o.sliceId === undefined || typeof o.sliceId === "string") &&
|
|
242
244
|
(o.phase === undefined ||
|
|
243
245
|
(typeof o.phase === "string" &&
|
|
244
246
|
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") &&
|
|
261
247
|
(o.refactorOutcome === undefined || isRefactorOutcomeShape(o.refactorOutcome)) &&
|
|
262
248
|
(o.riskTier === undefined ||
|
|
263
249
|
o.riskTier === "low" ||
|
|
@@ -374,7 +360,7 @@ export async function readDelegationLedger(projectRoot) {
|
|
|
374
360
|
}
|
|
375
361
|
}
|
|
376
362
|
/**
|
|
377
|
-
*
|
|
363
|
+
* Audit-only event types that live in
|
|
378
364
|
* `delegation-events.jsonl` but do NOT carry a delegation lifecycle
|
|
379
365
|
* payload (no agent/spanId). The parser must accept them so they
|
|
380
366
|
* don't show up as corrupt lines.
|
|
@@ -388,7 +374,8 @@ const NON_DELEGATION_AUDIT_EVENTS = new Set([
|
|
|
388
374
|
"cclaw_fanin_conflict",
|
|
389
375
|
"cclaw_fanin_resolved",
|
|
390
376
|
"cclaw_fanin_abandoned",
|
|
391
|
-
"cclaw_integration_overseer_skipped"
|
|
377
|
+
"cclaw_integration_overseer_skipped",
|
|
378
|
+
"slice-completed"
|
|
392
379
|
]);
|
|
393
380
|
function isAuditEventLine(parsed) {
|
|
394
381
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
@@ -396,47 +383,13 @@ function isAuditEventLine(parsed) {
|
|
|
396
383
|
const evt = parsed.event;
|
|
397
384
|
return typeof evt === "string" && NON_DELEGATION_AUDIT_EVENTS.has(evt);
|
|
398
385
|
}
|
|
399
|
-
const FAN_IN_AUDIT_EVENT_KINDS = new Set([
|
|
400
|
-
"cclaw_fanin_applied",
|
|
401
|
-
"cclaw_fanin_conflict",
|
|
402
|
-
"cclaw_fanin_resolved",
|
|
403
|
-
"cclaw_fanin_abandoned"
|
|
404
|
-
]);
|
|
405
|
-
function isFanInAuditRecord(value) {
|
|
406
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
407
|
-
return false;
|
|
408
|
-
const o = value;
|
|
409
|
-
const evt = o.event;
|
|
410
|
-
if (typeof evt !== "string" || !FAN_IN_AUDIT_EVENT_KINDS.has(evt)) {
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
return typeof o.ts === "string" && o.ts.length > 0;
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Append a deterministic fan-in audit row (not a delegation lifecycle event).
|
|
417
|
-
*/
|
|
418
|
-
export async function recordCclawFanInAudit(projectRoot, params) {
|
|
419
|
-
const filePath = delegationEventsPath(projectRoot);
|
|
420
|
-
const payload = {
|
|
421
|
-
event: params.kind,
|
|
422
|
-
runId: params.runId,
|
|
423
|
-
laneId: params.laneId,
|
|
424
|
-
sliceIds: params.sliceIds,
|
|
425
|
-
integrationBranch: params.integrationBranch,
|
|
426
|
-
details: params.details,
|
|
427
|
-
ts: new Date().toISOString()
|
|
428
|
-
};
|
|
429
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
430
|
-
await fs.appendFile(filePath, `${JSON.stringify(payload)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
431
|
-
}
|
|
432
386
|
export async function readDelegationEvents(projectRoot) {
|
|
433
387
|
const filePath = delegationEventsPath(projectRoot);
|
|
434
388
|
if (!(await exists(filePath))) {
|
|
435
|
-
return { events: [], corruptLines: []
|
|
389
|
+
return { events: [], corruptLines: [] };
|
|
436
390
|
}
|
|
437
391
|
const events = [];
|
|
438
392
|
const corruptLines = [];
|
|
439
|
-
const fanInAudits = [];
|
|
440
393
|
const text = await fs.readFile(filePath, "utf8").catch(() => "");
|
|
441
394
|
const lines = text.split(/\r?\n/gu);
|
|
442
395
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -448,11 +401,8 @@ export async function readDelegationEvents(projectRoot) {
|
|
|
448
401
|
if (isDelegationEvent(parsed)) {
|
|
449
402
|
events.push(parsed);
|
|
450
403
|
}
|
|
451
|
-
else if (isFanInAuditRecord(parsed)) {
|
|
452
|
-
fanInAudits.push(parsed);
|
|
453
|
-
}
|
|
454
404
|
else if (isAuditEventLine(parsed)) {
|
|
455
|
-
//
|
|
405
|
+
// Audit-only row (e.g. mandatory_delegations_skipped_by_track).
|
|
456
406
|
// Not a delegation lifecycle event but valid audit content.
|
|
457
407
|
continue;
|
|
458
408
|
}
|
|
@@ -464,7 +414,7 @@ export async function readDelegationEvents(projectRoot) {
|
|
|
464
414
|
corruptLines.push(index + 1);
|
|
465
415
|
}
|
|
466
416
|
}
|
|
467
|
-
return { events, corruptLines
|
|
417
|
+
return { events, corruptLines };
|
|
468
418
|
}
|
|
469
419
|
async function appendDelegationEvent(projectRoot, event) {
|
|
470
420
|
const filePath = delegationEventsPath(projectRoot);
|
|
@@ -537,7 +487,7 @@ export function computeActiveSubagents(entries) {
|
|
|
537
487
|
return folded;
|
|
538
488
|
}
|
|
539
489
|
/**
|
|
540
|
-
*
|
|
490
|
+
* Thrown by `validateMonotonicTimestamps` when an incoming row
|
|
541
491
|
* would push a span's timeline backwards. Carries enough context that
|
|
542
492
|
* the CLI / hook surface can format a `delegation_timestamp_non_monotonic`
|
|
543
493
|
* JSON payload without re-deriving the offending field.
|
|
@@ -558,7 +508,7 @@ export class DelegationTimestampError extends Error {
|
|
|
558
508
|
}
|
|
559
509
|
}
|
|
560
510
|
/**
|
|
561
|
-
*
|
|
511
|
+
* Enforce that lifecycle timestamps on a delegation span move
|
|
562
512
|
* forward (or stay equal). Validates both per-row invariants
|
|
563
513
|
* (`startTs ≤ launchedTs ≤ ackTs ≤ completedTs`) and a cross-row
|
|
564
514
|
* invariant: the union of prior rows for this `spanId` plus the
|
|
@@ -616,7 +566,7 @@ export function validateMonotonicTimestamps(stamped, prior) {
|
|
|
616
566
|
}
|
|
617
567
|
}
|
|
618
568
|
/**
|
|
619
|
-
*
|
|
569
|
+
* Thrown by `appendDelegation` when the operator opens a
|
|
620
570
|
* second `scheduled` span on the same `(stage, agent)` pair while an
|
|
621
571
|
* earlier span on the same pair is still active. Callers can catch and
|
|
622
572
|
* either pass the existing span id via `--supersede=<id>` (which
|
|
@@ -639,13 +589,12 @@ export class DispatchDuplicateError extends Error {
|
|
|
639
589
|
}
|
|
640
590
|
}
|
|
641
591
|
/**
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
* deliberately to acknowledge the conflict.
|
|
592
|
+
* Thrown by `validateFileOverlap` when a new `slice-builder` is scheduled
|
|
593
|
+
* on a TDD stage with at least one `claimedPaths` entry that overlaps an
|
|
594
|
+
* active span. The scheduler auto-allows parallel dispatch when paths are
|
|
595
|
+
* disjoint, so an explicit overlap is treated as a serialization signal:
|
|
596
|
+
* the operator must wait for the existing span to terminate or pass
|
|
597
|
+
* `--allow-parallel` deliberately to acknowledge the conflict.
|
|
649
598
|
*/
|
|
650
599
|
export class DispatchOverlapError extends Error {
|
|
651
600
|
existingSpanId;
|
|
@@ -653,7 +602,7 @@ export class DispatchOverlapError extends Error {
|
|
|
653
602
|
pair;
|
|
654
603
|
conflictingPaths;
|
|
655
604
|
constructor(params) {
|
|
656
|
-
super(`dispatch_overlap — slice-
|
|
605
|
+
super(`dispatch_overlap — slice-builder span ${params.newSpanId} claims path(s) ${params.conflictingPaths.join(", ")} already held by active spanId=${params.existingSpanId} on stage=${params.pair.stage}. ` +
|
|
657
606
|
`Wait for ${params.existingSpanId} to finish, dispatch a non-overlapping slice, or pass --allow-parallel to acknowledge the conflict.`);
|
|
658
607
|
this.name = "DispatchOverlapError";
|
|
659
608
|
this.existingSpanId = params.existingSpanId;
|
|
@@ -663,11 +612,10 @@ export class DispatchOverlapError extends Error {
|
|
|
663
612
|
}
|
|
664
613
|
}
|
|
665
614
|
/**
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
* `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<N>` env.
|
|
615
|
+
* Thrown when the count of active `slice-builder` spans reaches
|
|
616
|
+
* `MAX_PARALLEL_SLICE_BUILDERS` and a new scheduled row would push it past
|
|
617
|
+
* the cap. Cap can be overridden once via `--override-cap=N` on the hook
|
|
618
|
+
* flag or globally via `CCLAW_MAX_PARALLEL_SLICE_BUILDERS=<N>` env.
|
|
671
619
|
*/
|
|
672
620
|
export class DispatchCapError extends Error {
|
|
673
621
|
cap;
|
|
@@ -675,7 +623,7 @@ export class DispatchCapError extends Error {
|
|
|
675
623
|
pair;
|
|
676
624
|
constructor(params) {
|
|
677
625
|
super(`dispatch_cap — ${params.active} active ${params.pair.agent}(s) at the cap of ${params.cap}. ` +
|
|
678
|
-
`Complete one before scheduling another, or pass --override-cap=N (or
|
|
626
|
+
`Complete one before scheduling another, or pass --override-cap=N (or CCLAW_MAX_PARALLEL_SLICE_BUILDERS=N) to lift the cap for this run.`);
|
|
679
627
|
this.name = "DispatchCapError";
|
|
680
628
|
this.cap = params.cap;
|
|
681
629
|
this.active = params.active;
|
|
@@ -683,27 +631,16 @@ export class DispatchCapError extends Error {
|
|
|
683
631
|
}
|
|
684
632
|
}
|
|
685
633
|
/**
|
|
686
|
-
*
|
|
634
|
+
* Default cap on active `slice-builder` spans in a single TDD run. Override
|
|
635
|
+
* via `CCLAW_MAX_PARALLEL_SLICE_BUILDERS=<int>` (validated `>=1`).
|
|
687
636
|
*/
|
|
688
|
-
export
|
|
689
|
-
constructor(message) {
|
|
690
|
-
super(message);
|
|
691
|
-
this.name = "DispatchClaimInvalidError";
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* v6.10.0 (P2) — default cap on active `slice-implementer` spans in a
|
|
696
|
-
* single TDD run. Aligned with evanflow's parallel cap. Override via
|
|
697
|
-
* `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<int>` (validated `>=1`).
|
|
698
|
-
*/
|
|
699
|
-
export const MAX_PARALLEL_SLICE_IMPLEMENTERS = 5;
|
|
637
|
+
export const MAX_PARALLEL_SLICE_BUILDERS = 5;
|
|
700
638
|
/**
|
|
701
639
|
* Return up to `cap` slice units whose dependsOn are satisfied, avoiding
|
|
702
640
|
* `claimedPaths` intersections with already-selected units and active holders.
|
|
703
641
|
*/
|
|
704
642
|
export function selectReadySlices(units, opts) {
|
|
705
|
-
const
|
|
706
|
-
const ordered = [...pool].sort((a, b) => compareCanonicalUnitIds(a.unitId, b.unitId));
|
|
643
|
+
const ordered = [...units].sort((a, b) => compareCanonicalUnitIds(a.unitId, b.unitId));
|
|
707
644
|
const selected = [];
|
|
708
645
|
const blockedPaths = new Set();
|
|
709
646
|
for (const holder of opts.activePathHolders) {
|
|
@@ -747,7 +684,7 @@ export function selectReadySlices(units, opts) {
|
|
|
747
684
|
return selected;
|
|
748
685
|
}
|
|
749
686
|
/**
|
|
750
|
-
*
|
|
687
|
+
* Build scheduler rows from merged parallel wave definitions + plan units.
|
|
751
688
|
*/
|
|
752
689
|
export function readySliceUnitsFromMergedWaves(mergedWaves, planMarkdown, options) {
|
|
753
690
|
const units = parseImplementationUnits(planMarkdown);
|
|
@@ -787,7 +724,25 @@ export function readySliceUnitsFromMergedWaves(mergedWaves, planMarkdown, option
|
|
|
787
724
|
}
|
|
788
725
|
return out;
|
|
789
726
|
}
|
|
790
|
-
|
|
727
|
+
/**
|
|
728
|
+
* Heuristic helper deciding whether a multi-slice wave needs
|
|
729
|
+
* the `integration-overseer` dispatch.
|
|
730
|
+
*
|
|
731
|
+
* Triggers (any one):
|
|
732
|
+
* - **two or more closed slices share import boundaries** (heuristic:
|
|
733
|
+
* two slices declare a `claimedPaths` whose first 2 path segments
|
|
734
|
+
* match — same package/module directory);
|
|
735
|
+
* - any slice has `riskTier === "high"`.
|
|
736
|
+
*
|
|
737
|
+
* When none fire, the verdict is `{ required: false, reasons: ["disjoint-paths"] }`
|
|
738
|
+
* and the caller should record a `cclaw_integration_overseer_skipped`
|
|
739
|
+
* audit before bypassing the dispatch.
|
|
740
|
+
*
|
|
741
|
+
* Note on inputs: this function reads from the supplied delegation
|
|
742
|
+
* events list directly so callers can inject synthetic data in tests.
|
|
743
|
+
* Use `readDelegationEvents(projectRoot)` in production paths.
|
|
744
|
+
*/
|
|
745
|
+
export function integrationCheckRequired(events) {
|
|
791
746
|
const reasons = [];
|
|
792
747
|
// Closed slices = ones whose phase=green or phase=refactor row is
|
|
793
748
|
// completed. We collect each unique sliceId's representative paths
|
|
@@ -858,18 +813,13 @@ export function integrationCheckRequired(events, fanInAudits) {
|
|
|
858
813
|
}
|
|
859
814
|
if (sharedFound)
|
|
860
815
|
reasons.push("shared-import-boundary");
|
|
861
|
-
// Fan-in conflict trigger — any `cclaw_fanin_conflict` in the supplied
|
|
862
|
-
// audits forces the overseer regardless of paths/risk.
|
|
863
|
-
if (Array.isArray(fanInAudits) && fanInAudits.some((a) => a.event === "cclaw_fanin_conflict")) {
|
|
864
|
-
reasons.push("fanin-conflict");
|
|
865
|
-
}
|
|
866
816
|
if (reasons.length > 0) {
|
|
867
817
|
return { required: true, reasons };
|
|
868
818
|
}
|
|
869
819
|
return { required: false, reasons: ["disjoint-paths"] };
|
|
870
820
|
}
|
|
871
821
|
/**
|
|
872
|
-
*
|
|
822
|
+
* Append a non-delegation audit event recording that the
|
|
873
823
|
* integration-overseer dispatch was skipped because
|
|
874
824
|
* `integrationCheckRequired()` returned `required: false`. Best-effort;
|
|
875
825
|
* never throws.
|
|
@@ -892,14 +842,14 @@ export async function recordIntegrationOverseerSkipped(projectRoot, params) {
|
|
|
892
842
|
}
|
|
893
843
|
}
|
|
894
844
|
/**
|
|
895
|
-
*
|
|
845
|
+
* Load merged wave plan (Parallel Execution Plan block + wave-plans/) and map to `ReadySliceUnit[]`.
|
|
896
846
|
*/
|
|
897
847
|
export async function loadTddReadySlicePool(planMarkdown, artifactsDir, options) {
|
|
898
848
|
const merged = mergeParallelWaveDefinitions(parseParallelExecutionPlanWaves(planMarkdown), await parseWavePlanDirectory(artifactsDir));
|
|
899
849
|
return readySliceUnitsFromMergedWaves(merged, planMarkdown, options);
|
|
900
850
|
}
|
|
901
851
|
function readMaxParallelOverrideFromEnv() {
|
|
902
|
-
const raw = process.env.
|
|
852
|
+
const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_BUILDERS;
|
|
903
853
|
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
904
854
|
return null;
|
|
905
855
|
const parsed = Number(raw);
|
|
@@ -908,17 +858,16 @@ function readMaxParallelOverrideFromEnv() {
|
|
|
908
858
|
return parsed;
|
|
909
859
|
}
|
|
910
860
|
/**
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
* `
|
|
918
|
-
* `{ autoParallel: false }` and the legacy dedup path takes over.
|
|
861
|
+
* When scheduling a `slice-builder` on a TDD stage, compare `claimedPaths`
|
|
862
|
+
* against every currently active span on the same `(stage, agent)` pair.
|
|
863
|
+
* Overlap → throw `DispatchOverlapError`; disjoint paths → return
|
|
864
|
+
* `{ autoParallel: true }` so the caller can mark the new entry
|
|
865
|
+
* `allowParallel = true` without explicit operator intent. When the agent
|
|
866
|
+
* is not a slice-builder or no `claimedPaths` are supplied, the function
|
|
867
|
+
* returns `{ autoParallel: false }` and the standard dedup path takes over.
|
|
919
868
|
*/
|
|
920
869
|
export function validateFileOverlap(stamped, activeEntries) {
|
|
921
|
-
if (stamped.agent
|
|
870
|
+
if (!isParallelTddSliceWorker(stamped.agent) || stamped.stage !== "tdd") {
|
|
922
871
|
return { autoParallel: false };
|
|
923
872
|
}
|
|
924
873
|
const newPaths = Array.isArray(stamped.claimedPaths) ? stamped.claimedPaths : [];
|
|
@@ -935,7 +884,7 @@ export function validateFileOverlap(stamped, activeEntries) {
|
|
|
935
884
|
const existingPaths = Array.isArray(existing.claimedPaths) ? existing.claimedPaths : [];
|
|
936
885
|
if (existingPaths.length === 0) {
|
|
937
886
|
// We can't prove disjoint without the other side declaring paths;
|
|
938
|
-
// be conservative and let the
|
|
887
|
+
// be conservative and let the standard dedup error path fire.
|
|
939
888
|
return { autoParallel: false };
|
|
940
889
|
}
|
|
941
890
|
const overlap = newPaths.filter((p) => existingPaths.includes(p));
|
|
@@ -951,24 +900,25 @@ export function validateFileOverlap(stamped, activeEntries) {
|
|
|
951
900
|
return { autoParallel: true };
|
|
952
901
|
}
|
|
953
902
|
/**
|
|
954
|
-
*
|
|
955
|
-
*
|
|
956
|
-
*
|
|
903
|
+
* Enforce the slice-builder fan-out cap. The new scheduled row pushes the
|
|
904
|
+
* active count from N to N+1; if that would exceed the cap (default 5,
|
|
905
|
+
* env-overridable via `CCLAW_MAX_PARALLEL_SLICE_BUILDERS`), throw
|
|
906
|
+
* `DispatchCapError`.
|
|
957
907
|
*
|
|
958
|
-
* Caller passes the already-folded list of active entries (latest row
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
908
|
+
* Caller passes the already-folded list of active entries (latest row per
|
|
909
|
+
* spanId, ACTIVE statuses only). The function counts entries that match
|
|
910
|
+
* the agent on the same `stage`. The new row's own spanId is excluded so
|
|
911
|
+
* re-recording a `scheduled` doesn't trip the cap on a span that's already
|
|
912
|
+
* counted.
|
|
963
913
|
*/
|
|
964
914
|
export function validateFanOutCap(stamped, activeEntries, override) {
|
|
965
|
-
if (stamped.agent
|
|
915
|
+
if (!isParallelTddSliceWorker(stamped.agent) || stamped.stage !== "tdd")
|
|
966
916
|
return;
|
|
967
917
|
if (stamped.status !== "scheduled")
|
|
968
918
|
return;
|
|
969
919
|
const cap = (override !== null && override !== undefined && Number.isInteger(override) && override >= 1)
|
|
970
920
|
? override
|
|
971
|
-
: (readMaxParallelOverrideFromEnv() ??
|
|
921
|
+
: (readMaxParallelOverrideFromEnv() ?? MAX_PARALLEL_SLICE_BUILDERS);
|
|
972
922
|
const sameLaneActive = activeEntries.filter((entry) => entry.stage === stamped.stage &&
|
|
973
923
|
entry.agent === stamped.agent &&
|
|
974
924
|
entry.spanId !== stamped.spanId);
|
|
@@ -981,18 +931,18 @@ export function validateFanOutCap(stamped, activeEntries, override) {
|
|
|
981
931
|
}
|
|
982
932
|
}
|
|
983
933
|
/**
|
|
984
|
-
*
|
|
934
|
+
* Find the latest active span for a given `(stage, agent)`
|
|
985
935
|
* pair in the supplied ledger entries. Returns the row whose latest
|
|
986
936
|
* status (after the latest-by-spanId fold) is still in the active set
|
|
987
937
|
* (`scheduled | launched | acknowledged`).
|
|
988
938
|
*
|
|
989
939
|
* Run-scope is **strict**: only entries whose `runId` matches the
|
|
990
940
|
* supplied `runId` are folded. Entries with empty/missing `runId`
|
|
991
|
-
* (
|
|
941
|
+
* (older ledgers without explicit run scoping) are treated as NOT belonging
|
|
992
942
|
* to the current run, so they cannot keep an old span "active" across
|
|
993
943
|
* a fresh dispatch and trip a spurious `dispatch_duplicate`. This
|
|
994
|
-
*
|
|
995
|
-
* slice-
|
|
944
|
+
* Ensures a slice-builder that ran in run-1 does not block a
|
|
945
|
+
* slice-builder scheduled in run-2.
|
|
996
946
|
*
|
|
997
947
|
* keep in sync with the inline copy in
|
|
998
948
|
* `src/content/hooks.ts::delegationRecordScript`.
|
|
@@ -1027,51 +977,12 @@ async function writeSubagentTracker(projectRoot, entries) {
|
|
|
1027
977
|
}));
|
|
1028
978
|
await writeFileSafe(subagentsStatePath(projectRoot), `${JSON.stringify({ active, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
1029
979
|
}
|
|
1030
|
-
function latestClaimTokenForSpan(entries, spanId) {
|
|
1031
|
-
if (!spanId)
|
|
1032
|
-
return null;
|
|
1033
|
-
let latest = null;
|
|
1034
|
-
for (const e of entries) {
|
|
1035
|
-
if (e.spanId !== spanId)
|
|
1036
|
-
continue;
|
|
1037
|
-
if (typeof e.claimToken === "string" && e.claimToken.trim().length > 0) {
|
|
1038
|
-
latest = e.claimToken.trim();
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
return latest;
|
|
1042
|
-
}
|
|
1043
|
-
function assertSliceClaimInvariant(flow, stamped, prior) {
|
|
1044
|
-
if (stamped.stage !== "tdd" || stamped.agent !== "slice-implementer")
|
|
1045
|
-
return;
|
|
1046
|
-
if (effectiveWorktreeExecutionMode(flow) !== "worktree-first")
|
|
1047
|
-
return;
|
|
1048
|
-
if (stamped.status === "scheduled" && typeof stamped.sliceId === "string" && stamped.sliceId.length > 0) {
|
|
1049
|
-
const tok = stamped.claimToken?.trim() ?? "";
|
|
1050
|
-
if (tok.length === 0) {
|
|
1051
|
-
throw new DispatchClaimInvalidError("dispatch_claim_invalid — worktree-first requires --claim-token when scheduling slice-implementer with --slice");
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
if (!TERMINAL_DELEGATION_STATUSES.has(stamped.status))
|
|
1055
|
-
return;
|
|
1056
|
-
if (stamped.status === "waived" || stamped.status === "stale")
|
|
1057
|
-
return;
|
|
1058
|
-
const expected = latestClaimTokenForSpan(prior, stamped.spanId);
|
|
1059
|
-
if (!expected)
|
|
1060
|
-
return;
|
|
1061
|
-
const got = stamped.claimToken?.trim() ?? "";
|
|
1062
|
-
if (got !== expected) {
|
|
1063
|
-
throw new DispatchClaimInvalidError("dispatch_claim_invalid — claimToken must match the scheduled claim for this span");
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
980
|
export async function appendDelegation(projectRoot, entry) {
|
|
1067
981
|
const flowState = await readFlowState(projectRoot);
|
|
1068
982
|
const { activeRunId } = flowState;
|
|
1069
983
|
await withDirectoryLock(delegationLockPath(projectRoot), async () => {
|
|
1070
984
|
const filePath = delegationLogPath(projectRoot);
|
|
1071
985
|
const prior = await readDelegationLedger(projectRoot);
|
|
1072
|
-
// Span start anchor: prefer explicit `startTs`; otherwise fall back to
|
|
1073
|
-
// the earliest provided lifecycle marker so the monotonic validator
|
|
1074
|
-
// never sees a synthetic `now` overshoot a real event timestamp.
|
|
1075
986
|
const lifecycleCandidates = [
|
|
1076
987
|
entry.startTs,
|
|
1077
988
|
entry.launchedTs,
|
|
@@ -1120,21 +1031,11 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
1120
1031
|
stamped.fulfillmentMode = expectedFulfillmentMode(fallbacks);
|
|
1121
1032
|
}
|
|
1122
1033
|
}
|
|
1123
|
-
// Idempotency: a retried hook may replay the same lifecycle row. Allow a
|
|
1124
|
-
// terminal row to close an existing scheduled span, but drop exact same
|
|
1125
|
-
// span/status duplicates so checks do not mis-count repeated writes.
|
|
1126
1034
|
if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
|
|
1127
1035
|
return;
|
|
1128
1036
|
}
|
|
1129
|
-
assertSliceClaimInvariant(flowState, stamped, prior.entries);
|
|
1130
1037
|
validateMonotonicTimestamps(stamped, prior.entries);
|
|
1131
1038
|
if (stamped.status === "scheduled") {
|
|
1132
|
-
// v6.10.0 (P1+P2): for slice-implementer rows with declared
|
|
1133
|
-
// claimedPaths, the file-overlap scheduler runs first. Disjoint
|
|
1134
|
-
// paths auto-promote the row to allowParallel so the legacy
|
|
1135
|
-
// dispatch_duplicate guard does not fire. Overlapping paths
|
|
1136
|
-
// throw DispatchOverlapError. The fan-out cap then runs against
|
|
1137
|
-
// the active set (excluding the new row's spanId).
|
|
1138
1039
|
const sameRunPrior = prior.entries.filter((entry) => entry.runId === activeRunId);
|
|
1139
1040
|
const activeForRun = computeActiveSubagents(sameRunPrior);
|
|
1140
1041
|
const overlap = validateFileOverlap(stamped, activeForRun);
|
|
@@ -1164,39 +1065,6 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
1164
1065
|
await writeSubagentTracker(projectRoot, ledger.entries);
|
|
1165
1066
|
});
|
|
1166
1067
|
}
|
|
1167
|
-
/**
|
|
1168
|
-
* Scan delegation events for expired `leasedUntil` timestamps and append
|
|
1169
|
-
* best-effort `cclaw_slice_lease_expired` audit rows (one per span/slice key).
|
|
1170
|
-
*/
|
|
1171
|
-
export async function reclaimExpiredDelegationClaims(projectRoot, now = new Date()) {
|
|
1172
|
-
const { events } = await readDelegationEvents(projectRoot);
|
|
1173
|
-
const seen = new Set();
|
|
1174
|
-
let count = 0;
|
|
1175
|
-
const ts = now.toISOString();
|
|
1176
|
-
const filePath = delegationEventsPath(projectRoot);
|
|
1177
|
-
const cutoff = Date.parse(ts);
|
|
1178
|
-
for (const e of events) {
|
|
1179
|
-
if (e.leaseState !== "claimed")
|
|
1180
|
-
continue;
|
|
1181
|
-
if (typeof e.leasedUntil !== "string" || e.leasedUntil.length === 0)
|
|
1182
|
-
continue;
|
|
1183
|
-
if (Date.parse(e.leasedUntil) > cutoff)
|
|
1184
|
-
continue;
|
|
1185
|
-
const key = `${e.spanId ?? ""}|${e.sliceId ?? ""}`;
|
|
1186
|
-
if (seen.has(key))
|
|
1187
|
-
continue;
|
|
1188
|
-
seen.add(key);
|
|
1189
|
-
await fs.appendFile(filePath, `${JSON.stringify({
|
|
1190
|
-
event: "cclaw_slice_lease_expired",
|
|
1191
|
-
spanId: e.spanId,
|
|
1192
|
-
sliceId: e.sliceId,
|
|
1193
|
-
leasedUntil: e.leasedUntil,
|
|
1194
|
-
eventTs: ts
|
|
1195
|
-
})}\n`, { encoding: "utf8", mode: 0o600 });
|
|
1196
|
-
count += 1;
|
|
1197
|
-
}
|
|
1198
|
-
return count;
|
|
1199
|
-
}
|
|
1200
1068
|
/**
|
|
1201
1069
|
* Aggregate the fulfillment mode cclaw expects for the active harness set.
|
|
1202
1070
|
* Priority native > generic-dispatch > role-switch > waiver — the best
|
|
@@ -1218,7 +1086,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
1218
1086
|
const flowState = await readFlowState(projectRoot, {
|
|
1219
1087
|
repairFeatureSystem: options.repairFeatureSystem
|
|
1220
1088
|
});
|
|
1221
|
-
//
|
|
1089
|
+
// Read `flowState.taskClass` as a fallback
|
|
1222
1090
|
// when the caller doesn't pass an explicit override. The
|
|
1223
1091
|
// `cclaw advance-stage` path (`buildValidationReport` →
|
|
1224
1092
|
// `checkMandatoryDelegations`) never forwarded `taskClass`, which left
|
|
@@ -1343,7 +1211,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
1343
1211
|
};
|
|
1344
1212
|
}
|
|
1345
1213
|
/**
|
|
1346
|
-
*
|
|
1214
|
+
* Append a non-delegation audit event to
|
|
1347
1215
|
* `delegation-events.jsonl` recording that the mandatory delegation
|
|
1348
1216
|
* gate was skipped because of the active track / task class. Plays the
|
|
1349
1217
|
* same audit role as a `waived` row but does NOT carry an agent —
|
|
@@ -1373,12 +1241,12 @@ async function recordMandatorySkippedByTrack(projectRoot, params) {
|
|
|
1373
1241
|
}
|
|
1374
1242
|
}
|
|
1375
1243
|
/**
|
|
1376
|
-
*
|
|
1244
|
+
* Append a non-delegation audit event recording
|
|
1377
1245
|
* that one or more required artifact-validation findings were
|
|
1378
1246
|
* demoted from blocking to advisory because the active run is on a
|
|
1379
1247
|
* small-fix lane (`track === "quick"` or `taskClass === "software-bugfix"`).
|
|
1380
1248
|
*
|
|
1381
|
-
* The event mirrors
|
|
1249
|
+
* The event mirrors `mandatory_delegations_skipped_by_track`
|
|
1382
1250
|
* audit pattern: best-effort write to `delegation-events.jsonl`, no
|
|
1383
1251
|
* agent payload, recognized by `readDelegationEvents` so it does not
|
|
1384
1252
|
* corrupt downstream parsers. Failures are swallowed.
|
|
@@ -1405,12 +1273,12 @@ export async function recordArtifactValidationDemotedByTrack(projectRoot, params
|
|
|
1405
1273
|
}
|
|
1406
1274
|
}
|
|
1407
1275
|
/**
|
|
1408
|
-
*
|
|
1276
|
+
* Append a non-delegation audit event recording
|
|
1409
1277
|
* that the scope-stage Expansion Strategist (`product-discovery`)
|
|
1410
1278
|
* delegation requirement was skipped because the active run is on a
|
|
1411
1279
|
* small-fix lane (`track === "quick"` or `taskClass === "software-bugfix"`).
|
|
1412
1280
|
*
|
|
1413
|
-
* Mirrors the
|
|
1281
|
+
* Mirrors the `mandatory_delegations_skipped_by_track`
|
|
1414
1282
|
* audit pattern: best-effort write to `delegation-events.jsonl`, no
|
|
1415
1283
|
* agent payload, recognized by `readDelegationEvents` so it does not
|
|
1416
1284
|
* corrupt downstream parsers. Failures are swallowed.
|
package/dist/early-loop.js
CHANGED
|
@@ -137,7 +137,7 @@ export function parseEarlyLoopLog(text, options = {}) {
|
|
|
137
137
|
continue;
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
//
|
|
140
|
+
// schema repair: legacy logs may carry rows with no runId
|
|
141
141
|
// (the prior parser silently coerced them to "active", which then
|
|
142
142
|
// collided across runs). Surface a structured warning on read but
|
|
143
143
|
// skip the row so derived status doesn't fold cross-run state.
|