@whitehatd/crag 0.2.7 → 0.2.9
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/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const path = require('path');
|
|
23
23
|
const { execSync } = require('child_process');
|
|
24
|
-
const { parseGovernance, flattenGates } = require('../governance/parse');
|
|
24
|
+
const { parseGovernance, flattenGates, extractSection } = require('../governance/parse');
|
|
25
25
|
const { isModified, readFrontmatter } = require('../update/integrity');
|
|
26
26
|
const { EXIT_USER, EXIT_INTERNAL } = require('../cli-errors');
|
|
27
27
|
|
|
@@ -381,6 +381,68 @@ function diagnoseHooks(cwd) {
|
|
|
381
381
|
// Section: Drift (reuses crag diff logic)
|
|
382
382
|
// ============================================================================
|
|
383
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Detect the branch strategy declared in a governance.md document.
|
|
386
|
+
*
|
|
387
|
+
* Scopes the text scan to the `## Branch Strategy` section (avoiding false
|
|
388
|
+
* matches against unrelated prose). Within that section, the FIRST keyword
|
|
389
|
+
* to appear wins — this matches human reading order where the opening
|
|
390
|
+
* statement is the rule and later lines are qualifications.
|
|
391
|
+
*
|
|
392
|
+
* Returns 'feature-branches', 'trunk-based', or null.
|
|
393
|
+
*
|
|
394
|
+
* Exported for unit testing.
|
|
395
|
+
*/
|
|
396
|
+
function detectBranchStrategy(content) {
|
|
397
|
+
if (typeof content !== 'string' || content.length === 0) return null;
|
|
398
|
+
const section = extractSection(content, 'Branch Strategy');
|
|
399
|
+
const scope = section || content; // fall back to whole file if section absent
|
|
400
|
+
const featureIdx = scope.search(/[Ff]eature branches/);
|
|
401
|
+
const trunkIdx = scope.search(/[Tt]runk-based/);
|
|
402
|
+
if (featureIdx === -1 && trunkIdx === -1) return null;
|
|
403
|
+
if (featureIdx === -1) return 'trunk-based';
|
|
404
|
+
if (trunkIdx === -1) return 'feature-branches';
|
|
405
|
+
return featureIdx < trunkIdx ? 'feature-branches' : 'trunk-based';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Given the raw output of `git branch -a --format="%(refname:short)"`, return
|
|
410
|
+
* the list of unique feature branches (by short name, after stripping any
|
|
411
|
+
* remote prefix).
|
|
412
|
+
*
|
|
413
|
+
* This is the piece that decides whether a repo is practicing feature-branch
|
|
414
|
+
* development. A repo whose LOCAL branches have all been merged+deleted but
|
|
415
|
+
* whose REMOTE still has open feat/* branches is absolutely still practicing
|
|
416
|
+
* feature-branch development — so we count remote-tracking refs as equivalent
|
|
417
|
+
* to local branches, and we dedupe.
|
|
418
|
+
*
|
|
419
|
+
* Exported for unit testing — avoids the need to set up a real git fixture.
|
|
420
|
+
*/
|
|
421
|
+
const FEATURE_PREFIXES = ['feat', 'fix', 'docs', 'chore', 'feature', 'hotfix'];
|
|
422
|
+
|
|
423
|
+
function countFeatureBranches(gitBranchOutput) {
|
|
424
|
+
if (typeof gitBranchOutput !== 'string' || gitBranchOutput.length === 0) {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
const rawList = gitBranchOutput
|
|
428
|
+
.split('\n')
|
|
429
|
+
.map(b => b.trim())
|
|
430
|
+
.filter(Boolean)
|
|
431
|
+
// Skip symbolic refs like "origin/HEAD" (no payload branch underneath).
|
|
432
|
+
.filter(b => !b.endsWith('/HEAD'));
|
|
433
|
+
|
|
434
|
+
const prefixGroup = FEATURE_PREFIXES.join('|');
|
|
435
|
+
const remoteStripRe = new RegExp(`^[A-Za-z0-9_.-]+/((?:${prefixGroup})/.+)$`);
|
|
436
|
+
const featureRe = new RegExp(`^(${prefixGroup})/`);
|
|
437
|
+
|
|
438
|
+
const normalized = rawList.map(b => {
|
|
439
|
+
const m = b.match(remoteStripRe);
|
|
440
|
+
return m ? m[1] : b;
|
|
441
|
+
});
|
|
442
|
+
const unique = [...new Set(normalized)];
|
|
443
|
+
return unique.filter(b => featureRe.test(b));
|
|
444
|
+
}
|
|
445
|
+
|
|
384
446
|
function diagnoseDrift(cwd) {
|
|
385
447
|
const checks = [];
|
|
386
448
|
const govPath = path.join(cwd, '.claude', 'governance.md');
|
|
@@ -390,20 +452,19 @@ function diagnoseDrift(cwd) {
|
|
|
390
452
|
|
|
391
453
|
const content = fs.readFileSync(govPath, 'utf-8');
|
|
392
454
|
|
|
393
|
-
// Branch strategy alignment
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
455
|
+
// Branch strategy alignment — detect from the `## Branch Strategy` section
|
|
456
|
+
// rather than the whole file so unrelated prose ("feature branches in each
|
|
457
|
+
// sub-repo") doesn't override a workspace wrapper's actual trunk-based
|
|
458
|
+
// policy. Within that section, the FIRST keyword to appear wins: this
|
|
459
|
+
// matches the human reading order where the opening bullet states the rule.
|
|
460
|
+
const govBranchStrategy = detectBranchStrategy(content);
|
|
399
461
|
|
|
400
462
|
if (govBranchStrategy) {
|
|
401
463
|
try {
|
|
402
464
|
const branches = execSync('git branch -a --format="%(refname:short)"', {
|
|
403
465
|
cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
404
466
|
});
|
|
405
|
-
const
|
|
406
|
-
const featureBranches = list.filter(b => /^(feat|fix|docs|chore|feature|hotfix)\//.test(b));
|
|
467
|
+
const featureBranches = countFeatureBranches(branches);
|
|
407
468
|
const actualStrategy = featureBranches.length > 2 ? 'feature-branches' : 'trunk-based';
|
|
408
469
|
|
|
409
470
|
checks.push({
|
|
@@ -583,4 +644,4 @@ function printReport(report, { ciMode = false, strict = false } = {}) {
|
|
|
583
644
|
}
|
|
584
645
|
}
|
|
585
646
|
|
|
586
|
-
module.exports = { doctor, runDiagnostics };
|
|
647
|
+
module.exports = { doctor, runDiagnostics, countFeatureBranches, detectBranchStrategy };
|
package/src/governance/parse.js
CHANGED
|
@@ -107,8 +107,40 @@ function parseGovernance(content) {
|
|
|
107
107
|
if (gatesBody) {
|
|
108
108
|
let section = 'default';
|
|
109
109
|
let sectionMeta = { path: null, condition: null };
|
|
110
|
+
// Fenced code blocks (```bash / ```sh / ```shell) are treated as an
|
|
111
|
+
// alternative command carrier: every non-blank, non-comment line inside
|
|
112
|
+
// is extracted as a MANDATORY command for the current section. This
|
|
113
|
+
// matches the very common markdown pattern of documenting gate commands
|
|
114
|
+
// in a bash fence instead of a bullet list.
|
|
115
|
+
let inCodeBlock = false;
|
|
110
116
|
|
|
111
117
|
for (const line of gatesBody.split('\n')) {
|
|
118
|
+
// Fence toggle. Accepts ``` , ```bash , ```sh , ```shell (and any other
|
|
119
|
+
// language tag) — we treat all fenced blocks inside ## Gates as gate
|
|
120
|
+
// command carriers. A non-shell language tag would be unusual here.
|
|
121
|
+
if (/^\s*```/.test(line)) {
|
|
122
|
+
inCodeBlock = !inCodeBlock;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (inCodeBlock) {
|
|
127
|
+
const cmd = line.trim();
|
|
128
|
+
// Skip blanks and pure comments. Do NOT strip inline comments from
|
|
129
|
+
// the tail of a command — shell parsers treat `#` mid-line as a
|
|
130
|
+
// comment only when preceded by whitespace, and losing the rest of
|
|
131
|
+
// the line could silently drop logic like `echo "# header"`.
|
|
132
|
+
if (!cmd || cmd.startsWith('#')) continue;
|
|
133
|
+
if (!result.gates[section]) {
|
|
134
|
+
result.gates[section] = {
|
|
135
|
+
commands: [],
|
|
136
|
+
path: sectionMeta.path,
|
|
137
|
+
condition: sectionMeta.condition,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
result.gates[section].commands.push({ cmd, classification: 'MANDATORY' });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
112
144
|
// Match ### Section or ### Section (path: dir/) or ### Section (if: file)
|
|
113
145
|
const sub = line.match(/^### (.+?)(?:\s*\((?:(path|if):\s*(.+?))\))?\s*$/);
|
|
114
146
|
if (sub) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: post-start-validation
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.9
|
|
4
4
|
source_hash: 5a64dfe68b13577dff818fa63ddb6185be360c80b100f205bc586aac39e19e80
|
|
5
5
|
description: Universal validation and knowledge capture. Detects what changed, runs governance gates, captures knowledge, verifies deployment. Works for any project.
|
|
6
6
|
---
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pre-start-context
|
|
3
|
-
version: 0.2.
|
|
4
|
-
source_hash:
|
|
3
|
+
version: 0.2.9
|
|
4
|
+
source_hash: 4ace3388804f528a7e7a446466a506ca5f0da4c23e42b540b9ae8923eaf35e4e
|
|
5
5
|
description: Universal context loader. Discovers any project's stack, architecture, and state at runtime. Reads governance.md for project-specific rules. Works for any language, framework, or deployment target.
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -54,17 +54,20 @@ Detect OS and shell. Use appropriate syntax (Unix forward slashes if Git Bash on
|
|
|
54
54
|
|
|
55
55
|
## 0.05. Skill Currency Check
|
|
56
56
|
|
|
57
|
-
Check installed
|
|
57
|
+
Check whether installed skills are behind the crag CLI:
|
|
58
58
|
|
|
59
|
+
// turbo
|
|
59
60
|
```
|
|
60
|
-
|
|
61
|
+
crag upgrade --check
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
If the
|
|
64
|
-
- Report: `"
|
|
65
|
-
- Continue with current version — never block startup.
|
|
64
|
+
If the output lists any skill as needing an upgrade (e.g. `0.2.x → 0.2.y`):
|
|
65
|
+
- Report: `"Skills outdated — run: crag upgrade"`
|
|
66
|
+
- Continue with the current version — never block startup.
|
|
66
67
|
|
|
67
|
-
> This check
|
|
68
|
+
> This check uses `crag upgrade --check` so the skill never has to hard-code
|
|
69
|
+
> its own version number; it asks the CLI, which is always in lockstep with
|
|
70
|
+
> the package. If `crag` is not on PATH (bare CI runner), skip silently.
|
|
68
71
|
|
|
69
72
|
---
|
|
70
73
|
|