cclaw-cli 0.46.14 → 0.47.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.
@@ -16,21 +16,36 @@ function unique(values) {
16
16
  const TEST_COMMAND_HINT_PATTERN = /\b(?:npm test|pnpm test|yarn test|bun test|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|dotnet test)\b/iu;
17
17
  const SHA_WITH_LABEL_PATTERN = /\b(?:sha|commit)(?:\s*[:=]|\s+)\s*[0-9a-f]{7,40}\b/iu;
18
18
  const PASS_STATUS_PATTERN = /\b(?:pass|passed|green|ok)\b/iu;
19
- function validateGateEvidenceShape(stage, gateId, evidence) {
20
- if (stage !== "tdd" || gateId !== "tdd_verified_before_complete") {
19
+ const SHIP_FINALIZATION_MODE_PATTERN = /\bFINALIZE_(?:MERGE_LOCAL|OPEN_PR|QUEUE|HANDOFF|SKIP)\b/u;
20
+ // Per-gate validators keyed by `${stage}:${gateId}`. Returning a non-null
21
+ // string surfaces the reason as an `advance-stage` failure so evidence is
22
+ // guaranteed to carry the structural breadcrumbs downstream tooling
23
+ // expects. Previously only `tdd:tdd_verified_before_complete` was checked.
24
+ const GATE_EVIDENCE_VALIDATORS = {
25
+ "tdd:tdd_verified_before_complete": (evidence) => {
26
+ if (!TEST_COMMAND_HINT_PATTERN.test(evidence)) {
27
+ return "must include the fresh verification command that was run (for example `npm test`, `pytest`, `go test`, or equivalent).";
28
+ }
29
+ if (!SHA_WITH_LABEL_PATTERN.test(evidence)) {
30
+ return "must include a commit SHA token prefixed with `sha` or `commit` (for example `sha: abc1234`).";
31
+ }
32
+ if (!PASS_STATUS_PATTERN.test(evidence)) {
33
+ return "must include explicit success status (for example `PASS` or `GREEN`).";
34
+ }
35
+ return null;
36
+ },
37
+ "ship:ship_finalization_executed": (evidence) => {
38
+ if (!SHIP_FINALIZATION_MODE_PATTERN.test(evidence)) {
39
+ return "must name the finalization mode that ran (for example `FINALIZE_MERGE_LOCAL`, `FINALIZE_OPEN_PR`, `FINALIZE_HANDOFF`, `FINALIZE_QUEUE`, or `FINALIZE_SKIP`).";
40
+ }
21
41
  return null;
22
42
  }
23
- const trimmed = evidence.trim();
24
- if (!TEST_COMMAND_HINT_PATTERN.test(trimmed)) {
25
- return "must include the fresh verification command that was run (for example `npm test`, `pytest`, `go test`, or equivalent).";
26
- }
27
- if (!SHA_WITH_LABEL_PATTERN.test(trimmed)) {
28
- return "must include a commit SHA token prefixed with `sha` or `commit` (for example `sha: abc1234`).";
29
- }
30
- if (!PASS_STATUS_PATTERN.test(trimmed)) {
31
- return "must include explicit success status (for example `PASS` or `GREEN`).";
32
- }
33
- return null;
43
+ };
44
+ function validateGateEvidenceShape(stage, gateId, evidence) {
45
+ const validator = GATE_EVIDENCE_VALIDATORS[`${stage}:${gateId}`];
46
+ if (!validator)
47
+ return null;
48
+ return validator(evidence.trim());
34
49
  }
35
50
  function parseStringList(raw) {
36
51
  if (!Array.isArray(raw))
@@ -58,10 +73,23 @@ function parseGuardEvidence(value) {
58
73
  }
59
74
  return next;
60
75
  }
76
+ function emptyGateState() {
77
+ return {
78
+ required: [],
79
+ recommended: [],
80
+ conditional: [],
81
+ triggered: [],
82
+ passed: [],
83
+ blocked: []
84
+ };
85
+ }
61
86
  function parseCandidateGateCatalog(value, fallback) {
62
87
  const next = {};
63
88
  for (const stage of FLOW_STAGES) {
64
- const base = fallback[stage];
89
+ // Guard against stale on-disk flow-state files that persisted a partial
90
+ // stageGateCatalog (missing a stage key). Previously `fallback[stage]`
91
+ // could be undefined and the spread below would throw at runtime.
92
+ const base = fallback[stage] ?? emptyGateState();
65
93
  next[stage] = {
66
94
  required: [...base.required],
67
95
  recommended: [...base.recommended],
@@ -81,7 +109,7 @@ function parseCandidateGateCatalog(value, fallback) {
81
109
  continue;
82
110
  }
83
111
  const typed = rawStage;
84
- const base = fallback[stage];
112
+ const base = fallback[stage] ?? emptyGateState();
85
113
  const allowed = new Set([...base.required, ...base.recommended, ...base.conditional]);
86
114
  const conditional = new Set(base.conditional);
87
115
  const passed = unique(parseStringList(typed.passed)).filter((gateId) => allowed.has(gateId));
@@ -114,13 +142,22 @@ function coerceCandidateFlowState(raw, fallback) {
114
142
  const completedStages = unique(parseStringList(typed.completedStages).filter((stage) => isFlowStageValue(stage)));
115
143
  const skippedStagesRaw = parseStringList(typed.skippedStages).filter((stage) => isFlowStageValue(stage));
116
144
  const skippedStages = skippedStagesRaw.length > 0 ? skippedStagesRaw : fallback.skippedStages;
145
+ // When the candidate payload omits `guardEvidence` entirely we must keep
146
+ // the on-disk fallback — otherwise a partial update (e.g. a tooling call
147
+ // that only passes stage + passedGateIds) would silently wipe every
148
+ // previously recorded evidence string and fail the next
149
+ // `verifyCurrentStageGateEvidence` check.
150
+ const candidateEvidence = parseGuardEvidence(typed.guardEvidence);
151
+ const guardEvidence = typed.guardEvidence === undefined
152
+ ? { ...fallback.guardEvidence }
153
+ : candidateEvidence;
117
154
  return {
118
155
  ...fallback,
119
156
  currentStage,
120
157
  completedStages,
121
158
  track,
122
159
  skippedStages,
123
- guardEvidence: parseGuardEvidence(typed.guardEvidence),
160
+ guardEvidence,
124
161
  stageGateCatalog: parseCandidateGateCatalog(typed.stageGateCatalog, fallback.stageGateCatalog)
125
162
  };
126
163
  }
@@ -1,6 +1,7 @@
1
1
  import { type FlowStage } from "./types.js";
2
2
  export type KnowledgeEntryType = "rule" | "pattern" | "lesson" | "compound";
3
3
  export type KnowledgeEntryConfidence = "high" | "medium" | "low";
4
+ export type KnowledgeEntrySeverity = "critical" | "important" | "suggestion";
4
5
  export type KnowledgeEntryUniversality = "project" | "personal" | "universal";
5
6
  export type KnowledgeEntryMaturity = "raw" | "lifted-to-rule" | "lifted-to-enforcement";
6
7
  export type KnowledgeEntrySource = "stage" | "retro" | "compound" | "ideate" | "manual";
@@ -9,6 +10,7 @@ export interface KnowledgeEntry {
9
10
  trigger: string;
10
11
  action: string;
11
12
  confidence: KnowledgeEntryConfidence;
13
+ severity?: KnowledgeEntrySeverity;
12
14
  domain: string | null;
13
15
  stage: FlowStage | null;
14
16
  origin_stage: FlowStage | null;
@@ -27,6 +29,7 @@ export interface KnowledgeSeedEntry {
27
29
  trigger: string;
28
30
  action: string;
29
31
  confidence: KnowledgeEntryConfidence;
32
+ severity?: KnowledgeEntrySeverity;
30
33
  domain?: string | null;
31
34
  stage?: FlowStage | null;
32
35
  origin_stage?: FlowStage | null;
@@ -5,6 +5,7 @@ import { withDirectoryLock } from "./fs-utils.js";
5
5
  import { FLOW_STAGES } from "./types.js";
6
6
  const KNOWLEDGE_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
7
7
  const KNOWLEDGE_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
8
+ const KNOWLEDGE_SEVERITY_SET = new Set(["critical", "important", "suggestion"]);
8
9
  const KNOWLEDGE_UNIVERSALITY_SET = new Set(["project", "personal", "universal"]);
9
10
  const KNOWLEDGE_MATURITY_SET = new Set(["raw", "lifted-to-rule", "lifted-to-enforcement"]);
10
11
  const KNOWLEDGE_SOURCE_SET = new Set([
@@ -34,6 +35,7 @@ const KNOWLEDGE_REQUIRED_KEYS = [
34
35
  ];
35
36
  const KNOWLEDGE_ALLOWED_KEYS = new Set(KNOWLEDGE_REQUIRED_KEYS);
36
37
  KNOWLEDGE_ALLOWED_KEYS.add("source");
38
+ KNOWLEDGE_ALLOWED_KEYS.add("severity");
37
39
  function knowledgePath(projectRoot) {
38
40
  return path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
39
41
  }
@@ -60,7 +62,8 @@ function dedupeKey(entry) {
60
62
  entry.origin_feature === null ? "null" : normalizeText(entry.origin_feature),
61
63
  entry.universality,
62
64
  entry.project === null ? "null" : normalizeText(entry.project),
63
- entry.source === undefined || entry.source === null ? "null" : entry.source
65
+ entry.source === undefined || entry.source === null ? "null" : entry.source,
66
+ entry.severity === undefined ? "none" : entry.severity
64
67
  ].join("|");
65
68
  }
66
69
  function isIsoUtcTimestamp(value) {
@@ -100,6 +103,10 @@ export function validateKnowledgeEntry(entry) {
100
103
  if (!KNOWLEDGE_CONFIDENCE_SET.has(obj.confidence)) {
101
104
  errors.push("confidence must be one of: high, medium, low.");
102
105
  }
106
+ if (obj.severity !== undefined &&
107
+ (typeof obj.severity !== "string" || !KNOWLEDGE_SEVERITY_SET.has(obj.severity))) {
108
+ errors.push("severity must be one of: critical, important, suggestion.");
109
+ }
103
110
  if (!isNullableString(obj.domain)) {
104
111
  errors.push("domain must be string or null.");
105
112
  }
@@ -161,6 +168,9 @@ export function materializeKnowledgeEntry(seed, defaults = {}) {
161
168
  last_seen_ts: normalizeUtcIso(seed.last_seen_ts ?? now),
162
169
  project: seed.project ?? defaults.project ?? null
163
170
  };
171
+ if (seed.severity !== undefined) {
172
+ entry.severity = seed.severity;
173
+ }
164
174
  if (source !== null) {
165
175
  entry.source = source;
166
176
  }
@@ -73,7 +73,17 @@ export async function evaluateRetroGate(projectRoot, state) {
73
73
  compoundEntries = 0;
74
74
  }
75
75
  }
76
- const completed = required ? (hasRetroArtifact && compoundEntries > 0) : true;
76
+ // A retro is considered complete when either:
77
+ // - at least one compound learning was promoted during the retro window, or
78
+ // - the operator explicitly skipped retro or compound (`retroSkipped` /
79
+ // `compoundSkipped` recorded in the closeout substate) after reviewing
80
+ // the draft. Previously the gate required `compoundEntries > 0`
81
+ // unconditionally, which dead-locked ship closeout whenever the retro
82
+ // yielded no new patterns worth promoting.
83
+ const explicitSkip = Boolean(state.closeout.retroSkipped || state.closeout.compoundSkipped);
84
+ const completed = required
85
+ ? hasRetroArtifact && (compoundEntries > 0 || explicitSkip)
86
+ : true;
77
87
  return {
78
88
  required,
79
89
  completed,
@@ -235,7 +235,7 @@ function sanitizeCloseoutState(value) {
235
235
  return fallback;
236
236
  }
237
237
  const typed = value;
238
- const shipSubstate = isShipSubstate(typed.shipSubstate) ? typed.shipSubstate : fallback.shipSubstate;
238
+ let shipSubstate = isShipSubstate(typed.shipSubstate) ? typed.shipSubstate : fallback.shipSubstate;
239
239
  const retroDraftedAt = typeof typed.retroDraftedAt === "string" ? typed.retroDraftedAt : undefined;
240
240
  const retroAcceptedAt = typeof typed.retroAcceptedAt === "string" ? typed.retroAcceptedAt : undefined;
241
241
  const retroSkipped = typeof typed.retroSkipped === "boolean" ? typed.retroSkipped : undefined;
@@ -246,6 +246,16 @@ function sanitizeCloseoutState(value) {
246
246
  const compoundPromoted = typeof promotedRaw === "number" && Number.isFinite(promotedRaw) && promotedRaw >= 0
247
247
  ? Math.floor(promotedRaw)
248
248
  : 0;
249
+ // Demote shipSubstate when its retro invariant is violated on disk. A
250
+ // hand-edited flow-state could claim `ready_to_archive` or `compound_review`
251
+ // without ever going through the retro step, which would let `archive`
252
+ // proceed and skip the gate. Compound completion is not independently
253
+ // tracked in all flows (some runs rely on knowledge.jsonl + the retro
254
+ // window), so we only demote when the retro leg is missing outright.
255
+ const retroDone = retroAcceptedAt !== undefined || retroSkipped === true;
256
+ if (!retroDone && (shipSubstate === "ready_to_archive" || shipSubstate === "compound_review")) {
257
+ shipSubstate = "retro_review";
258
+ }
249
259
  return {
250
260
  shipSubstate,
251
261
  retroDraftedAt,
package/dist/tdd-cycle.js CHANGED
@@ -31,6 +31,7 @@ export function parseTddCycleLog(text) {
31
31
  }
32
32
  return out;
33
33
  }
34
+ const SLICE_ID_PATTERN = /^S-\d+$/u;
34
35
  export function validateTddCycleOrder(entries, options = {}) {
35
36
  const targetRun = options.runId;
36
37
  const filtered = targetRun
@@ -44,6 +45,15 @@ export function validateTddCycleOrder(entries, options = {}) {
44
45
  }
45
46
  const issues = [];
46
47
  const openRedSlices = [];
48
+ // Reject slices whose ID does not match the stable `S-<number>` contract.
49
+ // Entries that drop the slice field entirely were previously coerced to
50
+ // `S-unknown` and silently bucketed together, which means multiple distinct
51
+ // cycles could appear to share a RED/GREEN pair.
52
+ for (const slice of bySlice.keys()) {
53
+ if (!SLICE_ID_PATTERN.test(slice)) {
54
+ issues.push(`slice "${slice}": id must match /^S-\\d+$/ (e.g. S-1)`);
55
+ }
56
+ }
47
57
  for (const [slice, sliceEntries] of bySlice.entries()) {
48
58
  let state = "need_red";
49
59
  for (const entry of sliceEntries) {
@@ -79,7 +89,15 @@ export function validateTddCycleOrder(entries, options = {}) {
79
89
  state = "green_done";
80
90
  continue;
81
91
  }
82
- // refactor
92
+ // refactor — must preserve the passing state established by green.
93
+ if (entry.exitCode === undefined) {
94
+ issues.push(`slice ${slice}: refactor entry must record exitCode 0`);
95
+ continue;
96
+ }
97
+ if (entry.exitCode !== 0) {
98
+ issues.push(`slice ${slice}: refactor entry exitCode must be 0 (tests must stay green)`);
99
+ continue;
100
+ }
83
101
  if (state !== "green_done") {
84
102
  issues.push(`slice ${slice}: refactor logged before green`);
85
103
  }
package/dist/types.d.ts CHANGED
@@ -88,6 +88,27 @@ export interface SliceReviewConfig {
88
88
  /** Tracks on which missed reviews escalate to a doctor warning. */
89
89
  enforceOnTracks?: FlowTrack[];
90
90
  }
91
+ /**
92
+ * File-path routing hints used by workflow-guard during `tdd` stage.
93
+ *
94
+ * - `testPathPatterns`: paths considered test-side changes (RED writes).
95
+ * - `productionPathPatterns`: optional allowlist for production paths that
96
+ * participate in GREEN/REFACTOR checks. When omitted, workflow-guard treats
97
+ * non-test code files as production writes.
98
+ */
99
+ export interface TddPathConfig {
100
+ testPathPatterns?: string[];
101
+ productionPathPatterns?: string[];
102
+ }
103
+ /**
104
+ * Compound-stage clustering policy.
105
+ *
106
+ * `recurrenceThreshold` is the base minimum repeat count for a trigger/action
107
+ * cluster before it is eligible for promotion into durable rules/skills.
108
+ */
109
+ export interface CompoundConfig {
110
+ recurrenceThreshold?: number;
111
+ }
91
112
  export interface VibyConfig {
92
113
  version: string;
93
114
  flowVersion: string;
@@ -112,13 +133,20 @@ export interface VibyConfig {
112
133
  */
113
134
  promptGuardMode?: "advisory" | "strict";
114
135
  /**
115
- * TDD red->green->refactor enforcement mode used by workflow guard hooks.
136
+ * TDD RED -> GREEN -> REFACTOR enforcement mode used by workflow guard hooks.
116
137
  *
117
138
  * Since v0.43.0 this is an advanced override — see `strictness`.
118
139
  */
119
140
  tddEnforcement?: "advisory" | "strict";
120
- /** Optional test file globs used by guard guidance and /cc-ops tdd-log docs. */
141
+ /**
142
+ * Legacy alias for test-side path detection in workflow-guard.
143
+ * Prefer `tdd.testPathPatterns` in new configs.
144
+ */
121
145
  tddTestGlobs?: string[];
146
+ /** Path-pattern routing for TDD test/production write classification. */
147
+ tdd?: TddPathConfig;
148
+ /** Compound-stage recurrence policy overrides. */
149
+ compound?: CompoundConfig;
122
150
  /** When true, cclaw installs managed git pre-commit/pre-push wrappers. */
123
151
  gitHookGuards?: boolean;
124
152
  /** Default flow track for new runs (quick = shortened path, standard = full pipeline). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.46.14",
3
+ "version": "0.47.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {