cclaw-cli 7.0.3 → 7.0.5

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.
@@ -5,6 +5,37 @@ import { FORBIDDEN_PLACEHOLDER_TOKENS, CONFIDENCE_FINDING_REGEX_SOURCE } from ".
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { PLAN_SPLIT_SMALL_PLAN_THRESHOLD, parseImplementationUnits, parseImplementationUnitParallelFields } from "../internal/plan-split-waves.js";
8
+ const PARALLEL_EXEC_MANAGED_START = "<!-- parallel-exec-managed-start -->";
9
+ const PARALLEL_EXEC_MANAGED_END = "<!-- parallel-exec-managed-end -->";
10
+ const TASK_ID_PATTERN = /\bT-\d{3}[a-z]?(?:\.\d{1,3})?\b/giu;
11
+ /**
12
+ * Extract every distinct T-NNN[a-z]?(.NNN)? id from a markdown body.
13
+ *
14
+ * Used by the `plan_parallel_exec_full_coverage` linter to compute the
15
+ * authored task set (from `## Task List`) vs. the wave-claimed task set
16
+ * (from inside `<!-- parallel-exec-managed-start -->`).
17
+ */
18
+ function extractTaskIds(body) {
19
+ const ids = new Set();
20
+ for (const match of body.matchAll(TASK_ID_PATTERN)) {
21
+ ids.add(match[0]);
22
+ }
23
+ return ids;
24
+ }
25
+ /**
26
+ * Return the body between the parallel-exec managed comment markers, or
27
+ * an empty string if the block is absent. The TDD wave parser uses the
28
+ * same delimiters; keeping the regex local avoids cross-package import
29
+ * cycles in the linter.
30
+ */
31
+ function extractParallelExecManagedBody(planMarkdown) {
32
+ const startIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_START);
33
+ const endIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_END);
34
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
35
+ return "";
36
+ }
37
+ return planMarkdown.slice(startIdx + PARALLEL_EXEC_MANAGED_START.length, endIdx);
38
+ }
8
39
  export async function lintPlanStage(ctx) {
9
40
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
10
41
  evaluateInvestigationTrace(ctx, "Implementation Units");
@@ -277,4 +308,52 @@ export async function lintPlanStage(ctx) {
277
308
  : "Parallel-ready units detected or plan is single-unit."
278
309
  });
279
310
  }
311
+ // plan_parallel_exec_full_coverage: every T-NNN task listed in the
312
+ // plan's Task List must be assigned to a slice inside the
313
+ // <!-- parallel-exec-managed-start --> block. Without this, TDD
314
+ // cannot fan out work the plan never authored as waves; the previous
315
+ // failure mode was `stage-complete tdd` succeeding when only the
316
+ // first batch of tasks had been wave-assigned.
317
+ //
318
+ // Spike rows (`S-N`) live in the same Task List but are excluded
319
+ // because they are wall-clock spikes that produce evidence files
320
+ // and are not part of the regular slice fan-out. A task is also
321
+ // excluded when it appears under a `## Deferred Tasks` (or
322
+ // `## Backlog`) heading inside the plan with an explicit reason.
323
+ if (strictPlanGuards) {
324
+ const taskListSection = sectionBodyByName(sections, "Task List") ?? "";
325
+ const authoredTaskIds = extractTaskIds(taskListSection);
326
+ // Collect deferred / backlog task ids so they don't trigger the
327
+ // "uncovered" finding. Both heading variants are accepted.
328
+ const deferredBody = (sectionBodyByName(sections, "Deferred Tasks") ?? "") +
329
+ "\n" +
330
+ (sectionBodyByName(sections, "Backlog") ?? "");
331
+ const deferredIds = extractTaskIds(deferredBody);
332
+ const parallelExecBody = extractParallelExecManagedBody(raw);
333
+ const claimedIds = extractTaskIds(parallelExecBody);
334
+ const uncovered = [];
335
+ for (const id of authoredTaskIds) {
336
+ if (claimedIds.has(id))
337
+ continue;
338
+ if (deferredIds.has(id))
339
+ continue;
340
+ uncovered.push(id);
341
+ }
342
+ uncovered.sort();
343
+ const blockPresent = parallelExecBody.length > 0;
344
+ const taskListPresent = authoredTaskIds.size > 0;
345
+ findings.push({
346
+ section: "plan_parallel_exec_full_coverage",
347
+ required: taskListPresent,
348
+ rule: "Every T-NNN task in `## Task List` must be assigned to at least one slice inside the `<!-- parallel-exec-managed-start -->` block (or moved to an explicit `## Deferred Tasks` / `## Backlog` section). TDD cannot fan out waves the plan never authored.",
349
+ found: taskListPresent && blockPresent && uncovered.length === 0,
350
+ details: !taskListPresent
351
+ ? "Task List section is empty or missing T-NNN ids; full-coverage check skipped."
352
+ : !blockPresent
353
+ ? "`<!-- parallel-exec-managed-start -->` block is missing or empty. Author the Parallel Execution Plan with W-02..W-N covering every task before plan-final-approval."
354
+ : uncovered.length === 0
355
+ ? `Parallel Execution Plan covers all ${authoredTaskIds.size} authored task id(s); ${deferredIds.size} task id(s) are explicitly deferred.`
356
+ : `Uncovered task id(s) — author waves for: ${uncovered.slice(0, 25).join(", ")}${uncovered.length > 25 ? `, … (${uncovered.length - 25} more)` : ""}. Either add slices for them inside <!-- parallel-exec-managed-start --> or move them under \`## Deferred Tasks\` with a reason.`
357
+ });
358
+ }
280
359
  }
