@luizsantiago/spec-guardrails 3.1.10 → 3.1.11
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 +6 -0
- package/index.js +2 -0
- package/lib/companion-doctor.js +140 -0
- package/lib/companions.js +97 -0
- package/lib/doctor.js +67 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,6 +41,12 @@ npx @luizsantiago/spec-guardrails install
|
|
|
41
41
|
|
|
42
42
|
Re-run `install` anytime to refresh skills; your `.specs/` decisions and `STATE.md` are kept.
|
|
43
43
|
|
|
44
|
+
### Optional Atlas companions (Lego)
|
|
45
|
+
|
|
46
|
+
Spec Guardrails is a **complete product alone**. Optional **Atlas** packages add domain specialization (Path Domains, catalogs). The first is [**Tech Atlas**](https://github.com/luizssantiago92/tech-atlas) (`@luizsantiago/tech-atlas`) — tech engineering context routing.
|
|
47
|
+
|
|
48
|
+
When paired, install Guardrails first, then the Atlas. Re-running Guardrails `install` **preserves** companion assets: Atlas skills/catalog, `.specs/atlas/`, mirrored `validate_layer_routing.py`, and `.specs/desks/` if present. Contract: [Companion: Tech Atlas](docs/guide/Companion-tech-atlas.md).
|
|
49
|
+
|
|
44
50
|
| Need | Command |
|
|
45
51
|
| --- | --- |
|
|
46
52
|
| First time / upgrade | `install` |
|
package/index.js
CHANGED
|
@@ -50,6 +50,8 @@ Commands:
|
|
|
50
50
|
[--json] Machine-readable output
|
|
51
51
|
phase-context <phase> Print .specs/config.yaml context + rules for a phase
|
|
52
52
|
doctor [path] Audit guardrails readiness (score + next actions)
|
|
53
|
+
When Atlas companions are installed, also probes their
|
|
54
|
+
gates, rules, and PROJECT.md registry via INDEX.json
|
|
53
55
|
[--json] Machine-readable output
|
|
54
56
|
[--no-suggest] Hide per-check remediation hints
|
|
55
57
|
validate-spec [spec.md|feature] Closure gate for a feature spec
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { GUARDRAILS_SCRIPTS_DIR } from "./constants.js";
|
|
4
|
+
import {
|
|
5
|
+
listInstalledCompanions,
|
|
6
|
+
readCompanionsIndex,
|
|
7
|
+
} from "./companions.js";
|
|
8
|
+
import { readFileSafe } from "./fs-utils.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import("./doctor.js").DoctorCheck} DoctorCheck
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} cwd
|
|
16
|
+
* @param {string} relativePath
|
|
17
|
+
* @returns {Promise<boolean>}
|
|
18
|
+
*/
|
|
19
|
+
async function pathExists(cwd, relativePath) {
|
|
20
|
+
try {
|
|
21
|
+
const { access } = await import("node:fs/promises");
|
|
22
|
+
await access(path.join(cwd, relativePath));
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Filesystem probes for installed Atlas companions (no Atlas CLI spawn).
|
|
31
|
+
* Used by Guardrails doctor when `.specs/companions/INDEX.json` exists.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} cwd
|
|
34
|
+
* @returns {Promise<{
|
|
35
|
+
* paired: boolean,
|
|
36
|
+
* companions: import("./companions.js").CompanionIndexEntry[],
|
|
37
|
+
* checks: DoctorCheck[],
|
|
38
|
+
* }>}
|
|
39
|
+
*/
|
|
40
|
+
export async function runCompanionDoctorChecks(cwd) {
|
|
41
|
+
const index = await readCompanionsIndex(cwd);
|
|
42
|
+
const companions = await listInstalledCompanions(cwd);
|
|
43
|
+
/** @type {DoctorCheck[]} */
|
|
44
|
+
const checks = [];
|
|
45
|
+
|
|
46
|
+
if (companions.length === 0) {
|
|
47
|
+
return { paired: false, companions: [], checks };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const paired = Boolean(index?.paired);
|
|
51
|
+
const projectContent = await readFileSafe(path.join(cwd, ".specs/project/PROJECT.md"));
|
|
52
|
+
|
|
53
|
+
checks.push({
|
|
54
|
+
id: "atlas-companions",
|
|
55
|
+
label: "Atlas companions registry (.specs/companions/INDEX.json)",
|
|
56
|
+
weight: 5,
|
|
57
|
+
pass: true,
|
|
58
|
+
optional: !paired,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
for (const companion of companions) {
|
|
62
|
+
const prefix = `atlas-${companion.id}`;
|
|
63
|
+
const installHint = `npx ${companion.npm} install`;
|
|
64
|
+
const optional = !paired;
|
|
65
|
+
|
|
66
|
+
const primaryGate = companion.gates[0] ?? "";
|
|
67
|
+
const gateOk = primaryGate
|
|
68
|
+
? await pathExists(cwd, path.join(companion.scriptsDir, primaryGate))
|
|
69
|
+
: false;
|
|
70
|
+
checks.push({
|
|
71
|
+
id: `${prefix}-gate`,
|
|
72
|
+
label: `${companion.displayName}: routing gate (${companion.scriptsDir})`,
|
|
73
|
+
weight: 4,
|
|
74
|
+
pass: gateOk,
|
|
75
|
+
optional,
|
|
76
|
+
suggest: gateOk ? undefined : installHint,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const ruleOk = await pathExists(cwd, companion.ruleFile);
|
|
80
|
+
checks.push({
|
|
81
|
+
id: `${prefix}-rule`,
|
|
82
|
+
label: `${companion.displayName}: Cursor rule (${companion.ruleFile})`,
|
|
83
|
+
weight: 2,
|
|
84
|
+
pass: ruleOk,
|
|
85
|
+
optional,
|
|
86
|
+
suggest: ruleOk ? undefined : installHint,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const registryOk = projectContent
|
|
90
|
+
? projectContent.includes(companion.projectSection)
|
|
91
|
+
: false;
|
|
92
|
+
checks.push({
|
|
93
|
+
id: `${prefix}-registry`,
|
|
94
|
+
label: `${companion.displayName}: PROJECT.md registry section`,
|
|
95
|
+
weight: 2,
|
|
96
|
+
pass: registryOk,
|
|
97
|
+
optional: true,
|
|
98
|
+
suggest: registryOk
|
|
99
|
+
? undefined
|
|
100
|
+
: `${installHint} --sync-registry`,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (paired) {
|
|
104
|
+
const mirrorName =
|
|
105
|
+
companion.gates.find((name) => name !== primaryGate) ?? "";
|
|
106
|
+
if (mirrorName) {
|
|
107
|
+
const mirrorOk = await pathExists(
|
|
108
|
+
cwd,
|
|
109
|
+
path.join(GUARDRAILS_SCRIPTS_DIR, mirrorName),
|
|
110
|
+
);
|
|
111
|
+
checks.push({
|
|
112
|
+
id: `${prefix}-mirror`,
|
|
113
|
+
label: `${companion.displayName}: gate mirror (${GUARDRAILS_SCRIPTS_DIR}/${mirrorName})`,
|
|
114
|
+
weight: 3,
|
|
115
|
+
pass: mirrorOk,
|
|
116
|
+
optional,
|
|
117
|
+
suggest: mirrorOk ? undefined : installHint,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { paired, companions, checks };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {DoctorCheck[]} companionChecks
|
|
128
|
+
* @returns {{ ready: boolean, passed: number, total: number }}
|
|
129
|
+
*/
|
|
130
|
+
export function summarizeCompanionChecks(companionChecks) {
|
|
131
|
+
const scoped = companionChecks.filter((check) => check.id !== "atlas-companions");
|
|
132
|
+
const required = scoped.filter((check) => !check.optional);
|
|
133
|
+
const passed = scoped.filter((check) => check.pass).length;
|
|
134
|
+
const requiredPassed = required.filter((check) => check.pass).length;
|
|
135
|
+
return {
|
|
136
|
+
ready: required.length === 0 || requiredPassed === required.length,
|
|
137
|
+
passed,
|
|
138
|
+
total: scoped.length,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const COMPANIONS_DIR = ".specs/companions";
|
|
5
|
+
|
|
6
|
+
export const COMPANIONS_INDEX_FILE = "INDEX.json";
|
|
7
|
+
|
|
8
|
+
export const ATLAS_SCHEMA_VERSION = "1.0.0";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{
|
|
12
|
+
* id: string,
|
|
13
|
+
* npm: string,
|
|
14
|
+
* version: string,
|
|
15
|
+
* displayName: string,
|
|
16
|
+
* scriptsDir: string,
|
|
17
|
+
* gates: string[],
|
|
18
|
+
* projectSection: string,
|
|
19
|
+
* ruleFile: string,
|
|
20
|
+
* preservePaths: string[],
|
|
21
|
+
* }} CompanionIndexEntry
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {{
|
|
26
|
+
* schemaVersion: string,
|
|
27
|
+
* generatedAt: string,
|
|
28
|
+
* paired: boolean,
|
|
29
|
+
* companions: CompanionIndexEntry[],
|
|
30
|
+
* }} CompanionsIndex
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} cwd
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function companionsIndexPath(cwd) {
|
|
38
|
+
return path.join(cwd, COMPANIONS_DIR, COMPANIONS_INDEX_FILE);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} cwd
|
|
43
|
+
* @returns {Promise<CompanionsIndex | null>}
|
|
44
|
+
*/
|
|
45
|
+
export async function readCompanionsIndex(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
const raw = await fs.readFile(companionsIndexPath(cwd), "utf8");
|
|
48
|
+
return /** @type {CompanionsIndex} */ (JSON.parse(raw));
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err && /** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collect preserve paths from all registered Atlas companions.
|
|
59
|
+
* Falls back to known Tech Atlas 0.5 paths when INDEX is absent.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} cwd
|
|
62
|
+
* @returns {Promise<string[]>}
|
|
63
|
+
*/
|
|
64
|
+
export async function collectCompanionPreservePaths(cwd) {
|
|
65
|
+
const index = await readCompanionsIndex(cwd);
|
|
66
|
+
if (!index) {
|
|
67
|
+
return [
|
|
68
|
+
".specs/tech-atlas",
|
|
69
|
+
".specs/atlas",
|
|
70
|
+
".specs/desks",
|
|
71
|
+
".specs/companions",
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const paths = new Set();
|
|
76
|
+
for (const companion of index.companions) {
|
|
77
|
+
for (const preservePath of companion.preservePaths) {
|
|
78
|
+
paths.add(preservePath);
|
|
79
|
+
}
|
|
80
|
+
for (const gate of companion.gates) {
|
|
81
|
+
paths.add(path.posix.join(companion.scriptsDir, gate));
|
|
82
|
+
}
|
|
83
|
+
paths.add(companion.scriptsDir);
|
|
84
|
+
paths.add(path.dirname(companion.ruleFile));
|
|
85
|
+
}
|
|
86
|
+
paths.add(COMPANIONS_DIR);
|
|
87
|
+
return [...paths];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} cwd
|
|
92
|
+
* @returns {Promise<CompanionIndexEntry[]>}
|
|
93
|
+
*/
|
|
94
|
+
export async function listInstalledCompanions(cwd) {
|
|
95
|
+
const index = await readCompanionsIndex(cwd);
|
|
96
|
+
return index?.companions ?? [];
|
|
97
|
+
}
|
package/lib/doctor.js
CHANGED
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
NPX,
|
|
9
9
|
SKILL_DIRS,
|
|
10
10
|
} from "./constants.js";
|
|
11
|
+
import { listInstalledCompanions } from "./companions.js";
|
|
12
|
+
import {
|
|
13
|
+
runCompanionDoctorChecks,
|
|
14
|
+
summarizeCompanionChecks,
|
|
15
|
+
} from "./companion-doctor.js";
|
|
11
16
|
import { resolvePython, resolveScriptsDir } from "./gates.js";
|
|
12
17
|
import { readFileSafe } from "./fs-utils.js";
|
|
13
18
|
import { listFeatureIds, readActiveFeatureFromState } from "./specs-utils.js";
|
|
@@ -198,6 +203,21 @@ export async function runDoctorChecks(cwd) {
|
|
|
198
203
|
suggest: NPX("project-init"),
|
|
199
204
|
});
|
|
200
205
|
|
|
206
|
+
const companionReport = await runCompanionDoctorChecks(cwd);
|
|
207
|
+
if (companionReport.checks.length > 0) {
|
|
208
|
+
checks.push(...companionReport.checks);
|
|
209
|
+
} else {
|
|
210
|
+
checks.push({
|
|
211
|
+
id: "atlas-companions",
|
|
212
|
+
label: "Atlas companions registry (.specs/companions/INDEX.json)",
|
|
213
|
+
weight: 5,
|
|
214
|
+
pass: false,
|
|
215
|
+
optional: true,
|
|
216
|
+
suggest:
|
|
217
|
+
"Optional: npx @luizsantiago/tech-atlas install (or another Atlas package)",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
201
221
|
const activeFeature = await readActiveFeature(cwd);
|
|
202
222
|
let activeFeatureOk = true;
|
|
203
223
|
let activeFeatureSuggest;
|
|
@@ -379,16 +399,45 @@ export async function doctor(cwd, options = {}) {
|
|
|
379
399
|
const executeHint = await resolveExecuteHint(cwd, activeFeature);
|
|
380
400
|
const pythonCheck = checks.find((check) => check.id === "python");
|
|
381
401
|
const pythonMissing = pythonCheck ? !pythonCheck.pass : false;
|
|
402
|
+
const companions = await listInstalledCompanions(cwd);
|
|
403
|
+
const companionChecks = checks.filter((check) => check.id.startsWith("atlas-"));
|
|
404
|
+
const companionSummary = summarizeCompanionChecks(companionChecks);
|
|
405
|
+
const stackPaired = companions.length > 0 && companionChecks.some((c) => !c.optional);
|
|
382
406
|
|
|
383
407
|
if (options.json) {
|
|
384
408
|
console.log(
|
|
385
409
|
JSON.stringify(
|
|
386
|
-
{
|
|
410
|
+
{
|
|
411
|
+
score,
|
|
412
|
+
modes,
|
|
413
|
+
checks,
|
|
414
|
+
suggestions,
|
|
415
|
+
executeHint,
|
|
416
|
+
pythonMissing,
|
|
417
|
+
stack: {
|
|
418
|
+
paired: stackPaired,
|
|
419
|
+
companions: companions.map((c) => ({
|
|
420
|
+
id: c.id,
|
|
421
|
+
npm: c.npm,
|
|
422
|
+
version: c.version,
|
|
423
|
+
displayName: c.displayName,
|
|
424
|
+
})),
|
|
425
|
+
atlasReady: companionSummary.ready,
|
|
426
|
+
},
|
|
427
|
+
},
|
|
387
428
|
null,
|
|
388
429
|
2,
|
|
389
430
|
),
|
|
390
431
|
);
|
|
391
|
-
return {
|
|
432
|
+
return {
|
|
433
|
+
score,
|
|
434
|
+
modes,
|
|
435
|
+
checks,
|
|
436
|
+
suggestions,
|
|
437
|
+
executeHint,
|
|
438
|
+
pythonMissing,
|
|
439
|
+
stack: { paired: stackPaired, companions, atlasReady: companionSummary.ready },
|
|
440
|
+
};
|
|
392
441
|
}
|
|
393
442
|
|
|
394
443
|
if (pythonMissing) {
|
|
@@ -407,6 +456,13 @@ export async function doctor(cwd, options = {}) {
|
|
|
407
456
|
console.log(` Brakes: ${modes.brakes.score}/100${brakesHint}\n`);
|
|
408
457
|
console.log(`Guardrails Ready: ${score}/100\n`);
|
|
409
458
|
|
|
459
|
+
if (companions.length > 0) {
|
|
460
|
+
const stackLabel = stackPaired ? "paired Lego stack" : "solo + Atlas companions";
|
|
461
|
+
console.log(
|
|
462
|
+
`Atlas stack: ${stackLabel} — ${companions.length} companion(s), ${companionSummary.passed}/${companionSummary.total} checks passed\n`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
410
466
|
for (const check of checks) {
|
|
411
467
|
const mark = check.pass ? "✓" : "✗";
|
|
412
468
|
const optional = check.optional ? " (optional)" : "";
|
|
@@ -427,5 +483,13 @@ export async function doctor(cwd, options = {}) {
|
|
|
427
483
|
console.log(`\nExecute hint:\n → ${executeHint}`);
|
|
428
484
|
}
|
|
429
485
|
|
|
430
|
-
return {
|
|
486
|
+
return {
|
|
487
|
+
score,
|
|
488
|
+
modes,
|
|
489
|
+
checks,
|
|
490
|
+
suggestions,
|
|
491
|
+
executeHint,
|
|
492
|
+
pythonMissing,
|
|
493
|
+
stack: { paired: stackPaired, companions, atlasReady: companionSummary.ready },
|
|
494
|
+
};
|
|
431
495
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luizsantiago/spec-guardrails",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.11",
|
|
4
4
|
"description": "Keep AI coding agents honest — specify the work, prove each step, verify independently. Process mode (Node) for flexibility; Brakes mode (Node + Python) for structural gates and a Guarantees matrix. Progressive loading, independent verify — any AI agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"guardrails": "node index.js",
|
|
14
14
|
"test": "npm run test:node && npm run test:gates",
|
|
15
|
-
"test:node": "node --test test/install.test.js test/test_feature_init.test.js test/test_config.test.js test/test_archive.test.js test/test_delta_merge.test.js test/test_presets.test.js test/test_brownfield.test.js test/test_doctor.test.js test/test_token_cost.test.js test/test_next_steps.test.js test/test_classify_change.test.js test/test_feature_status.test.js test/test_agent_contract.test.js test/test_gates_python.test.js test/test_specs_utils.test.js",
|
|
15
|
+
"test:node": "node --test test/install.test.js test/test_feature_init.test.js test/test_config.test.js test/test_archive.test.js test/test_delta_merge.test.js test/test_presets.test.js test/test_brownfield.test.js test/test_doctor.test.js test/test_token_cost.test.js test/test_next_steps.test.js test/test_classify_change.test.js test/test_feature_status.test.js test/test_agent_contract.test.js test/test_gates_python.test.js test/test_specs_utils.test.js test/test_companions.test.js test/test_companion_doctor.test.js",
|
|
16
16
|
"test:gates": "node test/run-gate-tests.mjs",
|
|
17
17
|
"prepublishOnly": "npm test"
|
|
18
18
|
},
|