cclaw-cli 6.14.3 → 6.14.4

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.
@@ -1339,11 +1339,19 @@ function pickEventTs(rows) {
1339
1339
  return undefined;
1340
1340
  }
1341
1341
  /**
1342
- * v6.14.2 — slices whose terminal `refactor` / `refactor-deferred` /
1343
- * `resolve-conflict` row recorded a `completedTs` that PRECEDES the
1344
- * latest `leasedUntil` for the same slice. The lease was never
1345
- * reclaimed but the wave closed in time; the missing audit row is
1346
- * advisory bookkeeping, not a correctness failure.
1342
+ * v6.14.2 — slices whose terminal closure event recorded a `completedTs`
1343
+ * that PRECEDES the latest `leasedUntil` for the same slice. The lease
1344
+ * was never reclaimed but the wave closed in time; the missing audit
1345
+ * row is advisory bookkeeping, not a correctness failure.
1346
+ *
1347
+ * v6.14.4 — also recognize stream-mode (per-slice checkpoint) closure:
1348
+ * a `phase=green status=completed` row carrying `refactorOutcome`
1349
+ * (`inline` or `deferred`) IS the slice closure. Without this, every
1350
+ * stream-mode slice incorrectly fired `tdd_lease_expired_unreclaimed`
1351
+ * once its lease expired, even though the slice was already closed
1352
+ * (this mirrors the same predicate already used in
1353
+ * `src/internal/wave-status.ts` for `closedSlices` tracking — same
1354
+ * decision, just now applied to lease-closure detection).
1347
1355
  */
