@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 +12 -5
- package/index.js +163 -2
- package/lib/archive.js +2 -10
- package/lib/assets.js +11 -7
- package/lib/doctor.js +3 -67
- package/lib/download.js +2 -2
- package/lib/execution-policy.js +291 -0
- package/lib/feature-status.js +2 -12
- package/lib/feature.js +35 -6
- package/lib/fs-utils.js +65 -7
- package/lib/install.js +1 -1
- package/lib/memory.js +6 -1
- package/lib/project-rules.js +2 -2
- package/lib/validation-verdict.js +101 -0
- package/lib/workspace-isolation.js +156 -0
- package/package.json +2 -2
- package/scripts/_common.py +10 -4
- package/scripts/validate_spec.py +30 -0
- package/skills/references/implement.md +23 -2
- package/skills/references/sub-agents.md +3 -0
- package/templates/config.yaml.example +18 -0
- package/lib/companion-doctor.js +0 -140
- package/lib/companions.js +0 -97
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.
|
|
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
|
-
###
|
|
44
|
+
### Governance focus (3.2+)
|
|
45
45
|
|
|
46
|
-
Spec Guardrails is a **
|
|
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
|
-
|
|
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.
|
|
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 (!
|
|
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
|
|