cclaw-cli 0.10.1 → 0.11.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.
@@ -36,21 +36,56 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
36
36
  const stage = flowState.currentStage;
37
37
  const schema = stageSchema(stage);
38
38
  const catalog = flowState.stageGateCatalog[stage];
39
- const required = schema.requiredGates.map((gate) => gate.id);
39
+ const required = schema.requiredGates
40
+ .filter((gate) => gate.tier === "required")
41
+ .map((gate) => gate.id);
42
+ const recommended = schema.requiredGates
43
+ .filter((gate) => gate.tier === "recommended")
44
+ .map((gate) => gate.id);
45
+ const conditional = schema.requiredGates
46
+ .filter((gate) => gate.tier === "conditional")
47
+ .map((gate) => gate.id);
40
48
  const requiredSet = new Set(required);
49
+ const recommendedSet = new Set(recommended);
50
+ const conditionalSet = new Set(conditional);
51
+ const allowedSet = new Set([...required, ...recommended, ...conditional]);
41
52
  const issues = [];
42
53
  const catalogRequired = unique(catalog.required);
54
+ const catalogRecommended = unique(catalog.recommended ?? []);
55
+ const catalogConditional = unique(catalog.conditional ?? []);
56
+ const catalogTriggered = unique(catalog.triggered ?? []);
43
57
  const missingInCatalog = required.filter((gateId) => !catalogRequired.includes(gateId));
44
58
  const unexpectedInCatalog = catalogRequired.filter((gateId) => !requiredSet.has(gateId));
59
+ const missingRecommendedInCatalog = recommended.filter((gateId) => !catalogRecommended.includes(gateId));
60
+ const unexpectedRecommendedInCatalog = catalogRecommended.filter((gateId) => !recommendedSet.has(gateId));
61
+ const missingConditionalInCatalog = conditional.filter((gateId) => !catalogConditional.includes(gateId));
62
+ const unexpectedConditionalInCatalog = catalogConditional.filter((gateId) => !conditionalSet.has(gateId));
45
63
  for (const gateId of missingInCatalog) {
46
64
  issues.push(`gate "${gateId}" missing from stageGateCatalog.required for stage "${stage}".`);
47
65
  }
48
66
  for (const gateId of unexpectedInCatalog) {
49
67
  issues.push(`unexpected gate "${gateId}" found in stageGateCatalog.required for stage "${stage}".`);
50
68
  }
69
+ for (const gateId of missingRecommendedInCatalog) {
70
+ issues.push(`gate "${gateId}" missing from stageGateCatalog.recommended for stage "${stage}".`);
71
+ }
72
+ for (const gateId of unexpectedRecommendedInCatalog) {
73
+ issues.push(`unexpected gate "${gateId}" found in stageGateCatalog.recommended for stage "${stage}".`);
74
+ }
75
+ for (const gateId of missingConditionalInCatalog) {
76
+ issues.push(`gate "${gateId}" missing from stageGateCatalog.conditional for stage "${stage}".`);
77
+ }
78
+ for (const gateId of unexpectedConditionalInCatalog) {
79
+ issues.push(`unexpected gate "${gateId}" found in stageGateCatalog.conditional for stage "${stage}".`);
80
+ }
81
+ for (const gateId of catalogTriggered) {
82
+ if (!conditionalSet.has(gateId)) {
83
+ issues.push(`triggered gate "${gateId}" is not defined as conditional for stage "${stage}".`);
84
+ }
85
+ }
51
86
  const blockedSet = new Set(catalog.blocked);
52
87
  for (const gateId of catalog.passed) {
53
- if (!requiredSet.has(gateId)) {
88
+ if (!allowedSet.has(gateId)) {
54
89
  issues.push(`passed gate "${gateId}" is not defined for stage "${stage}".`);
55
90
  continue;
56
91
  }
@@ -63,7 +98,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
63
98
  }
64
99
  }
65
100
  for (const gateId of catalog.blocked) {
66
- if (!requiredSet.has(gateId)) {
101
+ if (!allowedSet.has(gateId)) {
67
102
  issues.push(`blocked gate "${gateId}" is not defined for stage "${stage}".`);
68
103
  }
69
104
  }
@@ -91,14 +126,25 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
91
126
  }
92
127
  }
93
128
  const passedSet = new Set(catalog.passed);
129
+ const triggeredConditionalSet = new Set([
130
+ ...catalogTriggered.filter((gateId) => conditionalSet.has(gateId)),
131
+ ...catalog.passed.filter((gateId) => conditionalSet.has(gateId)),
132
+ ...catalog.blocked.filter((gateId) => conditionalSet.has(gateId))
133
+ ]);
94
134
  const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