1348
1356
  function computeClosedBeforeLeaseExpiry(events) {
1349
1357
  const terminalPhases = new Set([
@@ -1353,6 +1361,15 @@ function computeClosedBeforeLeaseExpiry(events) {
1353
1361
  ]);
1354
1362
  const lastLease = new Map();
1355
1363
  const earliestTerminal = new Map();
1364
+ const recordTerminal = (sliceId, completedTs) => {
1365
+ const ts = Date.parse(completedTs);
1366
+ if (!Number.isFinite(ts))
1367
+ return;
1368
+ const prev = earliestTerminal.get(sliceId);
1369
+ if (prev === undefined || ts < prev) {
1370
+ earliestTerminal.set(sliceId, ts);
1371
+ }
1372
+ };
1356
1373
  for (const ev of events) {
1357
1374
  if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
1358
1375
  continue;
@@ -1367,16 +1384,21 @@ function computeClosedBeforeLeaseExpiry(events) {
1367
1384
  }
1368
1385
  }
1369
1386
  }
1370
- if (ev.status === "completed" &&
1371
- typeof ev.phase === "string" &&
1372
- terminalPhases.has(ev.phase) &&
1373
- typeof ev.completedTs === "string") {
1374
- const ts = Date.parse(ev.completedTs);
1375
- if (Number.isFinite(ts)) {
1376
- const prev = earliestTerminal.get(ev.sliceId);
1377
- if (prev === undefined || ts < prev) {
1378
- earliestTerminal.set(ev.sliceId, ts);
1379
- }
1387
+ if (ev.status !== "completed" || typeof ev.completedTs !== "string") {
1388
+ continue;
1389
+ }
1390
+ if (typeof ev.phase !== "string")
1391
+ continue;
1392
+ if (terminalPhases.has(ev.phase)) {
1393
+ recordTerminal(ev.sliceId, ev.completedTs);
1394
+ continue;
1395
+ }
1396
+ // v6.14.4 — stream-mode closure: GREEN-only with refactorOutcome
1397
+ // folded inline IS the slice's terminal row.
1398
+ if (ev.phase === "green" && ev.refactorOutcome) {
1399
+ const mode = ev.refactorOutcome.mode;
1400
+ if (mode === "inline" || mode === "deferred") {
1401
+ recordTerminal(ev.sliceId, ev.completedTs);
1380
1402
  }
1381
1403
  }
1382
1404
  }
@@ -57,8 +57,48 @@ export declare function extractParallelExecutionManagedBody(planMarkdown: string
57
57
  */
58
58
  export declare function extractMembersListFromLine(trimmedLine: string): string | null;
59
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.
60
+ * v6.14.4 extract a `(sliceId, unitId)` pair from a markdown table data
61
+ * row whose first column is an `S-NN` token. Used by the wave parser to
62
+ * recognize the table-format Parallel Execution Plan that hox-shape
63
+ * projects emit alongside (or instead of) the legacy `**Members:**`
64
+ * bullet line.
65
+ *
66
+ * Rules:
67
+ * - The line must start with `|` (after trimming).
68
+ * - Column 1 (after stripping markdown noise) must match `^S-(\d+)$` —
69
+ * header rows (`| sliceId | …`) and separator rows (`|---|---|…`) are
70
+ * silently skipped.
71
+ * - Column 2, when present and non-empty, becomes the `unitId`
72
+ * verbatim (after stripping whitespace + backticks/quotes/brackets).
73
+ * This preserves the hox convention of recording task ids
74
+ * (`T-010`, `T-008a`, …) in the `unit` column without forcing a
75
+ * `U-NN` derivation.
76
+ * - When column 2 is absent or empty, fall back to the legacy
77
+ * `S-NN → U-NN` derivation so the existing `**Members:**` parser path
78
+ * stays bit-identical for non-table plans.
79
+ */
80
+ export declare function parseTableRowMember(trimmedLine: string): ParsedParallelWaveMember | null;
81
+ /**
82
+ * Parse `## Parallel Execution Plan` managed block for wave headings and
83
+ * member declarations. Recognizes BOTH the legacy `**Members:**` /
84
+ * `Members:` line shape AND the markdown-table shape
85
+ * (`| sliceId | unit | dependsOn | …`) used by hox-shape projects and by
86
+ * any plan written by `cclaw-cli sync` after v6.13.x.
87
+ *
88
+ * Wave headings accepted (case-insensitive, trailing text allowed):
89
+ * - `### Wave 04`
90
+ * - `### Wave W-04`
91
+ * - `### Wave W-04 — после успешного fan-in W-03 (5 lanes …)`
92
+ *
93
+ * Within a single wave the parser dedupes by `sliceId`: if the same
94
+ * slice appears in both `**Members:**` and a table row, the first
95
+ * occurrence wins (line-order). Cross-wave duplicates still throw
96
+ * `WavePlanDuplicateSliceError`.
97
+ *
98
+ * Malformed member tokens are skipped. Empty waves (heading present
99
+ * but neither a Members line nor any matching `| S-NN |` row found
100
+ * before the next heading) are RETURNED with `members: []` so callers
101
+ * can surface the boundary; classification is up to the caller.
62
102
  */
63
103
  export declare function parseParallelExecutionPlanWaves(planMarkdown: string): ParsedParallelWave[];
64
104
  /**
@@ -61,8 +61,73 @@ export function extractMembersListFromLine(trimmedLine) {
61
61
  return null;
62
62
  }
63
63
  /**
64
- * Parse `## Parallel Execution Plan` managed block for wave headings and Members lines.
65
- * Malformed member tokens are skipped. Duplicate slice ids in one plan source throw.
64
+ * v6.14.4 extract a `(sliceId, unitId)` pair from a markdown table data
65
+ * row whose first column is an `S-NN` token. Used by the wave parser to
66
+ * recognize the table-format Parallel Execution Plan that hox-shape
67
+ * projects emit alongside (or instead of) the legacy `**Members:**`
68
+ * bullet line.
69
+ *
70
+ * Rules:
71
+ * - The line must start with `|` (after trimming).
72
+ * - Column 1 (after stripping markdown noise) must match `^S-(\d+)$` —
73
+ * header rows (`| sliceId | …`) and separator rows (`|---|---|…`) are
74
+ * silently skipped.
75
+ * - Column 2, when present and non-empty, becomes the `unitId`
76
+ * verbatim (after stripping whitespace + backticks/quotes/brackets).
77
+ * This preserves the hox convention of recording task ids
78
+ * (`T-010`, `T-008a`, …) in the `unit` column without forcing a
79
+ * `U-NN` derivation.
80
+ * - When column 2 is absent or empty, fall back to the legacy
81
+ * `S-NN → U-NN` derivation so the existing `**Members:**` parser path
82
+ * stays bit-identical for non-table plans.
83
+ */
84
+ export function parseTableRowMember(trimmedLine) {
85
+ if (!trimmedLine.startsWith("|"))
86
+ return null;
87
+ const inner = trimmedLine.replace(/^\|/u, "").replace(/\|\s*$/u, "");
88
+ if (inner.length === 0)
89
+ return null;
90
+ const cells = inner.split("|").map((cell) => cell.trim());
91
+ if (cells.length === 0)
92
+ return null;
93
+ const stripDecorations = (raw) => raw.replace(/^[`"'[\]()]+|[`"'[\]()]+$/gu, "").trim();
94
+ const col1 = stripDecorations(cells[0]);
95
+ const sliceMatch = /^S-(\d+)$/u.exec(col1);
96
+ if (!sliceMatch)
97
+ return null;
98
+ const sliceNum = sliceMatch[1];
99
+ const sliceId = `S-${sliceNum}`;
100
+ let unitId = `U-${sliceNum}`;
101
+ if (cells.length >= 2) {
102
+ const col2 = stripDecorations(cells[1]);
103
+ if (col2.length > 0) {
104
+ const normalized = tokenToSliceAndUnit(col2);
105
+ unitId = normalized ? normalized.unitId : col2;
106
+ }
107
+ }
108
+ return { sliceId, unitId };
109
+ }
110
+ /**
111
+ * Parse `## Parallel Execution Plan` managed block for wave headings and
112
+ * member declarations. Recognizes BOTH the legacy `**Members:**` /
113
+ * `Members:` line shape AND the markdown-table shape
114
+ * (`| sliceId | unit | dependsOn | …`) used by hox-shape projects and by
115
+ * any plan written by `cclaw-cli sync` after v6.13.x.
116
+ *
117
+ * Wave headings accepted (case-insensitive, trailing text allowed):
118
+ * - `### Wave 04`
119
+ * - `### Wave W-04`
120
+ * - `### Wave W-04 — после успешного fan-in W-03 (5 lanes …)`
121
+ *
122
+ * Within a single wave the parser dedupes by `sliceId`: if the same
123
+ * slice appears in both `**Members:**` and a table row, the first
124
+ * occurrence wins (line-order). Cross-wave duplicates still throw
125
+ * `WavePlanDuplicateSliceError`.
126
+ *
127
+ * Malformed member tokens are skipped. Empty waves (heading present
128
+ * but neither a Members line nor any matching `| S-NN |` row found
129
+ * before the next heading) are RETURNED with `members: []` so callers
130
+ * can surface the boundary; classification is up to the caller.
66
131
  */
67
132
  export function parseParallelExecutionPlanWaves(planMarkdown) {
68
133
  const body = extractParallelExecutionManagedBody(planMarkdown);
@@ -72,22 +137,60 @@ export function parseParallelExecutionPlanWaves(planMarkdown) {
72
137
  const waves = [];
73
138
  let current = null;
74
139
  const seenSlices = new Set();
140
+ let inWaveSlicesSeen = new Set();
75
141
  const flushCurrent = () => {
76
- if (current && current.members.length > 0) {
142
+ if (current) {
77
143
  waves.push(current);
78
144
  }
79
145
  };
146
+ /**
147
+ * Strict add: throw on duplicates within the same wave OR across waves.
148
+ * Used for the `**Members:**` path so v6.13.1's duplicate-detection
149
+ * contract is preserved bit-identically.
150
+ */
151
+ const addMemberStrict = (member) => {
152
+ if (!current)
153
+ return;
154
+ if (inWaveSlicesSeen.has(member.sliceId) ||
155
+ seenSlices.has(member.sliceId)) {
156
+ throw new WavePlanDuplicateSliceError(`duplicate slice ${member.sliceId} in Parallel Execution Plan managed block`);
157
+ }
158
+ seenSlices.add(member.sliceId);
159
+ inWaveSlicesSeen.add(member.sliceId);
160
+ current.members.push(member);
161
+ };
162
+ /**
163
+ * Lenient add: silently dedupe duplicates within the same wave (so the
164
+ * documented "Members + table both present" case keeps the Members
165
+ * declaration as authoritative); still throw on cross-wave duplicates
166
+ * to surface real plan-authoring bugs.
167
+ */
168
+ const addMemberDedupInWave = (member) => {
169
+ if (!current)
170
+ return;
171
+ if (inWaveSlicesSeen.has(member.sliceId))
172
+ return;
173
+ if (seenSlices.has(member.sliceId)) {
174
+ throw new WavePlanDuplicateSliceError(`duplicate slice ${member.sliceId} in Parallel Execution Plan managed block`);
175
+ }
176
+ seenSlices.add(member.sliceId);
177
+ inWaveSlicesSeen.add(member.sliceId);
178
+ current.members.push(member);
179
+ };
80
180
  for (const rawLine of lines) {
81
181
  const trimmed = rawLine.trim();
82
- const waveMatch = /^###\s+Wave\s+(\d+)\s*$/iu.exec(trimmed);
182
+ const waveMatch = /^###\s+Wave\s+(?:W-)?(\d+)\b/iu.exec(trimmed);
83
183
  if (waveMatch) {
84
184
  flushCurrent();
85
185
  const n = waveMatch[1];
86
186
  current = { waveId: `W-${n.padStart(2, "0")}`, members: [] };
187
+ inWaveSlicesSeen = new Set();
87
188
  continue;
88
189
  }
190
+ if (!current)
191
+ continue;
89
192
  const membersCsv = extractMembersListFromLine(trimmed);
90
- if (membersCsv !== null && current) {
193
+ if (membersCsv !== null) {
91
194
  const parts = membersCsv
92
195
  .split(/,/u)
93
196
  .map((p) => p.trim())
@@ -96,12 +199,13 @@ export function parseParallelExecutionPlanWaves(planMarkdown) {
96
199
  const ids = tokenToSliceAndUnit(part);
97
200
  if (!ids)
98
201
  continue;
99
- if (seenSlices.has(ids.sliceId)) {
100
- throw new WavePlanDuplicateSliceError(`duplicate slice ${ids.sliceId} in Parallel Execution Plan managed block`);
101
- }
102
- seenSlices.add(ids.sliceId);
103
- current.members.push(ids);
202
+ addMemberStrict(ids);
104
203
  }
204
+ continue;
205
+ }
206
+ const tableMember = parseTableRowMember(trimmed);
207
+ if (tableMember) {
208
+ addMemberDedupInWave(tableMember);
105
209
  }
106
210
  }
107
211
  flushCurrent();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "6.14.3",
3
+ "version": "6.14.4",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {