cclaw-cli 6.13.0 → 6.14.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/shared.d.ts +15 -0
- package/dist/artifact-linter/tdd.d.ts +53 -10
- package/dist/artifact-linter/tdd.js +315 -92
- package/dist/artifact-linter.js +10 -2
- package/dist/content/hooks.js +119 -3
- package/dist/content/skills.js +15 -12
- package/dist/content/stages/tdd.js +8 -8
- package/dist/content/start-command.js +6 -3
- package/dist/delegation.d.ts +88 -0
- package/dist/delegation.js +171 -3
- package/dist/flow-state.d.ts +45 -0
- package/dist/flow-state.js +18 -0
- package/dist/install.js +115 -2
- package/dist/internal/plan-split-waves.d.ts +46 -0
- package/dist/internal/plan-split-waves.js +225 -6
- package/dist/run-persistence.js +14 -0
- package/package.json +1 -1
package/dist/delegation.js
CHANGED
|
@@ -9,7 +9,7 @@ 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
11
|
import { effectiveWorktreeExecutionMode } from "./flow-state.js";
|
|
12
|
-
import { compareCanonicalUnitIds } from "./internal/plan-split-waves.js";
|
|
12
|
+
import { compareCanonicalUnitIds, mergeParallelWaveDefinitions, parseImplementationUnitParallelFields, parseImplementationUnits, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "./internal/plan-split-waves.js";
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
14
14
|
const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived", "stale"]);
|
|
15
15
|
export const DELEGATION_DISPATCH_SURFACES = [
|
|
@@ -257,7 +257,22 @@ function isDelegationEntry(value) {
|
|
|
257
257
|
o.integrationState === "applied" ||
|
|
258
258
|
o.integrationState === "conflict" ||
|
|
259
259
|
o.integrationState === "resolved" ||
|
|
260
|
-
o.integrationState === "abandoned")
|
|
260
|
+
o.integrationState === "abandoned") &&
|
|
261
|
+
(o.refactorOutcome === undefined || isRefactorOutcomeShape(o.refactorOutcome)) &&
|
|
262
|
+
(o.riskTier === undefined ||
|
|
263
|
+
o.riskTier === "low" ||
|
|
264
|
+
o.riskTier === "medium" ||
|
|
265
|
+
o.riskTier === "high"));
|
|
266
|
+
}
|
|
267
|
+
function isRefactorOutcomeShape(value) {
|
|
268
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
269
|
+
return false;
|
|
270
|
+
const o = value;
|
|
271
|
+
if (o.mode !== "inline" && o.mode !== "deferred")
|
|
272
|
+
return false;
|
|
273
|
+
if (o.rationale !== undefined && typeof o.rationale !== "string")
|
|
274
|
+
return false;
|
|
275
|
+
return true;
|
|
261
276
|
}
|
|
262
277
|
function isDelegationDispatchSurface(value) {
|
|
263
278
|
return typeof value === "string" && DELEGATION_DISPATCH_SURFACES.includes(value);
|
|
@@ -372,7 +387,8 @@ const NON_DELEGATION_AUDIT_EVENTS = new Set([
|
|
|
372
387
|
"cclaw_fanin_applied",
|
|
373
388
|
"cclaw_fanin_conflict",
|
|
374
389
|
"cclaw_fanin_resolved",
|
|
375
|
-
"cclaw_fanin_abandoned"
|
|
390
|
+
"cclaw_fanin_abandoned",
|
|
391
|
+
"cclaw_integration_overseer_skipped"
|
|
376
392
|
]);
|
|
377
393
|
function isAuditEventLine(parsed) {
|
|
378
394
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
@@ -730,6 +746,158 @@ export function selectReadySlices(units, opts) {
|
|
|
730
746
|
}
|
|
731
747
|
return selected;
|
|
732
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* v6.13.1 — build scheduler rows from merged parallel wave definitions + plan units.
|
|
751
|
+
*/
|
|
752
|
+
export function readySliceUnitsFromMergedWaves(mergedWaves, planMarkdown, options) {
|
|
753
|
+
const units = parseImplementationUnits(planMarkdown);
|
|
754
|
+
const metaByUnit = new Map(units.map((u) => {
|
|
755
|
+
const m = parseImplementationUnitParallelFields(u, options);
|
|
756
|
+
return [m.unitId, m];
|
|
757
|
+
}));
|
|
758
|
+
const sliceSet = new Set();
|
|
759
|
+
for (const w of mergedWaves) {
|
|
760
|
+
for (const m of w.members) {
|
|
761
|
+
sliceSet.add(m.sliceId);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const out = [];
|
|
765
|
+
for (const sliceId of [...sliceSet].sort((a, b) => a.localeCompare(b))) {
|
|
766
|
+
const member = mergedWaves.flatMap((w) => w.members).find((x) => x.sliceId === sliceId);
|
|
767
|
+
if (!member)
|
|
768
|
+
continue;
|
|
769
|
+
const meta = metaByUnit.get(member.unitId);
|
|
770
|
+
if (!meta) {
|
|
771
|
+
out.push({
|
|
772
|
+
unitId: member.unitId,
|
|
773
|
+
sliceId,
|
|
774
|
+
dependsOn: [],
|
|
775
|
+
claimedPaths: [],
|
|
776
|
+
parallelizable: true
|
|
777
|
+
});
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
out.push({
|
|
781
|
+
unitId: meta.unitId,
|
|
782
|
+
sliceId,
|
|
783
|
+
dependsOn: meta.dependsOn,
|
|
784
|
+
claimedPaths: meta.claimedPaths,
|
|
785
|
+
parallelizable: meta.parallelizable
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
return out;
|
|
789
|
+
}
|
|
790
|
+
export function integrationCheckRequired(events, fanInAudits) {
|
|
791
|
+
const reasons = [];
|
|
792
|
+
// Closed slices = ones whose phase=green or phase=refactor row is
|
|
793
|
+
// completed. We collect each unique sliceId's representative paths
|
|
794
|
+
// and risk tier so the heuristic looks at terminal state only.
|
|
795
|
+
const sliceState = new Map();
|
|
796
|
+
for (const evt of events) {
|
|
797
|
+
if (evt.stage !== "tdd")
|
|
798
|
+
continue;
|
|
799
|
+
if (typeof evt.sliceId !== "string" || evt.sliceId.length === 0)
|
|
800
|
+
continue;
|
|
801
|
+
if (evt.status !== "completed")
|
|
802
|
+
continue;
|
|
803
|
+
if (evt.phase !== "green" && evt.phase !== "refactor" && evt.phase !== "refactor-deferred") {
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
const existing = sliceState.get(evt.sliceId) ?? { sliceId: evt.sliceId };
|
|
807
|
+
if (Array.isArray(evt.claimedPaths) && evt.claimedPaths.length > 0) {
|
|
808
|
+
const merged = new Set(existing.claimedPaths ?? []);
|
|
809
|
+
for (const p of evt.claimedPaths)
|
|
810
|
+
merged.add(p);
|
|
811
|
+
existing.claimedPaths = [...merged];
|
|
812
|
+
}
|
|
813
|
+
if (evt.riskTier === "low" || evt.riskTier === "medium" || evt.riskTier === "high") {
|
|
814
|
+
// Highest-wins so the verdict is conservative.
|
|
815
|
+
const order = { low: 0, medium: 1, high: 2 };
|
|
816
|
+
const prev = existing.riskTier ?? "low";
|
|
817
|
+
if (order[evt.riskTier] >= order[prev]) {
|
|
818
|
+
existing.riskTier = evt.riskTier;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
sliceState.set(evt.sliceId, existing);
|
|
822
|
+
}
|
|
823
|
+
const slices = [...sliceState.values()];
|
|
824
|
+
if (slices.some((s) => s.riskTier === "high")) {
|
|
825
|
+
reasons.push("high-risk-slice");
|
|
826
|
+
}
|
|
827
|
+
// Shared-directory heuristic — two distinct slices with overlapping
|
|
828
|
+
// first-2-segment directory prefixes count as shared boundary.
|
|
829
|
+
const sliceDirs = new Map();
|
|
830
|
+
for (const s of slices) {
|
|
831
|
+
const dirs = new Set();
|
|
832
|
+
for (const raw of s.claimedPaths ?? []) {
|
|
833
|
+
const segments = raw.split("/").filter((seg) => seg.length > 0);
|
|
834
|
+
if (segments.length === 0)
|
|
835
|
+
continue;
|
|
836
|
+
// For top-level files like `package.json`, fall back to the
|
|
837
|
+
// first segment so single-segment paths still count as a shared
|
|
838
|
+
// directory when two slices both claim the file.
|
|
839
|
+
const prefix = segments.slice(0, Math.max(1, Math.min(2, segments.length))).join("/");
|
|
840
|
+
dirs.add(prefix);
|
|
841
|
+
}
|
|
842
|
+
if (dirs.size > 0)
|
|
843
|
+
sliceDirs.set(s.sliceId, dirs);
|
|
844
|
+
}
|
|
845
|
+
let sharedFound = false;
|
|
846
|
+
const ids = [...sliceDirs.keys()];
|
|
847
|
+
outer: for (let i = 0; i < ids.length; i += 1) {
|
|
848
|
+
const a = sliceDirs.get(ids[i]);
|
|
849
|
+
for (let j = i + 1; j < ids.length; j += 1) {
|
|
850
|
+
const b = sliceDirs.get(ids[j]);
|
|
851
|
+
for (const dir of a) {
|
|
852
|
+
if (b.has(dir)) {
|
|
853
|
+
sharedFound = true;
|
|
854
|
+
break outer;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (sharedFound)
|
|
860
|
+
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
|
+
if (reasons.length > 0) {
|
|
867
|
+
return { required: true, reasons };
|
|
868
|
+
}
|
|
869
|
+
return { required: false, reasons: ["disjoint-paths"] };
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* v6.14.0 — append a non-delegation audit event recording that the
|
|
873
|
+
* integration-overseer dispatch was skipped because
|
|
874
|
+
* `integrationCheckRequired()` returned `required: false`. Best-effort;
|
|
875
|
+
* never throws.
|
|
876
|
+
*/
|
|
877
|
+
export async function recordIntegrationOverseerSkipped(projectRoot, params) {
|
|
878
|
+
const eventsPath = delegationEventsPath(projectRoot);
|
|
879
|
+
const payload = {
|
|
880
|
+
event: "cclaw_integration_overseer_skipped",
|
|
881
|
+
runId: params.runId,
|
|
882
|
+
reasons: params.reasons,
|
|
883
|
+
sliceIds: params.sliceIds,
|
|
884
|
+
ts: new Date().toISOString()
|
|
885
|
+
};
|
|
886
|
+
try {
|
|
887
|
+
await fs.mkdir(path.dirname(eventsPath), { recursive: true });
|
|
888
|
+
await fs.appendFile(eventsPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
// best-effort audit; never block stage advance.
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* v6.13.1 — load merged wave plan (Parallel Execution Plan block + wave-plans/) and map to `ReadySliceUnit[]`.
|
|
896
|
+
*/
|
|
897
|
+
export async function loadTddReadySlicePool(planMarkdown, artifactsDir, options) {
|
|
898
|
+
const merged = mergeParallelWaveDefinitions(parseParallelExecutionPlanWaves(planMarkdown), await parseWavePlanDirectory(artifactsDir));
|
|
899
|
+
return readySliceUnitsFromMergedWaves(merged, planMarkdown, options);
|
|
900
|
+
}
|
|
733
901
|
function readMaxParallelOverrideFromEnv() {
|
|
734
902
|
const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS;
|
|
735
903
|
if (typeof raw !== "string" || raw.trim().length === 0)
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -151,12 +151,57 @@ export interface FlowState {
|
|
|
151
151
|
* defaults scheduler parallelism to opt-in only for those units.
|
|
152
152
|
*/
|
|
153
153
|
legacyContinuation?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* v6.14.0 — TDD wave checkpoint mode (stream-style parallel TDD).
|
|
156
|
+
*
|
|
157
|
+
* - `per-slice` — default for new projects. Each lane runs RED→GREEN as
|
|
158
|
+
* soon as its `dependsOn` closes; the linter enforces RED-before-GREEN
|
|
159
|
+
* per slice only (`tdd_slice_red_completed_before_green`). No global
|
|
160
|
+
* barrier between Phase A REDs and Phase B GREENs.
|
|
161
|
+
* - `global-red` — legacy v6.12/v6.13 behavior. ALL Phase A REDs in a
|
|
162
|
+
* wave must complete before ANY Phase B GREEN starts. Auto-applied
|
|
163
|
+
* for projects with `legacyContinuation: true` so hox-style runs
|
|
164
|
+
* continue to enforce the wave barrier.
|
|
165
|
+
*
|
|
166
|
+
* Omitted on legacy state files (treated as `"global-red"` for
|
|
167
|
+
* `legacyContinuation: true` and `"per-slice"` otherwise via
|
|
168
|
+
* `effectiveTddCheckpointMode`).
|
|
169
|
+
*/
|
|
170
|
+
tddCheckpointMode?: "per-slice" | "global-red";
|
|
171
|
+
/**
|
|
172
|
+
* v6.14.0 — integration-overseer dispatch mode.
|
|
173
|
+
*
|
|
174
|
+
* - `conditional` — default for new projects. The controller calls
|
|
175
|
+
* `integrationCheckRequired(events)` after wave closeout; the
|
|
176
|
+
* integration-overseer is dispatched only when (a) two or more
|
|
177
|
+
* closed slices share import boundaries (heuristic: shared
|
|
178
|
+
* directory in `evidenceRefs`/`claimedPaths`), (b) any slice has
|
|
179
|
+
* `riskTier === "high"`, or (c) deterministic fan-in reported a
|
|
180
|
+
* `cclaw_fanin_conflict`. Otherwise the linter emits the audit
|
|
181
|
+
* row `cclaw_integration_overseer_skipped` and skips dispatch.
|
|
182
|
+
* - `always` — legacy v6.13 behavior. Run integration-overseer
|
|
183
|
+
* after every multi-slice wave regardless of trigger.
|
|
184
|
+
*
|
|
185
|
+
* Omitted on legacy state files (treated as `"always"`).
|
|
186
|
+
*/
|
|
187
|
+
integrationOverseerMode?: "conditional" | "always";
|
|
154
188
|
}
|
|
155
189
|
/**
|
|
156
190
|
* Effective worktree mode: legacy state files without the field keep
|
|
157
191
|
* single-tree scheduling to avoid breaking existing runs on upgrade.
|
|
158
192
|
*/
|
|
159
193
|
export declare function effectiveWorktreeExecutionMode(state: FlowState): "single-tree" | "worktree-first";
|
|
194
|
+
/**
|
|
195
|
+
* Effective v6.14 TDD checkpoint mode: legacy state files without the
|
|
196
|
+
* field default to `global-red` when `legacyContinuation: true` (hox)
|
|
197
|
+
* and `per-slice` otherwise. Explicit values always win.
|
|
198
|
+
*/
|
|
199
|
+
export declare function effectiveTddCheckpointMode(state: FlowState): "per-slice" | "global-red";
|
|
200
|
+
/**
|
|
201
|
+
* Effective v6.14 integration-overseer mode: legacy state files without
|
|
202
|
+
* the field default to `always` (matches v6.13 behavior).
|
|
203
|
+
*/
|
|
204
|
+
export declare function effectiveIntegrationOverseerMode(state: FlowState): "conditional" | "always";
|
|
160
205
|
export interface StageInteractionHint {
|
|
161
206
|
skipQuestions?: boolean;
|
|
162
207
|
sourceStage?: FlowStage;
|
package/dist/flow-state.js
CHANGED
|
@@ -51,6 +51,24 @@ export function createInitialCloseoutState() {
|
|
|
51
51
|
export function effectiveWorktreeExecutionMode(state) {
|
|
52
52
|
return state.worktreeExecutionMode ?? "single-tree";
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Effective v6.14 TDD checkpoint mode: legacy state files without the
|
|
56
|
+
* field default to `global-red` when `legacyContinuation: true` (hox)
|
|
57
|
+
* and `per-slice` otherwise. Explicit values always win.
|
|
58
|
+
*/
|
|
59
|
+
export function effectiveTddCheckpointMode(state) {
|
|
60
|
+
if (state.tddCheckpointMode === "per-slice" || state.tddCheckpointMode === "global-red") {
|
|
61
|
+
return state.tddCheckpointMode;
|
|
62
|
+
}
|
|
63
|
+
return state.legacyContinuation === true ? "global-red" : "per-slice";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Effective v6.14 integration-overseer mode: legacy state files without
|
|
67
|
+
* the field default to `always` (matches v6.13 behavior).
|
|
68
|
+
*/
|
|
69
|
+
export function effectiveIntegrationOverseerMode(state) {
|
|
70
|
+
return state.integrationOverseerMode === "conditional" ? "conditional" : "always";
|
|
71
|
+
}
|
|
54
72
|
export function isFlowTrack(value) {
|
|
55
73
|
return typeof value === "string" && FLOW_TRACKS.includes(value);
|
|
56
74
|
}
|
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";
|
|
@@ -34,7 +34,7 @@ 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
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
|
+
import { PLAN_SPLIT_DEFAULT_WAVE_SIZE, buildParallelExecutionPlanSection, formatNextParallelWaveSyncHint, mergeParallelWaveDefinitions, parseParallelExecutionPlanWaves, parseWavePlanDirectory, planArtifactLacksV613ParallelMetadata, upsertParallelExecutionPlanSection } from "./internal/plan-split-waves.js";
|
|
38
38
|
import { FLOW_STAGES } from "./types.js";
|
|
39
39
|
const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
|
|
40
40
|
const CURSOR_RULE_REL_PATH = ".cursor/rules/cclaw-workflow.mdc";
|
|
@@ -1003,6 +1003,90 @@ async function applyPlanLegacyContinuationIfNeeded(projectRoot) {
|
|
|
1003
1003
|
// Best-effort: corrupt/missing state is handled elsewhere on sync.
|
|
1004
1004
|
}
|
|
1005
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* v6.14.0 — set stream-style defaults on `cclaw-cli sync` and print a
|
|
1008
|
+
* one-line hint when defaults change. Strategy:
|
|
1009
|
+
*
|
|
1010
|
+
* - When `legacyContinuation: true` and `tddCheckpointMode` is unset, force
|
|
1011
|
+
* `tddCheckpointMode: "global-red"` (preserves hox wave protocol).
|
|
1012
|
+
* - When `legacyContinuation: true` and `integrationOverseerMode` is unset,
|
|
1013
|
+
* force `integrationOverseerMode: "always"` (preserves v6.13 behavior).
|
|
1014
|
+
* - When `legacyContinuation` is NOT true (new / standard projects) and
|
|
1015
|
+
* neither field is set, default to `tddCheckpointMode: "per-slice"`,
|
|
1016
|
+
* `integrationOverseerMode: "conditional"`. Also default
|
|
1017
|
+
* `worktreeExecutionMode: "worktree-first"` if unset.
|
|
1018
|
+
*
|
|
1019
|
+
* Returns a one-line hint string (or `null` if nothing changed) so callers
|
|
1020
|
+
* can print it through the standard sync hint surface.
|
|
1021
|
+
*/
|
|
1022
|
+
async function applyV614DefaultsIfNeeded(projectRoot) {
|
|
1023
|
+
// Defensive read — match `applyTddCutoverIfNeeded`'s pattern (raw +
|
|
1024
|
+
// JSON.parse) so corrupt state is left untouched for the downstream
|
|
1025
|
+
// fail-fast check in `materializeRuntime` (which expects to see the
|
|
1026
|
+
// CorruptFlowStateError surfaced via `ensureRunSystem`). Calling
|
|
1027
|
+
// `readFlowState` directly would quarantine the corrupt file and hide
|
|
1028
|
+
// the failure from the caller.
|
|
1029
|
+
const flowStatePath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
1030
|
+
let flowStateRaw;
|
|
1031
|
+
try {
|
|
1032
|
+
flowStateRaw = await fs.readFile(flowStatePath, "utf8");
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
let parsed;
|
|
1038
|
+
try {
|
|
1039
|
+
parsed = JSON.parse(flowStateRaw);
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
const obj = parsed;
|
|
1048
|
+
const updates = {};
|
|
1049
|
+
const summary = [];
|
|
1050
|
+
const tddCheckpointModeSet = obj.tddCheckpointMode === "per-slice" || obj.tddCheckpointMode === "global-red";
|
|
1051
|
+
const integrationOverseerModeSet = obj.integrationOverseerMode === "conditional" || obj.integrationOverseerMode === "always";
|
|
1052
|
+
const worktreeExecutionModeSet = obj.worktreeExecutionMode === "worktree-first" || obj.worktreeExecutionMode === "single-tree";
|
|
1053
|
+
const legacyContinuation = obj.legacyContinuation === true;
|
|
1054
|
+
if (legacyContinuation) {
|
|
1055
|
+
if (!tddCheckpointModeSet) {
|
|
1056
|
+
updates.tddCheckpointMode = "global-red";
|
|
1057
|
+
summary.push("tddCheckpointMode=global-red (legacyContinuation)");
|
|
1058
|
+
}
|
|
1059
|
+
if (!integrationOverseerModeSet) {
|
|
1060
|
+
updates.integrationOverseerMode = "always";
|
|
1061
|
+
summary.push("integrationOverseerMode=always (legacyContinuation)");
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
if (!tddCheckpointModeSet) {
|
|
1066
|
+
updates.tddCheckpointMode = "per-slice";
|
|
1067
|
+
summary.push("tddCheckpointMode=per-slice");
|
|
1068
|
+
}
|
|
1069
|
+
if (!integrationOverseerModeSet) {
|
|
1070
|
+
updates.integrationOverseerMode = "conditional";
|
|
1071
|
+
summary.push("integrationOverseerMode=conditional");
|
|
1072
|
+
}
|
|
1073
|
+
if (!worktreeExecutionModeSet) {
|
|
1074
|
+
updates.worktreeExecutionMode = "worktree-first";
|
|
1075
|
+
summary.push("worktreeExecutionMode=worktree-first");
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (summary.length === 0) {
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
const merged = { ...obj, ...updates };
|
|
1082
|
+
try {
|
|
1083
|
+
await writeFileSafe(flowStatePath, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
return `v6.14.0 stream-style defaults applied: ${summary.join(", ")}. To opt out, edit .cclaw/state/flow-state.json directly or pin the legacy mode (tddCheckpointMode="global-red", integrationOverseerMode="always").`;
|
|
1089
|
+
}
|
|
1006
1090
|
async function cleanLegacyArtifacts(projectRoot) {
|
|
1007
1091
|
for (const legacyFolder of DEPRECATED_UTILITY_SKILL_FOLDERS) {
|
|
1008
1092
|
await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
|
|
@@ -1135,6 +1219,28 @@ async function assertExpectedHarnessShims(projectRoot, harnesses) {
|
|
|
1135
1219
|
}
|
|
1136
1220
|
}
|
|
1137
1221
|
}
|
|
1222
|
+
async function maybeLogParallelWaveDispatchHint(projectRoot) {
|
|
1223
|
+
const flowPath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
1224
|
+
if (!(await exists(flowPath)))
|
|
1225
|
+
return;
|
|
1226
|
+
try {
|
|
1227
|
+
const state = await readFlowState(projectRoot);
|
|
1228
|
+
if (effectiveWorktreeExecutionMode(state) !== "worktree-first")
|
|
1229
|
+
return;
|
|
1230
|
+
const planPath = runtimePath(projectRoot, "artifacts", "05-plan.md");
|
|
1231
|
+
if (!(await exists(planPath)))
|
|
1232
|
+
return;
|
|
1233
|
+
const planRaw = await fs.readFile(planPath, "utf8");
|
|
1234
|
+
const merged = mergeParallelWaveDefinitions(parseParallelExecutionPlanWaves(planRaw), await parseWavePlanDirectory(runtimePath(projectRoot, "artifacts")));
|
|
1235
|
+
const hint = formatNextParallelWaveSyncHint(merged);
|
|
1236
|
+
if (hint) {
|
|
1237
|
+
process.stdout.write(`cclaw: ${hint}\n`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
catch {
|
|
1241
|
+
// best-effort note only
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1138
1244
|
async function materializeRuntime(projectRoot, config, forceStateReset, operation = "sync") {
|
|
1139
1245
|
await warnStaleInitSentinel(projectRoot, operation);
|
|
1140
1246
|
const sentinelPath = await writeInitSentinel(projectRoot, operation);
|
|
@@ -1156,6 +1262,10 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
|
|
|
1156
1262
|
if (operation === "sync" || operation === "upgrade") {
|
|
1157
1263
|
await applyTddCutoverIfNeeded(projectRoot);
|
|
1158
1264
|
await applyPlanLegacyContinuationIfNeeded(projectRoot);
|
|
1265
|
+
const v614Hint = await applyV614DefaultsIfNeeded(projectRoot);
|
|
1266
|
+
if (v614Hint) {
|
|
1267
|
+
process.stdout.write(`cclaw: ${v614Hint}\n`);
|
|
1268
|
+
}
|
|
1159
1269
|
}
|
|
1160
1270
|
try {
|
|
1161
1271
|
await ensureRunSystem(projectRoot, { createIfMissing: false });
|
|
@@ -1175,6 +1285,9 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
|
|
|
1175
1285
|
await assertExpectedHarnessShims(projectRoot, harnesses);
|
|
1176
1286
|
await writeCursorWorkflowRule(projectRoot, harnesses);
|
|
1177
1287
|
await ensureGitignore(projectRoot);
|
|
1288
|
+
if (operation === "sync" || operation === "upgrade") {
|
|
1289
|
+
await maybeLogParallelWaveDispatchHint(projectRoot);
|
|
1290
|
+
}
|
|
1178
1291
|
await managedSession.commit();
|
|
1179
1292
|
await fs.unlink(sentinelPath).catch(() => undefined);
|
|
1180
1293
|
}
|
|
@@ -31,6 +31,52 @@ export interface PlanSplitWavesArgs {
|
|
|
31
31
|
}
|
|
32
32
|
export declare const PLAN_SPLIT_DEFAULT_WAVE_SIZE = 5;
|
|
33
33
|
export declare const PLAN_SPLIT_SMALL_PLAN_THRESHOLD = 50;
|
|
34
|
+
/** v6.13.1 — member line in Parallel Execution Plan or wave-NN.md */
|
|
35
|
+
export interface ParsedParallelWaveMember {
|
|
36
|
+
sliceId: string;
|
|
37
|
+
unitId: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ParsedParallelWave {
|
|
40
|
+
waveId: string;
|
|
41
|
+
members: ParsedParallelWaveMember[];
|
|
42
|
+
}
|
|
43
|
+
export declare class WavePlanDuplicateSliceError extends Error {
|
|
44
|
+
constructor(message: string);
|
|
45
|
+
}
|
|
46
|
+
export declare class WavePlanMergeConflictError extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Raw body between parallel execution managed markers (no markers included).
|
|
51
|
+
*/
|
|
52
|
+
export declare function extractParallelExecutionManagedBody(planMarkdown: string): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Members list after `Members:` in Parallel Execution Plan / wave-NN headers.
|
|
55
|
+
* Supports markdown bold `**Members:**` (colon between Members and closing `**`)
|
|
56
|
+
* and plain `Members:`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function extractMembersListFromLine(trimmedLine: string): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* Parse `## Parallel Execution Plan` managed block for wave headings and Members lines.
|
|
61
|
+
* Malformed member tokens are skipped. Duplicate slice ids in one plan source throw.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseParallelExecutionPlanWaves(planMarkdown: string): ParsedParallelWave[];
|
|
64
|
+
/**
|
|
65
|
+
* Parse a single wave-NN.md: prefer a `Members:` line in the header; otherwise
|
|
66
|
+
* collect distinct S-N tokens in the first lines (legacy).
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseWavePlanFileBody(body: string, waveId: string): ParsedParallelWave;
|
|
69
|
+
export declare function parseWavePlanDirectory(artifactsDir: string): Promise<ParsedParallelWave[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Merge wave definitions: managed Parallel Execution Plan first, then wave-NN.md.
|
|
72
|
+
* Same slice must map to the same wave id and unit id in both sources or a
|
|
73
|
+
* `WavePlanMergeConflictError` is thrown.
|
|
74
|
+
*/
|
|
75
|
+
export declare function mergeParallelWaveDefinitions(primary: ParsedParallelWave[], secondary: ParsedParallelWave[]): ParsedParallelWave[];
|
|
76
|
+
/**
|
|
77
|
+
* One-line operator hint after sync when a multi-member wave exists.
|
|
78
|
+
*/
|
|
79
|
+
export declare function formatNextParallelWaveSyncHint(merged: ParsedParallelWave[]): string | null;
|
|
34
80
|
export interface ParsedImplementationUnit {
|
|
35
81
|
id: string;
|
|
36
82
|
/**
|