@paths.design/caws-cli 10.0.1 → 10.2.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.
- package/README.md +13 -5
- package/dist/budget-derivation.js +221 -74
- package/dist/commands/agents.js +124 -0
- package/dist/commands/evaluate.js +26 -12
- package/dist/commands/gates.js +31 -4
- package/dist/commands/init.js +7 -4
- package/dist/commands/iterate.js +7 -3
- package/dist/commands/scope.js +264 -0
- package/dist/commands/sidecar.js +6 -3
- package/dist/commands/specs.js +359 -4
- package/dist/commands/status.js +29 -4
- package/dist/commands/templates.js +0 -8
- package/dist/commands/validate.js +34 -13
- package/dist/commands/verify-acs.js +25 -10
- package/dist/commands/waivers.js +147 -5
- package/dist/commands/worktree.js +200 -4
- package/dist/gates/budget-limit.js +6 -1
- package/dist/gates/scope-boundary.js +26 -7
- package/dist/gates/spec-completeness.js +8 -1
- package/dist/index.js +56 -0
- package/dist/policy/PolicyManager.js +14 -7
- package/dist/session/session-manager.js +34 -0
- package/dist/templates/.caws/schemas/policy.schema.json +101 -34
- package/dist/templates/.caws/schemas/scope.schema.json +3 -3
- package/dist/templates/.caws/schemas/waivers.schema.json +91 -21
- package/dist/templates/.caws/schemas/working-spec.schema.json +253 -89
- package/dist/templates/.caws/templates/working-spec.template.yml +3 -1
- package/dist/templates/.caws/tools/scope-guard.js +66 -15
- package/dist/templates/.claude/README.md +1 -1
- package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +106 -27
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +96 -3
- package/dist/templates/.claude/rules/worktree-isolation.md +21 -3
- package/dist/templates/.claude/settings.json +5 -0
- package/dist/templates/CLAUDE.md +56 -0
- package/dist/templates/agents.md +47 -0
- package/dist/utils/agent-display.js +210 -0
- package/dist/utils/agent-session.js +142 -0
- package/dist/utils/event-log.js +584 -0
- package/dist/utils/event-renderer.js +521 -0
- package/dist/utils/schema-validator.js +10 -2
- package/dist/utils/working-state.js +25 -0
- package/dist/validation/spec-validation.js +102 -9
- package/dist/waivers-manager.js +84 -0
- package/dist/worktree/worktree-manager.js +593 -26
- package/package.json +5 -4
- package/templates/.caws/schemas/policy.schema.json +101 -34
- package/templates/.caws/schemas/scope.schema.json +3 -3
- package/templates/.caws/schemas/waivers.schema.json +91 -21
- package/templates/.caws/schemas/working-spec.schema.json +253 -89
- package/templates/.caws/templates/working-spec.template.yml +3 -1
- package/templates/.caws/tools/scope-guard.js +66 -15
- package/templates/.claude/README.md +1 -1
- package/templates/.claude/hooks/protected-paths.sh +39 -0
- package/templates/.claude/hooks/scope-guard.sh +106 -27
- package/templates/.claude/hooks/worktree-write-guard.sh +96 -3
- package/templates/.claude/rules/worktree-isolation.md +21 -3
- package/templates/.claude/settings.json +5 -0
- package/templates/CLAUDE.md +56 -0
- package/templates/agents.md +47 -0
|
@@ -6,10 +6,45 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
const {
|
|
9
|
+
const { deriveBudgetSync, checkBudgetCompliance } = require('../budget-derivation');
|
|
10
10
|
const { execSync } = require('child_process');
|
|
11
11
|
const { createValidator, getSchemaPath } = require('../utils/schema-validator');
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* CAWSFIX-10: Canonical regex for valid spec IDs.
|
|
15
|
+
*
|
|
16
|
+
* Accepts:
|
|
17
|
+
* - Single-segment: FEAT-001, EVLOG-002, CAWSFIX-06 (legacy shape)
|
|
18
|
+
* - Multi-segment: P03-IMPL-01, ALG-001A-HARDEN-01, CAWS-FIX-03
|
|
19
|
+
* - Lowercase suffix: APC-01a, ALG-01b (CAWSFIX-25 / D2)
|
|
20
|
+
*
|
|
21
|
+
* Rejects:
|
|
22
|
+
* - lowercase prefix (feat-001)
|
|
23
|
+
* - lowercase in a non-final segment (AB-cd-01)
|
|
24
|
+
* - leading digit (01-FEAT)
|
|
25
|
+
* - missing number suffix (FEAT-)
|
|
26
|
+
* - trailing hyphen (FEAT-01-)
|
|
27
|
+
* - leading/double hyphen (--FEAT-01, FEAT--001)
|
|
28
|
+
* - empty string
|
|
29
|
+
*
|
|
30
|
+
* Grammar: [PREFIX](-[SEGMENT])*-NUMBER[SUFFIX]?
|
|
31
|
+
* - PREFIX = [A-Z] followed by zero+ [A-Z0-9]
|
|
32
|
+
* - SEGMENT = one+ [A-Z0-9] (alphanumeric, uppercase only)
|
|
33
|
+
* - NUMBER = one+ digits
|
|
34
|
+
* - SUFFIX = zero+ [a-z] (optional lowercase tail on final segment only)
|
|
35
|
+
*
|
|
36
|
+
* Defined once per A4 invariant; referenced by both the basic validator
|
|
37
|
+
* (line ~125 pre-fix) and the enhanced validator (line ~307 pre-fix).
|
|
38
|
+
*/
|
|
39
|
+
const SPEC_ID_PATTERN = /^[A-Z][A-Z0-9]*(-[A-Z0-9]+)*-\d+[a-z]*$/;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* User-facing error message for bad spec IDs (CAWSFIX-10 A5).
|
|
43
|
+
* Kept as a module constant so the message stays in sync with the pattern.
|
|
44
|
+
*/
|
|
45
|
+
const SPEC_ID_ERROR_MESSAGE =
|
|
46
|
+
'Project ID should be in format: PREFIX-NUMBER or PREFIX-SEGMENT-NUMBER with optional lowercase suffix (e.g., FEAT-001, P03-IMPL-01, APC-01a)';
|
|
47
|
+
|
|
13
48
|
/**
|
|
14
49
|
* Get actual budget statistics from git history
|
|
15
50
|
* Analyzes changes since last tag or initial commit
|
|
@@ -66,6 +101,42 @@ function getActualBudgetStats(specDir) {
|
|
|
66
101
|
}
|
|
67
102
|
}
|
|
68
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Alias the modern `acceptance_criteria` key into `acceptance` so the semantic
|
|
106
|
+
* validator (which historically keys off `acceptance`) accepts both shapes.
|
|
107
|
+
*
|
|
108
|
+
* Precedence (per CAWSFIX-09 A3 invariant):
|
|
109
|
+
* - If `acceptance` is present (legacy shape: {id,given,when,then}), it wins.
|
|
110
|
+
* - Otherwise `acceptance_criteria` (modern shape: {id,description,test_nodeids,status})
|
|
111
|
+
* is copied into `acceptance`.
|
|
112
|
+
*
|
|
113
|
+
* IMPORTANT: this function mutates the spec in place. The existing validator
|
|
114
|
+
* also mutates in place (risk_tier string→number coercion at line ~141; auto-fix
|
|
115
|
+
* writes via `current[pathParts[...]] = fix.value`). Callers of
|
|
116
|
+
* `validateWorkingSpecWithSuggestions({...}, {autoFix:true})` observe those
|
|
117
|
+
* mutations on the object they passed in — see `Multiple Auto-Fixes` tests.
|
|
118
|
+
* Returning a clone here would silently break that contract.
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} spec - Raw spec object (mutated in place)
|
|
121
|
+
* @returns {Object} Same spec reference
|
|
122
|
+
*/
|
|
123
|
+
function aliasAcceptanceCriteria(spec) {
|
|
124
|
+
if (!spec || typeof spec !== 'object') return spec;
|
|
125
|
+
|
|
126
|
+
const hasLegacy = Array.isArray(spec.acceptance) && spec.acceptance.length > 0;
|
|
127
|
+
const hasModern =
|
|
128
|
+
Array.isArray(spec.acceptance_criteria) && spec.acceptance_criteria.length > 0;
|
|
129
|
+
|
|
130
|
+
// Only alias when: legacy is absent AND modern has content.
|
|
131
|
+
// (Legacy wins when both present; empty modern arrays do not satisfy the
|
|
132
|
+
// required-field check — see edge-case tests in acceptance-criteria-alias.test.js.)
|
|
133
|
+
if (!hasLegacy && hasModern) {
|
|
134
|
+
spec.acceptance = spec.acceptance_criteria;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return spec;
|
|
138
|
+
}
|
|
139
|
+
|
|
69
140
|
/**
|
|
70
141
|
* Basic validation of working spec
|
|
71
142
|
* @param {Object} spec - Working spec object
|
|
@@ -74,6 +145,11 @@ function getActualBudgetStats(specDir) {
|
|
|
74
145
|
*/
|
|
75
146
|
const validateWorkingSpec = (spec, _options = {}) => {
|
|
76
147
|
try {
|
|
148
|
+
// CAWSFIX-09: Alias `acceptance_criteria` -> `acceptance` before any
|
|
149
|
+
// semantic checks so specs using the modern shape don't trigger
|
|
150
|
+
// "Missing required field: acceptance" false negatives.
|
|
151
|
+
aliasAcceptanceCriteria(spec);
|
|
152
|
+
|
|
77
153
|
// First pass: AJV schema validation (non-blocking — results collected as warnings)
|
|
78
154
|
let schemaWarnings = [];
|
|
79
155
|
try {
|
|
@@ -121,14 +197,14 @@ const validateWorkingSpec = (spec, _options = {}) => {
|
|
|
121
197
|
}
|
|
122
198
|
}
|
|
123
199
|
|
|
124
|
-
// Validate specific field formats
|
|
125
|
-
if (
|
|
200
|
+
// Validate specific field formats (CAWSFIX-10: DRY regex via SPEC_ID_PATTERN)
|
|
201
|
+
if (!SPEC_ID_PATTERN.test(spec.id)) {
|
|
126
202
|
return {
|
|
127
203
|
valid: false,
|
|
128
204
|
errors: [
|
|
129
205
|
{
|
|
130
206
|
instancePath: '/id',
|
|
131
|
-
message:
|
|
207
|
+
message: SPEC_ID_ERROR_MESSAGE,
|
|
132
208
|
},
|
|
133
209
|
],
|
|
134
210
|
};
|
|
@@ -252,6 +328,12 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
252
328
|
const { autoFix = false, checkBudget = false, projectRoot } = options;
|
|
253
329
|
|
|
254
330
|
try {
|
|
331
|
+
// CAWSFIX-09: Alias `acceptance_criteria` -> `acceptance` so the
|
|
332
|
+
// required-field check and the "No acceptance criteria defined" warning
|
|
333
|
+
// recognize the modern shape as valid. Mutates in place to preserve the
|
|
334
|
+
// existing auto-fix contract (callers observe fixes on their object).
|
|
335
|
+
aliasAcceptanceCriteria(spec);
|
|
336
|
+
|
|
255
337
|
let errors = [];
|
|
256
338
|
let warnings = [];
|
|
257
339
|
let fixes = [];
|
|
@@ -303,12 +385,12 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
303
385
|
|
|
304
386
|
// Semantic checks that AJV can't express
|
|
305
387
|
|
|
306
|
-
// Validate specific field formats
|
|
307
|
-
if (spec.id &&
|
|
388
|
+
// Validate specific field formats (CAWSFIX-10: DRY regex via SPEC_ID_PATTERN)
|
|
389
|
+
if (spec.id && !SPEC_ID_PATTERN.test(spec.id)) {
|
|
308
390
|
errors.push({
|
|
309
391
|
instancePath: '/id',
|
|
310
|
-
message:
|
|
311
|
-
suggestion: 'Use format like: PROJ-001, FEAT-002,
|
|
392
|
+
message: SPEC_ID_ERROR_MESSAGE,
|
|
393
|
+
suggestion: 'Use format like: PROJ-001, FEAT-002, P03-IMPL-01, ALG-001A-HARDEN-01',
|
|
312
394
|
canAutoFix: false,
|
|
313
395
|
});
|
|
314
396
|
}
|
|
@@ -646,10 +728,18 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
646
728
|
}
|
|
647
729
|
|
|
648
730
|
// Derive and check budget if requested
|
|
731
|
+
//
|
|
732
|
+
// CAWSFIX-07: use `deriveBudgetSync` here. The async `deriveBudget`
|
|
733
|
+
// returns a Promise; this synchronous function previously passed the
|
|
734
|
+
// Promise straight into `checkBudgetCompliance`, which then read
|
|
735
|
+
// `derivedBudget.effective.max_files` on an undefined `.effective` and
|
|
736
|
+
// threw "Cannot read properties of undefined (reading 'max_files')" —
|
|
737
|
+
// surfaced as the "Budget derivation failed" warning on every
|
|
738
|
+
// schema-compliant spec.
|
|
649
739
|
let budgetCheck = null;
|
|
650
740
|
if (checkBudget && projectRoot) {
|
|
651
741
|
try {
|
|
652
|
-
const derivedBudget =
|
|
742
|
+
const derivedBudget = deriveBudgetSync(spec, projectRoot);
|
|
653
743
|
|
|
654
744
|
// Get actual stats from git history
|
|
655
745
|
const actualStats = getActualBudgetStats(projectRoot) || {
|
|
@@ -828,4 +918,7 @@ module.exports = {
|
|
|
828
918
|
canAutoFixField,
|
|
829
919
|
calculateComplianceScore,
|
|
830
920
|
getComplianceGrade,
|
|
921
|
+
// CAWSFIX-10: exported so init.js and tests reference the same regex
|
|
922
|
+
SPEC_ID_PATTERN,
|
|
923
|
+
SPEC_ID_ERROR_MESSAGE,
|
|
831
924
|
};
|
package/dist/waivers-manager.js
CHANGED
|
@@ -275,6 +275,90 @@ class WaiversManager {
|
|
|
275
275
|
return activeWaivers;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Enumerate individual waiver files (WV-XXXX.yaml) on disk and return
|
|
280
|
+
* their parsed contents. These files are the source of truth per the
|
|
281
|
+
* CAWSFIX-04 invariants; active-waivers.yaml is an aggregate index.
|
|
282
|
+
*
|
|
283
|
+
* @returns {Array<{id: string, path: string, data: object}>}
|
|
284
|
+
*/
|
|
285
|
+
enumerateWaiverFiles() {
|
|
286
|
+
const out = [];
|
|
287
|
+
if (!fs.existsSync(this.waiversDir)) return out;
|
|
288
|
+
|
|
289
|
+
const files = fs.readdirSync(this.waiversDir);
|
|
290
|
+
for (const file of files) {
|
|
291
|
+
const match = file.match(/^(WV-\d{4})\.yaml$/);
|
|
292
|
+
if (!match) continue;
|
|
293
|
+
|
|
294
|
+
const filePath = path.join(this.waiversDir, file);
|
|
295
|
+
let data;
|
|
296
|
+
try {
|
|
297
|
+
data = yaml.load(fs.readFileSync(filePath, 'utf8'));
|
|
298
|
+
} catch (err) {
|
|
299
|
+
// Skip unparseable files; do not swallow — warn the caller.
|
|
300
|
+
console.warn(`Warning: could not parse ${file}: ${err.message}`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (data && typeof data === 'object') {
|
|
304
|
+
out.push({ id: match[1], path: filePath, data });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Identify waivers that are candidates for expiry-based pruning.
|
|
312
|
+
* A waiver is prunable iff `status === 'active'` AND
|
|
313
|
+
* `expires_at < now`. Already-expired or revoked waivers are skipped
|
|
314
|
+
* (their status is correct; pruning wouldn't change anything).
|
|
315
|
+
*
|
|
316
|
+
* @param {Date} [nowOverride] — inject clock for tests
|
|
317
|
+
* @returns {Array<{id: string, path: string, expires_at: string}>}
|
|
318
|
+
*/
|
|
319
|
+
findExpiredWaivers(nowOverride) {
|
|
320
|
+
const now = nowOverride instanceof Date ? nowOverride : new Date();
|
|
321
|
+
const records = this.enumerateWaiverFiles();
|
|
322
|
+
const candidates = [];
|
|
323
|
+
|
|
324
|
+
for (const rec of records) {
|
|
325
|
+
const w = rec.data;
|
|
326
|
+
const status = w.status;
|
|
327
|
+
// Only active waivers are prunable. Waivers with no status field are
|
|
328
|
+
// treated as active (matches existing loadActiveWaivers() assumption).
|
|
329
|
+
if (status && status !== 'active') continue;
|
|
330
|
+
if (!w.expires_at) continue;
|
|
331
|
+
|
|
332
|
+
const expiresAt = new Date(w.expires_at);
|
|
333
|
+
if (!Number.isFinite(expiresAt.getTime())) continue; // malformed date
|
|
334
|
+
if (expiresAt < now) {
|
|
335
|
+
candidates.push({
|
|
336
|
+
id: rec.id,
|
|
337
|
+
path: rec.path,
|
|
338
|
+
expires_at: w.expires_at,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return candidates;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Transition a single waiver file from `status: active` to
|
|
347
|
+
* `status: expired` in place. The file is rewritten with its existing
|
|
348
|
+
* field order where possible; a `status` field is added or replaced.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} filePath
|
|
351
|
+
* @returns {object} the updated waiver object
|
|
352
|
+
*/
|
|
353
|
+
markWaiverExpired(filePath) {
|
|
354
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
355
|
+
const data = yaml.load(raw) || {};
|
|
356
|
+
data.status = 'expired';
|
|
357
|
+
data.expired_at = new Date().toISOString();
|
|
358
|
+
fs.writeFileSync(filePath, yaml.dump(data, { lineWidth: -1 }), 'utf8');
|
|
359
|
+
return data;
|
|
360
|
+
}
|
|
361
|
+
|
|
278
362
|
/**
|
|
279
363
|
* Revoke a waiver
|
|
280
364
|
*/
|