@luizsantiago/spec-guardrails 3.1.11 → 3.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 CHANGED
@@ -11,7 +11,7 @@
11
11
  | **Solution** | One kit, two deliberate modes: **Process** (Node only) for a flexible spec-driven workflow; **Brakes** (Node + Python) for the **full product** — structural gates that exit non-zero when paperwork or evidence is missing. You approve specs/tasks in both. |
12
12
  | **Result** | Traceable `.specs/` memory, fewer fake finishes, cheaper turns (~70% less skill text on planning). Choose Process for light ceremony; add Python when you want the [Guarantees matrix](#guarantees-matrix) enforced automatically. |
13
13
 
14
- npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **3.1.x**
14
+ npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **3.2.x**
15
15
 
16
16
  ---
17
17
 
@@ -41,11 +41,16 @@ 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)
44
+ ### Governance focus (3.2+)
45
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.
46
+ Spec Guardrails is a **single product**: governance, evidence, verification, and controlled execution for agentic software development not a bundle of companion packages.
47
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).
48
+ | Capability | What it does |
49
+ | --- | --- |
50
+ | **Artifact gates** | Structural quality for spec, tasks, and cross-artifact consistency — with **blocking / warning / info** severity |
51
+ | **Parallel waves** | `loop-plan` computes safe parallel groups; `workspace-prepare` isolates tasks in git worktrees |
52
+ | **Execution policy** | Budgets, path scope, and escalation rules in `.specs/config.yaml` — consult via `execution-policy` |
53
+ | **Independent verify** | Fresh-context verification with evidence-or-zero (`validate-state`) |
49
54
 
50
55
  | Need | Command |
51
56
  | --- | --- |
@@ -196,6 +201,7 @@ Full reference: **[Gates](docs/guide/gates.md)** · [Guarantees matrix](docs/gui
196
201
  | [Skills and hub](docs/guide/skills-and-hub.md) | What each skill file does |
197
202
  | [Gates](docs/guide/gates.md) | How each gate works |
198
203
  | [Platform parity](docs/guide/Platform-parity.md) | Shipped adapters (Cursor, Claude, Copilot, Codex) — core works with any agent |
204
+ | [Restart PRD seed](docs/guide/Restart-prd-seed.md) | Clean-project PRD template for a single-package restart |
199
205
  | [FAQ](docs/guide/FAQ.md) | Common questions |
200
206
  | [Changelog](docs/CHANGELOG.md) | Full version history |
201
207
 
@@ -211,7 +217,8 @@ npx @luizsantiago/spec-guardrails install
211
217
 
212
218
  | Version | What you gain |
213
219
  | --- | --- |
214
- | **3.1.x** | Copilot/Codex/AGENTS.md adapters; doctor Process + Brakes scores; unified execution contracts; `validate-traceability` / `validate-quick`; `classify-change` / `feature-status` |
220
+ | **3.2.x** | Single-package focus; artifact gate severity labels; git worktree isolation CLI; execution policy (budget/scope/escalation) |
221
+ | **3.1.x** | Copilot/Codex/AGENTS.md adapters; doctor Process + Brakes scores; `validate-traceability` / `validate-quick`; `classify-change` / `feature-status` |
215
222
  | **3.0.x** | Final name Spec Guardrails; `.specs/guardrails/`; no dual-path ([Migration](docs/guide/Migration.md)) |
216
223
  | **2.2.x** | Seatbelt-era paths & markers; `doctor` Execute hints; docs split from README |
217
224
  | **2.1.x** | `loop-plan` + parallel `/loop` waves |
package/index.js CHANGED
@@ -8,10 +8,22 @@ import { classifyChange, formatClassifyChange } from "./lib/classify-change.js";
8
8
  import { PACKAGE_VERSION, CLI_NAME } from "./lib/constants.js";
9
9
  import { phaseContext } from "./lib/config.js";
10
10
  import { doctor } from "./lib/doctor.js";
11
+ import {
12
+ checkPathScope,
13
+ formatPolicyStatus,
14
+ loadExecutionPolicy,
15
+ loadPolicyState,
16
+ savePolicyState,
17
+ } from "./lib/execution-policy.js";
11
18
  import { featureInit } from "./lib/feature.js";
12
19
  import { featureStatus, formatFeatureStatus } from "./lib/feature-status.js";
13
20
  import { GATE_COMMANDS, AUX_COMMANDS, runGate, runGuardrailsScript } from "./lib/gates.js";
14
21
  import { install } from "./lib/install.js";
22
+ import {
23
+ cleanupWorkspaces,
24
+ formatWorkspaceResults,
25
+ prepareWorkspaces,
26
+ } from "./lib/workspace-isolation.js";
15
27
  import {
16
28
  initProjectConfig,
17
29
  listPresets,
@@ -50,10 +62,21 @@ Commands:
50
62
  [--json] Machine-readable output
51
63
  phase-context <phase> Print .specs/config.yaml context + rules for a phase
52
64
  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
55
65
  [--json] Machine-readable output
56
66
  [--no-suggest] Hide per-check remediation hints
67
+ workspace-prepare <feature> Create isolated git worktrees for parallel tasks
68
+ --tasks T1,T2 Task ids to isolate (required)
69
+ [--base-ref HEAD] Base ref for new worktrees
70
+ [--json] Machine-readable output
71
+ workspace-cleanup <feature> Remove isolated worktrees for a feature
72
+ [--tasks T1,T2] Limit cleanup to specific tasks
73
+ [--force] Force-remove dirty worktrees
74
+ [--json] Machine-readable output
75
+ execution-policy status Show configured budgets, scope, and runtime counters
76
+ [--json] Machine-readable output
77
+ execution-policy check-path <path> Check whether a relative path is allowed by scope policy
78
+ [--json] Machine-readable output
79
+ execution-policy record-retry <task> Increment retry counter for a task id
57
80
  validate-spec [spec.md|feature] Closure gate for a feature spec
58
81
  analyze-artifacts [feature] Cross-artifact consistency before task approval
59
82
  validate-tasks [tasks.md|feature] Granularity gate for a task breakdown
@@ -323,6 +346,144 @@ if (command === "--version" || command === "-v" || command === "version") {
323
346
  console.error(`❌ ${err.message}`);
324
347
  process.exit(1);
325
348
  }
349
+ } else if (command === "workspace-prepare") {
350
+ try {
351
+ let json = false;
352
+ let baseRef = "HEAD";
353
+ let tasksRaw = "";
354
+ const positional = [];
355
+
356
+ for (let i = 0; i < args.length; i++) {
357
+ const arg = args[i];
358
+ if (arg === "--json") {
359
+ json = true;
360
+ } else if (arg === "--base-ref") {
361
+ baseRef = args[++i];
362
+ if (!baseRef) {
363
+ throw new Error("--base-ref requires a git ref");
364
+ }
365
+ } else if (arg === "--tasks") {
366
+ tasksRaw = args[++i] ?? "";
367
+ if (!tasksRaw) {
368
+ throw new Error("--tasks requires a comma-separated list (e.g. T1,T2)");
369
+ }
370
+ } else {
371
+ positional.push(arg);
372
+ }
373
+ }
374
+
375
+ const featureId = positional[0];
376
+ if (!featureId) {
377
+ throw new Error("Usage: workspace-prepare <feature> --tasks T1,T2");
378
+ }
379
+
380
+ const taskIds = tasksRaw.split(",").map((item) => item.trim()).filter(Boolean);
381
+ const results = await prepareWorkspaces(process.cwd(), { featureId, taskIds, baseRef });
382
+ process.stdout.write(formatWorkspaceResults(results, { json }));
383
+
384
+ if (results.some((item) => item.status === "failed")) {
385
+ process.exit(1);
386
+ }
387
+ } catch (err) {
388
+ console.error(`❌ ${err.message}`);
389
+ process.exit(1);
390
+ }
391
+ } else if (command === "workspace-cleanup") {
392
+ try {
393
+ let json = false;
394
+ let force = false;
395
+ let tasksRaw = "";
396
+ const positional = [];
397
+
398
+ for (let i = 0; i < args.length; i++) {
399
+ const arg = args[i];
400
+ if (arg === "--json") {
401
+ json = true;
402
+ } else if (arg === "--force") {
403
+ force = true;
404
+ } else if (arg === "--tasks") {
405
+ tasksRaw = args[++i] ?? "";
406
+ } else {
407
+ positional.push(arg);
408
+ }
409
+ }
410
+
411
+ const featureId = positional[0];
412
+ if (!featureId) {
413
+ throw new Error("Usage: workspace-cleanup <feature> [--tasks T1,T2] [--force]");
414
+ }
415
+
416
+ const taskIds = tasksRaw
417
+ ? tasksRaw.split(",").map((item) => item.trim()).filter(Boolean)
418
+ : undefined;
419
+ const results = await cleanupWorkspaces(process.cwd(), { featureId, taskIds, force });
420
+ process.stdout.write(formatWorkspaceResults(results, { json }));
421
+
422
+ if (results.some((item) => item.status === "failed")) {
423
+ process.exit(1);
424
+ }
425
+ } catch (err) {
426
+ console.error(`❌ ${err.message}`);
427
+ process.exit(1);
428
+ }
429
+ } else if (command === "execution-policy") {
430
+ try {
431
+ const sub = args[0];
432
+ let json = false;
433
+ const rest = [];
434
+
435
+ for (let i = 1; i < args.length; i++) {
436
+ if (args[i] === "--json") {
437
+ json = true;
438
+ } else {
439
+ rest.push(args[i]);
440
+ }
441
+ }
442
+
443
+ const cwd = process.cwd();
444
+ const policy = await loadExecutionPolicy(cwd);
445
+ const state = await loadPolicyState(cwd);
446
+
447
+ if (sub === "status") {
448
+ process.stdout.write(formatPolicyStatus(policy, state, { json }));
449
+ } else if (sub === "check-path") {
450
+ const relativePath = rest[0];
451
+ if (!relativePath) {
452
+ throw new Error("Usage: execution-policy check-path <relative-path>");
453
+ }
454
+ const result = checkPathScope(relativePath, policy);
455
+ if (json) {
456
+ console.log(JSON.stringify({ path: relativePath, ...result }, null, 2));
457
+ } else {
458
+ console.log(
459
+ `${relativePath}: ${result.allowed ? "allowed" : "blocked"} (${result.reason})`,
460
+ );
461
+ }
462
+ if (!result.allowed) {
463
+ process.exit(1);
464
+ }
465
+ } else if (sub === "record-retry") {
466
+ const taskId = rest[0];
467
+ if (!taskId) {
468
+ throw new Error("Usage: execution-policy record-retry <task-id>");
469
+ }
470
+ state.retries[taskId] = (state.retries[taskId] ?? 0) + 1;
471
+ state.iterations += 1;
472
+ await savePolicyState(cwd, state);
473
+ if (json) {
474
+ console.log(JSON.stringify({ taskId, retries: state.retries[taskId], state }, null, 2));
475
+ } else {
476
+ console.log(`Recorded retry for ${taskId}: ${state.retries[taskId]}`);
477
+ }
478
+ } else {
479
+ throw new Error(
480
+ "Usage: execution-policy status | check-path <path> | record-retry <task>",
481
+ );
482
+ }
483
+ } catch (err) {
484
+ console.error(`❌ ${err.message}`);
485
+ process.exit(1);
486
+ }
326
487
  } else if (command === "classify-change") {
327
488
  try {
328
489
  let json = false;
package/lib/archive.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  import { ensureDir, readFileSafe, writeFileSafe } from "./fs-utils.js";
10
10
  import { runGate } from "./gates.js";
11
11
  import { assertSafeDomainSlug } from "./slug-utils.js";
12
+ import { isPassVerdict } from "./validation-verdict.js";
12
13
  import {
13
14
  featureDir,
14
15
  readFeatureArtifact,
@@ -23,15 +24,6 @@ Track milestones and archived features.
23
24
 
24
25
  `;
25
26
 
26
- /**
27
- * @param {string} text
28
- * @returns {boolean}
29
- */
30
- function validationPassed(text) {
31
- const visible = text.replace(/<!--[\s\S]*?-->/g, "");
32
- return /\b(?:PASS|PASSED)\b/.test(visible);
33
- }
34
-
35
27
  /**
36
28
  * @param {string} cwd
37
29
  * @param {string} featureId
@@ -150,7 +142,7 @@ export async function archiveFeature(featureArg, options = {}) {
150
142
  },
151
143
  );
152
144
 
153
- if (!validationPassed(validationText)) {
145
+ if (!isPassVerdict(validationText)) {
154
146
  throw new Error(
155
147
  `validation.md for ${featureId} does not contain PASS/PASSED. Run validate-state first.`,
156
148
  );
package/lib/assets.js CHANGED
@@ -55,8 +55,9 @@ export function resolveInstallSource(repoUrl) {
55
55
  /**
56
56
  * @param {string} remotePath
57
57
  * @param {string} destPath
58
+ * @param {{ boundary?: string }} [options]
58
59
  */
59
- export async function copyPackagedAsset(remotePath, destPath) {
60
+ export async function copyPackagedAsset(remotePath, destPath, options = {}) {
60
61
  const source = packagedAssetPath(remotePath);
61
62
 
62
63
  try {
@@ -71,7 +72,7 @@ export async function copyPackagedAsset(remotePath, destPath) {
71
72
  throw err;
72
73
  }
73
74
 
74
- await assertSafeWriteTarget(destPath);
75
+ await assertSafeWriteTarget(destPath, options);
75
76
 
76
77
  try {
77
78
  await fs.copyFile(source, destPath);
@@ -91,7 +92,7 @@ export async function copyPackagedAsset(remotePath, destPath) {
91
92
  * repo with no override (kept for the remote path; the default install no
92
93
  * longer uses it).
93
94
  *
94
- * @param {{ remotePath: string, destPath: string, repoUrl?: string, state: { warned: boolean }, log: (msg: string) => void }} params
95
+ * @param {{ remotePath: string, destPath: string, repoUrl?: string, state: { warned: boolean }, log: (msg: string) => void, boundary?: string }} params
95
96
  */
96
97
  export async function downloadRemoteAsset({
97
98
  remotePath,
@@ -99,9 +100,10 @@ export async function downloadRemoteAsset({
99
100
  repoUrl,
100
101
  state,
101
102
  log,
103
+ boundary,
102
104
  }) {
103
105
  try {
104
- await downloadToFile(resolveAssetUrl(remotePath, repoUrl), destPath);
106
+ await downloadToFile(resolveAssetUrl(remotePath, repoUrl), destPath, { boundary });
105
107
  return;
106
108
  } catch (err) {
107
109
  const missingPinnedAsset =
@@ -123,15 +125,16 @@ export async function downloadRemoteAsset({
123
125
  await downloadToFile(
124
126
  resolveAssetUrl(remotePath, FALLBACK_REPO_URL),
125
127
  destPath,
128
+ { boundary },
126
129
  );
127
130
  }
128
131
 
129
132
  /**
130
- * @param {{ remotePath: string, destPath: string, source: ReturnType<typeof resolveInstallSource>, state: { warned: boolean }, log: (msg: string) => void }} params
133
+ * @param {{ remotePath: string, destPath: string, source: ReturnType<typeof resolveInstallSource>, state: { warned: boolean }, log: (msg: string) => void, boundary?: string }} params
131
134
  */
132
- export async function installAsset({ remotePath, destPath, source, state, log }) {
135
+ export async function installAsset({ remotePath, destPath, source, state, log, boundary }) {
133
136
  if (source.mode === "package") {
134
- await copyPackagedAsset(remotePath, destPath);
137
+ await copyPackagedAsset(remotePath, destPath, { boundary });
135
138
  return;
136
139
  }
137
140
 
@@ -141,5 +144,6 @@ export async function installAsset({ remotePath, destPath, source, state, log })
141
144
  repoUrl: source.repoUrl,
142
145
  state,
143
146
  log,
147
+ boundary,
144
148
  });
145
149
  }
package/lib/doctor.js CHANGED
@@ -8,11 +8,6 @@ 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";
16
11
  import { resolvePython, resolveScriptsDir } from "./gates.js";
17
12
  import { readFileSafe } from "./fs-utils.js";
18
13
  import { listFeatureIds, readActiveFeatureFromState } from "./specs-utils.js";
@@ -203,21 +198,6 @@ export async function runDoctorChecks(cwd) {
203
198
  suggest: NPX("project-init"),
204
199
  });
205
200
 
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
-
221
201
  const activeFeature = await readActiveFeature(cwd);
222
202
  let activeFeatureOk = true;
223
203
  let activeFeatureSuggest;
@@ -399,45 +379,16 @@ export async function doctor(cwd, options = {}) {
399
379
  const executeHint = await resolveExecuteHint(cwd, activeFeature);
400
380
  const pythonCheck = checks.find((check) => check.id === "python");
401
381
  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);
406
382
 
407
383
  if (options.json) {
408
384
  console.log(
409
385
  JSON.stringify(
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
- },
386
+ { score, modes, checks, suggestions, executeHint, pythonMissing },
428
387
  null,
429
388
  2,
430
389
  ),
431
390
  );
432
- return {
433
- score,
434
- modes,
435
- checks,
436
- suggestions,
437
- executeHint,
438
- pythonMissing,
439
- stack: { paired: stackPaired, companions, atlasReady: companionSummary.ready },
440
- };
391
+ return { score, modes, checks, suggestions, executeHint, pythonMissing };
441
392
  }
442
393
 
443
394
  if (pythonMissing) {
@@ -456,13 +407,6 @@ export async function doctor(cwd, options = {}) {
456
407
  console.log(` Brakes: ${modes.brakes.score}/100${brakesHint}\n`);
457
408
  console.log(`Guardrails Ready: ${score}/100\n`);
458
409
 
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
-
466
410
  for (const check of checks) {
467
411
  const mark = check.pass ? "✓" : "✗";
468
412
  const optional = check.optional ? " (optional)" : "";
@@ -483,13 +427,5 @@ export async function doctor(cwd, options = {}) {
483
427
  console.log(`\nExecute hint:\n → ${executeHint}`);
484
428
  }
485
429
 
486
- return {
487
- score,
488
- modes,
489
- checks,
490
- suggestions,
491
- executeHint,
492
- pythonMissing,
493
- stack: { paired: stackPaired, companions, atlasReady: companionSummary.ready },
494
- };
430
+ return { score, modes, checks, suggestions, executeHint, pythonMissing };
495
431
  }
package/lib/download.js CHANGED
@@ -98,8 +98,8 @@ async function fetchWithSafeRedirects(startUrl) {
98
98
  );
99
99
  }
100
100
 
101
- export async function downloadToFile(url, destPath) {
102
- await assertSafeWriteTarget(destPath);
101
+ export async function downloadToFile(url, destPath, options = {}) {
102
+ await assertSafeWriteTarget(destPath, options);
103
103
 
104
104
  const response = await fetchWithSafeRedirects(url);
105
105