95
- const complete = missingRequired.length === 0 && catalog.blocked.length === 0;
135
+ const missingRecommended = recommended.filter((gateId) => !passedSet.has(gateId));
136
+ const missingTriggeredConditional = [...triggeredConditionalSet].filter((gateId) => !passedSet.has(gateId));
137
+ const blockingBlocked = catalog.blocked.filter((gateId) => requiredSet.has(gateId) || triggeredConditionalSet.has(gateId));
138
+ const complete = missingRequired.length === 0 && missingTriggeredConditional.length === 0 && blockingBlocked.length === 0;
96
139
  if (flowState.completedStages.includes(stage) && !complete) {
97
140
  if (missingRequired.length > 0) {
98
141
  issues.push(`stage "${stage}" is marked completed but required gates are not passed: ${missingRequired.join(", ")}.`);
99
142
  }
100
- if (catalog.blocked.length > 0) {
101
- issues.push(`stage "${stage}" is marked completed but has blocked gates: ${catalog.blocked.join(", ")}.`);
143
+ if (missingTriggeredConditional.length > 0) {
144
+ issues.push(`stage "${stage}" is marked completed but triggered conditional gates are not passed: ${missingTriggeredConditional.join(", ")}.`);
145
+ }
146
+ if (blockingBlocked.length > 0) {
147
+ issues.push(`stage "${stage}" is marked completed but has blocking blocked gates: ${blockingBlocked.join(", ")}.`);
102
148
  }
103
149
  }
104
150
  return {
@@ -106,10 +152,15 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
106
152
  stage,
107
153
  issues,
108
154
  requiredCount: required.length,
155
+ recommendedCount: recommended.length,
156
+ conditionalCount: conditional.length,
157
+ triggeredConditionalCount: triggeredConditionalSet.size,
109
158
  passedCount: catalog.passed.length,
110
159
  blockedCount: catalog.blocked.length,
111
160
  complete,
112
- missingRequired
161
+ missingRequired,
162
+ missingRecommended,
163
+ missingTriggeredConditional
113
164
  };
114
165
  }
