@luizsantiago/spec-guardrails 3.2.0 → 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/index.js +77 -17
- package/lib/adapter-registry.js +128 -0
- package/lib/adapters.js +3 -9
- package/lib/constants.js +2 -0
- package/lib/execution-policy.js +84 -1
- package/lib/gates.js +2 -0
- package/lib/install.js +0 -4
- package/lib/memory-index.js +59 -0
- package/lib/workspace-isolation.js +33 -0
- package/package.json +2 -2
- package/rules/engineering-baseline.mdc +3 -1
- 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 +1 -0
- package/skills/agent-architecture.md +4 -0
- package/skills/git-handoff.md +15 -0
- package/skills/references/lessons.md +16 -10
package/index.js
CHANGED
|
@@ -9,10 +9,12 @@ 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
11
|
import {
|
|
12
|
-
checkPathScope,
|
|
13
12
|
formatPolicyStatus,
|
|
14
13
|
loadExecutionPolicy,
|
|
15
14
|
loadPolicyState,
|
|
15
|
+
recordAgentRun,
|
|
16
|
+
recordTaskRetry,
|
|
17
|
+
resolvePathCheck,
|
|
16
18
|
savePolicyState,
|
|
17
19
|
} from "./lib/execution-policy.js";
|
|
18
20
|
import { featureInit } from "./lib/feature.js";
|
|
@@ -21,7 +23,9 @@ import { GATE_COMMANDS, AUX_COMMANDS, runGate, runGuardrailsScript } from "./lib
|
|
|
21
23
|
import { install } from "./lib/install.js";
|
|
22
24
|
import {
|
|
23
25
|
cleanupWorkspaces,
|
|
26
|
+
formatWorkspaceList,
|
|
24
27
|
formatWorkspaceResults,
|
|
28
|
+
listWorkspaces,
|
|
25
29
|
prepareWorkspaces,
|
|
26
30
|
} from "./lib/workspace-isolation.js";
|
|
27
31
|
import {
|
|
@@ -70,13 +74,20 @@ Commands:
|
|
|
70
74
|
[--json] Machine-readable output
|
|
71
75
|
workspace-cleanup <feature> Remove isolated worktrees for a feature
|
|
72
76
|
[--tasks T1,T2] Limit cleanup to specific tasks
|
|
73
|
-
[--force] Force-remove dirty worktrees
|
|
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
|
|
74
80
|
[--json] Machine-readable output
|
|
75
81
|
execution-policy status Show configured budgets, scope, and runtime counters
|
|
76
82
|
[--json] Machine-readable output
|
|
77
83
|
execution-policy check-path <path> Check whether a relative path is allowed by scope policy
|
|
78
84
|
[--json] Machine-readable output
|
|
79
|
-
execution-policy record-retry <task> Increment retry counter for a task id
|
|
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
|
|
80
91
|
validate-spec [spec.md|feature] Closure gate for a feature spec
|
|
81
92
|
analyze-artifacts [feature] Cross-artifact consistency before task approval
|
|
82
93
|
validate-tasks [tasks.md|feature] Granularity gate for a task breakdown
|
|
@@ -86,7 +97,7 @@ Commands:
|
|
|
86
97
|
validate-quick [quick-folder] Quick-mode TASK.md / SUMMARY.md structural gate
|
|
87
98
|
validate-state [feature] Completion gate before declaring a feature done
|
|
88
99
|
check-commit --message "<msg>" Conventional Commits gate
|
|
89
|
-
lessons <add|list|penalize|prune|status> Lessons engine
|
|
100
|
+
lessons <add|list|penalize|prune|promote|graduate|status> Lessons engine
|
|
90
101
|
--help Show this message
|
|
91
102
|
--version Print the package version
|
|
92
103
|
`;
|
|
@@ -426,6 +437,30 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
426
437
|
console.error(`❌ ${err.message}`);
|
|
427
438
|
process.exit(1);
|
|
428
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
|
+
}
|
|
429
464
|
} else if (command === "execution-policy") {
|
|
430
465
|
try {
|
|
431
466
|
const sub = args[0];
|
|
@@ -451,33 +486,57 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
451
486
|
if (!relativePath) {
|
|
452
487
|
throw new Error("Usage: execution-policy check-path <relative-path>");
|
|
453
488
|
}
|
|
454
|
-
const result =
|
|
489
|
+
const result = resolvePathCheck(relativePath, policy);
|
|
455
490
|
if (json) {
|
|
456
491
|
console.log(JSON.stringify({ path: relativePath, ...result }, null, 2));
|
|
457
492
|
} else {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
493
|
+
const label = result.allowed
|
|
494
|
+
? "allowed"
|
|
495
|
+
: result.severity === "warning"
|
|
496
|
+
? "blocked (warn)"
|
|
497
|
+
: "blocked";
|
|
498
|
+
console.log(`${relativePath}: ${label} (${result.reason})`);
|
|
461
499
|
}
|
|
462
|
-
if (
|
|
463
|
-
process.exit(
|
|
500
|
+
if (result.exitCode !== 0) {
|
|
501
|
+
process.exit(result.exitCode);
|
|
464
502
|
}
|
|
465
503
|
} else if (sub === "record-retry") {
|
|
466
504
|
const taskId = rest[0];
|
|
467
505
|
if (!taskId) {
|
|
468
506
|
throw new Error("Usage: execution-policy record-retry <task-id>");
|
|
469
507
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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);
|
|
473
514
|
if (json) {
|
|
474
|
-
console.log(
|
|
515
|
+
console.log(
|
|
516
|
+
JSON.stringify({ taskId, retries: recorded.retries, state: recorded.state }, null, 2),
|
|
517
|
+
);
|
|
475
518
|
} else {
|
|
476
|
-
console.log(`Recorded retry for ${taskId}: ${
|
|
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
|
+
);
|
|
477
536
|
}
|
|
478
537
|
} else {
|
|
479
538
|
throw new Error(
|
|
480
|
-
"Usage: execution-policy status | check-path <path> | record-retry <task>",
|
|
539
|
+
"Usage: execution-policy status | check-path <path> | record-retry <task> | record-run",
|
|
481
540
|
);
|
|
482
541
|
}
|
|
483
542
|
} catch (err) {
|
|
@@ -536,7 +595,8 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
536
595
|
console.error(`❌ ${err.message}`);
|
|
537
596
|
process.exit(1);
|
|
538
597
|
}
|
|
539
|
-
} else if (AUX_COMMANDS.includes(command)) {
|
|
598
|
+
} else if (AUX_COMMANDS.includes(command)) {
|
|
599
|
+
try {
|
|
540
600
|
const code = await runGuardrailsScript(command, args);
|
|
541
601
|
process.exit(code);
|
|
542
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/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/execution-policy.js
CHANGED
|
@@ -235,13 +235,32 @@ export function checkBudget(state, policy) {
|
|
|
235
235
|
return { ok: issues.length === 0, issues };
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* @param {string} relativePath
|
|
240
|
+
* @param {ExecutionPolicy} policy
|
|
241
|
+
* @returns {{ allowed: boolean, exitCode: number, severity: "blocking" | "warning" | "info", reason: string }}
|
|
242
|
+
*/
|
|
243
|
+
export function resolvePathCheck(relativePath, policy) {
|
|
244
|
+
const result = checkPathScope(relativePath, policy);
|
|
245
|
+
if (result.allowed) {
|
|
246
|
+
return { ...result, exitCode: 0 };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const mode = policy.escalation.on_policy_violation ?? "block";
|
|
250
|
+
if (mode === "warn") {
|
|
251
|
+
return { ...result, severity: "warning", exitCode: 0 };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { ...result, exitCode: 1 };
|
|
255
|
+
}
|
|
256
|
+
|
|
238
257
|
/**
|
|
239
258
|
* @param {string} taskId
|
|
240
259
|
* @param {ExecutionPolicyState} state
|
|
241
260
|
* @param {ExecutionPolicy} policy
|
|
242
261
|
* @returns {{ ok: boolean, severity?: "blocking", message?: string, retries: number }}
|
|
243
262
|
*/
|
|
244
|
-
export function
|
|
263
|
+
export function previewTaskRetry(taskId, state, policy) {
|
|
245
264
|
const retries = state.retries[taskId] ?? 0;
|
|
246
265
|
if (retries >= policy.budget.max_retries_per_task) {
|
|
247
266
|
return {
|
|
@@ -254,6 +273,70 @@ export function checkTaskRetries(taskId, state, policy) {
|
|
|
254
273
|
return { ok: true, retries };
|
|
255
274
|
}
|
|
256
275
|
|
|
276
|
+
/**
|
|
277
|
+
* @param {string} taskId
|
|
278
|
+
* @param {ExecutionPolicyState} state
|
|
279
|
+
* @param {ExecutionPolicy} policy
|
|
280
|
+
* @returns {{ ok: boolean, severity?: "blocking", message?: string, retries: number }}
|
|
281
|
+
*/
|
|
282
|
+
export function checkTaskRetries(taskId, state, policy) {
|
|
283
|
+
const preview = previewTaskRetry(taskId, state, policy);
|
|
284
|
+
return preview.ok
|
|
285
|
+
? { ok: true, retries: preview.retries }
|
|
286
|
+
: {
|
|
287
|
+
ok: false,
|
|
288
|
+
severity: preview.severity,
|
|
289
|
+
message: preview.message,
|
|
290
|
+
retries: preview.retries,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @param {ExecutionPolicyState} state
|
|
296
|
+
* @param {ExecutionPolicy} policy
|
|
297
|
+
* @returns {{ ok: boolean, message?: string }}
|
|
298
|
+
*/
|
|
299
|
+
export function previewAgentRun(state, policy) {
|
|
300
|
+
if (state.agent_runs >= policy.budget.max_agent_runs) {
|
|
301
|
+
return { ok: false, message: "max_agent_runs budget exhausted" };
|
|
302
|
+
}
|
|
303
|
+
return { ok: true };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param {ExecutionPolicyState} state
|
|
308
|
+
* @param {string} taskId
|
|
309
|
+
* @param {ExecutionPolicy} policy
|
|
310
|
+
* @returns {{ ok: boolean, state: ExecutionPolicyState, retries: number, message?: string }}
|
|
311
|
+
*/
|
|
312
|
+
export function recordTaskRetry(state, taskId, policy) {
|
|
313
|
+
const preview = previewTaskRetry(taskId, state, policy);
|
|
314
|
+
if (!preview.ok) {
|
|
315
|
+
return { ok: false, state, retries: preview.retries, message: preview.message };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const next = structuredClone(state);
|
|
319
|
+
next.retries[taskId] = (next.retries[taskId] ?? 0) + 1;
|
|
320
|
+
next.iterations += 1;
|
|
321
|
+
return { ok: true, state: next, retries: next.retries[taskId] };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {ExecutionPolicyState} state
|
|
326
|
+
* @param {ExecutionPolicy} policy
|
|
327
|
+
* @returns {{ ok: boolean, state: ExecutionPolicyState, message?: string }}
|
|
328
|
+
*/
|
|
329
|
+
export function recordAgentRun(state, policy) {
|
|
330
|
+
const preview = previewAgentRun(state, policy);
|
|
331
|
+
if (!preview.ok) {
|
|
332
|
+
return { ok: false, state, message: preview.message };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const next = structuredClone(state);
|
|
336
|
+
next.agent_runs += 1;
|
|
337
|
+
return { ok: true, state: next };
|
|
338
|
+
}
|
|
339
|
+
|
|
257
340
|
/**
|
|
258
341
|
* @param {ExecutionPolicy} policy
|
|
259
342
|
* @param {ExecutionPolicyState} state
|
package/lib/gates.js
CHANGED
package/lib/install.js
CHANGED
|
@@ -13,8 +13,6 @@ import {
|
|
|
13
13
|
resolveAssetOverride,
|
|
14
14
|
} from "./constants.js";
|
|
15
15
|
import { installPlatformAdapters } from "./adapters.js";
|
|
16
|
-
import { injectClaudeMd } from "./claude-md.js";
|
|
17
|
-
import { injectCursorRules } from "./cursorrules.js";
|
|
18
16
|
import { ensureDir, readFileSafe, writeFileIfMissing } from "./fs-utils.js";
|
|
19
17
|
import { hasPython } from "./gates.js";
|
|
20
18
|
import { initGuardrailsMemory } from "./memory.js";
|
|
@@ -117,8 +115,6 @@ export async function install(options = {}) {
|
|
|
117
115
|
}
|
|
118
116
|
|
|
119
117
|
log("🔗 Installing platform adapters...");
|
|
120
|
-
await injectCursorRules(cwd);
|
|
121
|
-
await injectClaudeMd(cwd);
|
|
122
118
|
await installPlatformAdapters(cwd);
|
|
123
119
|
log("✅ Adapters → .cursorrules, CLAUDE.md, copilot-instructions.md, AGENTS.md, .codex/AGENTS.md");
|
|
124
120
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { GUARDRAILS_SCRIPTS_DIR } from "./constants.js";
|
|
4
|
+
import { runGuardrailsScript } from "./gates.js";
|
|
5
|
+
|
|
6
|
+
export const MEMORY_DB_PATH = ".specs/memory/memory.db";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Rebuild the SQLite memory index from `.specs/` markdown artifacts.
|
|
10
|
+
*
|
|
11
|
+
* @param {{ cwd?: string }} [options]
|
|
12
|
+
* @returns {Promise<number>} exit code
|
|
13
|
+
*/
|
|
14
|
+
export async function rebuildMemoryIndex(options = {}) {
|
|
15
|
+
const cwd = options.cwd ?? process.cwd();
|
|
16
|
+
return runGuardrailsScript("memory-index", ["rebuild"], { cwd });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Query the knowledge graph for a bounded context package.
|
|
21
|
+
*
|
|
22
|
+
* @param {{ from: string, depth?: number, json?: boolean, cwd?: string }} options
|
|
23
|
+
* @returns {Promise<number>} exit code
|
|
24
|
+
*/
|
|
25
|
+
export async function queryMemory(options) {
|
|
26
|
+
const cwd = options.cwd ?? process.cwd();
|
|
27
|
+
const args = ["--from", options.from];
|
|
28
|
+
if (options.depth !== undefined) {
|
|
29
|
+
args.push("--depth", String(options.depth));
|
|
30
|
+
}
|
|
31
|
+
if (options.json) {
|
|
32
|
+
args.push("--json");
|
|
33
|
+
}
|
|
34
|
+
return runGuardrailsScript("memory-query", args, { cwd });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} [cwd]
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function memoryDbPath(cwd = process.cwd()) {
|
|
42
|
+
return path.join(cwd, MEMORY_DB_PATH);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function memoryDbRelativePath() {
|
|
49
|
+
return MEMORY_DB_PATH;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Relative scripts directory for memory tooling diagnostics.
|
|
54
|
+
*
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function memoryScriptsDir() {
|
|
58
|
+
return GUARDRAILS_SCRIPTS_DIR;
|
|
59
|
+
}
|
|
@@ -137,6 +137,39 @@ export async function cleanupWorkspaces(cwd, { featureId, taskIds, force = false
|
|
|
137
137
|
return results;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} cwd
|
|
142
|
+
* @param {string} featureId
|
|
143
|
+
* @returns {Promise<Array<{ taskId: string, path: string }>>}
|
|
144
|
+
*/
|
|
145
|
+
export async function listWorkspaces(cwd, featureId) {
|
|
146
|
+
const paths = await listWorkspacePaths(cwd, featureId);
|
|
147
|
+
return paths.map((wtPath) => ({
|
|
148
|
+
taskId: path.basename(wtPath),
|
|
149
|
+
path: wtPath,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {Awaited<ReturnType<typeof listWorkspaces>>} workspaces
|
|
155
|
+
* @param {{ json?: boolean, featureId?: string }} [options]
|
|
156
|
+
*/
|
|
157
|
+
export function formatWorkspaceList(workspaces, options = {}) {
|
|
158
|
+
if (options.json) {
|
|
159
|
+
return JSON.stringify({ feature: options.featureId, workspaces }, null, 2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const lines = ["Workspace list:"];
|
|
163
|
+
if (!workspaces.length) {
|
|
164
|
+
lines.push(" (none)");
|
|
165
|
+
} else {
|
|
166
|
+
for (const item of workspaces) {
|
|
167
|
+
lines.push(` ${item.taskId}: ${item.path}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return `${lines.join("\n")}\n`;
|
|
171
|
+
}
|
|
172
|
+
|
|
140
173
|
/**
|
|
141
174
|
* @param {ReturnType<typeof prepareWorkspaces> extends Promise<infer T> ? T : never} results
|
|
142
175
|
* @param {{ json?: boolean }} [options]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luizsantiago/spec-guardrails",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.1",
|
|
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 test/test_validation_verdict.test.js test/test_execution_policy.test.js test/test_workspace_isolation.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_validation_verdict.test.js test/test_execution_policy.test.js test/test_workspace_isolation.test.js test/test_adapter_registry.test.js",
|
|
16
16
|
"test:gates": "node test/run-gate-tests.mjs",
|
|
17
17
|
"prepublishOnly": "npm test"
|
|
18
18
|
},
|
|
@@ -47,8 +47,10 @@ Gates live in `.specs/guardrails/scripts/` (Python 3). **The agent runs them** a
|
|
|
47
47
|
| `feature-init "…"` | Optional — or ask the agent to `/specify` |
|
|
48
48
|
| `project-init` | Optional — brownfield repo with existing code |
|
|
49
49
|
| `doctor` | Install looks broken |
|
|
50
|
+
| `workspace-list <feature>` | Inspect parallel worktrees before cleanup |
|
|
51
|
+
| `execution-policy status` | Check budgets and scope before expanding Execute |
|
|
50
52
|
|
|
51
|
-
Everything else (`validate-spec`, `validate-tasks`, `check-commit`, …) is normally run **by the agent**, not memorized from this file. Full list: `npx @luizsantiago/spec-guardrails --help` or `.specs/GETTING_STARTED.md`.
|
|
53
|
+
Everything else (`validate-spec`, `validate-tasks`, `check-commit`, `workspace-prepare`, `execution-policy check-path`, …) is normally run **by the agent**, not memorized from this file. Full list: `npx @luizsantiago/spec-guardrails --help` or `.specs/GETTING_STARTED.md`.
|
|
52
54
|
|
|
53
55
|
Non-zero gate exit = STOP. Without Python, the agent performs the same checks manually.
|
|
54
56
|
|
|
@@ -46,6 +46,15 @@ STATE_FEATURE = re.compile(
|
|
|
46
46
|
r"^\s*-\s*Feature:\s*(.+)$",
|
|
47
47
|
re.IGNORECASE | re.MULTILINE,
|
|
48
48
|
)
|
|
49
|
+
TASK_HEADING = re.compile(
|
|
50
|
+
r"^#{2,6}\s*(?P<id>T\d{1,6})\s*[:\-–]?\s*(?P<title>.*)$",
|
|
51
|
+
re.MULTILINE | re.IGNORECASE,
|
|
52
|
+
)
|
|
53
|
+
VERIFY_HINT = re.compile(
|
|
54
|
+
r"\b(test strategy|verification|validate\.md|/verify|acceptance test)\b",
|
|
55
|
+
re.IGNORECASE,
|
|
56
|
+
)
|
|
57
|
+
MEDIUM_TASK_FLOOR = 5
|
|
49
58
|
|
|
50
59
|
|
|
51
60
|
def git_branch(root: Path) -> str | None:
|
|
@@ -121,6 +130,20 @@ def build_report(feature_dir: Path, root: Path) -> Report:
|
|
|
121
130
|
else:
|
|
122
131
|
report.warn("tasks.md missing or empty — task coverage checks skipped")
|
|
123
132
|
|
|
133
|
+
task_count = len(TASK_HEADING.findall(tasks_text or ""))
|
|
134
|
+
medium_plus = task_count >= MEDIUM_TASK_FLOOR
|
|
135
|
+
|
|
136
|
+
if medium_plus and design_text is None:
|
|
137
|
+
report.warn(
|
|
138
|
+
f"{task_count} tasks — design.md recommended for Medium+ features"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
plan_text = "\n".join(filter(None, [spec_text, design_text]))
|
|
142
|
+
if medium_plus and plan_text and not VERIFY_HINT.search(plan_text):
|
|
143
|
+
report.warn(
|
|
144
|
+
"no verification or test strategy mention in spec/design — add verify plan before Execute"
|
|
145
|
+
)
|
|
146
|
+
|
|
124
147
|
if design_text is not None and tasks_text:
|
|
125
148
|
visible_design = visible_markdown(design_text).strip()
|
|
126
149
|
if len(visible_design.splitlines()) < 5:
|