@@ -838,15 +838,24 @@ async function persistEntry(root, runId, clean, event, options = {}) {
838
838
  // Rerecord semantics: replace any pre-existing row with the same spanId
839
839
  // (regardless of its status) so the legacy v1/v2 row is upgraded to v3
840
840
  // shape on disk. The append path keeps the historical dedup semantics:
841
- // an exact (spanId, status) duplicate is dropped to keep retried hooks
842
- // idempotent.
841
+ // an exact (spanId, status, phase) triple is dropped to keep retried hooks
842
+ // idempotent. Including \`phase\` in the dedup key is required because a
843
+ // single TDD slice-builder span legitimately emits FOUR rows with
844
+ // status=completed (one each for phase=red|green|refactor|doc); a
845
+ // dedup on (spanId, status) alone would silently drop GREEN/REFACTOR/DOC
846
+ // and leave the linter reporting tdd_slice_green_missing for slices
847
+ // whose work actually landed.
843
848
  if (options.replaceBySpanId) {
844
849
  ledger.entries = ledger.entries.filter((entry) => entry.spanId !== clean.spanId);
845
850
  ledger.entries.push(clean);
846
851
  ledger.runId = runId;
847
852
  ledger.schemaVersion = LEDGER_SCHEMA_VERSION;
848
853
  await writeDelegationLedgerAtomic(ledgerPath, ledger);
849
- } else if (!ledger.entries.some((entry) => entry.spanId === clean.spanId && entry.status === clean.status)) {
854
+ } else if (!ledger.entries.some((entry) =>
855
+ entry.spanId === clean.spanId &&
856
+ entry.status === clean.status &&
857
+ (entry.phase ?? null) === (clean.phase ?? null)
858
+ )) {
850
859
  ledger.entries.push(clean);
851
860
  ledger.runId = runId;
852
861
  ledger.schemaVersion = LEDGER_SCHEMA_VERSION;
@@ -294,6 +294,7 @@ const REQUIRED_GATE_IDS = {
294
294
  "plan_dependency_batches_defined",
295
295
  "plan_acceptance_mapped",
296
296
  "plan_execution_posture_recorded",
297
+ "plan_parallel_exec_full_coverage",
297
298
  "plan_wait_for_confirm"
298
299
  ],
299
300
  tdd: (track) => [
@@ -51,6 +51,7 @@ export const PLAN = {
51
51
  "Run anti-placeholder + anti-scope-reduction scans — block `TODO/TBD/...` and phrasing like `v1`, `for now`, `later` for locked boundaries.",
52
52
  "Define validation points — mark where progress must be checked before continuing, with concrete command and expected evidence.",
53
53
  "Define execution posture — record whether execution should be sequential, dependency-batched, parallel-safe, or blocked; include risk triggers and RED/GREEN/REFACTOR checkpoint/commit expectations when the repo workflow supports them. This fulfills the `plan_execution_posture_recorded` gate.",
54
+ "**Author the FULL Parallel Execution Plan.** Inside the `<!-- parallel-exec-managed-start -->` block, enumerate ALL waves W-02..W-N covering EVERY T-NNN task in `## Task List` — no `we'll author waves later`, `next batch only`, or open-ended Backlog handwave is acceptable. Each task gets a slice with `sliceId | taskId | dependsOn | claimedPaths | parallelizable | riskTier | lane`. Spike rows (`S-N`) and tasks marked `deferred` in an explicit `Deferred:` column may be omitted, but every other T-NNN must be claimed. This fulfills the `plan_parallel_exec_full_coverage` gate. The TDD stage downstream is a pure consumer of these waves — if the plan does not author them, TDD cannot fan out that work.",
54
55
  "WAIT_FOR_CONFIRM — write plan artifact and explicitly pause. **STOP.** Do NOT proceed until user confirms. Then close the stage with `node .cclaw/hooks/stage-complete.mjs plan` and tell user to run `/cc`."
55
56
  ],
56
57
  interactionProtocol: [
@@ -80,6 +81,7 @@ export const PLAN = {
80
81
  { id: "plan_dependency_batches_defined", description: "Tasks are grouped into executable batches with gate checks and execution posture." },
81
82
  { id: "plan_acceptance_mapped", description: "Each task maps to a spec acceptance criterion." },
82
83
  { id: "plan_execution_posture_recorded", description: "Execution posture is recorded before implementation handoff." },
84
+ { id: "plan_parallel_exec_full_coverage", description: "Every T-NNN task in `## Task List` (other than spikes/explicitly-deferred) is assigned to at least one slice inside the `<!-- parallel-exec-managed-start -->` block; TDD cannot fan out work that the plan never authored as waves." },
83
85
  { id: "plan_wait_for_confirm", description: "Execution blocked until explicit user confirmation." }
84
86
  ],
85
87
  requiredEvidence: [
@@ -1031,7 +1031,9 @@ export async function appendDelegation(projectRoot, entry) {
1031
1031
  stamped.fulfillmentMode = expectedFulfillmentMode(fallbacks);
1032
1032
  }
1033
1033
  }
1034
- if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
1034
+ if (prior.entries.some((existing) => existing.spanId === stamped.spanId &&
1035
+ existing.status === stamped.status &&
1036
+ (existing.phase ?? null) === (stamped.phase ?? null))) {
1035
1037
  return;
1036
1038
  }
1037
1039
  validateMonotonicTimestamps(stamped, prior.entries);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "7.0.3",
3
+ "version": "7.0.5",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {