@paths.design/caws-cli 9.3.2 → 10.1.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.
Files changed (286) hide show
  1. package/README.md +71 -32
  2. package/dist/budget-derivation.js +221 -74
  3. package/dist/commands/archive.js +67 -28
  4. package/dist/commands/burnup.js +20 -11
  5. package/dist/commands/diagnose.js +34 -22
  6. package/dist/commands/evaluate.js +41 -15
  7. package/dist/commands/gates.js +149 -0
  8. package/dist/commands/init.js +150 -19
  9. package/dist/commands/iterate.js +81 -4
  10. package/dist/commands/parallel.js +4 -0
  11. package/dist/commands/plan.js +9 -19
  12. package/dist/commands/provenance.js +53 -17
  13. package/dist/commands/quality-monitor.js +64 -45
  14. package/dist/commands/scope.js +264 -0
  15. package/dist/commands/sidecar.js +74 -0
  16. package/dist/commands/specs.js +381 -45
  17. package/dist/commands/status.js +117 -9
  18. package/dist/commands/templates.js +0 -8
  19. package/dist/commands/tutorial.js +10 -9
  20. package/dist/commands/validate.js +70 -6
  21. package/dist/commands/verify-acs.js +48 -76
  22. package/dist/commands/waivers.js +212 -13
  23. package/dist/commands/worktree.js +131 -26
  24. package/dist/error-handler.js +2 -13
  25. package/dist/gates/budget-limit.js +121 -0
  26. package/dist/gates/feedback.js +260 -0
  27. package/dist/gates/format.js +179 -0
  28. package/dist/gates/god-object.js +117 -0
  29. package/dist/gates/pipeline.js +167 -0
  30. package/dist/gates/scope-boundary.js +93 -0
  31. package/dist/gates/spec-completeness.js +109 -0
  32. package/dist/gates/todo-detection.js +205 -0
  33. package/dist/index.js +157 -151
  34. package/dist/parallel/parallel-manager.js +3 -3
  35. package/dist/policy/PolicyManager.js +51 -17
  36. package/dist/scaffold/claude-hooks.js +24 -1
  37. package/dist/scaffold/git-hooks.js +45 -102
  38. package/dist/scaffold/index.js +4 -3
  39. package/dist/session/session-manager.js +105 -14
  40. package/dist/sidecars/index.js +33 -0
  41. package/dist/sidecars/listeners.js +40 -0
  42. package/dist/sidecars/provenance-summary.js +238 -0
  43. package/dist/sidecars/quality-gaps.js +258 -0
  44. package/dist/sidecars/schema.js +149 -0
  45. package/dist/sidecars/spec-drift.js +151 -0
  46. package/dist/sidecars/waiver-draft.js +176 -0
  47. package/dist/templates/.caws/schemas/policy.schema.json +112 -0
  48. package/dist/templates/.caws/schemas/scope.schema.json +3 -3
  49. package/dist/templates/.caws/schemas/waivers.schema.json +96 -20
  50. package/dist/templates/.caws/schemas/working-spec.schema.json +264 -57
  51. package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
  52. package/dist/templates/.caws/templates/working-spec.template.yml +10 -4
  53. package/dist/templates/.caws/tools/scope-guard.js +66 -15
  54. package/dist/templates/.claude/README.md +1 -1
  55. package/dist/templates/.claude/hooks/audit.sh +0 -0
  56. package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
  57. package/dist/templates/.claude/hooks/classify_command.py +592 -0
  58. package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  59. package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
  60. package/dist/templates/.claude/hooks/quality-check.sh +23 -10
  61. package/dist/templates/.claude/hooks/scope-guard.sh +136 -55
  62. package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
  63. package/dist/templates/.claude/hooks/session-log.sh +76 -3
  64. package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  65. package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
  66. package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  67. package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
  68. package/dist/templates/.claude/hooks/worktree-write-guard.sh +97 -4
  69. package/dist/templates/.claude/settings.json +31 -0
  70. package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  71. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  72. package/dist/templates/.cursor/hooks/session-log.sh +924 -0
  73. package/dist/templates/.cursor/hooks.json +25 -0
  74. package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  75. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  76. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  77. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  78. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  79. package/dist/templates/.github/copilot-instructions.md +5 -5
  80. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  81. package/dist/templates/.junie/guidelines.md +2 -2
  82. package/dist/templates/.vscode/settings.json +3 -1
  83. package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  84. package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  85. package/dist/templates/CLAUDE.md +77 -8
  86. package/dist/templates/agents.md +50 -9
  87. package/dist/templates/docs/README.md +8 -7
  88. package/dist/templates/scripts/new_feature.sh +80 -0
  89. package/dist/test-analysis.js +43 -30
  90. package/dist/tool-loader.js +1 -1
  91. package/dist/utils/agent-session.js +202 -0
  92. package/dist/utils/detection.js +8 -2
  93. package/dist/utils/event-log.js +584 -0
  94. package/dist/utils/event-renderer.js +521 -0
  95. package/dist/utils/finalization.js +7 -6
  96. package/dist/utils/gitignore-updater.js +3 -0
  97. package/dist/utils/lifecycle-events.js +94 -0
  98. package/dist/utils/quality-gates-utils.js +29 -44
  99. package/dist/utils/schema-validator.js +50 -0
  100. package/dist/utils/spec-resolver.js +93 -21
  101. package/dist/utils/working-state.js +530 -0
  102. package/dist/validation/spec-validation.js +191 -31
  103. package/dist/waivers-manager.js +144 -6
  104. package/dist/worktree/worktree-manager.js +598 -95
  105. package/package.json +9 -8
  106. package/templates/.caws/schemas/policy.schema.json +112 -0
  107. package/templates/.caws/schemas/scope.schema.json +3 -3
  108. package/templates/.caws/schemas/waivers.schema.json +96 -20
  109. package/templates/.caws/schemas/working-spec.schema.json +264 -57
  110. package/templates/.caws/schemas/worktrees.schema.json +3 -1
  111. package/templates/.caws/templates/working-spec.template.yml +10 -4
  112. package/templates/.caws/tools/scope-guard.js +66 -15
  113. package/templates/.claude/README.md +1 -1
  114. package/templates/.claude/hooks/block-dangerous.sh +52 -11
  115. package/templates/.claude/hooks/classify_command.py +592 -0
  116. package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  117. package/templates/.claude/hooks/protected-paths.sh +39 -0
  118. package/templates/.claude/hooks/quality-check.sh +23 -10
  119. package/templates/.claude/hooks/scope-guard.sh +136 -55
  120. package/templates/.claude/hooks/session-caws-status.sh +2 -2
  121. package/templates/.claude/hooks/session-log.sh +76 -3
  122. package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  123. package/templates/.claude/hooks/test_classify_command.py +370 -0
  124. package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  125. package/templates/.claude/hooks/worktree-guard.sh +2 -2
  126. package/templates/.claude/hooks/worktree-write-guard.sh +97 -4
  127. package/templates/.claude/settings.json +31 -0
  128. package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  129. package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  130. package/templates/.cursor/hooks/session-log.sh +924 -0
  131. package/templates/.cursor/hooks.json +25 -0
  132. package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  133. package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  134. package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  135. package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  136. package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  137. package/templates/.github/copilot-instructions.md +5 -5
  138. package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  139. package/templates/.junie/guidelines.md +2 -2
  140. package/templates/.vscode/settings.json +3 -1
  141. package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  142. package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  143. package/templates/CLAUDE.md +77 -8
  144. package/templates/{AGENTS.md → agents.md} +50 -9
  145. package/templates/docs/README.md +8 -7
  146. package/templates/scripts/new_feature.sh +80 -0
  147. package/dist/budget-derivation.d.ts +0 -74
  148. package/dist/budget-derivation.d.ts.map +0 -1
  149. package/dist/cicd-optimizer.d.ts +0 -142
  150. package/dist/cicd-optimizer.d.ts.map +0 -1
  151. package/dist/commands/archive.d.ts +0 -51
  152. package/dist/commands/archive.d.ts.map +0 -1
  153. package/dist/commands/burnup.d.ts +0 -6
  154. package/dist/commands/burnup.d.ts.map +0 -1
  155. package/dist/commands/diagnose.d.ts +0 -52
  156. package/dist/commands/diagnose.d.ts.map +0 -1
  157. package/dist/commands/evaluate.d.ts +0 -8
  158. package/dist/commands/evaluate.d.ts.map +0 -1
  159. package/dist/commands/init.d.ts +0 -5
  160. package/dist/commands/init.d.ts.map +0 -1
  161. package/dist/commands/iterate.d.ts +0 -8
  162. package/dist/commands/iterate.d.ts.map +0 -1
  163. package/dist/commands/mode.d.ts +0 -25
  164. package/dist/commands/mode.d.ts.map +0 -1
  165. package/dist/commands/parallel.d.ts +0 -7
  166. package/dist/commands/parallel.d.ts.map +0 -1
  167. package/dist/commands/plan.d.ts +0 -49
  168. package/dist/commands/plan.d.ts.map +0 -1
  169. package/dist/commands/provenance.d.ts +0 -32
  170. package/dist/commands/provenance.d.ts.map +0 -1
  171. package/dist/commands/quality-gates.d.ts +0 -6
  172. package/dist/commands/quality-gates.d.ts.map +0 -1
  173. package/dist/commands/quality-gates.js +0 -444
  174. package/dist/commands/quality-monitor.d.ts +0 -17
  175. package/dist/commands/quality-monitor.d.ts.map +0 -1
  176. package/dist/commands/session.d.ts +0 -7
  177. package/dist/commands/session.d.ts.map +0 -1
  178. package/dist/commands/specs.d.ts +0 -77
  179. package/dist/commands/specs.d.ts.map +0 -1
  180. package/dist/commands/status.d.ts +0 -44
  181. package/dist/commands/status.d.ts.map +0 -1
  182. package/dist/commands/templates.d.ts +0 -74
  183. package/dist/commands/templates.d.ts.map +0 -1
  184. package/dist/commands/tool.d.ts +0 -13
  185. package/dist/commands/tool.d.ts.map +0 -1
  186. package/dist/commands/troubleshoot.d.ts +0 -8
  187. package/dist/commands/troubleshoot.d.ts.map +0 -1
  188. package/dist/commands/troubleshoot.js +0 -104
  189. package/dist/commands/tutorial.d.ts +0 -55
  190. package/dist/commands/tutorial.d.ts.map +0 -1
  191. package/dist/commands/validate.d.ts +0 -15
  192. package/dist/commands/validate.d.ts.map +0 -1
  193. package/dist/commands/waivers.d.ts +0 -8
  194. package/dist/commands/waivers.d.ts.map +0 -1
  195. package/dist/commands/workflow.d.ts +0 -85
  196. package/dist/commands/workflow.d.ts.map +0 -1
  197. package/dist/commands/worktree.d.ts +0 -7
  198. package/dist/commands/worktree.d.ts.map +0 -1
  199. package/dist/config/index.d.ts +0 -29
  200. package/dist/config/index.d.ts.map +0 -1
  201. package/dist/config/lite-scope.d.ts +0 -33
  202. package/dist/config/lite-scope.d.ts.map +0 -1
  203. package/dist/config/modes.d.ts +0 -264
  204. package/dist/config/modes.d.ts.map +0 -1
  205. package/dist/constants/spec-types.d.ts +0 -93
  206. package/dist/constants/spec-types.d.ts.map +0 -1
  207. package/dist/error-handler.d.ts +0 -151
  208. package/dist/error-handler.d.ts.map +0 -1
  209. package/dist/generators/jest-config-generator.d.ts +0 -32
  210. package/dist/generators/jest-config-generator.d.ts.map +0 -1
  211. package/dist/generators/jest-config.d.ts +0 -32
  212. package/dist/generators/jest-config.d.ts.map +0 -1
  213. package/dist/generators/jest-config.js +0 -242
  214. package/dist/generators/working-spec.d.ts +0 -13
  215. package/dist/generators/working-spec.d.ts.map +0 -1
  216. package/dist/index-new.d.ts +0 -5
  217. package/dist/index-new.d.ts.map +0 -1
  218. package/dist/index-new.js +0 -317
  219. package/dist/index.d.ts +0 -5
  220. package/dist/index.d.ts.map +0 -1
  221. package/dist/index.js.backup +0 -4711
  222. package/dist/minimal-cli.d.ts +0 -3
  223. package/dist/minimal-cli.d.ts.map +0 -1
  224. package/dist/parallel/parallel-manager.d.ts +0 -67
  225. package/dist/parallel/parallel-manager.d.ts.map +0 -1
  226. package/dist/policy/PolicyManager.d.ts +0 -104
  227. package/dist/policy/PolicyManager.d.ts.map +0 -1
  228. package/dist/scaffold/claude-hooks.d.ts +0 -28
  229. package/dist/scaffold/claude-hooks.d.ts.map +0 -1
  230. package/dist/scaffold/cursor-hooks.d.ts +0 -7
  231. package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
  232. package/dist/scaffold/git-hooks.d.ts +0 -38
  233. package/dist/scaffold/git-hooks.d.ts.map +0 -1
  234. package/dist/scaffold/index.d.ts +0 -17
  235. package/dist/scaffold/index.d.ts.map +0 -1
  236. package/dist/session/session-manager.d.ts +0 -94
  237. package/dist/session/session-manager.d.ts.map +0 -1
  238. package/dist/spec/SpecFileManager.d.ts +0 -146
  239. package/dist/spec/SpecFileManager.d.ts.map +0 -1
  240. package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
  241. package/dist/templates/.github/copilot/instructions.md +0 -311
  242. package/dist/test-analysis.d.ts +0 -231
  243. package/dist/test-analysis.d.ts.map +0 -1
  244. package/dist/tool-interface.d.ts +0 -236
  245. package/dist/tool-interface.d.ts.map +0 -1
  246. package/dist/tool-loader.d.ts +0 -77
  247. package/dist/tool-loader.d.ts.map +0 -1
  248. package/dist/tool-validator.d.ts +0 -72
  249. package/dist/tool-validator.d.ts.map +0 -1
  250. package/dist/utils/async-utils.d.ts +0 -73
  251. package/dist/utils/async-utils.d.ts.map +0 -1
  252. package/dist/utils/command-wrapper.d.ts +0 -66
  253. package/dist/utils/command-wrapper.d.ts.map +0 -1
  254. package/dist/utils/detection.d.ts +0 -14
  255. package/dist/utils/detection.d.ts.map +0 -1
  256. package/dist/utils/error-categories.d.ts +0 -52
  257. package/dist/utils/error-categories.d.ts.map +0 -1
  258. package/dist/utils/finalization.d.ts +0 -17
  259. package/dist/utils/finalization.d.ts.map +0 -1
  260. package/dist/utils/git-lock.d.ts +0 -13
  261. package/dist/utils/git-lock.d.ts.map +0 -1
  262. package/dist/utils/gitignore-updater.d.ts +0 -39
  263. package/dist/utils/gitignore-updater.d.ts.map +0 -1
  264. package/dist/utils/ide-detection.d.ts +0 -89
  265. package/dist/utils/ide-detection.d.ts.map +0 -1
  266. package/dist/utils/project-analysis.d.ts +0 -34
  267. package/dist/utils/project-analysis.d.ts.map +0 -1
  268. package/dist/utils/promise-utils.d.ts +0 -30
  269. package/dist/utils/promise-utils.d.ts.map +0 -1
  270. package/dist/utils/quality-gates-utils.d.ts +0 -49
  271. package/dist/utils/quality-gates-utils.d.ts.map +0 -1
  272. package/dist/utils/quality-gates.d.ts +0 -49
  273. package/dist/utils/quality-gates.d.ts.map +0 -1
  274. package/dist/utils/quality-gates.js +0 -402
  275. package/dist/utils/spec-resolver.d.ts +0 -80
  276. package/dist/utils/spec-resolver.d.ts.map +0 -1
  277. package/dist/utils/typescript-detector.d.ts +0 -66
  278. package/dist/utils/typescript-detector.d.ts.map +0 -1
  279. package/dist/utils/yaml-validation.d.ts +0 -32
  280. package/dist/utils/yaml-validation.d.ts.map +0 -1
  281. package/dist/validation/spec-validation.d.ts +0 -43
  282. package/dist/validation/spec-validation.d.ts.map +0 -1
  283. package/dist/waivers-manager.d.ts +0 -167
  284. package/dist/waivers-manager.d.ts.map +0 -1
  285. package/dist/worktree/worktree-manager.d.ts +0 -54
  286. package/dist/worktree/worktree-manager.d.ts.map +0 -1
@@ -13,6 +13,7 @@ const path = require('path');
13
13
  const yaml = require('js-yaml');
14
14
  const chalk = require('chalk');
15
15
  const { initializeGlobalSetup } = require('../config');
16
+ const { getAgentSessionId } = require('../utils/agent-session');
16
17
  const WaiversManager = require('../waivers-manager');
17
18
  const { commandWrapper, Output } = require('../utils/command-wrapper');
18
19
 
@@ -24,16 +25,12 @@ const WAIVER_DIR = '.caws/waivers';
24
25
  * within the check-*.mjs files.
25
26
  */
26
27
  const VALID_GATES = [
27
- 'naming',
28
- 'code_freeze',
29
- 'duplication',
30
- 'duplication_gate',
31
- 'god_objects',
32
- 'placeholders',
33
- 'simplification',
34
- 'hidden-todo',
35
- 'documentation',
36
- '*',
28
+ 'budget_limit',
29
+ 'spec_completeness',
30
+ 'scope_boundary',
31
+ 'god_object',
32
+ 'todo_detection',
33
+ '*', // wildcard — waiver applies to all gates
37
34
  ];
38
35
 
39
36
  /**
@@ -49,7 +46,7 @@ async function waiversCommand(subcommand = 'list', options = {}) {
49
46
  const setup = initializeGlobalSetup();
50
47
 
51
48
  if (setup.hasWorkingSpec) {
52
- Output.success(`Detected ${setup.setupType} CAWS setup`, {
49
+ Output.success(`Detected ${setup.type} CAWS setup`, {
53
50
  capabilities: setup.capabilities,
54
51
  });
55
52
  }
@@ -69,10 +66,12 @@ async function waiversCommand(subcommand = 'list', options = {}) {
69
66
  return await showWaiver(options.id, options);
70
67
  case 'revoke':
71
68
  return await revokeWaiver(options.id, options);
69
+ case 'prune':
70
+ return await pruneWaivers(options);
72
71
  default:
73
72
  throw new Error(
74
73
  `Unknown waiver subcommand: ${subcommand}.\n` +
75
- 'Available subcommands: create, list, show, revoke'
74
+ 'Available subcommands: create, list, show, revoke, prune'
76
75
  );
77
76
  }
78
77
  },
@@ -138,6 +137,24 @@ async function createWaiver(options) {
138
137
  process.exit(1);
139
138
  }
140
139
 
140
+ // Self-approval prevention: creator cannot be approver
141
+ // Uses strict equality — the previous .includes() check was an asymmetric
142
+ // substring match that produced false positives (blocking legitimate approvers
143
+ // whose name happened to contain the session ID) while missing the reverse
144
+ // case (approver is a prefix of the session ID).
145
+ // When CLAUDE_SESSION_ID is unset or empty, we can't identify the creator,
146
+ // so self-approval prevention is skipped ('' || null → null → falsy guard).
147
+ const creatorSession = getAgentSessionId(process.cwd());
148
+ if (creatorSession && options.approvedBy) {
149
+ if (options.approvedBy === creatorSession) {
150
+ throw new Error(
151
+ 'Waiver creator cannot be the approver.\n' +
152
+ 'A different agent or human must approve this waiver.\n' +
153
+ `Creator session: ${creatorSession}`
154
+ );
155
+ }
156
+ }
157
+
141
158
  // Generate waiver ID
142
159
  const waiverId = `WV-${Date.now().toString().slice(-4)}`;
143
160
  const timestamp = new Date().toISOString();
@@ -155,8 +172,26 @@ async function createWaiver(options) {
155
172
  impact_level: options.impactLevel,
156
173
  mitigation_plan: options.mitigationPlan,
157
174
  status: 'active',
175
+ created_by_session: creatorSession,
158
176
  };
159
177
 
178
+ // Validate waiver against schema before persisting
179
+ try {
180
+ const { createValidator, getSchemaPath } = require('../utils/schema-validator');
181
+ const schemaPath = getSchemaPath('waivers.schema.json', process.cwd());
182
+ const validate = createValidator(schemaPath);
183
+ // waivers.schema.json validates a single waiver document directly (CAWSFIX-17)
184
+ const result = validate(waiver);
185
+ if (!result.valid) {
186
+ console.warn(chalk.yellow('Waiver has schema violations:'));
187
+ result.errors.forEach((err) => {
188
+ console.warn(chalk.yellow(` ${err.instancePath}: ${err.message}`));
189
+ });
190
+ }
191
+ } catch (schemaErr) {
192
+ // Non-fatal — don't block waiver creation on schema issues
193
+ }
194
+
160
195
  // Save individual waiver file
161
196
  const waiverPath = path.join(process.cwd(), WAIVER_DIR, `${waiverId}.yaml`);
162
197
  fs.writeFileSync(waiverPath, yaml.dump(waiver, { lineWidth: -1 }));
@@ -198,9 +233,32 @@ async function listWaivers(_options) {
198
233
  return;
199
234
  }
200
235
 
236
+ // Load a schema validator once for all waiver files (if available)
237
+ let waiverValidate = null;
238
+ try {
239
+ const { createValidator, getSchemaPath } = require('../utils/schema-validator');
240
+ const schemaPath = getSchemaPath('waivers.schema.json', process.cwd());
241
+ waiverValidate = createValidator(schemaPath);
242
+ } catch {
243
+ // Schema not available — skip validation
244
+ }
245
+
201
246
  const waivers = waiverFiles.map((file) => {
202
247
  const content = fs.readFileSync(path.join(waiversDir, file), 'utf8');
203
- return yaml.load(content);
248
+ const waiver = yaml.load(content);
249
+
250
+ // Validate each loaded waiver against schema
251
+ if (waiverValidate && waiver && waiver.id) {
252
+ const result = waiverValidate(waiver);
253
+ if (!result.valid) {
254
+ console.warn(chalk.yellow(`Schema warning for ${file}:`));
255
+ result.errors.forEach((err) => {
256
+ console.warn(chalk.yellow(` ${err.instancePath}: ${err.message}`));
257
+ });
258
+ }
259
+ }
260
+
261
+ return waiver;
204
262
  });
205
263
 
206
264
  // Filter by status
@@ -348,6 +406,147 @@ async function revokeWaiver(waiverId, options) {
348
406
  console.log(` Reason: ${waiver.revocation_reason}\n`);
349
407
  }
350
408
 
409
+ /**
410
+ * Prune expired waivers.
411
+ *
412
+ * Behavior per CAWSFIX-04 AC3-A6:
413
+ * --expired : dry run — list prunable waivers, no disk changes,
414
+ * no events emitted. Exit 0.
415
+ * --expired --apply : transition each prunable waiver from
416
+ * `status: active` to `status: expired` in place
417
+ * (file is updated, NOT deleted, to preserve the
418
+ * audit trail) and append a `waiver_pruned` event
419
+ * to the event log for each.
420
+ *
421
+ * A waiver is "prunable" iff `status === 'active'` AND `expires_at < now`.
422
+ * Waivers already `expired` or `revoked` are untouched (A4). Non-expired
423
+ * active waivers are untouched (A5). Empty registries exit 0 with a
424
+ * friendly message (A6).
425
+ *
426
+ * @param {object} options
427
+ * @param {boolean} [options.expired] — currently the only prune criterion
428
+ * @param {boolean} [options.apply] — if false, dry-run (default)
429
+ * @param {boolean} [options.json] — machine-readable output
430
+ */
431
+ async function pruneWaivers(options = {}) {
432
+ if (!options.expired) {
433
+ console.error(chalk.red('\n`caws waivers prune` requires --expired\n'));
434
+ console.log(chalk.yellow('Usage: caws waivers prune --expired [--apply]\n'));
435
+ process.exit(1);
436
+ }
437
+
438
+ const waiversManager = new WaiversManager();
439
+
440
+ // Fast path: no waivers directory or no waiver files at all.
441
+ const allWaivers = waiversManager.enumerateWaiverFiles();
442
+ if (allWaivers.length === 0) {
443
+ const msg = 'No active waivers to check.';
444
+ if (options.json) {
445
+ console.log(JSON.stringify({ status: 'ok', pruned: [], message: msg }));
446
+ } else {
447
+ console.log(chalk.yellow(`\n${msg}\n`));
448
+ }
449
+ return { pruned: [], applied: false };
450
+ }
451
+
452
+ const candidates = waiversManager.findExpiredWaivers();
453
+
454
+ // No prunable waivers — report and return.
455
+ if (candidates.length === 0) {
456
+ const msg = 'No expired waivers to prune.';
457
+ if (options.json) {
458
+ console.log(JSON.stringify({ status: 'ok', pruned: [], message: msg }));
459
+ } else {
460
+ console.log(chalk.green(`\n${msg}\n`));
461
+ }
462
+ return { pruned: [], applied: false };
463
+ }
464
+
465
+ const apply = Boolean(options.apply);
466
+
467
+ if (!apply) {
468
+ // Dry run — report only, no disk changes, no events.
469
+ if (options.json) {
470
+ console.log(
471
+ JSON.stringify({
472
+ status: 'dry_run',
473
+ applied: false,
474
+ pruned: candidates.map((c) => ({ id: c.id, expires_at: c.expires_at })),
475
+ })
476
+ );
477
+ } else {
478
+ console.log(chalk.yellow(`\nDry run — ${candidates.length} waiver(s) would be pruned:\n`));
479
+ candidates.forEach((c) => {
480
+ console.log(` ${chalk.bold(c.id)} expired at ${c.expires_at}`);
481
+ });
482
+ console.log(chalk.dim('\nRe-run with --apply to transition status to expired.\n'));
483
+ }
484
+ return { pruned: candidates, applied: false };
485
+ }
486
+
487
+ // Apply path — transition each file and emit events.
488
+ const { appendEvent } = require('../utils/event-log');
489
+ const pruned = [];
490
+ const failures = [];
491
+
492
+ for (const c of candidates) {
493
+ try {
494
+ const updated = waiversManager.markWaiverExpired(c.path);
495
+
496
+ // Emit waiver_pruned event. spec_id is optional for this event
497
+ // (waivers may or may not be tied to a spec). Using the waiver's
498
+ // applies_to field when available, otherwise omitting.
499
+ const spec_id =
500
+ updated && typeof updated.applies_to === 'string' && updated.applies_to.trim() !== ''
501
+ ? updated.applies_to
502
+ : undefined;
503
+
504
+ await appendEvent({
505
+ actor: 'cli',
506
+ event: 'waiver_pruned',
507
+ spec_id,
508
+ data: {
509
+ waiver_id: c.id,
510
+ expires_at: c.expires_at,
511
+ previous_status: 'active',
512
+ new_status: 'expired',
513
+ },
514
+ });
515
+
516
+ pruned.push({ id: c.id, expires_at: c.expires_at });
517
+ } catch (err) {
518
+ failures.push({ id: c.id, error: err.message });
519
+ }
520
+ }
521
+
522
+ if (options.json) {
523
+ console.log(
524
+ JSON.stringify({
525
+ status: failures.length === 0 ? 'ok' : 'partial',
526
+ applied: true,
527
+ pruned,
528
+ failures,
529
+ })
530
+ );
531
+ } else {
532
+ console.log(
533
+ chalk.green(`\nPruned ${pruned.length} expired waiver(s):\n`)
534
+ );
535
+ pruned.forEach((p) => {
536
+ console.log(` ${chalk.bold(p.id)} (was expired at ${p.expires_at})`);
537
+ });
538
+ if (failures.length > 0) {
539
+ console.log(chalk.red(`\n${failures.length} failure(s):\n`));
540
+ failures.forEach((f) => {
541
+ console.log(` ${chalk.bold(f.id)}: ${f.error}`);
542
+ });
543
+ }
544
+ console.log();
545
+ }
546
+
547
+ return { pruned, applied: true, failures };
548
+ }
549
+
351
550
  /**
352
551
  * Add waiver to active waivers file for quality gates integration
353
552
  */
@@ -12,7 +12,12 @@ const {
12
12
  mergeWorktree,
13
13
  pruneWorktrees,
14
14
  repairWorktrees,
15
+ loadRegistry,
16
+ saveRegistry,
17
+ getRepoRoot,
18
+ findFeatureSpecPath,
15
19
  } = require('../worktree/worktree-manager');
20
+ const { getAgentSessionId } = require('../utils/agent-session');
16
21
 
17
22
  /**
18
23
  * Handle worktree subcommands
@@ -34,9 +39,11 @@ async function worktreeCommand(subcommand, options = {}) {
34
39
  return handlePrune(options);
35
40
  case 'repair':
36
41
  return handleRepair(options);
42
+ case 'bind':
43
+ return handleBind(options);
37
44
  default:
38
45
  console.error(chalk.red(`Unknown worktree subcommand: ${subcommand}`));
39
- console.log(chalk.blue('Available: create, list, destroy, merge, prune, repair'));
46
+ console.log(chalk.blue('Available: create, list, destroy, merge, prune, repair, bind'));
40
47
  process.exit(1);
41
48
  }
42
49
  } catch (error) {
@@ -76,27 +83,47 @@ function handleList() {
76
83
  }
77
84
 
78
85
  const maxNameLen = Math.max(18, ...entries.map((e) => e.name.length + 2));
79
- const totalWidth = maxNameLen + 12 + 20 + 16 + 10;
86
+ const maxBranchLen = Math.max(20, ...entries.map((e) => (e.branch || '').length + 2));
87
+ const totalWidth = maxNameLen + 14 + maxBranchLen + 16 + 16;
80
88
  console.log(chalk.bold.cyan('CAWS Worktrees'));
81
89
  console.log(chalk.cyan('='.repeat(totalWidth)));
82
90
  console.log(
83
91
  chalk.bold(
84
92
  'Name'.padEnd(maxNameLen) +
85
- 'Status'.padEnd(12) +
86
- 'Branch'.padEnd(20) +
93
+ 'Status'.padEnd(14) +
94
+ 'Branch'.padEnd(maxBranchLen) +
87
95
  'Last Commit'.padEnd(16) +
88
- 'Owner'
96
+ 'Session'
89
97
  )
90
98
  );
91
99
  console.log(chalk.gray('-'.repeat(totalWidth)));
92
100
 
101
+ // Show current session for comparison
102
+ const currentSession = getAgentSessionId(process.cwd());
103
+ if (currentSession) {
104
+ const shortCurrent = currentSession.length > 8 ? '...' + currentSession.slice(-8) : currentSession;
105
+ console.log(chalk.gray(`You: ${shortCurrent}`));
106
+ console.log(chalk.gray('-'.repeat(totalWidth)));
107
+ }
108
+
93
109
  for (const entry of entries) {
94
- const statusColor =
95
- entry.status === 'active'
96
- ? chalk.green
97
- : entry.status === 'destroyed'
98
- ? chalk.gray
99
- : chalk.yellow;
110
+ const statusColors = {
111
+ active: chalk.green,
112
+ fresh: chalk.cyan,
113
+ merged: chalk.blue,
114
+ destroyed: chalk.gray,
115
+ missing: chalk.red,
116
+ 'stale-merged': chalk.yellow,
117
+ orphaned: chalk.yellow,
118
+ unregistered: chalk.yellow,
119
+ };
120
+ const statusColor = statusColors[entry.status] || chalk.white;
121
+
122
+ // Build status string with dirty indicator
123
+ let statusStr = entry.status;
124
+ if (entry.dirty && (entry.status === 'active' || entry.status === 'fresh')) {
125
+ statusStr += '*';
126
+ }
100
127
 
101
128
  // Format last commit age
102
129
  let commitAge = chalk.gray('-');
@@ -104,29 +131,29 @@ function handleList() {
104
131
  commitAge = chalk.white(entry.lastCommit.age);
105
132
  }
106
133
 
107
- // Format owner — show truncated session ID or '-'
134
+ // Format owner — show truncated session ID, highlight if it's the current session
108
135
  let ownerStr = chalk.gray('-');
109
136
  if (entry.owner) {
110
- // Show last 8 chars of session ID for readability
111
137
  const short = entry.owner.length > 8 ? '...' + entry.owner.slice(-8) : entry.owner;
112
- ownerStr = chalk.gray(short);
113
- }
114
-
115
- // Status suffix for merged branches
116
- let statusStr = entry.status;
117
- if (entry.merged && entry.status === 'active') {
118
- statusStr = 'merged';
138
+ if (currentSession && entry.owner === currentSession) {
139
+ ownerStr = chalk.green(short + ' (you)');
140
+ } else {
141
+ ownerStr = chalk.yellow(short);
142
+ }
119
143
  }
120
144
 
121
145
  console.log(
122
146
  entry.name.padEnd(maxNameLen) +
123
- statusColor(statusStr.padEnd(12)) +
124
- (entry.branch || '').padEnd(20) +
147
+ statusColor(statusStr.padEnd(14)) +
148
+ (entry.branch || '').padEnd(maxBranchLen) +
125
149
  commitAge.padEnd(16 + 10) + // +10 for chalk color codes
126
150
  ownerStr
127
151
  );
128
152
  }
129
153
 
154
+ // Legend
155
+ console.log('');
156
+ console.log(chalk.gray('Status: fresh = no commits yet, active = has commits/changes, active* = dirty files'));
130
157
  console.log('');
131
158
  }
132
159
 
@@ -204,9 +231,10 @@ function handleMerge(options) {
204
231
 
205
232
  function handlePrune(options) {
206
233
  const maxAge = options.maxAge !== undefined ? parseInt(options.maxAge, 10) : 30;
234
+ const force = options.force || false;
207
235
 
208
236
  console.log(chalk.cyan(`Pruning worktrees (max age: ${maxAge} days)`));
209
- const result = pruneWorktrees({ maxAgeDays: maxAge });
237
+ const result = pruneWorktrees({ maxAgeDays: maxAge, force });
210
238
 
211
239
  // Handle both old return format (array) and new format (object with pruned/skipped)
212
240
  const pruned = Array.isArray(result) ? result : result.pruned;
@@ -234,6 +262,7 @@ function handlePrune(options) {
234
262
  function handleRepair(options) {
235
263
  const dryRun = options.dryRun || false;
236
264
  const shouldPrune = options.prune || false;
265
+ const force = options.force || false;
237
266
 
238
267
  if (dryRun) {
239
268
  console.log(chalk.cyan('Repair dry-run (no changes will be persisted)'));
@@ -241,7 +270,7 @@ function handleRepair(options) {
241
270
  console.log(chalk.cyan('Repairing worktree registry'));
242
271
  }
243
272
 
244
- const result = repairWorktrees({ prune: shouldPrune, dryRun });
273
+ const result = repairWorktrees({ prune: shouldPrune, dryRun, force });
245
274
 
246
275
  if (result.repaired.length === 0 && result.pruned.length === 0 && result.skipped.length === 0) {
247
276
  console.log(chalk.green('Registry is consistent. Nothing to repair.'));
@@ -251,10 +280,11 @@ function handleRepair(options) {
251
280
  if (result.repaired.length > 0) {
252
281
  console.log(chalk.green('\nRepaired ' + result.repaired.length + ' entry/entries:'));
253
282
  for (const r of result.repaired) {
283
+ const ownerTag = r.owner ? chalk.yellow(` [owner: ${r.owner}]`) : '';
254
284
  if (r.action === 'registered') {
255
285
  console.log(chalk.gray(' + ' + r.name + ' (auto-registered from git)'));
256
286
  } else {
257
- console.log(chalk.gray(' ~ ' + r.name + ' (' + r.from + ' -> ' + r.to + ')'));
287
+ console.log(chalk.gray(' ~ ' + r.name + ' (' + r.from + ' -> ' + r.to + ')') + ownerTag);
258
288
  }
259
289
  }
260
290
  }
@@ -262,7 +292,8 @@ function handleRepair(options) {
262
292
  if (result.pruned.length > 0) {
263
293
  console.log(chalk.green('\nPruned ' + result.pruned.length + ' stale entry/entries:'));
264
294
  for (const p of result.pruned) {
265
- console.log(chalk.gray(' - ' + p.name + ' (' + p.status + ')'));
295
+ const ownerTag = p.owner ? chalk.yellow(` [owner: ${p.owner}]`) : '';
296
+ console.log(chalk.gray(' - ' + p.name + ' (' + p.status + ')') + ownerTag);
266
297
  }
267
298
  }
268
299
 
@@ -278,4 +309,78 @@ function handleRepair(options) {
278
309
  }
279
310
  }
280
311
 
312
+ function handleBind(options) {
313
+ const path = require('path');
314
+ const fs = require('fs-extra');
315
+ const yaml = require('js-yaml');
316
+ const { specId, name } = options;
317
+
318
+ if (!specId) {
319
+ console.error(chalk.red('Spec ID is required'));
320
+ console.log(chalk.blue('Usage: caws worktree bind <spec-id> [--name <worktree-name>]'));
321
+ process.exit(1);
322
+ }
323
+
324
+ // Determine worktree name: from option, or detect from cwd
325
+ let worktreeName = name;
326
+ if (!worktreeName) {
327
+ const root = getRepoRoot();
328
+ const cwd = process.cwd();
329
+ const worktreesBase = path.join(root, '.caws', 'worktrees');
330
+
331
+ if (cwd.startsWith(worktreesBase + path.sep)) {
332
+ const relative = path.relative(worktreesBase, cwd);
333
+ worktreeName = relative.split(path.sep)[0];
334
+ }
335
+ }
336
+
337
+ if (!worktreeName) {
338
+ console.error(chalk.red('Could not determine worktree name.'));
339
+ console.log(chalk.blue('Either run this from inside a worktree, or pass --name <worktree-name>'));
340
+ process.exit(1);
341
+ }
342
+
343
+ const root = getRepoRoot();
344
+ const registry = loadRegistry(root);
345
+
346
+ // Find the worktree entry in the registry
347
+ if (!registry.worktrees || !registry.worktrees[worktreeName]) {
348
+ console.error(chalk.red(`Worktree '${worktreeName}' not found in registry.`));
349
+ console.log(chalk.blue('Run: caws worktree list to see available worktrees'));
350
+ process.exit(1);
351
+ }
352
+
353
+ // Load the spec file
354
+ const specPath = findFeatureSpecPath(root, specId);
355
+ if (!specPath) {
356
+ console.error(chalk.red(`Spec '${specId}' not found in .caws/specs/`));
357
+ console.log(chalk.blue('Run: caws specs list to see available specs'));
358
+ process.exit(1);
359
+ }
360
+
361
+ const specContent = fs.readFileSync(specPath, 'utf8');
362
+ const specData = yaml.load(specContent);
363
+
364
+ // Warn if spec already bound to a different worktree
365
+ if (specData.worktree && specData.worktree !== worktreeName) {
366
+ console.log(chalk.yellow(`Warning: Spec '${specId}' is currently bound to worktree '${specData.worktree}'.`));
367
+ console.log(chalk.yellow(`Rebinding to '${worktreeName}'.`));
368
+ }
369
+
370
+ // Update registry side: set specId on the worktree entry
371
+ registry.worktrees[worktreeName].specId = specId;
372
+ saveRegistry(root, registry);
373
+
374
+ // Update spec side: set worktree field
375
+ specData.worktree = worktreeName;
376
+ const updatedYaml = yaml.dump(specData, { lineWidth: 120, noRefs: true });
377
+ fs.writeFileSync(specPath, updatedYaml, 'utf8');
378
+
379
+ console.log(chalk.green(`Binding established`));
380
+ console.log(chalk.gray(` Worktree: ${worktreeName} -> spec: ${specId}`));
381
+ console.log(chalk.gray(` Spec: ${specId} -> worktree: ${worktreeName}`));
382
+ console.log(chalk.gray(` Registry: ${path.join(root, '.caws', 'worktrees.json')}`));
383
+ console.log(chalk.gray(` Spec file: ${specPath}`));
384
+ }
385
+
281
386
  module.exports = { worktreeCommand };
@@ -7,9 +7,7 @@
7
7
  const chalk = require('chalk');
8
8
  const {
9
9
  ERROR_CATEGORIES,
10
- ERROR_CODES,
11
10
  getErrorCategory,
12
- getCategorySuggestions,
13
11
  } = require('./utils/error-categories');
14
12
 
15
13
  /**
@@ -73,7 +71,7 @@ async function safeAsync(operation, context = '', includeTiming = false) {
73
71
  try {
74
72
  const result = await operation();
75
73
 
76
- if (includeTiming && timer) {
74
+ if (includeTiming && timer && !isJsonOutput() && process.env.CAWS_QUIET !== '1') {
77
75
  const duration = timer.formatDuration();
78
76
  console.log(chalk.gray(` (completed in ${duration})`));
79
77
  }
@@ -110,7 +108,7 @@ function safeSync(operation, context = '', includeTiming = false) {
110
108
  try {
111
109
  const result = operation();
112
110
 
113
- if (includeTiming && timer) {
111
+ if (includeTiming && timer && !isJsonOutput() && process.env.CAWS_QUIET !== '1') {
114
112
  const duration = timer.formatDuration();
115
113
  console.log(chalk.gray(` (completed in ${duration})`));
116
114
  }
@@ -182,15 +180,6 @@ const COMMAND_SUGGESTIONS = {
182
180
  suggestions.push(`Did you mean: caws ${similar}?`);
183
181
  }
184
182
 
185
- // Context-aware suggestions based on command type
186
- const commandCategories = {
187
- setup: ['init', 'scaffold', 'templates'],
188
- validation: ['validate', 'status', 'diagnose'],
189
- analysis: ['evaluate', 'iterate', 'test-analysis'],
190
- compliance: ['waivers', 'workflow', 'quality-monitor'],
191
- history: ['provenance', 'hooks'],
192
- };
193
-
194
183
  // Suggest category based on what user might be trying to do
195
184
  if (command.includes('setup') || command.includes('start') || command.includes('create')) {
196
185
  suggestions.push('For project setup: caws init');