@luizsantiago/spec-guardrails 3.4.0 → 3.6.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 +3 -1
- package/index.js +166 -1
- package/lib/constants.js +4 -0
- package/lib/gates.js +1 -0
- package/lib/memory-index.js +36 -0
- package/lib/solution-exploration.js +528 -0
- package/package.json +2 -2
- package/scripts/_memory_config.py +97 -0
- package/scripts/_memory_embed.py +116 -0
- package/scripts/memory_index.py +261 -12
- package/scripts/memory_retrieve.py +289 -0
- package/scripts/memory_search.py +60 -12
- package/skills/agent-architecture.md +6 -1
- package/skills/references/design.md +1 -0
- package/skills/references/memory.md +32 -0
- package/skills/references/solution-exploration.md +86 -0
- package/templates/config.yaml.example +11 -0
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.6.x**
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -217,6 +217,8 @@ npx @luizsantiago/spec-guardrails install
|
|
|
217
217
|
|
|
218
218
|
| Version | What you gain |
|
|
219
219
|
| --- | --- |
|
|
220
|
+
| **3.6.x** | Hybrid retrieval (`memory-retrieve`); chunk FTS; optional semantic embed |
|
|
221
|
+
| **3.5.x** | Solution exploration (`solution-explore`) — compare candidates from approved spec |
|
|
220
222
|
| **3.4.x** | Contextual guards (`context-guard`); FTS memory search (`memory-search`) |
|
|
221
223
|
| **3.3.x** | Intent/effect policy — `check-path --op read|write|delete` and `effects` config block |
|
|
222
224
|
| **3.2.x** | Single-package focus; artifact gate severity labels; git worktree isolation CLI; execution policy (budget/scope/escalation) |
|
package/index.js
CHANGED
|
@@ -35,6 +35,16 @@ import {
|
|
|
35
35
|
listWorkspaces,
|
|
36
36
|
prepareWorkspaces,
|
|
37
37
|
} from "./lib/workspace-isolation.js";
|
|
38
|
+
import {
|
|
39
|
+
formatExplorationInit,
|
|
40
|
+
formatExplorationStatus,
|
|
41
|
+
formatExplorationValidation,
|
|
42
|
+
getExplorationStatus,
|
|
43
|
+
initExploration,
|
|
44
|
+
normalizeCandidateId,
|
|
45
|
+
recordExplorationDecision,
|
|
46
|
+
validateExplorationArtifact,
|
|
47
|
+
} from "./lib/solution-exploration.js";
|
|
38
48
|
import {
|
|
39
49
|
initProjectConfig,
|
|
40
50
|
listPresets,
|
|
@@ -93,12 +103,16 @@ Commands:
|
|
|
93
103
|
execution-policy record-retry <task> Increment retry counter for a task id (blocks at limit)
|
|
94
104
|
execution-policy record-run Increment agent-run counter (blocks at budget)
|
|
95
105
|
memory-index rebuild Rebuild SQLite memory index from .specs/ artifacts
|
|
106
|
+
memory-index embed [--force] Optional semantic embeddings (requires config + provider)
|
|
96
107
|
memory-query --from <id> Bounded context package from the knowledge graph
|
|
97
108
|
[--depth N] Traversal depth (default 2)
|
|
98
109
|
[--json] Machine-readable output
|
|
99
|
-
memory-search <query> Full-text search over
|
|
110
|
+
memory-search <query> Full-text search over indexed artifact chunks (FTS5)
|
|
100
111
|
[--limit N] Max results (default 10)
|
|
101
112
|
[--json] Machine-readable output
|
|
113
|
+
memory-retrieve "<query>" Hybrid retrieval (FTS + graph + optional semantic)
|
|
114
|
+
[--mode fts|hybrid|semantic] Strategy (default: hybrid)
|
|
115
|
+
[--json] Machine-readable output
|
|
102
116
|
context-guard status Execute readiness from STATE + tasks.md
|
|
103
117
|
[--json] Machine-readable output
|
|
104
118
|
context-guard check-edit <path> Contextual guard before editing a file
|
|
@@ -108,6 +122,22 @@ Commands:
|
|
|
108
122
|
context-guard check-complete Contextual guard before claiming feature done
|
|
109
123
|
[feature] Feature id (default: active in STATE)
|
|
110
124
|
[--json] Machine-readable output
|
|
125
|
+
solution-explore init <feature> Start solution exploration from approved spec
|
|
126
|
+
--candidates A,B[,C] Candidate ids (required, at least two)
|
|
127
|
+
[--labels "a,b,c"] Optional labels aligned to candidate ids
|
|
128
|
+
[--base-ref HEAD] Base ref for candidate worktrees
|
|
129
|
+
[--force] Replace existing exploration.md
|
|
130
|
+
[--json] Machine-readable output
|
|
131
|
+
solution-explore status [feature] Show exploration candidates and decision state
|
|
132
|
+
[--json] Machine-readable output
|
|
133
|
+
solution-explore validate [feature] Gate: comparison matrix complete before select
|
|
134
|
+
[--json] Machine-readable output
|
|
135
|
+
solution-explore select <feature> Record exploration decision
|
|
136
|
+
--candidate A Selected candidate id (required)
|
|
137
|
+
[--merge B] Optional secondary candidate to merge from
|
|
138
|
+
--rationale "…" Why this candidate won (required)
|
|
139
|
+
[--cleanup] Remove non-selected candidate worktrees
|
|
140
|
+
[--json] Machine-readable output
|
|
111
141
|
validate-spec [spec.md|feature] Closure gate for a feature spec
|
|
112
142
|
analyze-artifacts [feature] Cross-artifact consistency before task approval
|
|
113
143
|
validate-tasks [tasks.md|feature] Granularity gate for a task breakdown
|
|
@@ -661,6 +691,141 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
661
691
|
console.error(`❌ ${err.message}`);
|
|
662
692
|
process.exit(1);
|
|
663
693
|
}
|
|
694
|
+
} else if (command === "solution-explore") {
|
|
695
|
+
try {
|
|
696
|
+
const sub = args[0];
|
|
697
|
+
let json = false;
|
|
698
|
+
const rest = [];
|
|
699
|
+
|
|
700
|
+
for (let i = 1; i < args.length; i++) {
|
|
701
|
+
if (args[i] === "--json") {
|
|
702
|
+
json = true;
|
|
703
|
+
} else {
|
|
704
|
+
rest.push(args[i]);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const cwd = process.cwd();
|
|
709
|
+
|
|
710
|
+
if (sub === "init") {
|
|
711
|
+
let candidatesRaw = "";
|
|
712
|
+
let labelsRaw = "";
|
|
713
|
+
let baseRef = "HEAD";
|
|
714
|
+
let force = false;
|
|
715
|
+
const positional = [];
|
|
716
|
+
|
|
717
|
+
for (let i = 0; i < rest.length; i++) {
|
|
718
|
+
const arg = rest[i];
|
|
719
|
+
if (arg === "--candidates") {
|
|
720
|
+
candidatesRaw = rest[++i] ?? "";
|
|
721
|
+
} else if (arg === "--labels") {
|
|
722
|
+
labelsRaw = rest[++i] ?? "";
|
|
723
|
+
} else if (arg === "--base-ref") {
|
|
724
|
+
baseRef = rest[++i];
|
|
725
|
+
if (!baseRef) {
|
|
726
|
+
throw new Error("--base-ref requires a git ref");
|
|
727
|
+
}
|
|
728
|
+
} else if (arg === "--force") {
|
|
729
|
+
force = true;
|
|
730
|
+
} else {
|
|
731
|
+
positional.push(arg);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const featureId = positional[0];
|
|
736
|
+
if (!featureId || !candidatesRaw) {
|
|
737
|
+
throw new Error(
|
|
738
|
+
"Usage: solution-explore init <feature> --candidates A,B [--labels \"a,b\"] [--base-ref HEAD] [--force]",
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const ids = candidatesRaw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
743
|
+
const labels = labelsRaw
|
|
744
|
+
? labelsRaw.split(",").map((item) => item.trim())
|
|
745
|
+
: [];
|
|
746
|
+
const candidateSpecs = ids.map((id, index) => ({
|
|
747
|
+
id: normalizeCandidateId(id),
|
|
748
|
+
label: labels[index] || undefined,
|
|
749
|
+
}));
|
|
750
|
+
|
|
751
|
+
const result = await initExploration(cwd, featureId, candidateSpecs, { baseRef, force });
|
|
752
|
+
process.stdout.write(formatExplorationInit(result, { json }));
|
|
753
|
+
|
|
754
|
+
if (result.workspaces.some((item) => item.status === "failed")) {
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
} else if (sub === "status") {
|
|
758
|
+
const featureId = rest[0];
|
|
759
|
+
const status = await getExplorationStatus(cwd, featureId);
|
|
760
|
+
process.stdout.write(formatExplorationStatus(status, { json }));
|
|
761
|
+
} else if (sub === "validate") {
|
|
762
|
+
const featureId = rest[0];
|
|
763
|
+
if (!featureId) {
|
|
764
|
+
throw new Error("Usage: solution-explore validate <feature>");
|
|
765
|
+
}
|
|
766
|
+
const result = await validateExplorationArtifact(cwd, featureId);
|
|
767
|
+
process.stdout.write(formatExplorationValidation(result, { json }));
|
|
768
|
+
if (!result.ok) {
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
} else if (sub === "select") {
|
|
772
|
+
let selected = "";
|
|
773
|
+
let mergedFrom = "";
|
|
774
|
+
let rationale = "";
|
|
775
|
+
let cleanup = false;
|
|
776
|
+
const positional = [];
|
|
777
|
+
|
|
778
|
+
for (let i = 0; i < rest.length; i++) {
|
|
779
|
+
const arg = rest[i];
|
|
780
|
+
if (arg === "--candidate") {
|
|
781
|
+
selected = rest[++i] ?? "";
|
|
782
|
+
} else if (arg === "--merge") {
|
|
783
|
+
mergedFrom = rest[++i] ?? "";
|
|
784
|
+
} else if (arg === "--rationale") {
|
|
785
|
+
rationale = rest[++i] ?? "";
|
|
786
|
+
} else if (arg === "--cleanup") {
|
|
787
|
+
cleanup = true;
|
|
788
|
+
} else {
|
|
789
|
+
positional.push(arg);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const featureId = positional[0];
|
|
794
|
+
if (!featureId || !selected || !rationale) {
|
|
795
|
+
throw new Error(
|
|
796
|
+
'Usage: solution-explore select <feature> --candidate A --rationale "…" [--merge B] [--cleanup]',
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const result = await recordExplorationDecision(cwd, featureId, {
|
|
801
|
+
selected,
|
|
802
|
+
mergedFrom: mergedFrom || null,
|
|
803
|
+
rationale,
|
|
804
|
+
cleanup,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
if (json) {
|
|
808
|
+
console.log(JSON.stringify(result, null, 2));
|
|
809
|
+
} else {
|
|
810
|
+
console.log(`Recorded decision for ${result.featureId}: ${result.selected}`);
|
|
811
|
+
if (result.mergedFrom) {
|
|
812
|
+
console.log(` Merged from: ${result.mergedFrom}`);
|
|
813
|
+
}
|
|
814
|
+
if (result.cleanup?.length) {
|
|
815
|
+
for (const item of result.cleanup) {
|
|
816
|
+
console.log(` Cleanup ${path.basename(item.path)}: ${item.status}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} else {
|
|
821
|
+
throw new Error(
|
|
822
|
+
"Usage: solution-explore init | status | validate | select",
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
} catch (err) {
|
|
826
|
+
console.error(`❌ ${err.message}`);
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
664
829
|
} else if (command === "classify-change") {
|
|
665
830
|
try {
|
|
666
831
|
let json = false;
|
package/lib/constants.js
CHANGED
|
@@ -77,6 +77,7 @@ export const SKILL_ASSETS = [
|
|
|
77
77
|
/** @type {{ file: string, remotePath: string }[]} */
|
|
78
78
|
export const REFERENCE_ASSETS = [
|
|
79
79
|
{ file: "explore.md", remotePath: "skills/references/explore.md" },
|
|
80
|
+
{ file: "solution-exploration.md", remotePath: "skills/references/solution-exploration.md" },
|
|
80
81
|
{ file: "project-init.md", remotePath: "skills/references/project-init.md" },
|
|
81
82
|
{ file: "constitution.md", remotePath: "skills/references/constitution.md" },
|
|
82
83
|
{ file: "specify.md", remotePath: "skills/references/specify.md" },
|
|
@@ -111,6 +112,9 @@ export const SCRIPT_ASSETS = [
|
|
|
111
112
|
{ file: "memory_index.py", remotePath: "scripts/memory_index.py" },
|
|
112
113
|
{ file: "memory_query.py", remotePath: "scripts/memory_query.py" },
|
|
113
114
|
{ file: "memory_search.py", remotePath: "scripts/memory_search.py" },
|
|
115
|
+
{ file: "memory_retrieve.py", remotePath: "scripts/memory_retrieve.py" },
|
|
116
|
+
{ file: "_memory_config.py", remotePath: "scripts/_memory_config.py" },
|
|
117
|
+
{ file: "_memory_embed.py", remotePath: "scripts/_memory_embed.py" },
|
|
114
118
|
];
|
|
115
119
|
|
|
116
120
|
/** @type {{ file: string, remotePath: string }[]} */
|
package/lib/gates.js
CHANGED
package/lib/memory-index.js
CHANGED
|
@@ -16,6 +16,42 @@ export async function rebuildMemoryIndex(options = {}) {
|
|
|
16
16
|
return runGuardrailsScript("memory-index", ["rebuild"], { cwd });
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Build optional semantic embeddings for indexed chunks.
|
|
21
|
+
*
|
|
22
|
+
* @param {{ force?: boolean, json?: boolean, cwd?: string }} [options]
|
|
23
|
+
* @returns {Promise<number>} exit code
|
|
24
|
+
*/
|
|
25
|
+
export async function embedMemoryIndex(options = {}) {
|
|
26
|
+
const cwd = options.cwd ?? process.cwd();
|
|
27
|
+
const args = ["embed"];
|
|
28
|
+
if (options.force) {
|
|
29
|
+
args.push("--force");
|
|
30
|
+
}
|
|
31
|
+
if (options.json) {
|
|
32
|
+
args.push("--json");
|
|
33
|
+
}
|
|
34
|
+
return runGuardrailsScript("memory-index", args, { cwd });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Hybrid retrieval over indexed artifacts.
|
|
39
|
+
*
|
|
40
|
+
* @param {{ query: string, mode?: string, json?: boolean, cwd?: string }} options
|
|
41
|
+
* @returns {Promise<number>} exit code
|
|
42
|
+
*/
|
|
43
|
+
export async function retrieveMemory(options) {
|
|
44
|
+
const cwd = options.cwd ?? process.cwd();
|
|
45
|
+
const args = [options.query];
|
|
46
|
+
if (options.mode) {
|
|
47
|
+
args.push("--mode", options.mode);
|
|
48
|
+
}
|
|
49
|
+
if (options.json) {
|
|
50
|
+
args.push("--json");
|
|
51
|
+
}
|
|
52
|
+
return runGuardrailsScript("memory-retrieve", args, { cwd });
|
|
53
|
+
}
|
|
54
|
+
|
|
19
55
|
/**
|
|
20
56
|
* Query the knowledge graph for a bounded context package.
|
|
21
57
|
*
|