cclaw-cli 0.48.1 → 0.48.3

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.
Files changed (41) hide show
  1. package/README.md +10 -3
  2. package/dist/artifact-linter.js +2 -8
  3. package/dist/cli.js +8 -1
  4. package/dist/config.d.ts +3 -0
  5. package/dist/config.js +13 -3
  6. package/dist/constants.d.ts +6 -0
  7. package/dist/constants.js +11 -0
  8. package/dist/content/contracts.d.ts +2 -2
  9. package/dist/content/contracts.js +2 -2
  10. package/dist/content/core-agents.d.ts +1 -1
  11. package/dist/content/core-agents.js +1 -1
  12. package/dist/content/hooks.js +16 -15
  13. package/dist/content/next-command.js +4 -2
  14. package/dist/content/observe.d.ts +2 -2
  15. package/dist/content/observe.js +83 -13
  16. package/dist/content/opencode-plugin.js +227 -45
  17. package/dist/content/stage-schema.js +1 -1
  18. package/dist/content/stages/ship.js +2 -5
  19. package/dist/content/templates.js +3 -6
  20. package/dist/delegation.d.ts +5 -1
  21. package/dist/delegation.js +12 -8
  22. package/dist/doctor.js +132 -15
  23. package/dist/eval/runner.js +36 -4
  24. package/dist/feature-system.d.ts +11 -4
  25. package/dist/feature-system.js +54 -10
  26. package/dist/flow-state.d.ts +2 -0
  27. package/dist/flow-state.js +19 -2
  28. package/dist/fs-utils.d.ts +4 -1
  29. package/dist/fs-utils.js +20 -4
  30. package/dist/gate-evidence.d.ts +2 -0
  31. package/dist/gate-evidence.js +13 -4
  32. package/dist/install.js +25 -23
  33. package/dist/internal/advance-stage.js +49 -10
  34. package/dist/knowledge-store.d.ts +8 -0
  35. package/dist/knowledge-store.js +113 -33
  36. package/dist/retro-gate.js +33 -23
  37. package/dist/run-archive.js +166 -128
  38. package/dist/run-persistence.d.ts +8 -1
  39. package/dist/run-persistence.js +7 -6
  40. package/dist/trace-matrix.js +7 -7
  41. package/package.json +1 -1
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  import { RUNTIME_ROOT } from "./constants.js";
4
4
  import { createInitialFlowState } from "./flow-state.js";
5
5
  import { readActiveFeature, syncActiveFeatureSnapshot } from "./feature-system.js";
6
- import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
6
+ import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
7
+ import { readKnowledgeSafely } from "./knowledge-store.js";
7
8
  import { evaluateRetroGate } from "./retro-gate.js";
8
9
  import { ensureRunSystem, readFlowState, writeFlowState } from "./run-persistence.js";
9
10
  const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
@@ -17,6 +18,12 @@ const STATE_SNAPSHOT_EXCLUDE = new Set([
17
18
  const DELEGATION_LOG_FILE = "delegation-log.json";
18
19
  const TDD_CYCLE_LOG_FILE = "tdd-cycle-log.jsonl";
19
20
  const RECONCILIATION_NOTICES_FILE = "reconciliation-notices.json";
21
+ const CRITICAL_STATE_SNAPSHOT_FILES = new Set([
22
+ "flow-state.json",
23
+ DELEGATION_LOG_FILE,
24
+ TDD_CYCLE_LOG_FILE,
25
+ RECONCILIATION_NOTICES_FILE
26
+ ]);
20
27
  function runsRoot(projectRoot) {
21
28
  return path.join(projectRoot, RUNS_DIR_REL_PATH);
22
29
  }
@@ -26,6 +33,9 @@ function activeArtifactsPath(projectRoot) {
26
33
  function stateDirPath(projectRoot) {
27
34
  return path.join(projectRoot, STATE_DIR_REL_PATH);
28
35
  }
36
+ function archiveLockPath(projectRoot) {
37
+ return path.join(projectRoot, RUNTIME_ROOT, "state", ".archive.lock");
38
+ }
29
39
  async function snapshotStateDirectory(projectRoot, destinationRoot) {
30
40
  const sourceDir = stateDirPath(projectRoot);
31
41
  if (!(await exists(sourceDir))) {
@@ -57,8 +67,12 @@ async function snapshotStateDirectory(projectRoot, destinationRoot) {
57
67
  copied.push(entry.name);
58
68
  }
59
69
  }
60
- catch {
61
- // best-effort snapshot; continue on individual failures
70
+ catch (error) {
71
+ if (CRITICAL_STATE_SNAPSHOT_FILES.has(entry.name)) {
72
+ const details = error instanceof Error ? error.message : String(error);
73
+ throw new Error(`Archive snapshot failed for critical state file "${entry.name}" (${details}).`);
74
+ }
75
+ // Non-critical snapshot files are best-effort and may be skipped.
62
76
  }
63
77
  }
64
78
  return copied.sort((a, b) => a.localeCompare(b));
@@ -66,9 +80,9 @@ async function snapshotStateDirectory(projectRoot, destinationRoot) {
66
80
  async function resetCarryoverStateFiles(projectRoot, activeRunId) {
67
81
  const stateDir = stateDirPath(projectRoot);
68
82
  await ensureDir(stateDir);
69
- await writeFileSafe(path.join(stateDir, DELEGATION_LOG_FILE), `${JSON.stringify({ runId: activeRunId, entries: [] }, null, 2)}\n`);
70
- await writeFileSafe(path.join(stateDir, TDD_CYCLE_LOG_FILE), "");
71
- await writeFileSafe(path.join(stateDir, RECONCILIATION_NOTICES_FILE), `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`);
83
+ await writeFileSafe(path.join(stateDir, DELEGATION_LOG_FILE), `${JSON.stringify({ runId: activeRunId, entries: [] }, null, 2)}\n`, { mode: 0o600 });
84
+ await writeFileSafe(path.join(stateDir, TDD_CYCLE_LOG_FILE), "", { mode: 0o600 });
85
+ await writeFileSafe(path.join(stateDir, RECONCILIATION_NOTICES_FILE), `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`, { mode: 0o600 });
72
86
  }
73
87
  function toArchiveDate(date = new Date()) {
74
88
  const yyyy = date.getFullYear().toString();
@@ -147,136 +161,160 @@ export async function listRuns(projectRoot) {
147
161
  }
148
162
  export async function archiveRun(projectRoot, featureName, options = {}) {
149
163
  await ensureRunSystem(projectRoot);
150
- const activeFeature = await readActiveFeature(projectRoot);
151
- const artifactsDir = activeArtifactsPath(projectRoot);
152
- const runsDir = runsRoot(projectRoot);
153
- await ensureDir(runsDir);
154
- await ensureDir(artifactsDir);
155
- const feature = (featureName?.trim() && featureName.trim().length > 0)
156
- ? featureName.trim()
157
- : await inferFeatureNameFromArtifacts(projectRoot);
158
- const archiveBaseId = `${toArchiveDate()}-${slugifyFeatureName(feature)}`;
159
- const archiveId = await uniqueArchiveId(projectRoot, archiveBaseId);
160
- const archivePath = path.join(runsDir, archiveId);
161
- const archiveArtifactsPath = path.join(archivePath, "artifacts");
162
- let sourceState = await readFlowState(projectRoot);
163
- const retroGate = await evaluateRetroGate(projectRoot, sourceState);
164
- const shipCompleted = sourceState.completedStages.includes("ship");
165
- const skipRetro = options.skipRetro === true;
166
- const skipRetroReason = options.skipRetroReason?.trim();
167
- if (skipRetro && (!skipRetroReason || skipRetroReason.length === 0)) {
168
- throw new Error("archive --skip-retro requires --retro-reason=<text>.");
169
- }
170
- const retroSkippedInCloseout = sourceState.closeout.retroSkipped === true &&
171
- typeof sourceState.closeout.retroSkipReason === "string" &&
172
- sourceState.closeout.retroSkipReason.trim().length > 0;
173
- const readyForArchive = sourceState.closeout.shipSubstate === "ready_to_archive";
174
- if (shipCompleted && !readyForArchive && !skipRetro) {
175
- throw new Error("Archive blocked: closeout is not ready_to_archive. " +
176
- "Resume /cc-next until closeout reaches ready_to_archive, " +
177
- "or run `cclaw archive --skip-retro --retro-reason=<text>` for CLI-only flows.");
178
- }
179
- if (retroGate.required && !retroGate.completed && !skipRetro && !retroSkippedInCloseout) {
180
- throw new Error("Archive blocked: retro gate is required after ship completion. " +
181
- "Run /cc-next (auto-runs retro) or, for CLI-only flows, re-run `cclaw archive --skip-retro --retro-reason=<text>`.");
182
- }
183
- if (retroGate.completed) {
184
- const completedAt = sourceState.retro.completedAt ?? new Date().toISOString();
185
- sourceState = {
186
- ...sourceState,
187
- retro: {
188
- required: retroGate.required,
189
- completedAt,
190
- compoundEntries: retroGate.compoundEntries
191
- }
192
- };
193
- await writeFlowState(projectRoot, sourceState, { allowReset: true });
194
- }
195
- const retroSummary = {
196
- required: retroGate.required,
197
- completed: retroGate.completed,
198
- skipped: skipRetro || retroSkippedInCloseout,
199
- skipReason: skipRetro
200
- ? skipRetroReason
201
- : retroSkippedInCloseout
202
- ? sourceState.closeout.retroSkipReason
203
- : undefined,
204
- compoundEntries: retroGate.compoundEntries
205
- };
206
- await ensureDir(archivePath);
207
- // Drop an `.archive-in-progress` sentinel immediately so that a crash
208
- // between the artifact rename and the final manifest write leaves a
209
- // recoverable marker (doctor surfaces these; re-running archive on an
210
- // orphan attempts to complete or roll back). The sentinel is removed
211
- // only after the manifest lands successfully.
212
- const sentinelPath = path.join(archivePath, ".archive-in-progress");
213
- const archivedAt = new Date().toISOString();
214
- await writeFileSafe(sentinelPath, `${JSON.stringify({ archiveId, startedAt: archivedAt, sourceRunId: sourceState.activeRunId }, null, 2)}\n`);
215
- let artifactsMoved = false;
216
- try {
217
- await fs.rename(artifactsDir, archiveArtifactsPath);
218
- artifactsMoved = true;
164
+ return withDirectoryLock(archiveLockPath(projectRoot), async () => {
165
+ const activeFeature = await readActiveFeature(projectRoot);
166
+ const artifactsDir = activeArtifactsPath(projectRoot);
167
+ const runsDir = runsRoot(projectRoot);
168
+ await ensureDir(runsDir);
219
169
  await ensureDir(artifactsDir);
220
- const archiveStatePath = path.join(archivePath, "state");
221
- const snapshottedStateFiles = await snapshotStateDirectory(projectRoot, archiveStatePath);
222
- const resetState = createInitialFlowState();
223
- await writeFlowState(projectRoot, resetState, { allowReset: true });
224
- await resetCarryoverStateFiles(projectRoot, resetState.activeRunId);
225
- const manifest = {
226
- version: 1,
227
- archiveId,
228
- archivedAt,
229
- featureName: feature,
230
- activeFeature,
231
- sourceRunId: sourceState.activeRunId,
232
- sourceCurrentStage: sourceState.currentStage,
233
- sourceCompletedStages: sourceState.completedStages,
234
- snapshottedStateFiles,
235
- retro: retroSummary
236
- };
237
- await writeFileSafe(path.join(archivePath, "archive-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
238
- // Manifest landed sentinel is no longer needed.
239
- await fs.unlink(sentinelPath).catch(() => undefined);
240
- const knowledgeStats = await readKnowledgeStats(projectRoot);
241
- await syncActiveFeatureSnapshot(projectRoot);
242
- return {
243
- archiveId,
244
- archivePath,
245
- archivedAt,
246
- featureName: feature,
247
- activeFeature,
248
- resetState,
249
- snapshottedStateFiles,
250
- knowledge: knowledgeStats,
251
- retro: retroSummary
170
+ const feature = (featureName?.trim() && featureName.trim().length > 0)
171
+ ? featureName.trim()
172
+ : await inferFeatureNameFromArtifacts(projectRoot);
173
+ const archiveBaseId = `${toArchiveDate()}-${slugifyFeatureName(feature)}`;
174
+ const archiveId = await uniqueArchiveId(projectRoot, archiveBaseId);
175
+ const archivePath = path.join(runsDir, archiveId);
176
+ const archiveArtifactsPath = path.join(archivePath, "artifacts");
177
+ let sourceState = await readFlowState(projectRoot);
178
+ const retroGate = await evaluateRetroGate(projectRoot, sourceState);
179
+ const shipCompleted = sourceState.completedStages.includes("ship");
180
+ const skipRetro = options.skipRetro === true;
181
+ const skipRetroReason = options.skipRetroReason?.trim();
182
+ if (skipRetro && (!skipRetroReason || skipRetroReason.length === 0)) {
183
+ throw new Error("archive --skip-retro requires --retro-reason=<text>.");
184
+ }
185
+ const retroSkippedInCloseout = sourceState.closeout.retroSkipped === true &&
186
+ typeof sourceState.closeout.retroSkipReason === "string" &&
187
+ sourceState.closeout.retroSkipReason.trim().length > 0;
188
+ const readyForArchive = sourceState.closeout.shipSubstate === "ready_to_archive";
189
+ const inShipCloseout = sourceState.currentStage === "ship";
190
+ if (inShipCloseout && skipRetro) {
191
+ throw new Error("Archive blocked: --skip-retro is not allowed while current stage is ship. " +
192
+ "Complete closeout to ready_to_archive via /cc-next.");
193
+ }
194
+ if (inShipCloseout && !readyForArchive) {
195
+ throw new Error("Archive blocked: closeout is not ready_to_archive. " +
196
+ "Resume /cc-next until closeout reaches ready_to_archive.");
197
+ }
198
+ if (shipCompleted && !readyForArchive && !skipRetro) {
199
+ throw new Error("Archive blocked: closeout is not ready_to_archive. " +
200
+ "Resume /cc-next until closeout reaches ready_to_archive, " +
201
+ "or run `cclaw archive --skip-retro --retro-reason=<text>` for CLI-only flows.");
202
+ }
203
+ if (retroGate.required && !retroGate.completed && !skipRetro && !retroSkippedInCloseout) {
204
+ throw new Error("Archive blocked: retro gate is required after ship completion. " +
205
+ "Run /cc-next (auto-runs retro) or, for CLI-only flows, re-run `cclaw archive --skip-retro --retro-reason=<text>`.");
206
+ }
207
+ if (retroGate.completed) {
208
+ const completedAt = sourceState.retro.completedAt ?? new Date().toISOString();
209
+ sourceState = {
210
+ ...sourceState,
211
+ retro: {
212
+ required: retroGate.required,
213
+ completedAt,
214
+ compoundEntries: retroGate.compoundEntries
215
+ }
216
+ };
217
+ await writeFlowState(projectRoot, sourceState, { allowReset: true });
218
+ }
219
+ const retroSummary = {
220
+ required: retroGate.required,
221
+ completed: retroGate.completed,
222
+ skipped: skipRetro || retroSkippedInCloseout,
223
+ skipReason: skipRetro
224
+ ? skipRetroReason
225
+ : retroSkippedInCloseout
226
+ ? sourceState.closeout.retroSkipReason
227
+ : undefined,
228
+ compoundEntries: retroGate.compoundEntries
252
229
  };
253
- }
254
- catch (err) {
255
- // Best-effort rollback: if artifacts were moved but the subsequent
256
- // steps failed, put artifacts back so the user is not left without
257
- // a working run. The sentinel is intentionally left behind for
258
- // inspection; doctor surfaces it.
259
- if (artifactsMoved) {
260
- try {
261
- await fs.rm(artifactsDir, { recursive: true, force: true });
262
- await fs.rename(archiveArtifactsPath, artifactsDir);
230
+ await ensureDir(archivePath);
231
+ // Drop an `.archive-in-progress` sentinel immediately so that a crash
232
+ // between the artifact rename and the final manifest write leaves a
233
+ // recoverable marker (doctor surfaces these; re-running archive on an
234
+ // orphan attempts to complete or roll back). The sentinel is removed
235
+ // only after the manifest lands successfully.
236
+ const sentinelPath = path.join(archivePath, ".archive-in-progress");
237
+ const archivedAt = new Date().toISOString();
238
+ await writeFileSafe(sentinelPath, `${JSON.stringify({ archiveId, startedAt: archivedAt, sourceRunId: sourceState.activeRunId }, null, 2)}\n`);
239
+ const stateBeforeReset = sourceState;
240
+ let artifactsMoved = false;
241
+ let stateReset = false;
242
+ try {
243
+ await fs.rename(artifactsDir, archiveArtifactsPath);
244
+ artifactsMoved = true;
245
+ await ensureDir(artifactsDir);
246
+ const archiveStatePath = path.join(archivePath, "state");
247
+ const snapshottedStateFiles = await snapshotStateDirectory(projectRoot, archiveStatePath);
248
+ const resetState = createInitialFlowState();
249
+ await writeFlowState(projectRoot, resetState, { allowReset: true });
250
+ stateReset = true;
251
+ await resetCarryoverStateFiles(projectRoot, resetState.activeRunId);
252
+ const manifest = {
253
+ version: 1,
254
+ archiveId,
255
+ archivedAt,
256
+ featureName: feature,
257
+ activeFeature,
258
+ sourceRunId: sourceState.activeRunId,
259
+ sourceCurrentStage: sourceState.currentStage,
260
+ sourceCompletedStages: sourceState.completedStages,
261
+ snapshottedStateFiles,
262
+ retro: retroSummary
263
+ };
264
+ await writeFileSafe(path.join(archivePath, "archive-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
265
+ // Manifest landed — sentinel is no longer needed.
266
+ await fs.unlink(sentinelPath).catch(() => undefined);
267
+ const knowledgeStats = await readKnowledgeStats(projectRoot);
268
+ await syncActiveFeatureSnapshot(projectRoot);
269
+ return {
270
+ archiveId,
271
+ archivePath,
272
+ archivedAt,
273
+ featureName: feature,
274
+ activeFeature,
275
+ resetState,
276
+ snapshottedStateFiles,
277
+ knowledge: knowledgeStats,
278
+ retro: retroSummary
279
+ };
280
+ }
281
+ catch (err) {
282
+ // Best-effort rollback: if artifacts were moved but the subsequent
283
+ // steps failed, put artifacts back so the user is not left without
284
+ // a working run. The sentinel is intentionally left behind for
285
+ // inspection; doctor surfaces it.
286
+ if (artifactsMoved) {
287
+ try {
288
+ await fs.rm(artifactsDir, { recursive: true, force: true });
289
+ await fs.rename(archiveArtifactsPath, artifactsDir);
290
+ }
291
+ catch {
292
+ // Rollback failed — sentinel + orphaned archive dir will be
293
+ // surfaced by doctor and can be reconciled manually.
294
+ }
263
295
  }
264
- catch {
265
- // Rollback failed — sentinel + orphaned archive dir will be
266
- // surfaced by doctor and can be reconciled manually.
296
+ if (stateReset) {
297
+ try {
298
+ await writeFlowState(projectRoot, stateBeforeReset, { allowReset: true });
299
+ await resetCarryoverStateFiles(projectRoot, stateBeforeReset.activeRunId);
300
+ }
301
+ catch {
302
+ // If rollback of state fails, keep sentinel + archive remnants for
303
+ // manual reconciliation.
304
+ }
267
305
  }
306
+ throw err;
268
307
  }
269
- throw err;
270
- }
308
+ }, {
309
+ retries: 400,
310
+ retryDelayMs: 25,
311
+ staleAfterMs: 120_000
312
+ });
271
313
  }
272
314
  const KNOWLEDGE_SOFT_THRESHOLD = 50;
273
315
  async function readKnowledgeStats(projectRoot) {
274
- const knowledgePath = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
275
- let activeEntryCount = 0;
276
- if (await exists(knowledgePath)) {
277
- const text = await fs.readFile(knowledgePath, "utf8");
278
- activeEntryCount = countActiveKnowledgeEntries(text);
279
- }
316
+ const { entries } = await readKnowledgeSafely(projectRoot);
317
+ const activeEntryCount = entries.length;
280
318
  return {
281
319
  activeEntryCount,
282
320
  softThreshold: KNOWLEDGE_SOFT_THRESHOLD,
@@ -12,12 +12,19 @@ export interface WriteFlowStateOptions {
12
12
  */
13
13
  allowReset?: boolean;
14
14
  }
15
+ export interface ReadFlowStateOptions {
16
+ /**
17
+ * When false, skip feature-system auto-repair writes and read flow-state in
18
+ * pure diagnostic mode.
19
+ */
20
+ repairFeatureSystem?: boolean;
21
+ }
15
22
  export declare class CorruptFlowStateError extends Error {
16
23
  readonly statePath: string;
17
24
  readonly quarantinedPath: string;
18
25
  constructor(statePath: string, quarantinedPath: string, cause: unknown);
19
26
  }
20
- export declare function readFlowState(projectRoot: string): Promise<FlowState>;
27
+ export declare function readFlowState(projectRoot: string, options?: ReadFlowStateOptions): Promise<FlowState>;
21
28
  export declare function writeFlowState(projectRoot: string, state: FlowState, options?: WriteFlowStateOptions): Promise<void>;
22
29
  interface EnsureRunSystemOptions {
23
30
  createIfMissing?: boolean;
@@ -277,7 +277,7 @@ function sanitizeCloseoutState(value) {
277
277
  }
278
278
  function coerceFlowState(parsed) {
279
279
  const track = coerceTrack(parsed.track);
280
- const next = createInitialFlowState("active", track);
280
+ const next = createInitialFlowState({ track });
281
281
  const activeRunIdRaw = parsed.activeRunId;
282
282
  const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
283
283
  ? activeRunIdRaw.trim()
@@ -333,8 +333,10 @@ async function quarantineCorruptState(statePath, cause) {
333
333
  }
334
334
  throw new CorruptFlowStateError(statePath, quarantinedPath, cause);
335
335
  }
336
- export async function readFlowState(projectRoot) {
337
- await ensureFeatureSystem(projectRoot);
336
+ export async function readFlowState(projectRoot, options = {}) {
337
+ if (options.repairFeatureSystem !== false) {
338
+ await ensureFeatureSystem(projectRoot);
339
+ }
338
340
  const statePath = flowStatePath(projectRoot);
339
341
  if (!(await exists(statePath))) {
340
342
  return createInitialFlowState();
@@ -375,12 +377,11 @@ export async function writeFlowState(projectRoot, state, options = {}) {
375
377
  if (err instanceof InvalidStageTransitionError) {
376
378
  throw err;
377
379
  }
378
- // A corrupt prior file is surfaced by readFlowState elsewhere; don't
379
- // block a legitimate write attempt on parse errors here.
380
+ throw new Error(`cannot validate flow-state transition because ${FLOW_STATE_REL_PATH} is unreadable or corrupt (${err instanceof Error ? err.message : String(err)}). Run \`cclaw doctor\` and reconcile the state before retrying.`);
380
381
  }
381
382
  }
382
383
  const safe = coerceFlowState({ ...state });
383
- await writeFileSafe(statePath, `${JSON.stringify(safe, null, 2)}\n`);
384
+ await writeFileSafe(statePath, `${JSON.stringify(safe, null, 2)}\n`, { mode: 0o600 });
384
385
  });
385
386
  await syncActiveFeatureSnapshot(projectRoot);
386
387
  }
@@ -124,17 +124,17 @@ export async function buildTraceMatrix(projectRoot) {
124
124
  const acToTasks = new Map();
125
125
  for (const [task, acs] of taskToAcs) {
126
126
  for (const ac of acs) {
127
- const prev = acToTasks.get(ac) ?? [];
128
- if (!prev.includes(task)) {
129
- acToTasks.set(ac, [...prev, task]);
130
- }
127
+ const prev = acToTasks.get(ac) ?? new Set();
128
+ prev.add(task);
129
+ acToTasks.set(ac, prev);
131
130
  }
132
131
  }
133
132
  const entries = criterionIds.map((criterionId) => {
134
- const taskIds = acToTasks.get(criterionId) ?? [];
133
+ const taskIds = [...(acToTasks.get(criterionId) ?? new Set())];
134
+ const taskIdSet = new Set(taskIds);
135
135
  const testSlices = [];
136
136
  for (const [slice, tasks] of sliceToTasks) {
137
- if (tasks.some((t) => taskIds.includes(t))) {
137
+ if (tasks.some((t) => taskIdSet.has(t))) {
138
138
  testSlices.push(slice);
139
139
  }
140
140
  }
@@ -145,7 +145,7 @@ export async function buildTraceMatrix(projectRoot) {
145
145
  reviewFindings: layer1LinesForCriterion(layer1, criterionId)
146
146
  };
147
147
  });
148
- const orphanedCriteria = criterionIds.filter((ac) => (acToTasks.get(ac) ?? []).length === 0);
148
+ const orphanedCriteria = criterionIds.filter((ac) => (acToTasks.get(ac)?.size ?? 0) === 0);
149
149
  const tasksWithSlice = new Set();
150
150
  for (const tasks of sliceToTasks.values()) {
151
151
  for (const t of tasks) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.1",
3
+ "version": "0.48.3",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {