cclaw-cli 7.7.1 → 8.1.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 +210 -134
- package/dist/artifact-frontmatter.d.ts +51 -0
- package/dist/artifact-frontmatter.js +131 -0
- package/dist/artifact-paths.d.ts +7 -27
- package/dist/artifact-paths.js +20 -249
- package/dist/cancel.d.ts +16 -0
- package/dist/cancel.js +66 -0
- package/dist/cli.d.ts +2 -27
- package/dist/cli.js +90 -508
- package/dist/compound.d.ts +26 -0
- package/dist/compound.js +96 -0
- package/dist/config.d.ts +14 -51
- package/dist/config.js +23 -359
- package/dist/constants.d.ts +11 -18
- package/dist/constants.js +19 -106
- package/dist/content/antipatterns.d.ts +1 -0
- package/dist/content/antipatterns.js +109 -0
- package/dist/content/artifact-templates.d.ts +10 -0
- package/dist/content/artifact-templates.js +550 -0
- package/dist/content/cancel-command.d.ts +2 -2
- package/dist/content/cancel-command.js +25 -17
- package/dist/content/core-agents.d.ts +9 -233
- package/dist/content/core-agents.js +39 -768
- package/dist/content/decision-protocol.d.ts +1 -12
- package/dist/content/decision-protocol.js +27 -20
- package/dist/content/examples.d.ts +8 -42
- package/dist/content/examples.js +293 -425
- package/dist/content/idea-command.d.ts +2 -0
- package/dist/content/idea-command.js +38 -0
- package/dist/content/iron-laws.d.ts +4 -138
- package/dist/content/iron-laws.js +18 -197
- package/dist/content/meta-skill.d.ts +1 -3
- package/dist/content/meta-skill.js +57 -134
- package/dist/content/node-hooks.d.ts +12 -8
- package/dist/content/node-hooks.js +188 -838
- package/dist/content/recovery.d.ts +8 -0
- package/dist/content/recovery.js +179 -0
- package/dist/content/reference-patterns.d.ts +4 -13
- package/dist/content/reference-patterns.js +260 -389
- package/dist/content/research-playbooks.d.ts +8 -8
- package/dist/content/research-playbooks.js +108 -121
- package/dist/content/review-loop.d.ts +6 -192
- package/dist/content/review-loop.js +29 -731
- package/dist/content/skills.d.ts +8 -38
- package/dist/content/skills.js +681 -732
- package/dist/content/specialist-prompts/architect.d.ts +1 -0
- package/dist/content/specialist-prompts/architect.js +225 -0
- package/dist/content/specialist-prompts/brainstormer.d.ts +1 -0
- package/dist/content/specialist-prompts/brainstormer.js +168 -0
- package/dist/content/specialist-prompts/index.d.ts +2 -0
- package/dist/content/specialist-prompts/index.js +14 -0
- package/dist/content/specialist-prompts/planner.d.ts +1 -0
- package/dist/content/specialist-prompts/planner.js +182 -0
- package/dist/content/specialist-prompts/reviewer.d.ts +1 -0
- package/dist/content/specialist-prompts/reviewer.js +193 -0
- package/dist/content/specialist-prompts/security-reviewer.d.ts +1 -0
- package/dist/content/specialist-prompts/security-reviewer.js +133 -0
- package/dist/content/specialist-prompts/slice-builder.d.ts +1 -0
- package/dist/content/specialist-prompts/slice-builder.js +232 -0
- package/dist/content/stage-playbooks.d.ts +8 -0
- package/dist/content/stage-playbooks.js +404 -0
- package/dist/content/start-command.d.ts +2 -12
- package/dist/content/start-command.js +221 -207
- package/dist/flow-state.d.ts +21 -178
- package/dist/flow-state.js +67 -170
- package/dist/fs-utils.d.ts +6 -26
- package/dist/fs-utils.js +29 -162
- package/dist/gitignore.d.ts +2 -1
- package/dist/gitignore.js +51 -34
- package/dist/harness-detect.d.ts +10 -0
- package/dist/harness-detect.js +29 -0
- package/dist/install.d.ts +27 -15
- package/dist/install.js +230 -1342
- package/dist/knowledge-store.d.ts +19 -163
- package/dist/knowledge-store.js +56 -590
- package/dist/logger.d.ts +8 -3
- package/dist/logger.js +13 -4
- package/dist/orchestrator-routing.d.ts +29 -0
- package/dist/orchestrator-routing.js +156 -0
- package/dist/run-persistence.d.ts +7 -118
- package/dist/run-persistence.js +29 -845
- package/dist/runtime/run-hook.entry.d.ts +1 -3
- package/dist/runtime/run-hook.entry.js +19 -4
- package/dist/runtime/run-hook.mjs +13 -1024
- package/dist/types.d.ts +25 -261
- package/dist/types.js +8 -36
- package/package.json +6 -3
- package/dist/artifact-linter/brainstorm.d.ts +0 -2
- package/dist/artifact-linter/brainstorm.js +0 -353
- package/dist/artifact-linter/design.d.ts +0 -18
- package/dist/artifact-linter/design.js +0 -444
- package/dist/artifact-linter/findings-dedup.d.ts +0 -56
- package/dist/artifact-linter/findings-dedup.js +0 -232
- package/dist/artifact-linter/plan.d.ts +0 -2
- package/dist/artifact-linter/plan.js +0 -826
- package/dist/artifact-linter/review-army.d.ts +0 -49
- package/dist/artifact-linter/review-army.js +0 -520
- package/dist/artifact-linter/review.d.ts +0 -2
- package/dist/artifact-linter/review.js +0 -113
- package/dist/artifact-linter/scope.d.ts +0 -2
- package/dist/artifact-linter/scope.js +0 -158
- package/dist/artifact-linter/shared.d.ts +0 -637
- package/dist/artifact-linter/shared.js +0 -2163
- package/dist/artifact-linter/ship.d.ts +0 -2
- package/dist/artifact-linter/ship.js +0 -250
- package/dist/artifact-linter/spec.d.ts +0 -2
- package/dist/artifact-linter/spec.js +0 -176
- package/dist/artifact-linter/tdd.d.ts +0 -118
- package/dist/artifact-linter/tdd.js +0 -1404
- package/dist/artifact-linter.d.ts +0 -15
- package/dist/artifact-linter.js +0 -517
- package/dist/codex-feature-flag.d.ts +0 -58
- package/dist/codex-feature-flag.js +0 -193
- package/dist/content/closeout-guidance.d.ts +0 -14
- package/dist/content/closeout-guidance.js +0 -44
- package/dist/content/diff-command.d.ts +0 -1
- package/dist/content/diff-command.js +0 -43
- package/dist/content/harness-doc.d.ts +0 -1
- package/dist/content/harness-doc.js +0 -65
- package/dist/content/hook-events.d.ts +0 -9
- package/dist/content/hook-events.js +0 -23
- package/dist/content/hook-manifest.d.ts +0 -81
- package/dist/content/hook-manifest.js +0 -156
- package/dist/content/hooks.d.ts +0 -11
- package/dist/content/hooks.js +0 -1972
- package/dist/content/idea.d.ts +0 -60
- package/dist/content/idea.js +0 -416
- package/dist/content/language-policy.d.ts +0 -2
- package/dist/content/language-policy.js +0 -13
- package/dist/content/learnings.d.ts +0 -6
- package/dist/content/learnings.js +0 -141
- package/dist/content/observe.d.ts +0 -19
- package/dist/content/observe.js +0 -86
- package/dist/content/opencode-plugin.d.ts +0 -1
- package/dist/content/opencode-plugin.js +0 -635
- package/dist/content/review-prompts.d.ts +0 -1
- package/dist/content/review-prompts.js +0 -104
- package/dist/content/runtime-shared-snippets.d.ts +0 -8
- package/dist/content/runtime-shared-snippets.js +0 -80
- package/dist/content/session-hooks.d.ts +0 -7
- package/dist/content/session-hooks.js +0 -107
- package/dist/content/skills-elicitation.d.ts +0 -1
- package/dist/content/skills-elicitation.js +0 -167
- package/dist/content/stage-command.d.ts +0 -2
- package/dist/content/stage-command.js +0 -17
- package/dist/content/stage-schema.d.ts +0 -117
- package/dist/content/stage-schema.js +0 -955
- package/dist/content/stages/_lint-metadata/index.d.ts +0 -2
- package/dist/content/stages/_lint-metadata/index.js +0 -97
- package/dist/content/stages/brainstorm.d.ts +0 -2
- package/dist/content/stages/brainstorm.js +0 -184
- package/dist/content/stages/design.d.ts +0 -2
- package/dist/content/stages/design.js +0 -288
- package/dist/content/stages/index.d.ts +0 -8
- package/dist/content/stages/index.js +0 -11
- package/dist/content/stages/plan.d.ts +0 -2
- package/dist/content/stages/plan.js +0 -191
- package/dist/content/stages/review.d.ts +0 -2
- package/dist/content/stages/review.js +0 -240
- package/dist/content/stages/schema-types.d.ts +0 -203
- package/dist/content/stages/schema-types.js +0 -1
- package/dist/content/stages/scope.d.ts +0 -2
- package/dist/content/stages/scope.js +0 -254
- package/dist/content/stages/ship.d.ts +0 -2
- package/dist/content/stages/ship.js +0 -159
- package/dist/content/stages/spec.d.ts +0 -2
- package/dist/content/stages/spec.js +0 -170
- package/dist/content/stages/tdd.d.ts +0 -4
- package/dist/content/stages/tdd.js +0 -273
- package/dist/content/state-contracts.d.ts +0 -1
- package/dist/content/state-contracts.js +0 -63
- package/dist/content/status-command.d.ts +0 -4
- package/dist/content/status-command.js +0 -109
- package/dist/content/subagent-context-skills.d.ts +0 -4
- package/dist/content/subagent-context-skills.js +0 -279
- package/dist/content/subagents.d.ts +0 -3
- package/dist/content/subagents.js +0 -997
- package/dist/content/templates.d.ts +0 -26
- package/dist/content/templates.js +0 -1692
- package/dist/content/track-render-context.d.ts +0 -18
- package/dist/content/track-render-context.js +0 -53
- package/dist/content/tree-command.d.ts +0 -1
- package/dist/content/tree-command.js +0 -64
- package/dist/content/utility-skills.d.ts +0 -30
- package/dist/content/utility-skills.js +0 -160
- package/dist/content/view-command.d.ts +0 -2
- package/dist/content/view-command.js +0 -92
- package/dist/delegation.d.ts +0 -649
- package/dist/delegation.js +0 -1539
- package/dist/early-loop.d.ts +0 -70
- package/dist/early-loop.js +0 -302
- package/dist/execution-topology.d.ts +0 -44
- package/dist/execution-topology.js +0 -95
- package/dist/gate-evidence.d.ts +0 -85
- package/dist/gate-evidence.js +0 -631
- package/dist/harness-adapters.d.ts +0 -151
- package/dist/harness-adapters.js +0 -756
- package/dist/harness-selection.d.ts +0 -31
- package/dist/harness-selection.js +0 -214
- package/dist/hook-schema.d.ts +0 -6
- package/dist/hook-schema.js +0 -114
- package/dist/hook-schemas/claude-hooks.v1.json +0 -10
- package/dist/hook-schemas/codex-hooks.v1.json +0 -10
- package/dist/hook-schemas/cursor-hooks.v1.json +0 -13
- package/dist/init-detect.d.ts +0 -2
- package/dist/init-detect.js +0 -50
- package/dist/internal/advance-stage/advance.d.ts +0 -89
- package/dist/internal/advance-stage/advance.js +0 -655
- package/dist/internal/advance-stage/cancel-run.d.ts +0 -8
- package/dist/internal/advance-stage/cancel-run.js +0 -19
- package/dist/internal/advance-stage/flow-state-coercion.d.ts +0 -3
- package/dist/internal/advance-stage/flow-state-coercion.js +0 -81
- package/dist/internal/advance-stage/helpers.d.ts +0 -14
- package/dist/internal/advance-stage/helpers.js +0 -145
- package/dist/internal/advance-stage/hook.d.ts +0 -8
- package/dist/internal/advance-stage/hook.js +0 -40
- package/dist/internal/advance-stage/parsers.d.ts +0 -72
- package/dist/internal/advance-stage/parsers.js +0 -357
- package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +0 -24
- package/dist/internal/advance-stage/proactive-delegation-trace.js +0 -56
- package/dist/internal/advance-stage/review-loop.d.ts +0 -16
- package/dist/internal/advance-stage/review-loop.js +0 -199
- package/dist/internal/advance-stage/rewind.d.ts +0 -14
- package/dist/internal/advance-stage/rewind.js +0 -108
- package/dist/internal/advance-stage/start-flow.d.ts +0 -13
- package/dist/internal/advance-stage/start-flow.js +0 -241
- package/dist/internal/advance-stage/verify.d.ts +0 -21
- package/dist/internal/advance-stage/verify.js +0 -185
- package/dist/internal/advance-stage.d.ts +0 -7
- package/dist/internal/advance-stage.js +0 -138
- package/dist/internal/cohesion-contract-stub.d.ts +0 -24
- package/dist/internal/cohesion-contract-stub.js +0 -148
- package/dist/internal/compound-readiness.d.ts +0 -23
- package/dist/internal/compound-readiness.js +0 -102
- package/dist/internal/detect-public-api-changes.d.ts +0 -5
- package/dist/internal/detect-public-api-changes.js +0 -45
- package/dist/internal/detect-supply-chain-changes.d.ts +0 -6
- package/dist/internal/detect-supply-chain-changes.js +0 -138
- package/dist/internal/early-loop-status.d.ts +0 -7
- package/dist/internal/early-loop-status.js +0 -93
- package/dist/internal/envelope-validate.d.ts +0 -7
- package/dist/internal/envelope-validate.js +0 -66
- package/dist/internal/flow-state-repair.d.ts +0 -20
- package/dist/internal/flow-state-repair.js +0 -104
- package/dist/internal/plan-split-waves.d.ts +0 -190
- package/dist/internal/plan-split-waves.js +0 -764
- package/dist/internal/runtime-integrity.d.ts +0 -7
- package/dist/internal/runtime-integrity.js +0 -268
- package/dist/internal/slice-commit.d.ts +0 -7
- package/dist/internal/slice-commit.js +0 -619
- package/dist/internal/tdd-loop-status.d.ts +0 -14
- package/dist/internal/tdd-loop-status.js +0 -68
- package/dist/internal/tdd-red-evidence.d.ts +0 -7
- package/dist/internal/tdd-red-evidence.js +0 -153
- package/dist/internal/waiver-grant.d.ts +0 -62
- package/dist/internal/waiver-grant.js +0 -294
- package/dist/internal/wave-status.d.ts +0 -74
- package/dist/internal/wave-status.js +0 -506
- package/dist/managed-resources.d.ts +0 -53
- package/dist/managed-resources.js +0 -313
- package/dist/policy.d.ts +0 -10
- package/dist/policy.js +0 -167
- package/dist/retro-gate.d.ts +0 -9
- package/dist/retro-gate.js +0 -47
- package/dist/run-archive.d.ts +0 -61
- package/dist/run-archive.js +0 -391
- package/dist/runs.d.ts +0 -2
- package/dist/runs.js +0 -2
- package/dist/stack-detection.d.ts +0 -116
- package/dist/stack-detection.js +0 -489
- package/dist/streaming/event-stream.d.ts +0 -31
- package/dist/streaming/event-stream.js +0 -114
- package/dist/tdd-cycle.d.ts +0 -107
- package/dist/tdd-cycle.js +0 -289
- package/dist/tdd-verification-evidence.d.ts +0 -17
- package/dist/tdd-verification-evidence.js +0 -122
- package/dist/track-heuristics.d.ts +0 -27
- package/dist/track-heuristics.js +0 -154
- package/dist/util/slice-id.d.ts +0 -58
- package/dist/util/slice-id.js +0 -89
- package/dist/worktree-manager.d.ts +0 -20
- package/dist/worktree-manager.js +0 -108
|
@@ -1,764 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { resolveArtifactPath } from "../artifact-paths.js";
|
|
4
|
-
import { exists, writeFileSafe } from "../fs-utils.js";
|
|
5
|
-
import { readFlowState } from "../runs.js";
|
|
6
|
-
import { compareSliceIds, parseSliceId } from "../util/slice-id.js";
|
|
7
|
-
export const PLAN_SPLIT_DEFAULT_WAVE_SIZE = 5;
|
|
8
|
-
export const PLAN_SPLIT_SMALL_PLAN_THRESHOLD = 50;
|
|
9
|
-
const WAVE_PLANS_DIR = "wave-plans";
|
|
10
|
-
const WAVE_MANAGED_START = "<!-- wave-split-managed-start -->";
|
|
11
|
-
const WAVE_MANAGED_END = "<!-- wave-split-managed-end -->";
|
|
12
|
-
const PARALLEL_EXEC_MANAGED_START = "<!-- parallel-exec-managed-start -->";
|
|
13
|
-
const PARALLEL_EXEC_MANAGED_END = "<!-- parallel-exec-managed-end -->";
|
|
14
|
-
export class WavePlanDuplicateSliceError extends Error {
|
|
15
|
-
constructor(message) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.name = "WavePlanDuplicateSliceError";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
export class WavePlanMergeConflictError extends Error {
|
|
21
|
-
constructor(message) {
|
|
22
|
-
super(message);
|
|
23
|
-
this.name = "WavePlanMergeConflictError";
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Raw body between parallel execution managed markers (no markers included).
|
|
28
|
-
*/
|
|
29
|
-
export function extractParallelExecutionManagedBody(planMarkdown) {
|
|
30
|
-
const startIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_START);
|
|
31
|
-
const endIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_END);
|
|
32
|
-
if (startIdx < 0 || endIdx <= startIdx)
|
|
33
|
-
return null;
|
|
34
|
-
return planMarkdown.slice(startIdx + PARALLEL_EXEC_MANAGED_START.length, endIdx).trim();
|
|
35
|
-
}
|
|
36
|
-
function tokenToSliceAndUnit(token) {
|
|
37
|
-
const t = token.trim().replace(/^[`"'[\]()]+|[`"'[\]()]+$/gu, "");
|
|
38
|
-
const u = /^U-(\d+)([a-z][a-z0-9]*)?$/iu.exec(t);
|
|
39
|
-
if (u) {
|
|
40
|
-
const num = u[1];
|
|
41
|
-
const suffix = (u[2] ?? "").toLowerCase();
|
|
42
|
-
const tail = suffix.length > 0 ? `${num}${suffix}` : num;
|
|
43
|
-
return { unitId: `U-${tail}`, sliceId: `S-${tail}` };
|
|
44
|
-
}
|
|
45
|
-
const parsed = parseSliceId(t);
|
|
46
|
-
if (parsed) {
|
|
47
|
-
const tail = parsed.suffix.length > 0 ? `${parsed.numeric}${parsed.suffix}` : `${parsed.numeric}`;
|
|
48
|
-
return { unitId: `U-${tail}`, sliceId: parsed.id };
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Members list after `Members:` in Parallel Execution Plan / wave-NN headers.
|
|
54
|
-
* Supports markdown bold `**Members:**` (colon between Members and closing `**`)
|
|
55
|
-
* and plain `Members:`.
|
|
56
|
-
*/
|
|
57
|
-
export function extractMembersListFromLine(trimmedLine) {
|
|
58
|
-
const bold = /^[-*]?\s*\*\*Members:\*\*\s*(.+)$/iu.exec(trimmedLine);
|
|
59
|
-
if (bold)
|
|
60
|
-
return bold[1].trim();
|
|
61
|
-
const plain = /^[-*]?\s*Members\s*:\s*(.+)$/iu.exec(trimmedLine);
|
|
62
|
-
if (plain)
|
|
63
|
-
return plain[1].trim();
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Extract a `(sliceId, unitId)` pair from a markdown table data row
|
|
68
|
-
* whose first column is an `S-NN` token. Used by the wave parser to
|
|
69
|
-
* recognize the table-format Parallel Execution Plan alongside (or
|
|
70
|
-
* instead of) the `**Members:**` bullet line.
|
|
71
|
-
*
|
|
72
|
-
* Rules:
|
|
73
|
-
* - The line must start with `|` (after trimming).
|
|
74
|
-
* - Column 1 (after stripping markdown noise) may be either a slice id
|
|
75
|
-
* (`S-N`) or an implementation-unit id (`U-N`). Unit ids derive their
|
|
76
|
-
* execution slice as `S-N`, which lets 7.7+ plans schedule feature-atomic
|
|
77
|
-
* units without inventing a tiny `T-NNN` row per dispatch lane. Header rows
|
|
78
|
-
* (`| sliceId | …`, `| unit | …`) and separator rows (`|---|---|…`) are
|
|
79
|
-
* silently skipped.
|
|
80
|
-
* - Column 2, when present and non-empty, becomes the `unitId`
|
|
81
|
-
* verbatim (after stripping whitespace + backticks/quotes/brackets).
|
|
82
|
-
* This lets authors record task ids (`T-010`, `T-008a`, …) in the
|
|
83
|
-
* `unit` column without forcing a `U-NN` derivation.
|
|
84
|
-
* - When column 2 is absent or empty, fall back to the
|
|
85
|
-
* `S-NN → U-NN` derivation so the `**Members:**` parser path stays
|
|
86
|
-
* bit-identical for non-table plans.
|
|
87
|
-
*/
|
|
88
|
-
export function parseTableRowMember(trimmedLine) {
|
|
89
|
-
if (!trimmedLine.startsWith("|"))
|
|
90
|
-
return null;
|
|
91
|
-
const inner = trimmedLine.replace(/^\|/u, "").replace(/\|\s*$/u, "");
|
|
92
|
-
if (inner.length === 0)
|
|
93
|
-
return null;
|
|
94
|
-
const cells = inner.split("|").map((cell) => cell.trim());
|
|
95
|
-
if (cells.length === 0)
|
|
96
|
-
return null;
|
|
97
|
-
const stripDecorations = (raw) => raw.replace(/^[`"'[\]()]+|[`"'[\]()]+$/gu, "").trim();
|
|
98
|
-
const col1 = stripDecorations(cells[0]);
|
|
99
|
-
const parsedSlice = parseSliceId(col1);
|
|
100
|
-
const parsedUnit = tokenToSliceAndUnit(col1);
|
|
101
|
-
if (!parsedSlice && !parsedUnit)
|
|
102
|
-
return null;
|
|
103
|
-
const sliceTail = parsedSlice
|
|
104
|
-
? parsedSlice.suffix.length > 0
|
|
105
|
-
? `${parsedSlice.numeric}${parsedSlice.suffix}`
|
|
106
|
-
: `${parsedSlice.numeric}`
|
|
107
|
-
: "";
|
|
108
|
-
const sliceId = parsedSlice ? parsedSlice.id : parsedUnit.sliceId;
|
|
109
|
-
let unitId = parsedSlice ? `U-${sliceTail}` : parsedUnit.unitId;
|
|
110
|
-
if (cells.length >= 2) {
|
|
111
|
-
const col2 = stripDecorations(cells[1]);
|
|
112
|
-
if (col2.length > 0) {
|
|
113
|
-
const normalized = tokenToSliceAndUnit(col2);
|
|
114
|
-
unitId = normalized ? normalized.unitId : col2;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return { sliceId, unitId };
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Parse `## Parallel Execution Plan` managed block for wave headings and
|
|
121
|
-
* member declarations. Recognizes BOTH the `**Members:**` / `Members:`
|
|
122
|
-
* line shape AND the markdown-table shape
|
|
123
|
-
* (`| sliceId | unit | dependsOn | …`).
|
|
124
|
-
*
|
|
125
|
-
* Wave headings accepted (case-insensitive, trailing text allowed):
|
|
126
|
-
* - `### Wave 04`
|
|
127
|
-
* - `### Wave W-04`
|
|
128
|
-
* - `### Wave W-04 — after fan-in W-03 (5 lanes …)`
|
|
129
|
-
*
|
|
130
|
-
* Within a single wave the parser dedupes by `sliceId`: if the same
|
|
131
|
-
* slice appears in both `**Members:**` and a table row, the first
|
|
132
|
-
* occurrence wins (line-order). Cross-wave duplicates still throw
|
|
133
|
-
* `WavePlanDuplicateSliceError`.
|
|
134
|
-
*
|
|
135
|
-
* Malformed member tokens are skipped. Empty waves (heading present
|
|
136
|
-
* but neither a Members line nor any matching `| S-NN |` row found
|
|
137
|
-
* before the next heading) are RETURNED with `members: []` so callers
|
|
138
|
-
* can surface the boundary; classification is up to the caller.
|
|
139
|
-
*/
|
|
140
|
-
export function parseParallelExecutionPlanWaves(planMarkdown) {
|
|
141
|
-
const body = extractParallelExecutionManagedBody(planMarkdown);
|
|
142
|
-
if (!body)
|
|
143
|
-
return [];
|
|
144
|
-
const lines = body.split(/\r?\n/u);
|
|
145
|
-
const waves = [];
|
|
146
|
-
let current = null;
|
|
147
|
-
const seenSlices = new Set();
|
|
148
|
-
let inWaveSlicesSeen = new Set();
|
|
149
|
-
const flushCurrent = () => {
|
|
150
|
-
if (current) {
|
|
151
|
-
waves.push(current);
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
/**
|
|
155
|
-
* Strict add: throw on duplicates within the same wave OR across waves.
|
|
156
|
-
* Used for the `**Members:**` path so the duplicate-detection
|
|
157
|
-
* contract is preserved bit-identically.
|
|
158
|
-
*/
|
|
159
|
-
const addMemberStrict = (member) => {
|
|
160
|
-
if (!current)
|
|
161
|
-
return;
|
|
162
|
-
if (inWaveSlicesSeen.has(member.sliceId) ||
|
|
163
|
-
seenSlices.has(member.sliceId)) {
|
|
164
|
-
throw new WavePlanDuplicateSliceError(`duplicate slice ${member.sliceId} in Parallel Execution Plan managed block`);
|
|
165
|
-
}
|
|
166
|
-
seenSlices.add(member.sliceId);
|
|
167
|
-
inWaveSlicesSeen.add(member.sliceId);
|
|
168
|
-
current.members.push(member);
|
|
169
|
-
};
|
|
170
|
-
/**
|
|
171
|
-
* Lenient add: silently dedupe duplicates within the same wave (so the
|
|
172
|
-
* documented "Members + table both present" case keeps the Members
|
|
173
|
-
* declaration as authoritative); still throw on cross-wave duplicates
|
|
174
|
-
* to surface real plan-authoring bugs.
|
|
175
|
-
*/
|
|
176
|
-
const addMemberDedupInWave = (member) => {
|
|
177
|
-
if (!current)
|
|
178
|
-
return;
|
|
179
|
-
if (inWaveSlicesSeen.has(member.sliceId))
|
|
180
|
-
return;
|
|
181
|
-
if (seenSlices.has(member.sliceId)) {
|
|
182
|
-
throw new WavePlanDuplicateSliceError(`duplicate slice ${member.sliceId} in Parallel Execution Plan managed block`);
|
|
183
|
-
}
|
|
184
|
-
seenSlices.add(member.sliceId);
|
|
185
|
-
inWaveSlicesSeen.add(member.sliceId);
|
|
186
|
-
current.members.push(member);
|
|
187
|
-
};
|
|
188
|
-
for (const rawLine of lines) {
|
|
189
|
-
const trimmed = rawLine.trim();
|
|
190
|
-
const waveMatch = /^###\s+Wave\s+(?:W-)?(\d+)\b/iu.exec(trimmed);
|
|
191
|
-
if (waveMatch) {
|
|
192
|
-
flushCurrent();
|
|
193
|
-
const n = waveMatch[1];
|
|
194
|
-
current = { waveId: `W-${n.padStart(2, "0")}`, members: [] };
|
|
195
|
-
inWaveSlicesSeen = new Set();
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (!current)
|
|
199
|
-
continue;
|
|
200
|
-
const membersCsv = extractMembersListFromLine(trimmed);
|
|
201
|
-
if (membersCsv !== null) {
|
|
202
|
-
const parts = membersCsv
|
|
203
|
-
.split(/,/u)
|
|
204
|
-
.map((p) => p.trim())
|
|
205
|
-
.filter((p) => p.length > 0);
|
|
206
|
-
for (const part of parts) {
|
|
207
|
-
const ids = tokenToSliceAndUnit(part);
|
|
208
|
-
if (!ids)
|
|
209
|
-
continue;
|
|
210
|
-
addMemberStrict(ids);
|
|
211
|
-
}
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
const tableMember = parseTableRowMember(trimmed);
|
|
215
|
-
if (tableMember) {
|
|
216
|
-
addMemberDedupInWave(tableMember);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
flushCurrent();
|
|
220
|
-
return waves;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Parse a single wave-NN.md: prefer a `Members:` line in the header; otherwise
|
|
224
|
-
* collect distinct S-N tokens in the first lines (legacy).
|
|
225
|
-
*/
|
|
226
|
-
export function parseWavePlanFileBody(body, waveId) {
|
|
227
|
-
const members = [];
|
|
228
|
-
const seen = new Set();
|
|
229
|
-
const headLines = body.split(/\r?\n/u).slice(0, 120);
|
|
230
|
-
let membersCsv = null;
|
|
231
|
-
for (const raw of headLines) {
|
|
232
|
-
membersCsv = extractMembersListFromLine(raw.trim());
|
|
233
|
-
if (membersCsv !== null)
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
if (membersCsv !== null) {
|
|
237
|
-
for (const part of membersCsv.split(/,/u)) {
|
|
238
|
-
const ids = tokenToSliceAndUnit(part);
|
|
239
|
-
if (!ids)
|
|
240
|
-
continue;
|
|
241
|
-
if (seen.has(ids.sliceId)) {
|
|
242
|
-
throw new WavePlanDuplicateSliceError(`duplicate slice ${ids.sliceId} in ${waveId} wave file`);
|
|
243
|
-
}
|
|
244
|
-
seen.add(ids.sliceId);
|
|
245
|
-
members.push(ids);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (members.length === 0) {
|
|
249
|
-
const regex = /\b(S-\d+(?:[a-z][a-z0-9]*)?)\b/giu;
|
|
250
|
-
let match;
|
|
251
|
-
while ((match = regex.exec(body)) !== null) {
|
|
252
|
-
const ids = tokenToSliceAndUnit(match[1]);
|
|
253
|
-
if (!ids)
|
|
254
|
-
continue;
|
|
255
|
-
if (seen.has(ids.sliceId))
|
|
256
|
-
continue;
|
|
257
|
-
seen.add(ids.sliceId);
|
|
258
|
-
members.push(ids);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return { waveId, members };
|
|
262
|
-
}
|
|
263
|
-
export async function parseWavePlanDirectory(artifactsDir) {
|
|
264
|
-
const wavePlansDir = path.join(artifactsDir, "wave-plans");
|
|
265
|
-
let entries = [];
|
|
266
|
-
try {
|
|
267
|
-
entries = await fs.readdir(wavePlansDir);
|
|
268
|
-
}
|
|
269
|
-
catch {
|
|
270
|
-
return [];
|
|
271
|
-
}
|
|
272
|
-
const out = [];
|
|
273
|
-
for (const name of [...entries].sort()) {
|
|
274
|
-
const match = /^wave-(\d+)\.md$/u.exec(name);
|
|
275
|
-
if (!match)
|
|
276
|
-
continue;
|
|
277
|
-
const waveId = `W-${match[1].padStart(2, "0")}`;
|
|
278
|
-
const body = await fs.readFile(path.join(wavePlansDir, name), "utf8");
|
|
279
|
-
const wave = parseWavePlanFileBody(body, waveId);
|
|
280
|
-
if (wave.members.length > 0) {
|
|
281
|
-
out.push(wave);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return out;
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Merge wave definitions: managed Parallel Execution Plan first, then wave-NN.md.
|
|
288
|
-
* Same slice must map to the same wave id and unit id in both sources or a
|
|
289
|
-
* `WavePlanMergeConflictError` is thrown.
|
|
290
|
-
*/
|
|
291
|
-
export function mergeParallelWaveDefinitions(primary, secondary) {
|
|
292
|
-
const byWave = new Map();
|
|
293
|
-
const sliceBinding = new Map();
|
|
294
|
-
const addWaves = (waves) => {
|
|
295
|
-
for (const wave of waves) {
|
|
296
|
-
let memMap = byWave.get(wave.waveId);
|
|
297
|
-
if (!memMap) {
|
|
298
|
-
memMap = new Map();
|
|
299
|
-
byWave.set(wave.waveId, memMap);
|
|
300
|
-
}
|
|
301
|
-
for (const member of wave.members) {
|
|
302
|
-
const prev = sliceBinding.get(member.sliceId);
|
|
303
|
-
if (prev) {
|
|
304
|
-
if (prev.waveId !== wave.waveId || prev.unitId !== member.unitId) {
|
|
305
|
-
throw new WavePlanMergeConflictError(`slice ${member.sliceId}: conflicting wave plan sources (wave ${prev.waveId} vs ${wave.waveId}, unit ${prev.unitId} vs ${member.unitId})`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
sliceBinding.set(member.sliceId, { waveId: wave.waveId, unitId: member.unitId });
|
|
310
|
-
}
|
|
311
|
-
memMap.set(member.sliceId, member);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
addWaves(primary);
|
|
316
|
-
addWaves(secondary);
|
|
317
|
-
return [...byWave.entries()]
|
|
318
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
319
|
-
.map(([wid, memMap]) => ({
|
|
320
|
-
waveId: wid,
|
|
321
|
-
members: [...memMap.values()].sort((p, q) => compareSliceIds(p.sliceId, q.sliceId))
|
|
322
|
-
}));
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* One-line operator hint after sync when a multi-member wave exists.
|
|
326
|
-
*/
|
|
327
|
-
export function formatNextParallelWaveSyncHint(merged) {
|
|
328
|
-
const candidate = merged.find((w) => w.members.length >= 2);
|
|
329
|
-
if (!candidate)
|
|
330
|
-
return null;
|
|
331
|
-
const ids = candidate.members.map((m) => m.sliceId).join(", ");
|
|
332
|
-
return `Parallel Execution Plan: ${candidate.waveId} has ${candidate.members.length} parallel members (${ids}).`;
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Parse parallel-metadata bullets from an implementation unit body.
|
|
336
|
-
* Missing keys use conservative defaults (`dependsOn: []`, `parallelizable: true`
|
|
337
|
-
* unless `legacyParallelDefaultSerial` is set).
|
|
338
|
-
*/
|
|
339
|
-
export function parseImplementationUnitParallelFields(unit, options) {
|
|
340
|
-
const text = unit.body;
|
|
341
|
-
const pick = (label) => {
|
|
342
|
-
const esc = label.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
343
|
-
const bold = new RegExp(`^[-*]\\s*\\*\\*${esc}:\\*\\*\\s*(.*)$`, "imu");
|
|
344
|
-
const legacy = new RegExp(`^[-*]\\s*\\*{0,2}${esc}\\*{0,2}\\s*:\\s*(.*)$`, "imu");
|
|
345
|
-
for (const rawLine of text.split(/\r?\n/u)) {
|
|
346
|
-
const line = rawLine.trim();
|
|
347
|
-
const mb = bold.exec(line);
|
|
348
|
-
if (mb)
|
|
349
|
-
return mb[1]?.trim();
|
|
350
|
-
const ml = legacy.exec(line);
|
|
351
|
-
if (ml)
|
|
352
|
-
return ml[1]?.trim();
|
|
353
|
-
}
|
|
354
|
-
return undefined;
|
|
355
|
-
};
|
|
356
|
-
const id = pick("id") ?? unit.id;
|
|
357
|
-
const depRaw = pick("dependsOn") ?? pick("depends on") ?? "";
|
|
358
|
-
const dependsOn = depRaw
|
|
359
|
-
.split(/,/u)
|
|
360
|
-
.map((s) => s.trim())
|
|
361
|
-
.filter((s) => s.length > 0 && !/^none$/iu.test(s));
|
|
362
|
-
const pathsRaw = pick("claimedPaths") ?? pick("claimed paths") ?? "";
|
|
363
|
-
const claimedPaths = pathsRaw.length > 0
|
|
364
|
-
? pathsRaw
|
|
365
|
-
.split(",")
|
|
366
|
-
.map((s) => s.replace(/[`\s]/gu, "").trim())
|
|
367
|
-
.filter((s) => s.length > 0)
|
|
368
|
-
: [...unit.paths];
|
|
369
|
-
const explicitParallel = pick("parallelizable");
|
|
370
|
-
const parallelRaw = (explicitParallel ?? "true").toLowerCase();
|
|
371
|
-
let parallelizable = parallelRaw === "true" || parallelRaw === "yes" || parallelRaw === "y";
|
|
372
|
-
if (options?.legacyParallelDefaultSerial && explicitParallel === undefined) {
|
|
373
|
-
parallelizable = false;
|
|
374
|
-
}
|
|
375
|
-
const riskRaw = (pick("riskTier") ?? pick("risk tier") ?? "standard").toLowerCase();
|
|
376
|
-
const riskTier = riskRaw === "low" ? "low" : riskRaw === "high" ? "high" : "standard";
|
|
377
|
-
const laneRaw = pick("lane");
|
|
378
|
-
const lane = laneRaw && laneRaw.length > 0 ? laneRaw : undefined;
|
|
379
|
-
return { unitId: id, dependsOn, claimedPaths, parallelizable, riskTier, lane };
|
|
380
|
-
}
|
|
381
|
-
function unitBodyHasV613ParallelBullet(body, label) {
|
|
382
|
-
const esc = label.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
383
|
-
const bold = new RegExp(`^[-*]\\s*\\*\\*${esc}:\\*\\*`, "imu");
|
|
384
|
-
const legacy = new RegExp(`^[-*]\\s*\\*{0,2}${esc}\\*{0,2}\\s*:`, "imu");
|
|
385
|
-
return body.split(/\r?\n/u).some((raw) => {
|
|
386
|
-
const line = raw.trim();
|
|
387
|
-
return bold.test(line) || legacy.test(line);
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* True when the plan has implementation units but any unit is missing
|
|
392
|
-
* `dependsOn` / `claimedPaths` / `parallelizable` / `riskTier` bullets.
|
|
393
|
-
*/
|
|
394
|
-
export function planArtifactLacksV613ParallelMetadata(planMarkdown) {
|
|
395
|
-
const units = parseImplementationUnits(planMarkdown);
|
|
396
|
-
if (units.length === 0)
|
|
397
|
-
return false;
|
|
398
|
-
const labels = ["dependsOn", "claimedPaths", "parallelizable", "riskTier"];
|
|
399
|
-
return units.some((u) => !labels.every((lab) => unitBodyHasV613ParallelBullet(u.body, lab)));
|
|
400
|
-
}
|
|
401
|
-
export function compareCanonicalUnitIds(a, b) {
|
|
402
|
-
const ma = /^U-(\d+)$/u.exec(a);
|
|
403
|
-
const mb = /^U-(\d+)$/u.exec(b);
|
|
404
|
-
if (ma && mb)
|
|
405
|
-
return Number(ma[1]) - Number(mb[1]);
|
|
406
|
-
return a.localeCompare(b);
|
|
407
|
-
}
|
|
408
|
-
function topoSortPlanUnits(meta) {
|
|
409
|
-
const idSet = new Set(meta.map((m) => m.unitId));
|
|
410
|
-
const incoming = new Map();
|
|
411
|
-
for (const m of meta)
|
|
412
|
-
incoming.set(m.unitId, 0);
|
|
413
|
-
for (const m of meta) {
|
|
414
|
-
for (const d of m.dependsOn) {
|
|
415
|
-
if (!idSet.has(d))
|
|
416
|
-
continue;
|
|
417
|
-
incoming.set(m.unitId, (incoming.get(m.unitId) ?? 0) + 1);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
const queue = meta
|
|
421
|
-
.filter((m) => (incoming.get(m.unitId) ?? 0) === 0)
|
|
422
|
-
.sort((a, b) => compareCanonicalUnitIds(a.unitId, b.unitId));
|
|
423
|
-
const out = [];
|
|
424
|
-
while (queue.length > 0) {
|
|
425
|
-
const m = queue.shift();
|
|
426
|
-
out.push(m);
|
|
427
|
-
for (const other of meta) {
|
|
428
|
-
if (!other.dependsOn.includes(m.unitId))
|
|
429
|
-
continue;
|
|
430
|
-
const v = (incoming.get(other.unitId) ?? 0) - 1;
|
|
431
|
-
incoming.set(other.unitId, v);
|
|
432
|
-
if (v === 0) {
|
|
433
|
-
queue.push(other);
|
|
434
|
-
queue.sort((a, b) => compareCanonicalUnitIds(a.unitId, b.unitId));
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
if (out.length !== meta.length) {
|
|
439
|
-
return [...meta].sort((a, b) => compareCanonicalUnitIds(a.unitId, b.unitId));
|
|
440
|
-
}
|
|
441
|
-
return out;
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Group implementation units into waves: topological order, then greedy
|
|
445
|
-
* placement with disjoint `claimedPaths` and `cap` members per wave.
|
|
446
|
-
*/
|
|
447
|
-
export function buildConflictAwareWavesFromUnits(units, cap) {
|
|
448
|
-
const metaList = units.map((u) => parseImplementationUnitParallelFields(u));
|
|
449
|
-
const ordered = topoSortPlanUnits(metaList);
|
|
450
|
-
const unitById = new Map(units.map((u) => [parseImplementationUnitParallelFields(u).unitId, u]));
|
|
451
|
-
const waves = [];
|
|
452
|
-
const allMetaIds = new Set(metaList.map((m) => m.unitId));
|
|
453
|
-
for (const m of ordered) {
|
|
454
|
-
const u = unitById.get(m.unitId);
|
|
455
|
-
if (!u)
|
|
456
|
-
continue;
|
|
457
|
-
let placed = false;
|
|
458
|
-
for (let wi = 0; wi < waves.length; wi++) {
|
|
459
|
-
const wave = waves[wi];
|
|
460
|
-
if (wave.length >= cap)
|
|
461
|
-
continue;
|
|
462
|
-
const priorIds = new Set(waves
|
|
463
|
-
.slice(0, wi)
|
|
464
|
-
.flat()
|
|
465
|
-
.map((wu) => parseImplementationUnitParallelFields(wu).unitId));
|
|
466
|
-
const depsOk = m.dependsOn.every((d) => priorIds.has(d) || !allMetaIds.has(d));
|
|
467
|
-
if (!depsOk)
|
|
468
|
-
continue;
|
|
469
|
-
const pathsInWave = new Set();
|
|
470
|
-
for (const wu of wave) {
|
|
471
|
-
for (const p of parseImplementationUnitParallelFields(wu).claimedPaths) {
|
|
472
|
-
pathsInWave.add(p);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
const clash = m.claimedPaths.some((p) => pathsInWave.has(p));
|
|
476
|
-
if (clash)
|
|
477
|
-
continue;
|
|
478
|
-
wave.push(u);
|
|
479
|
-
placed = true;
|
|
480
|
-
break;
|
|
481
|
-
}
|
|
482
|
-
if (!placed) {
|
|
483
|
-
waves.push([u]);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
return waves;
|
|
487
|
-
}
|
|
488
|
-
export function buildParallelExecutionPlanSection(waves, cap) {
|
|
489
|
-
const lines = [];
|
|
490
|
-
lines.push(PARALLEL_EXEC_MANAGED_START);
|
|
491
|
-
lines.push("## Parallel Execution Plan");
|
|
492
|
-
lines.push("");
|
|
493
|
-
lines.push(`- **Cap:** ${cap} parallel units per wave (conflict-aware via \`claimedPaths\`).`);
|
|
494
|
-
lines.push("");
|
|
495
|
-
for (let i = 0; i < waves.length; i += 1) {
|
|
496
|
-
const w = waves[i];
|
|
497
|
-
const ids = w.map((unit) => parseImplementationUnitParallelFields(unit).unitId);
|
|
498
|
-
const union = new Set();
|
|
499
|
-
for (const unit of w) {
|
|
500
|
-
for (const p of parseImplementationUnitParallelFields(unit).claimedPaths) {
|
|
501
|
-
union.add(p);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
lines.push(`### Wave ${padWaveIndex(i + 1)}`);
|
|
505
|
-
lines.push(`- **Members:** ${ids.join(", ")}`);
|
|
506
|
-
lines.push(`- **Claimed paths union:** ${[...union].sort().join(", ") || "(none)"}`);
|
|
507
|
-
lines.push("");
|
|
508
|
-
}
|
|
509
|
-
lines.push(PARALLEL_EXEC_MANAGED_END);
|
|
510
|
-
return lines.join("\n");
|
|
511
|
-
}
|
|
512
|
-
/**
|
|
513
|
-
* Replace or append the managed Parallel Execution Plan block.
|
|
514
|
-
*/
|
|
515
|
-
export function upsertParallelExecutionPlanSection(planMarkdown, managedBlock) {
|
|
516
|
-
const startIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_START);
|
|
517
|
-
const endIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_END);
|
|
518
|
-
if (startIdx >= 0 && endIdx > startIdx) {
|
|
519
|
-
const before = planMarkdown.slice(0, startIdx);
|
|
520
|
-
const after = planMarkdown.slice(endIdx + PARALLEL_EXEC_MANAGED_END.length);
|
|
521
|
-
const joined = `${before}${managedBlock}${after}`;
|
|
522
|
-
return joined.endsWith("\n") ? joined : `${joined}\n`;
|
|
523
|
-
}
|
|
524
|
-
const trimmed = planMarkdown.replace(/\s+$/u, "");
|
|
525
|
-
return `${trimmed}\n\n${managedBlock}\n`;
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* Parse `## Implementation Units` section into individual unit blocks.
|
|
529
|
-
* Recognizes the canonical heading shape in the TDD-velocity plan template
|
|
530
|
-
* (`### Implementation Unit U-<n>`). Tolerant of `Files:` listed either
|
|
531
|
-
* inline or as a `- **Files (...):**` bullet block.
|
|
532
|
-
*/
|
|
533
|
-
export function parseImplementationUnits(planMarkdown) {
|
|
534
|
-
const units = [];
|
|
535
|
-
const headingRegex = /(^|\n)###\s+Implementation Unit\s+(U-\d+)\b/gu;
|
|
536
|
-
const matches = [];
|
|
537
|
-
let match;
|
|
538
|
-
while ((match = headingRegex.exec(planMarkdown)) !== null) {
|
|
539
|
-
const offset = match[1] === "" ? 0 : 1; // strip the leading newline if present
|
|
540
|
-
matches.push({
|
|
541
|
-
id: match[2],
|
|
542
|
-
start: match.index + offset,
|
|
543
|
-
headingEnd: match.index + match[0].length
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
for (let i = 0; i < matches.length; i += 1) {
|
|
547
|
-
const current = matches[i];
|
|
548
|
-
const next = matches[i + 1];
|
|
549
|
-
let endIndex = next ? next.start : planMarkdown.length;
|
|
550
|
-
// If a higher-level H2 (`## ...`) appears before the next unit, end at the H2.
|
|
551
|
-
const tail = planMarkdown.slice(current.headingEnd, endIndex);
|
|
552
|
-
const sectionBreak = /\n##\s+\S/u.exec(tail);
|
|
553
|
-
if (sectionBreak) {
|
|
554
|
-
endIndex = current.headingEnd + sectionBreak.index + 1; // include the trailing newline
|
|
555
|
-
}
|
|
556
|
-
const body = planMarkdown.slice(current.start, endIndex).replace(/\s+$/u, "");
|
|
557
|
-
units.push({
|
|
558
|
-
id: current.id,
|
|
559
|
-
body,
|
|
560
|
-
paths: extractPathsLine(body)
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
return units;
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Pull repo-relative paths from a `Files:` line or the `Files (...)` bullet
|
|
567
|
-
* block. Both shapes appear in the wild; the parser extracts after the colon
|
|
568
|
-
* and splits on commas. Empty/whitespace items are dropped.
|
|
569
|
-
*/
|
|
570
|
-
export function extractPathsLine(unitBody) {
|
|
571
|
-
const lines = unitBody.split(/\r?\n/u);
|
|
572
|
-
for (const rawLine of lines) {
|
|
573
|
-
const line = rawLine.trim();
|
|
574
|
-
const filesMatch = /^[-*]?\s*\*?\*?Files\s*(?:\([^)]*\))?\s*:\*?\*?\s*(.*)$/iu.exec(line);
|
|
575
|
-
if (!filesMatch)
|
|
576
|
-
continue;
|
|
577
|
-
const remainder = filesMatch[1].trim();
|
|
578
|
-
if (remainder.length === 0)
|
|
579
|
-
continue;
|
|
580
|
-
return remainder
|
|
581
|
-
.split(",")
|
|
582
|
-
.map((item) => item.replace(/[`*]/gu, "").trim())
|
|
583
|
-
.filter((item) => item.length > 0);
|
|
584
|
-
}
|
|
585
|
-
return [];
|
|
586
|
-
}
|
|
587
|
-
export function parsePlanSplitWavesArgs(tokens) {
|
|
588
|
-
let waveSize = PLAN_SPLIT_DEFAULT_WAVE_SIZE;
|
|
589
|
-
let dryRun = false;
|
|
590
|
-
let force = false;
|
|
591
|
-
let json = false;
|
|
592
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
593
|
-
const token = tokens[i];
|
|
594
|
-
const next = tokens[i + 1];
|
|
595
|
-
if (token === "--dry-run") {
|
|
596
|
-
dryRun = true;
|
|
597
|
-
continue;
|
|
598
|
-
}
|
|
599
|
-
if (token === "--force") {
|
|
600
|
-
force = true;
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
if (token === "--json") {
|
|
604
|
-
json = true;
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
if (token === "--wave-size" || token.startsWith("--wave-size=")) {
|
|
608
|
-
let raw = "";
|
|
609
|
-
if (token.startsWith("--wave-size=")) {
|
|
610
|
-
raw = token.slice("--wave-size=".length);
|
|
611
|
-
}
|
|
612
|
-
else {
|
|
613
|
-
if (next === undefined || next.startsWith("--")) {
|
|
614
|
-
throw new Error("--wave-size requires an integer value.");
|
|
615
|
-
}
|
|
616
|
-
raw = next;
|
|
617
|
-
i += 1;
|
|
618
|
-
}
|
|
619
|
-
const trimmed = raw.trim();
|
|
620
|
-
if (!/^[0-9]+$/u.test(trimmed)) {
|
|
621
|
-
throw new Error("--wave-size must be a positive integer.");
|
|
622
|
-
}
|
|
623
|
-
waveSize = Number(trimmed);
|
|
624
|
-
if (waveSize < 1) {
|
|
625
|
-
throw new Error("--wave-size must be >= 1.");
|
|
626
|
-
}
|
|
627
|
-
continue;
|
|
628
|
-
}
|
|
629
|
-
throw new Error(`Unknown flag for internal plan-split-waves: ${token}`);
|
|
630
|
-
}
|
|
631
|
-
return { waveSize, dryRun, force, json };
|
|
632
|
-
}
|
|
633
|
-
function padWaveIndex(index) {
|
|
634
|
-
return index.toString().padStart(2, "0");
|
|
635
|
-
}
|
|
636
|
-
function buildWaveFileBody(waveIndex, units, sourceLabel) {
|
|
637
|
-
const idsRange = `${units[0].id}..${units[units.length - 1].id}`;
|
|
638
|
-
return [
|
|
639
|
-
`# Wave ${padWaveIndex(waveIndex)}`,
|
|
640
|
-
"",
|
|
641
|
-
`Source: ${sourceLabel} units ${idsRange}`,
|
|
642
|
-
"",
|
|
643
|
-
"## Implementation Units",
|
|
644
|
-
"",
|
|
645
|
-
units.map((unit) => unit.body.trim()).join("\n\n"),
|
|
646
|
-
""
|
|
647
|
-
].join("\n");
|
|
648
|
-
}
|
|
649
|
-
function buildWavePlansSection(waveFiles) {
|
|
650
|
-
const lines = [];
|
|
651
|
-
lines.push(WAVE_MANAGED_START);
|
|
652
|
-
lines.push("## Wave Plans");
|
|
653
|
-
lines.push("");
|
|
654
|
-
for (let i = 0; i < waveFiles.length; i += 1) {
|
|
655
|
-
lines.push(`- Wave ${padWaveIndex(i + 1)}: \`${waveFiles[i]}\``);
|
|
656
|
-
}
|
|
657
|
-
lines.push("");
|
|
658
|
-
lines.push(WAVE_MANAGED_END);
|
|
659
|
-
return lines.join("\n");
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Replace any existing managed Wave Plans block with the new one, or append
|
|
663
|
-
* it at the end of the file when no markers are present yet. The helper
|
|
664
|
-
* never touches text outside the markers.
|
|
665
|
-
*/
|
|
666
|
-
export function upsertWavePlansSection(planMarkdown, managedBlock) {
|
|
667
|
-
const startIdx = planMarkdown.indexOf(WAVE_MANAGED_START);
|
|
668
|
-
const endIdx = planMarkdown.indexOf(WAVE_MANAGED_END);
|
|
669
|
-
if (startIdx >= 0 && endIdx > startIdx) {
|
|
670
|
-
const before = planMarkdown.slice(0, startIdx);
|
|
671
|
-
const after = planMarkdown.slice(endIdx + WAVE_MANAGED_END.length);
|
|
672
|
-
const joined = `${before}${managedBlock}${after}`;
|
|
673
|
-
return joined.endsWith("\n") ? joined : `${joined}\n`;
|
|
674
|
-
}
|
|
675
|
-
const trimmed = planMarkdown.replace(/\s+$/u, "");
|
|
676
|
-
return `${trimmed}\n\n${managedBlock}\n`;
|
|
677
|
-
}
|
|
678
|
-
export async function runPlanSplitWaves(projectRoot, args, io) {
|
|
679
|
-
const flow = await readFlowState(projectRoot).catch(() => null);
|
|
680
|
-
const track = flow?.track;
|
|
681
|
-
const planResolved = await resolveArtifactPath("plan", {
|
|
682
|
-
projectRoot,
|
|
683
|
-
track,
|
|
684
|
-
intent: "read"
|
|
685
|
-
});
|
|
686
|
-
if (!(await exists(planResolved.absPath))) {
|
|
687
|
-
io.stderr.write(`cclaw internal plan-split-waves: plan artifact not found at ${planResolved.relPath}.\n`);
|
|
688
|
-
return 1;
|
|
689
|
-
}
|
|
690
|
-
const raw = await fs.readFile(planResolved.absPath, "utf8");
|
|
691
|
-
const units = parseImplementationUnits(raw);
|
|
692
|
-
if (units.length < PLAN_SPLIT_SMALL_PLAN_THRESHOLD) {
|
|
693
|
-
const outcome = {
|
|
694
|
-
ok: true,
|
|
695
|
-
command: "plan-split-waves",
|
|
696
|
-
totalUnits: units.length,
|
|
697
|
-
waveCount: 0,
|
|
698
|
-
waveSize: args.waveSize,
|
|
699
|
-
smallPlanNoOp: true,
|
|
700
|
-
dryRun: args.dryRun,
|
|
701
|
-
waveFiles: [],
|
|
702
|
-
planUpdated: false
|
|
703
|
-
};
|
|
704
|
-
if (args.json) {
|
|
705
|
-
io.stdout.write(`${JSON.stringify(outcome)}\n`);
|
|
706
|
-
}
|
|
707
|
-
else {
|
|
708
|
-
io.stdout.write(`plan is small (${units.length} unit(s), threshold ${PLAN_SPLIT_SMALL_PLAN_THRESHOLD}); no wave split needed.\n`);
|
|
709
|
-
}
|
|
710
|
-
return 0;
|
|
711
|
-
}
|
|
712
|
-
const waves = buildConflictAwareWavesFromUnits(units, args.waveSize);
|
|
713
|
-
const artifactsDir = path.dirname(planResolved.absPath);
|
|
714
|
-
const wavePlansAbsDir = path.join(artifactsDir, WAVE_PLANS_DIR);
|
|
715
|
-
const waveFileNames = waves.map((_, idx) => `${WAVE_PLANS_DIR}/wave-${padWaveIndex(idx + 1)}.md`);
|
|
716
|
-
if (!args.dryRun && !args.force) {
|
|
717
|
-
for (const fileName of waveFileNames) {
|
|
718
|
-
const abs = path.join(artifactsDir, fileName);
|
|
719
|
-
if (await exists(abs)) {
|
|
720
|
-
io.stderr.write(`cclaw internal plan-split-waves: wave file already exists: ${path.relative(projectRoot, abs)}. Pass --force to overwrite.\n`);
|
|
721
|
-
return 1;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
if (!args.dryRun) {
|
|
726
|
-
await fs.mkdir(wavePlansAbsDir, { recursive: true });
|
|
727
|
-
for (let i = 0; i < waves.length; i += 1) {
|
|
728
|
-
const fileName = waveFileNames[i];
|
|
729
|
-
const body = buildWaveFileBody(i + 1, waves[i], planResolved.fileName);
|
|
730
|
-
await writeFileSafe(path.join(artifactsDir, fileName), body);
|
|
731
|
-
}
|
|
732
|
-
const managed = buildWavePlansSection(waveFileNames);
|
|
733
|
-
let updatedPlan = upsertWavePlansSection(raw, managed);
|
|
734
|
-
const parallelBlock = buildParallelExecutionPlanSection(waves, args.waveSize);
|
|
735
|
-
updatedPlan = upsertParallelExecutionPlanSection(updatedPlan, parallelBlock);
|
|
736
|
-
if (updatedPlan !== raw) {
|
|
737
|
-
await writeFileSafe(planResolved.absPath, updatedPlan);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
const outcome = {
|
|
741
|
-
ok: true,
|
|
742
|
-
command: "plan-split-waves",
|
|
743
|
-
totalUnits: units.length,
|
|
744
|
-
waveCount: waves.length,
|
|
745
|
-
waveSize: args.waveSize,
|
|
746
|
-
smallPlanNoOp: false,
|
|
747
|
-
dryRun: args.dryRun,
|
|
748
|
-
waveFiles: waveFileNames,
|
|
749
|
-
planUpdated: !args.dryRun
|
|
750
|
-
};
|
|
751
|
-
if (args.json) {
|
|
752
|
-
io.stdout.write(`${JSON.stringify(outcome)}\n`);
|
|
753
|
-
}
|
|
754
|
-
else if (args.dryRun) {
|
|
755
|
-
io.stdout.write(`dry run: would split ${units.length} unit(s) into ${waves.length} wave file(s) of size ${args.waveSize}:\n`);
|
|
756
|
-
for (const fileName of waveFileNames) {
|
|
757
|
-
io.stdout.write(` - ${fileName}\n`);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
else {
|
|
761
|
-
io.stdout.write(`wrote ${waves.length} wave file(s) under ${path.relative(projectRoot, wavePlansAbsDir)} and refreshed Wave Plans section in ${planResolved.relPath}.\n`);
|
|
762
|
-
}
|
|
763
|
-
return 0;
|
|
764
|
-
}
|