@luizsantiago/spec-guardrails 3.1.11 → 3.2.1
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 +225 -4
- package/lib/adapter-registry.js +128 -0
- package/lib/adapters.js +3 -9
- package/lib/archive.js +2 -10
- package/lib/assets.js +11 -7
- package/lib/constants.js +2 -0
- package/lib/doctor.js +3 -67
- package/lib/download.js +2 -2
- package/lib/execution-policy.js +374 -0
- package/lib/feature-status.js +2 -12
- package/lib/feature.js +35 -6
- package/lib/fs-utils.js +65 -7
- package/lib/gates.js +2 -0
- package/lib/install.js +1 -5
- package/lib/memory-index.js +59 -0
- 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 +189 -0
- package/package.json +2 -2
- package/rules/engineering-baseline.mdc +3 -1
- package/scripts/_common.py +10 -4
- package/scripts/analyze_artifacts.py +23 -0
- package/scripts/lessons.py +163 -30
- package/scripts/memory_index.py +325 -0
- package/scripts/memory_query.py +177 -0
- package/scripts/validate_spec.py +31 -0
- package/skills/agent-architecture.md +4 -0
- package/skills/git-handoff.md +15 -0
- package/skills/references/implement.md +23 -2
- package/skills/references/lessons.md +16 -10
- 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,26 @@ 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
|
+
formatPolicyStatus,
|
|
13
|
+
loadExecutionPolicy,
|
|
14
|
+
loadPolicyState,
|
|
15
|
+
recordAgentRun,
|
|
16
|
+
recordTaskRetry,
|
|
17
|
+
resolvePathCheck,
|
|
18
|
+
savePolicyState,
|
|
19
|
+
} from "./lib/execution-policy.js";
|
|
11
20
|
import { featureInit } from "./lib/feature.js";
|
|
12
21
|
import { featureStatus, formatFeatureStatus } from "./lib/feature-status.js";
|
|
13
22
|
import { GATE_COMMANDS, AUX_COMMANDS, runGate, runGuardrailsScript } from "./lib/gates.js";
|
|
14
23
|
import { install } from "./lib/install.js";
|
|
24
|
+
import {
|
|
25
|
+
cleanupWorkspaces,
|
|
26
|
+
formatWorkspaceList,
|
|
27
|
+
formatWorkspaceResults,
|
|
28
|
+
listWorkspaces,
|
|
29
|
+
prepareWorkspaces,
|
|
30
|
+
} from "./lib/workspace-isolation.js";
|
|
15
31
|
import {
|
|
16
32
|
initProjectConfig,
|
|
17
33
|
listPresets,
|
|
@@ -50,10 +66,28 @@ Commands:
|
|
|
50
66
|
[--json] Machine-readable output
|
|
51
67
|
phase-context <phase> Print .specs/config.yaml context + rules for a phase
|
|
52
68
|
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
69
|
[--json] Machine-readable output
|
|
56
70
|
[--no-suggest] Hide per-check remediation hints
|
|
71
|
+
workspace-prepare <feature> Create isolated git worktrees for parallel tasks
|
|
72
|
+
--tasks T1,T2 Task ids to isolate (required)
|
|
73
|
+
[--base-ref HEAD] Base ref for new worktrees
|
|
74
|
+
[--json] Machine-readable output
|
|
75
|
+
workspace-cleanup <feature> Remove isolated worktrees for a feature
|
|
76
|
+
[--tasks T1,T2] Limit cleanup to specific tasks
|
|
77
|
+
[--force] Force-remove dirty worktrees (recovery after worker FAIL)
|
|
78
|
+
[--json] Machine-readable output
|
|
79
|
+
workspace-list <feature> List isolated worktrees for a feature
|
|
80
|
+
[--json] Machine-readable output
|
|
81
|
+
execution-policy status Show configured budgets, scope, and runtime counters
|
|
82
|
+
[--json] Machine-readable output
|
|
83
|
+
execution-policy check-path <path> Check whether a relative path is allowed by scope policy
|
|
84
|
+
[--json] Machine-readable output
|
|
85
|
+
execution-policy record-retry <task> Increment retry counter for a task id (blocks at limit)
|
|
86
|
+
execution-policy record-run Increment agent-run counter (blocks at budget)
|
|
87
|
+
memory-index rebuild Rebuild SQLite memory index from .specs/ artifacts
|
|
88
|
+
memory-query --from <id> Bounded context package from the knowledge graph
|
|
89
|
+
[--depth N] Traversal depth (default 2)
|
|
90
|
+
[--json] Machine-readable output
|
|
57
91
|
validate-spec [spec.md|feature] Closure gate for a feature spec
|
|
58
92
|
analyze-artifacts [feature] Cross-artifact consistency before task approval
|
|
59
93
|
validate-tasks [tasks.md|feature] Granularity gate for a task breakdown
|
|
@@ -63,7 +97,7 @@ Commands:
|
|
|
63
97
|
validate-quick [quick-folder] Quick-mode TASK.md / SUMMARY.md structural gate
|
|
64
98
|
validate-state [feature] Completion gate before declaring a feature done
|
|
65
99
|
check-commit --message "<msg>" Conventional Commits gate
|
|
66
|
-
lessons <add|list|penalize|prune|status> Lessons engine
|
|
100
|
+
lessons <add|list|penalize|prune|promote|graduate|status> Lessons engine
|
|
67
101
|
--help Show this message
|
|
68
102
|
--version Print the package version
|
|
69
103
|
`;
|
|
@@ -323,6 +357,192 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
323
357
|
console.error(`❌ ${err.message}`);
|
|
324
358
|
process.exit(1);
|
|
325
359
|
}
|
|
360
|
+
} else if (command === "workspace-prepare") {
|
|
361
|
+
try {
|
|
362
|
+
let json = false;
|
|
363
|
+
let baseRef = "HEAD";
|
|
364
|
+
let tasksRaw = "";
|
|
365
|
+
const positional = [];
|
|
366
|
+
|
|
367
|
+
for (let i = 0; i < args.length; i++) {
|
|
368
|
+
const arg = args[i];
|
|
369
|
+
if (arg === "--json") {
|
|
370
|
+
json = true;
|
|
371
|
+
} else if (arg === "--base-ref") {
|
|
372
|
+
baseRef = args[++i];
|
|
373
|
+
if (!baseRef) {
|
|
374
|
+
throw new Error("--base-ref requires a git ref");
|
|
375
|
+
}
|
|
376
|
+
} else if (arg === "--tasks") {
|
|
377
|
+
tasksRaw = args[++i] ?? "";
|
|
378
|
+
if (!tasksRaw) {
|
|
379
|
+
throw new Error("--tasks requires a comma-separated list (e.g. T1,T2)");
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
positional.push(arg);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const featureId = positional[0];
|
|
387
|
+
if (!featureId) {
|
|
388
|
+
throw new Error("Usage: workspace-prepare <feature> --tasks T1,T2");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const taskIds = tasksRaw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
392
|
+
const results = await prepareWorkspaces(process.cwd(), { featureId, taskIds, baseRef });
|
|
393
|
+
process.stdout.write(formatWorkspaceResults(results, { json }));
|
|
394
|
+
|
|
395
|
+
if (results.some((item) => item.status === "failed")) {
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
} catch (err) {
|
|
399
|
+
console.error(`❌ ${err.message}`);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
} else if (command === "workspace-cleanup") {
|
|
403
|
+
try {
|
|
404
|
+
let json = false;
|
|
405
|
+
let force = false;
|
|
406
|
+
let tasksRaw = "";
|
|
407
|
+
const positional = [];
|
|
408
|
+
|
|
409
|
+
for (let i = 0; i < args.length; i++) {
|
|
410
|
+
const arg = args[i];
|
|
411
|
+
if (arg === "--json") {
|
|
412
|
+
json = true;
|
|
413
|
+
} else if (arg === "--force") {
|
|
414
|
+
force = true;
|
|
415
|
+
} else if (arg === "--tasks") {
|
|
416
|
+
tasksRaw = args[++i] ?? "";
|
|
417
|
+
} else {
|
|
418
|
+
positional.push(arg);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const featureId = positional[0];
|
|
423
|
+
if (!featureId) {
|
|
424
|
+
throw new Error("Usage: workspace-cleanup <feature> [--tasks T1,T2] [--force]");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const taskIds = tasksRaw
|
|
428
|
+
? tasksRaw.split(",").map((item) => item.trim()).filter(Boolean)
|
|
429
|
+
: undefined;
|
|
430
|
+
const results = await cleanupWorkspaces(process.cwd(), { featureId, taskIds, force });
|
|
431
|
+
process.stdout.write(formatWorkspaceResults(results, { json }));
|
|
432
|
+
|
|
433
|
+
if (results.some((item) => item.status === "failed")) {
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error(`❌ ${err.message}`);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
} else if (command === "workspace-list") {
|
|
441
|
+
try {
|
|
442
|
+
let json = false;
|
|
443
|
+
const positional = [];
|
|
444
|
+
|
|
445
|
+
for (const arg of args) {
|
|
446
|
+
if (arg === "--json") {
|
|
447
|
+
json = true;
|
|
448
|
+
} else {
|
|
449
|
+
positional.push(arg);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const featureId = positional[0];
|
|
454
|
+
if (!featureId) {
|
|
455
|
+
throw new Error("Usage: workspace-list <feature> [--json]");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const workspaces = await listWorkspaces(process.cwd(), featureId);
|
|
459
|
+
process.stdout.write(formatWorkspaceList(workspaces, { json, featureId }));
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.error(`❌ ${err.message}`);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
} else if (command === "execution-policy") {
|
|
465
|
+
try {
|
|
466
|
+
const sub = args[0];
|
|
467
|
+
let json = false;
|
|
468
|
+
const rest = [];
|
|
469
|
+
|
|
470
|
+
for (let i = 1; i < args.length; i++) {
|
|
471
|
+
if (args[i] === "--json") {
|
|
472
|
+
json = true;
|
|
473
|
+
} else {
|
|
474
|
+
rest.push(args[i]);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const cwd = process.cwd();
|
|
479
|
+
const policy = await loadExecutionPolicy(cwd);
|
|
480
|
+
const state = await loadPolicyState(cwd);
|
|
481
|
+
|
|
482
|
+
if (sub === "status") {
|
|
483
|
+
process.stdout.write(formatPolicyStatus(policy, state, { json }));
|
|
484
|
+
} else if (sub === "check-path") {
|
|
485
|
+
const relativePath = rest[0];
|
|
486
|
+
if (!relativePath) {
|
|
487
|
+
throw new Error("Usage: execution-policy check-path <relative-path>");
|
|
488
|
+
}
|
|
489
|
+
const result = resolvePathCheck(relativePath, policy);
|
|
490
|
+
if (json) {
|
|
491
|
+
console.log(JSON.stringify({ path: relativePath, ...result }, null, 2));
|
|
492
|
+
} else {
|
|
493
|
+
const label = result.allowed
|
|
494
|
+
? "allowed"
|
|
495
|
+
: result.severity === "warning"
|
|
496
|
+
? "blocked (warn)"
|
|
497
|
+
: "blocked";
|
|
498
|
+
console.log(`${relativePath}: ${label} (${result.reason})`);
|
|
499
|
+
}
|
|
500
|
+
if (result.exitCode !== 0) {
|
|
501
|
+
process.exit(result.exitCode);
|
|
502
|
+
}
|
|
503
|
+
} else if (sub === "record-retry") {
|
|
504
|
+
const taskId = rest[0];
|
|
505
|
+
if (!taskId) {
|
|
506
|
+
throw new Error("Usage: execution-policy record-retry <task-id>");
|
|
507
|
+
}
|
|
508
|
+
const recorded = recordTaskRetry(state, taskId, policy);
|
|
509
|
+
if (!recorded.ok) {
|
|
510
|
+
console.error(`❌ ${recorded.message}`);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
await savePolicyState(cwd, recorded.state);
|
|
514
|
+
if (json) {
|
|
515
|
+
console.log(
|
|
516
|
+
JSON.stringify({ taskId, retries: recorded.retries, state: recorded.state }, null, 2),
|
|
517
|
+
);
|
|
518
|
+
} else {
|
|
519
|
+
console.log(`Recorded retry for ${taskId}: ${recorded.retries}`);
|
|
520
|
+
}
|
|
521
|
+
} else if (sub === "record-run") {
|
|
522
|
+
const recorded = recordAgentRun(state, policy);
|
|
523
|
+
if (!recorded.ok) {
|
|
524
|
+
console.error(`❌ ${recorded.message}`);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
await savePolicyState(cwd, recorded.state);
|
|
528
|
+
if (json) {
|
|
529
|
+
console.log(
|
|
530
|
+
JSON.stringify({ agent_runs: recorded.state.agent_runs, state: recorded.state }, null, 2),
|
|
531
|
+
);
|
|
532
|
+
} else {
|
|
533
|
+
console.log(
|
|
534
|
+
`Recorded agent run: ${recorded.state.agent_runs}/${policy.budget.max_agent_runs}`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
throw new Error(
|
|
539
|
+
"Usage: execution-policy status | check-path <path> | record-retry <task> | record-run",
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
} catch (err) {
|
|
543
|
+
console.error(`❌ ${err.message}`);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
326
546
|
} else if (command === "classify-change") {
|
|
327
547
|
try {
|
|
328
548
|
let json = false;
|
|
@@ -375,7 +595,8 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
375
595
|
console.error(`❌ ${err.message}`);
|
|
376
596
|
process.exit(1);
|
|
377
597
|
}
|
|
378
|
-
} else if (AUX_COMMANDS.includes(command)) {
|
|
598
|
+
} else if (AUX_COMMANDS.includes(command)) {
|
|
599
|
+
try {
|
|
379
600
|
const code = await runGuardrailsScript(command, args);
|
|
380
601
|
process.exit(code);
|
|
381
602
|
} catch (err) {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { injectAgentsMd } from "./agents-md.js";
|
|
2
|
+
import { injectCodexAgents } from "./codex-agents.js";
|
|
3
|
+
import { injectCopilotInstructions } from "./copilot-instructions.js";
|
|
4
|
+
import { injectClaudeMd } from "./claude-md.js";
|
|
5
|
+
import { injectCursorRules } from "./cursorrules.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{
|
|
9
|
+
* id: string,
|
|
10
|
+
* label: string,
|
|
11
|
+
* skillsDir: string | null,
|
|
12
|
+
* entryFiles: string[],
|
|
13
|
+
* capabilities: {
|
|
14
|
+
* supports_hooks: boolean,
|
|
15
|
+
* supports_commands: boolean,
|
|
16
|
+
* supports_rules: boolean,
|
|
17
|
+
* supports_skills: boolean,
|
|
18
|
+
* },
|
|
19
|
+
* install: (cwd: string) => Promise<void>,
|
|
20
|
+
* }} PlatformAdapter
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @type {PlatformAdapter[]} */
|
|
24
|
+
export const ADAPTER_REGISTRY = [
|
|
25
|
+
{
|
|
26
|
+
id: "cursor",
|
|
27
|
+
label: "Cursor",
|
|
28
|
+
skillsDir: ".cursor/skills",
|
|
29
|
+
entryFiles: [".cursorrules", ".cursor/rules/engineering-baseline.mdc"],
|
|
30
|
+
capabilities: {
|
|
31
|
+
supports_hooks: true,
|
|
32
|
+
supports_commands: true,
|
|
33
|
+
supports_rules: true,
|
|
34
|
+
supports_skills: true,
|
|
35
|
+
},
|
|
36
|
+
install: injectCursorRules,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "claude",
|
|
40
|
+
label: "Claude Code",
|
|
41
|
+
skillsDir: ".claude/skills",
|
|
42
|
+
entryFiles: [".claude/CLAUDE.md"],
|
|
43
|
+
capabilities: {
|
|
44
|
+
supports_hooks: false,
|
|
45
|
+
supports_commands: true,
|
|
46
|
+
supports_rules: false,
|
|
47
|
+
supports_skills: true,
|
|
48
|
+
},
|
|
49
|
+
install: injectClaudeMd,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "copilot",
|
|
53
|
+
label: "GitHub Copilot",
|
|
54
|
+
skillsDir: ".github/skills",
|
|
55
|
+
entryFiles: [".github/copilot-instructions.md"],
|
|
56
|
+
capabilities: {
|
|
57
|
+
supports_hooks: false,
|
|
58
|
+
supports_commands: false,
|
|
59
|
+
supports_rules: false,
|
|
60
|
+
supports_skills: true,
|
|
61
|
+
},
|
|
62
|
+
install: injectCopilotInstructions,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "codex",
|
|
66
|
+
label: "OpenAI Codex",
|
|
67
|
+
skillsDir: ".codex/skills",
|
|
68
|
+
entryFiles: [".codex/AGENTS.md"],
|
|
69
|
+
capabilities: {
|
|
70
|
+
supports_hooks: false,
|
|
71
|
+
supports_commands: false,
|
|
72
|
+
supports_rules: false,
|
|
73
|
+
supports_skills: true,
|
|
74
|
+
},
|
|
75
|
+
install: injectCodexAgents,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "agents-md",
|
|
79
|
+
label: "AGENTS.md (open standard)",
|
|
80
|
+
skillsDir: null,
|
|
81
|
+
entryFiles: ["AGENTS.md"],
|
|
82
|
+
capabilities: {
|
|
83
|
+
supports_hooks: false,
|
|
84
|
+
supports_commands: false,
|
|
85
|
+
supports_rules: false,
|
|
86
|
+
supports_skills: false,
|
|
87
|
+
},
|
|
88
|
+
install: injectAgentsMd,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} id
|
|
94
|
+
* @returns {PlatformAdapter | undefined}
|
|
95
|
+
*/
|
|
96
|
+
export function getAdapter(id) {
|
|
97
|
+
return ADAPTER_REGISTRY.find((adapter) => adapter.id === id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @returns {PlatformAdapter[]} */
|
|
101
|
+
export function listAdapters() {
|
|
102
|
+
return [...ADAPTER_REGISTRY];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {keyof PlatformAdapter["capabilities"]} capability
|
|
107
|
+
* @returns {PlatformAdapter[]}
|
|
108
|
+
*/
|
|
109
|
+
export function getAdaptersWithCapability(capability) {
|
|
110
|
+
return ADAPTER_REGISTRY.filter((adapter) => adapter.capabilities[capability]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} cwd
|
|
115
|
+
* @param {PlatformAdapter} [adapter]
|
|
116
|
+
*/
|
|
117
|
+
export async function installAdapter(cwd, adapter) {
|
|
118
|
+
await adapter.install(cwd);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Install all registered platform adapters.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} cwd
|
|
125
|
+
*/
|
|
126
|
+
export async function installAllAdapters(cwd) {
|
|
127
|
+
await Promise.all(ADAPTER_REGISTRY.map((adapter) => installAdapter(cwd, adapter)));
|
|
128
|
+
}
|
package/lib/adapters.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { injectCodexAgents } from "./codex-agents.js";
|
|
3
|
-
import { injectCopilotInstructions } from "./copilot-instructions.js";
|
|
1
|
+
import { installAllAdapters } from "./adapter-registry.js";
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Install shipped platform adapter entry files (Copilot, Codex, AGENTS.md).
|
|
7
|
-
* Cursor and Claude adapters are injected
|
|
5
|
+
* Cursor and Claude adapters are injected via the same registry during install.
|
|
8
6
|
*
|
|
9
7
|
* @param {string} cwd
|
|
10
8
|
*/
|
|
11
9
|
export async function installPlatformAdapters(cwd) {
|
|
12
|
-
return
|
|
13
|
-
injectCopilotInstructions(cwd),
|
|
14
|
-
injectAgentsMd(cwd),
|
|
15
|
-
injectCodexAgents(cwd),
|
|
16
|
-
]);
|
|
10
|
+
return installAllAdapters(cwd);
|
|
17
11
|
}
|
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/constants.js
CHANGED
|
@@ -108,6 +108,8 @@ export const SCRIPT_ASSETS = [
|
|
|
108
108
|
{ file: "check_commit.py", remotePath: "scripts/check_commit.py" },
|
|
109
109
|
{ file: "lessons.py", remotePath: "scripts/lessons.py" },
|
|
110
110
|
{ file: "loop_plan.py", remotePath: "scripts/loop_plan.py" },
|
|
111
|
+
{ file: "memory_index.py", remotePath: "scripts/memory_index.py" },
|
|
112
|
+
{ file: "memory_query.py", remotePath: "scripts/memory_query.py" },
|
|
111
113
|
];
|
|
112
114
|
|
|
113
115
|
/** @type {{ file: string, remotePath: string }[]} */
|
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
|
}
|