@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 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
- { score, modes, checks, suggestions, executeHint, pythonMissing },
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 { score, modes, checks, suggestions, executeHint, pythonMissing };
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 { score, modes, checks, suggestions, executeHint, pythonMissing };
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.10",
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
  },