115
166
  export function verifyCompletedStagesGateClosure(flowState) {
@@ -118,16 +169,37 @@ export function verifyCompletedStagesGateClosure(flowState) {
118
169
  for (const stage of flowState.completedStages) {
119
170
  const schema = stageSchema(stage);
120
171
  const catalog = flowState.stageGateCatalog[stage];
121
- const required = schema.requiredGates.map((gate) => gate.id);
172
+ const required = schema.requiredGates
173
+ .filter((gate) => gate.tier === "required")
174
+ .map((gate) => gate.id);
175
+ const conditional = schema.requiredGates
176
+ .filter((gate) => gate.tier === "conditional")
177
+ .map((gate) => gate.id);
178
+ const conditionalSet = new Set(conditional);
122
179
  const passedSet = new Set(catalog.passed);
180
+ const triggeredSet = new Set([
181
+ ...(catalog.triggered ?? []).filter((gateId) => conditionalSet.has(gateId)),
182
+ ...catalog.passed.filter((gateId) => conditionalSet.has(gateId)),
183
+ ...catalog.blocked.filter((gateId) => conditionalSet.has(gateId))
184
+ ]);
123
185
  const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
124
- if (missingRequired.length > 0 || catalog.blocked.length > 0) {
125
- openStages.push({ stage, missingRequired, blocked: [...catalog.blocked] });
186
+ const missingTriggeredConditional = [...triggeredSet].filter((gateId) => !passedSet.has(gateId));
187
+ const blockingBlocked = catalog.blocked.filter((gateId) => required.includes(gateId) || triggeredSet.has(gateId));
188
+ if (missingRequired.length > 0 || missingTriggeredConditional.length > 0 || blockingBlocked.length > 0) {
189
+ openStages.push({
190
+ stage,
191
+ missingRequired,
192
+ missingTriggeredConditional,
193
+ blocked: [...blockingBlocked]
194
+ });
126
195
  if (missingRequired.length > 0) {
127
196
  issues.push(`completed stage "${stage}" has unpassed required gates: ${missingRequired.join(", ")}.`);
128
197
  }
129
- if (catalog.blocked.length > 0) {
130
- issues.push(`completed stage "${stage}" still has blocked gates: ${catalog.blocked.join(", ")}.`);
198
+ if (missingTriggeredConditional.length > 0) {
199
+ issues.push(`completed stage "${stage}" has unpassed triggered conditional gates: ${missingTriggeredConditional.join(", ")}.`);
200
+ }
201
+ if (blockingBlocked.length > 0) {
202
+ issues.push(`completed stage "${stage}" still has blocking blocked gates: ${blockingBlocked.join(", ")}.`);
131
203
  }
132
204
  }
133
205
  }
@@ -135,29 +207,55 @@ export function verifyCompletedStagesGateClosure(flowState) {
135
207
  }
136
208
  export function reconcileCurrentStageGateCatalog(flowState) {
137
209
  const stage = flowState.currentStage;
138
- const required = stageSchema(stage).requiredGates.map((gate) => gate.id);
210
+ const required = stageSchema(stage).requiredGates
211
+ .filter((gate) => gate.tier === "required")
212
+ .map((gate) => gate.id);
213
+ const recommended = stageSchema(stage).requiredGates
214
+ .filter((gate) => gate.tier === "recommended")
215
+ .map((gate) => gate.id);
216
+ const conditional = stageSchema(stage).requiredGates
217
+ .filter((gate) => gate.tier === "conditional")
218
+ .map((gate) => gate.id);
139
219
  const requiredSet = new Set(required);
220
+ const recommendedSet = new Set(recommended);
221
+ const conditionalSet = new Set(conditional);
222
+ const allowedSet = new Set([...required, ...recommended, ...conditional]);
140
223
  const catalog = flowState.stageGateCatalog[stage];
141
224
  const notes = [];
142
225
  const before = {
143
226
  required: [...catalog.required],
227
+ recommended: [...catalog.recommended],
228
+ conditional: [...catalog.conditional],
229
+ triggered: [...catalog.triggered],
144
230
  passed: [...catalog.passed],
145
231
  blocked: [...catalog.blocked]
146
232
  };
147
233
  const passedSet = new Set(unique(catalog.passed).filter((gateId) => {
148
- const keep = requiredSet.has(gateId);
234
+ const keep = allowedSet.has(gateId);
149
235
  if (!keep) {
150
236
  notes.push(`removed unknown passed gate "${gateId}"`);
151
237
  }
152
238
  return keep;
153
239
  }));
154
240
  const blockedSet = new Set(unique(catalog.blocked).filter((gateId) => {
155
- const keep = requiredSet.has(gateId);
241
+ const keep = allowedSet.has(gateId);
156
242
  if (!keep) {
157
243
  notes.push(`removed unknown blocked gate "${gateId}"`);
158
244
  }
159
245
  return keep;
160
246
  }));
247
+ const triggeredSet = new Set(unique(catalog.triggered).filter((gateId) => {
248
+ const keep = conditionalSet.has(gateId);
249
+ if (!keep) {
250
+ notes.push(`removed unknown triggered gate "${gateId}"`);
251
+ }
252
+ return keep;
253
+ }));
254
+ for (const gateId of [...passedSet, ...blockedSet]) {
255
+ if (conditionalSet.has(gateId)) {
256
+ triggeredSet.add(gateId);
257
+ }
258
+ }
161
259
  for (const gateId of [...passedSet]) {
162
260
  if (!blockedSet.has(gateId))
163
261
  continue;
@@ -180,10 +278,16 @@ export function reconcileCurrentStageGateCatalog(flowState) {
180
278
  }
181
279
  const after = {
182
280
  required: [...required],
183
- passed: required.filter((gateId) => passedSet.has(gateId)),
184
- blocked: required.filter((gateId) => blockedSet.has(gateId) && !passedSet.has(gateId))
281
+ recommended: [...recommended],
282
+ conditional: [...conditional],
283
+ triggered: conditional.filter((gateId) => triggeredSet.has(gateId)),
284
+ passed: [...required, ...recommended, ...conditional].filter((gateId) => passedSet.has(gateId)),
285
+ blocked: [...required, ...recommended, ...conditional].filter((gateId) => blockedSet.has(gateId) && !passedSet.has(gateId))
185
286
  };
186
287
  const changed = !sameStringArray(before.required, after.required) ||
288
+ !sameStringArray(before.recommended, after.recommended) ||
289
+ !sameStringArray(before.conditional, after.conditional) ||
290
+ !sameStringArray(before.triggered, after.triggered) ||
187
291
  !sameStringArray(before.passed, after.passed) ||
188
292
  !sameStringArray(before.blocked, after.blocked);
189
293
  const nextState = changed
package/dist/install.js CHANGED
@@ -15,8 +15,10 @@ import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
15
15
  import { sessionStartScript, stopCheckpointScript, preCompactScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
16
16
  import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
17
17
  import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
18
+ import { decisionProtocolMarkdown, completionProtocolMarkdown, ethosProtocolMarkdown } from "./content/protocols.js";
18
19
  import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
19
20
  import { TDD_WAVE_WALKTHROUGH_MARKDOWN, stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
21
+ import { stageCommonGuidanceMarkdown } from "./content/stage-common-guidance.js";
20
22
  import { STAGE_EXAMPLES_REFERENCE_DIR, stageExamplesReferenceMarkdown } from "./content/examples.js";
21
23
  import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LANGUAGE_RULE_PACK_GENERATORS, LEGACY_LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS, UTILITY_SKILL_MAP } from "./content/utility-skills.js";
22
24
  import { HARNESS_TOOL_REFS_DIR, HARNESS_TOOL_REFS_INDEX_MD, harnessToolRefMarkdown } from "./content/harness-tool-refs.js";
@@ -185,6 +187,7 @@ async function writeSkills(projectRoot, config) {
185
187
  // always-rendered TDD skill stays under the line-budget and the reference
186
188
  // is loaded on demand.
187
189
  await writeFileSafe(runtimePath(projectRoot, ...STAGE_EXAMPLES_REFERENCE_DIR.split("/"), "tdd-wave-walkthrough.md"), TDD_WAVE_WALKTHROUGH_MARKDOWN);
190
+ await writeFileSafe(runtimePath(projectRoot, ...STAGE_EXAMPLES_REFERENCE_DIR.split("/"), "common-guidance.md"), stageCommonGuidanceMarkdown());
188
191
  // Utility skills (not flow stages)
189
192
  await writeFileSafe(runtimePath(projectRoot, "skills", "learnings", "SKILL.md"), learnSkillMarkdown());
190
193
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-next-step", "SKILL.md"), nextCommandSkillMarkdown());
@@ -194,6 +197,9 @@ async function writeSkills(projectRoot, config) {
194
197
  await writeFileSafe(runtimePath(projectRoot, "skills", "parallel-dispatch", "SKILL.md"), parallelAgentsSkill());
195
198
  await writeFileSafe(runtimePath(projectRoot, "skills", "session", "SKILL.md"), sessionHooksSkillMarkdown());
196
199
  await writeFileSafe(runtimePath(projectRoot, "skills", META_SKILL_NAME, "SKILL.md"), usingCclawSkillMarkdown());
200
+ await writeFileSafe(runtimePath(projectRoot, "references", "protocols", "decision.md"), decisionProtocolMarkdown());
201
+ await writeFileSafe(runtimePath(projectRoot, "references", "protocols", "completion.md"), completionProtocolMarkdown());
202
+ await writeFileSafe(runtimePath(projectRoot, "references", "protocols", "ethos.md"), ethosProtocolMarkdown());
197
203
  for (const folder of UTILITY_SKILL_FOLDERS) {
198
204
  const generator = UTILITY_SKILL_MAP[folder];
199
205
  await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), generator());
@@ -773,6 +779,10 @@ async function ensureSessionStateFiles(projectRoot) {
773
779
  if (!(await exists(contextModePath))) {
774
780
  await writeFileSafe(contextModePath, `${JSON.stringify(createInitialContextModeState(), null, 2)}\n`);
775
781
  }
782
+ const knowledgeDigestPath = path.join(stateDir, "knowledge-digest.md");
783
+ if (!(await exists(knowledgeDigestPath))) {
784
+ await writeFileSafe(knowledgeDigestPath, "# Knowledge digest (auto-generated)\n\n(no entries yet)\n");
785
+ }
776
786
  }
777
787
  async function writeRulebook(projectRoot) {
778
788
  await writeFileSafe(runtimePath(projectRoot, "rules", "RULES.md"), RULEBOOK_MARKDOWN);
package/dist/policy.js CHANGED
@@ -40,16 +40,15 @@ export async function policyChecks(projectRoot, options = {}) {
40
40
  "## Process",
41
41
  "## Verification",
42
42
  "## Interaction Protocol",
43
- "## Common Rationalizations",
44
43
  "## Anti-Patterns & Red Flags",
45
44
  "## HARD-GATE",
46
45
  "## Checklist",
47
46
  "## Context Loading",
48
47
  "## Automatic Subagent Dispatch",
49
- "## Cognitive Patterns",
50
48
  "## Cross-Stage Traceability",
51
- "## Completion Status",
52
- "## Artifact Validation"
49
+ "## Artifact Validation",
50
+ "## Completion Parameters",
51
+ "## Shared Stage Guidance"
53
52
  ]) {
54
53
  rules.push({
55
54
  filePath: skillFile,
@@ -102,15 +101,17 @@ export async function policyChecks(projectRoot, options = {}) {
102
101
  { file: runtimeFile("skills/session/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:session:hard_gate" },
103
102
  { file: runtimeFile("skills/session/SKILL.md"), needle: "## Session Start Protocol", name: "utility_skill:session:start" },
104
103
  { file: runtimeFile("skills/session/SKILL.md"), needle: "## Session Stop Protocol", name: "utility_skill:session:stop" },
105
- { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Skill Discovery Flowchart", name: "meta_skill:discovery" },
106
- { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Activation Rules", name: "meta_skill:activation" },
107
- { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Stage Quick Reference", name: "meta_skill:reference" },
108
- { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Failure Modes", name: "meta_skill:failure_modes" },
109
- { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Contextual Skills", name: "meta_skill:contextual_skills" },
110
- { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Decision Protocol", name: "meta_skill:decision_protocol" },
111
- { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Progressive Disclosure (Depth / See Also)", name: "meta_skill:progressive_disclosure" },
104
+ { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Routing flow", name: "meta_skill:routing_flow" },
105
+ { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Task classification", name: "meta_skill:task_classification" },
106
+ { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Stage quick map", name: "meta_skill:stage_quick_map" },
107
+ { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Contextual skill activation", name: "meta_skill:contextual_skills" },
108
+ { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Protocol references", name: "meta_skill:protocol_refs" },
109
+ { file: runtimeFile("skills/using-cclaw/SKILL.md"), needle: "## Failure guardrails", name: "meta_skill:failure_guardrails" },
110
+ { file: runtimeFile("references/protocols/decision.md"), needle: "# Decision Protocol", name: "protocol:decision" },
111
+ { file: runtimeFile("references/protocols/completion.md"), needle: "# Stage Completion Protocol", name: "protocol:completion" },
112
+ { file: runtimeFile("references/protocols/ethos.md"), needle: "# Engineering Ethos", name: "protocol:ethos" },
112
113
  { file: runtimeFile("skills/session/SKILL.md"), needle: "## Session Resume Protocol", name: "utility_skill:session:resume" },
113
- { file: runtimeFile("skills/brainstorming/SKILL.md"), needle: "## Progressive Disclosure", name: "stage_skill:progressive_disclosure" },
114
+ { file: runtimeFile("skills/brainstorming/SKILL.md"), needle: "common-guidance.md", name: "stage_skill:shared_guidance_reference" },
114
115
  { file: runtimeFile("skills/security/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:security:hard_gate" },
115
116
  { file: runtimeFile("skills/security/SKILL.md"), needle: "## Checklist", name: "utility_skill:security:checklist" },
116
117
  { file: runtimeFile("skills/security/SKILL.md"), needle: "## Severity Classification", name: "utility_skill:security:severity" },
@@ -153,7 +154,9 @@ export async function policyChecks(projectRoot, options = {}) {
153
154
  { file: runtimeFile("hooks/workflow-guard.sh"), needle: "stage_invocation_without_recent_flow_read", name: "hooks:workflow_guard:flow_read_reason" },
154
155
  { file: runtimeFile("hooks/workflow-guard.sh"), needle: "stage_jump_", name: "hooks:workflow_guard:stage_jump_reason" },
155
156
  { file: runtimeFile("hooks/context-monitor.sh"), needle: "remaining is", name: "hooks:context:threshold_warning" },
156
- { file: runtimeFile("hooks/opencode-plugin.mjs"), needle: "activeRunId", name: "hooks:opencode:active_run" }
157
+ { file: runtimeFile("hooks/opencode-plugin.mjs"), needle: "activeRunId", name: "hooks:opencode:active_run" },
158
+ { file: runtimeFile("hooks/session-start.sh"), needle: "Knowledge digest", name: "hooks:session_start:knowledge_digest" },
159
+ { file: runtimeFile("hooks/opencode-plugin.mjs"), needle: "Knowledge digest", name: "hooks:opencode:knowledge_digest" }
157
160
  ];
158
161
  if (activeHarnesses.has("opencode")) {
159
162
  utilitySkillChecks.push({
package/dist/runs.js CHANGED
@@ -128,11 +128,15 @@ function sanitizeGuardEvidence(value) {
128
128
  return next;
129
129
  }
130
130
  function sanitizeStageGateCatalog(value, fallback) {
131
+ const uniqueStrings = (items) => [...new Set(items)];
131
132
  const next = {};
132
133
  for (const stage of COMMAND_FILE_ORDER) {
133
134
  const base = fallback[stage];
134
135
  next[stage] = {
135
136
  required: [...base.required],
137
+ recommended: [...base.recommended],
138
+ conditional: [...base.conditional],
139
+ triggered: [...base.triggered],
136
140
  passed: [...base.passed],
137
141
  blocked: [...base.blocked]
138
142
  };
@@ -147,11 +151,24 @@ function sanitizeStageGateCatalog(value, fallback) {
147
151
  continue;
148
152
  }
149
153
  const typed = rawStage;
150
- const allowedGateIds = new Set(next[stage].required);
154
+ const stageState = next[stage];
155
+ const allowedGateIds = new Set([
156
+ ...stageState.required,
157
+ ...stageState.recommended,
158
+ ...stageState.conditional
159
+ ]);
160
+ const conditionalGateIds = new Set(stageState.conditional);
161
+ const passed = sanitizeStringArray(typed.passed).filter((gate) => allowedGateIds.has(gate));
162
+ const blocked = sanitizeStringArray(typed.blocked).filter((gate) => allowedGateIds.has(gate));
163
+ const triggeredFromState = sanitizeStringArray(typed.triggered).filter((gate) => conditionalGateIds.has(gate));
164
+ const touchedConditionals = [...passed, ...blocked].filter((gate) => conditionalGateIds.has(gate));
151
165
  next[stage] = {
152
- required: [...next[stage].required],
153
- passed: sanitizeStringArray(typed.passed).filter((gate) => allowedGateIds.has(gate)),
154
- blocked: sanitizeStringArray(typed.blocked).filter((gate) => allowedGateIds.has(gate))
166
+ required: [...stageState.required],
167
+ recommended: [...stageState.recommended],
168
+ conditional: [...stageState.conditional],
169
+ triggered: uniqueStrings([...triggeredFromState, ...touchedConditionals]),
170
+ passed,
171
+ blocked
155
172
  };
156
173
  }
157
174
  return next;
@@ -0,0 +1,12 @@
1
+ import type { FlowTrack, TrackHeuristicRule, TrackHeuristicsConfig } from "./types.js";
2
+ export interface TrackResolution {
3
+ track: FlowTrack;
4
+ reason: string;
5
+ matchedTokens: string[];
6
+ }
7
+ export declare function resolveTrackFromPrompt(prompt: string, config: TrackHeuristicsConfig | undefined): TrackResolution;
8
+ export declare const TRACK_HEURISTICS_DEFAULTS: {
9
+ readonly fallback: "standard";
10
+ readonly priority: ("quick" | "medium" | "standard")[];
11
+ readonly tracks: Record<"quick" | "medium" | "standard", TrackHeuristicRule>;
12
+ };
@@ -0,0 +1,144 @@
1
+ import { FLOW_TRACKS } from "./types.js";
2
+ const DEFAULT_RULES = {
3
+ quick: {
4
+ triggers: [
5
+ "bug",
6
+ "bugfix",
7
+ "fix",
8
+ "hotfix",
9
+ "patch",
10
+ "typo",
11
+ "regression",
12
+ "copy change",
13
+ "rename",
14
+ "bump",
15
+ "upgrade dep",
16
+ "config tweak",
17
+ "docs only",
18
+ "comment",
19
+ "lint",
20
+ "format",
21
+ "small",
22
+ "tiny",
23
+ "one-liner",
24
+ "revert"
25
+ ]
26
+ },
27
+ medium: {
28
+ triggers: [
29
+ "add endpoint",
30
+ "add field",
31
+ "extend existing",
32
+ "wire integration",
33
+ "small migration",
34
+ "new screen following existing pattern"
35
+ ]
36
+ },
37
+ standard: {
38
+ triggers: [
39
+ "new feature",
40
+ "refactor",
41
+ "migration",
42
+ "platform",
43
+ "architecture",
44
+ "schema",
45
+ "integrate",
46
+ "workflow",
47
+ "onboarding"
48
+ ]
49
+ }
50
+ };
51
+ const DEFAULT_PRIORITY = ["standard", "medium", "quick"];
52
+ const DEFAULT_FALLBACK = "standard";
53
+ function hasToken(promptLower, token) {
54
+ return promptLower.includes(token.toLowerCase());
55
+ }
56
+ function matchRule(promptLower, rule) {
57
+ if (!rule)
58
+ return [];
59
+ const matches = [];
60
+ for (const trigger of rule.triggers ?? []) {
61
+ if (hasToken(promptLower, trigger)) {
62
+ matches.push(trigger);
63
+ }
64
+ }
65
+ for (const pattern of rule.patterns ?? []) {
66
+ try {
67
+ const regex = new RegExp(pattern, "iu");
68
+ if (regex.test(promptLower)) {
69
+ matches.push(`/${pattern}/`);
70
+ }
71
+ }
72
+ catch {
73
+ // Ignore invalid custom regex entries; config validation should catch these.
74
+ }
75
+ }
76
+ return [...new Set(matches)];
77
+ }
78
+ function isValidTrack(value) {
79
+ return FLOW_TRACKS.includes(value);
80
+ }
81
+ function mergeRules(base, overrides) {
82
+ const merged = { ...base };
83
+ const over = overrides?.tracks;
84
+ if (!over)
85
+ return merged;
86
+ for (const track of FLOW_TRACKS) {
87
+ const rule = over[track];
88
+ if (!rule)
89
+ continue;
90
+ merged[track] = {
91
+ triggers: rule.triggers ?? merged[track].triggers,
92
+ patterns: rule.patterns ?? merged[track].patterns,
93
+ veto: rule.veto ?? merged[track].veto
94
+ };
95
+ }
96
+ return merged;
97
+ }
98
+ function resolvePriority(config) {
99
+ const configured = config?.priority ?? [];
100
+ const filtered = configured.filter((track) => isValidTrack(track));
101
+ const unique = [...new Set(filtered)];
102
+ if (unique.length === 0)
103
+ return [...DEFAULT_PRIORITY];
104
+ // Ensure all tracks are still represented in deterministic order.
105
+ for (const track of FLOW_TRACKS) {
106
+ if (!unique.includes(track))
107
+ unique.push(track);
108
+ }
109
+ return unique;
110
+ }
111
+ function resolveFallback(config) {
112
+ return config?.fallback && isValidTrack(config.fallback) ? config.fallback : DEFAULT_FALLBACK;
113
+ }
114
+ export function resolveTrackFromPrompt(prompt, config) {
115
+ const promptLower = prompt.toLowerCase();
116
+ const rules = mergeRules(DEFAULT_RULES, config);
117
+ const priority = resolvePriority(config);
118
+ const fallback = resolveFallback(config);
119
+ for (const track of priority) {
120
+ const rule = rules[track];
121
+ const vetoes = rule.veto ?? [];
122
+ if (vetoes.some((token) => hasToken(promptLower, token))) {
123
+ continue;
124
+ }
125
+ const matched = matchRule(promptLower, rule);
126
+ if (matched.length > 0) {
127
+ return {
128
+ track,
129
+ reason: `matched ${track} heuristic`,
130
+ matchedTokens: matched
131
+ };
132
+ }
133
+ }
134
+ return {
135
+ track: fallback,
136
+ reason: `no explicit match, fallback=${fallback}`,
137
+ matchedTokens: []
138
+ };
139
+ }
140
+ export const TRACK_HEURISTICS_DEFAULTS = {
141
+ fallback: DEFAULT_FALLBACK,
142
+ priority: DEFAULT_PRIORITY,
143
+ tracks: DEFAULT_RULES
144
+ };
package/dist/types.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  export declare const FLOW_STAGES: readonly ["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"];
2
2
  export type FlowStage = (typeof FLOW_STAGES)[number];
3
- export declare const FLOW_TRACKS: readonly ["quick", "standard"];
3
+ export declare const FLOW_TRACKS: readonly ["quick", "medium", "standard"];
4
4
  export type FlowTrack = (typeof FLOW_TRACKS)[number];
5
5
  /**
6
6
  * Ordered stages that make up each flow track.
7
7
  *
8
8
  * - `standard` runs the full 8-stage pipeline (default — same as before tracks existed).
9
+ * - `medium` keeps product framing but skips heavy scope/design lock-in:
10
+ * brainstorm -> spec -> plan -> tdd -> review -> ship.
9
11
  * - `quick` skips the upstream product stages (brainstorm/scope/design/plan) for
10
12
  * small bug fixes or single-purpose changes where the spec is already known.
11
13
  * It still keeps the non-negotiable safety gates: spec → tdd → review → ship.
@@ -16,8 +18,8 @@ export type HarnessId = (typeof HARNESS_IDS)[number];
16
18
  /**
17
19
  * Init profiles pre-fill `cclaw init` flags for common install shapes.
18
20
  *
19
- * - `minimal` — single-harness (claude), quick track default, no git hook guards. For solo
20
- * contributors or bugfix-heavy repos where most work is \`quick\` scope.
21
+ * - `minimal` — single-harness (claude), medium track default, no git hook guards. For solo
22
+ * contributors who still want brainstorm/spec/plan rigor without full scope+design overhead.
21
23
  * - `standard` — default harness set, standard track, no git hook guards, advisory guards.
22
24
  * Matches the pre-profile default behavior.
23
25
  * - `full` — default harness set, standard track, git hook guards on, strict prompt guards.
@@ -35,6 +37,22 @@ export type InitProfile = (typeof INIT_PROFILES)[number];
35
37
  */
36
38
  export declare const LANGUAGE_RULE_PACKS: readonly ["typescript", "python", "go"];
37
39
  export type LanguageRulePack = (typeof LANGUAGE_RULE_PACKS)[number];
40
+ export interface TrackHeuristicRule {
41
+ triggers?: string[];
42
+ patterns?: string[];
43
+ veto?: string[];
44
+ }
45
+ export interface TrackHeuristicsConfig {
46
+ /** Track used when no trigger/pattern matches. */
47
+ fallback?: FlowTrack;
48
+ /**
49
+ * Track evaluation order. First matching track wins.
50
+ * Example: ["standard", "medium", "quick"].
51
+ */
52
+ priority?: FlowTrack[];
53
+ /** Per-track matching rules. */
54
+ tracks?: Partial<Record<FlowTrack, TrackHeuristicRule>>;
55
+ }
38
56
  export interface VibyConfig {
39
57
  version: string;
40
58
  flowVersion: string;
@@ -54,6 +72,11 @@ export interface VibyConfig {
54
72
  * the language in question. Disabled packs have no on-disk footprint.
55
73
  */
56
74
  languageRulePacks?: LanguageRulePack[];
75
+ /**
76
+ * Optional prompt-to-track mapping overrides for /cc classification.
77
+ * If omitted, cclaw uses built-in defaults.
78
+ */
79
+ trackHeuristics?: TrackHeuristicsConfig;
57
80
  }
58
81
  export interface TransitionRule {
59
82
  from: FlowStage;
package/dist/types.js CHANGED
@@ -8,25 +8,28 @@ export const FLOW_STAGES = [
8
8
  "review",
9
9
  "ship"
10
10
  ];
11
- export const FLOW_TRACKS = ["quick", "standard"];
11
+ export const FLOW_TRACKS = ["quick", "medium", "standard"];
12
12
  /**
13
13
  * Ordered stages that make up each flow track.
14
14
  *
15
15
  * - `standard` runs the full 8-stage pipeline (default — same as before tracks existed).
16
+ * - `medium` keeps product framing but skips heavy scope/design lock-in:
17
+ * brainstorm -> spec -> plan -> tdd -> review -> ship.
16
18
  * - `quick` skips the upstream product stages (brainstorm/scope/design/plan) for
17
19
  * small bug fixes or single-purpose changes where the spec is already known.
18
20
  * It still keeps the non-negotiable safety gates: spec → tdd → review → ship.
19
21
  */
20
22
  export const TRACK_STAGES = {
21
23
  standard: FLOW_STAGES,
24
+ medium: ["brainstorm", "spec", "plan", "tdd", "review", "ship"],
22
25
  quick: ["spec", "tdd", "review", "ship"]
23
26
  };
24
27
  export const HARNESS_IDS = ["claude", "cursor", "opencode", "codex"];
25
28
  /**
26
29
  * Init profiles pre-fill `cclaw init` flags for common install shapes.
27
30
  *
28
- * - `minimal` — single-harness (claude), quick track default, no git hook guards. For solo
29
- * contributors or bugfix-heavy repos where most work is \`quick\` scope.
31
+ * - `minimal` — single-harness (claude), medium track default, no git hook guards. For solo
32
+ * contributors who still want brainstorm/spec/plan rigor without full scope+design overhead.
30
33
  * - `standard` — default harness set, standard track, no git hook guards, advisory guards.
31
34
  * Matches the pre-profile default behavior.
32
35
  * - `full` — default harness set, standard track, git hook guards on, strict prompt guards.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {