cclaw-cli 0.48.4 → 0.48.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.
@@ -1289,7 +1289,7 @@ export function codexHooksJsonWithObservation() {
1289
1289
  command: hookDispatcherCommand("workflow-guard.sh")
1290
1290
  }, {
1291
1291
  type: "command",
1292
- command: "bash -lc 'if ! command -v cclaw >/dev/null 2>&1; then echo \"[cclaw] codex hook: cclaw binary is required for verify-current-state\" >&2; exit 1; fi; cclaw internal verify-current-state --quiet >/dev/null || true'"
1292
+ command: "bash -lc 'if ! command -v cclaw >/dev/null 2>&1; then echo \"[cclaw] codex hook: cclaw binary is required for verify-current-state\" >&2; exit 1; fi; MODE=\"${CCLAW_WORKFLOW_GUARD_MODE:-advisory}\"; if [ \"$MODE\" = \"strict\" ]; then cclaw internal verify-current-state --quiet >/dev/null; else cclaw internal verify-current-state --quiet >/dev/null || true; fi'"
1293
1293
  }]
1294
1294
  }],
1295
1295
  PreToolUse: [{
@@ -29,9 +29,17 @@ import fs from "node:fs/promises";
29
29
  import path from "node:path";
30
30
  import { createSandbox } from "../sandbox.js";
31
31
  import { loadStageSkill } from "./single-shot.js";
32
- import { runWithTools } from "./with-tools.js";
32
+ import { MaxTurnsExceededError, runWithTools } from "./with-tools.js";
33
33
  const STAGES_SUBDIR = "stages";
34
34
  const ARTIFACT_CANDIDATES = ["artifact.md", "artifact.txt", "ARTIFACT.md"];
35
+ const DEFAULT_WORKFLOW_MAX_TOTAL_TURNS = 40;
36
+ const DEFAULT_STAGE_TURN_CAP = 8;
37
+ function clampPositive(value, fallback) {
38
+ if (!Number.isInteger(value) || value < 1) {
39
+ return fallback;
40
+ }
41
+ return value;
42
+ }
35
43
  export async function runWorkflow(input) {
36
44
  const { workflow, config, projectRoot, client } = input;
37
45
  const sandboxFactory = input.createSandboxFn ?? createSandbox;
@@ -43,9 +51,17 @@ export async function runWorkflow(input) {
43
51
  const artifacts = new Map();
44
52
  let totalUsageUsd = 0;
45
53
  let totalDurationMs = 0;
54
+ let totalTurns = 0;
55
+ const workflowTurnBudget = clampPositive(config.workflowMaxTotalTurns, DEFAULT_WORKFLOW_MAX_TOTAL_TURNS);
46
56
  try {
47
57
  await fs.mkdir(await sandbox.resolve(STAGES_SUBDIR, { allowMissing: true }), { recursive: true });
48
58
  for (const step of workflow.stages) {
59
+ const remainingWorkflowTurns = workflowTurnBudget - totalTurns;
60
+ if (remainingWorkflowTurns < 1) {
61
+ throw new MaxTurnsExceededError(workflowTurnBudget);
62
+ }
63
+ const perStageTurnCap = clampPositive(config.toolMaxTurns, DEFAULT_STAGE_TURN_CAP);
64
+ const stageTurnBudget = Math.min(perStageTurnCap, remainingWorkflowTurns);
49
65
  input.onStageStart?.(step.name);
50
66
  await clearArtifactFile(sandbox);
51
67
  const priorStages = stageResults.map((r) => r.stage);
@@ -58,7 +74,10 @@ export async function runWorkflow(input) {
58
74
  };
59
75
  const result = await runWithTools({
60
76
  caseEntry,
61
- config,
77
+ config: {
78
+ ...config,
79
+ toolMaxTurns: stageTurnBudget
80
+ },
62
81
  projectRoot,
63
82
  client,
64
83
  ...(input.tools ? { tools: input.tools } : {}),
@@ -87,6 +106,7 @@ export async function runWorkflow(input) {
87
106
  input.onStageEnd?.(step.name, stageResult);
88
107
  totalUsageUsd += result.usageUsd;
89
108
  totalDurationMs += result.durationMs;
109
+ totalTurns += result.toolUse.turns;
90
110
  }
91
111
  return {
92
112
  caseId: workflow.id,
@@ -7,6 +7,7 @@ import { readDelegationLedger } from "./delegation.js";
7
7
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
8
8
  import { detectPublicApiChanges } from "./internal/detect-public-api-changes.js";
9
9
  import { readFlowState, writeFlowState } from "./runs.js";
10
+ import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
10
11
  import { buildTraceMatrix } from "./trace-matrix.js";
11
12
  import { FLOW_STAGES } from "./types.js";
12
13
  async function currentStageArtifactExists(projectRoot, stage, track) {
@@ -200,6 +201,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
200
201
  issues.push(`stale triggered conditional gate "${gateId}" found in stageGateCatalog.triggered for stage "${stage}" (conditional gate DSL removed).`);
201
202
  }
202
203
  const blockedSet = new Set(catalog.blocked);
204
+ const passedSet = new Set(catalog.passed);
203
205
  for (const gateId of catalog.passed) {
204
206
  if (!allowedSet.has(gateId)) {
205
207
  issues.push(`passed gate "${gateId}" is not defined for stage "${stage}".`);
@@ -239,6 +241,13 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
239
241
  if (!verdictConsistency.ok) {
240
242
  issues.push(`review verdict inconsistency: ${verdictConsistency.errors.join("; ")}`);
241
243
  }
244
+ const reviewCriticalsClaimedResolved = passedSet.has("review_criticals_resolved") || flowState.completedStages.includes("review");
245
+ const unresolvedCriticals = verdictConsistency.openCriticalCount > 0 || verdictConsistency.shipBlockerCount > 0;
246
+ if (reviewCriticalsClaimedResolved && unresolvedCriticals) {
247
+ issues.push(`review criticals gate blocked (review_criticals_resolved): review-army still reports ` +
248
+ `${verdictConsistency.openCriticalCount} open critical(s) and ` +
249
+ `${verdictConsistency.shipBlockerCount} ship blocker(s).`);
250
+ }
242
251
  const securityAttestation = await checkReviewSecurityNoChangeAttestation(projectRoot);
243
252
  if (!securityAttestation.ok) {
244
253
  issues.push(`review security attestation failed: ${securityAttestation.errors.join("; ")}`);
@@ -309,9 +318,29 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
309
318
  issues.push(`tdd docs drift gate blocked (tdd_docs_drift_check): public surface changes detected (${docsDriftDetection.changedFiles.join(", ")}) but no completed doc-updater delegation exists for the active run.`);
310
319
  }
311
320
  }
321
+ const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
322
+ if (await exists(tddLogPath)) {
323
+ try {
324
+ const tddLogRaw = await fs.readFile(tddLogPath, "utf8");
325
+ const parsedCycles = parseTddCycleLog(tddLogRaw);
326
+ const tddOrderValidation = validateTddCycleOrder(parsedCycles, {
327
+ runId: flowState.activeRunId
328
+ });
329
+ if (!tddOrderValidation.ok) {
330
+ const details = [...tddOrderValidation.issues];
331
+ if (tddOrderValidation.openRedSlices.length > 0) {
332
+ details.push(`open red slices: ${tddOrderValidation.openRedSlices.join(", ")}`);
333
+ }
334
+ issues.push(`tdd cycle order gate blocked: ${details.join("; ")}`);
335
+ }
336
+ }
337
+ catch (err) {
338
+ const reason = err instanceof Error ? err.message : String(err);
339
+ issues.push(`tdd cycle order gate blocked: unable to read tdd-cycle-log.jsonl (${reason}).`);
340
+ }
341
+ }
312
342
  }
313
343
  }
314
- const passedSet = new Set(catalog.passed);
315
344
  const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
316
345
  const missingRecommended = recommended.filter((gateId) => !passedSet.has(gateId));
317
346
  const missingTriggeredConditional = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.4",
3
+ "version": "0.48.5",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {