@esneiderbravo/speclaw 2.0.2 → 2.0.4
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/dist/cli/commands/check.js +11 -1
- package/dist/cli/commands/cortex.js +17 -0
- package/dist/cli/commands/index-build.js +13 -2
- package/dist/cli/commands/lawbook.js +31 -10
- package/dist/cli/commands/query.js +13 -0
- package/dist/cli/index.js +4 -1
- package/dist/modules/compass/indexer.js +14 -0
- package/dist/modules/compass/query.js +209 -62
- package/dist/modules/compass/register.js +17 -1
- package/dist/modules/cortex/compass-gate.js +84 -0
- package/dist/modules/cortex/harness.js +20 -11
- package/dist/modules/cortex/types.js +14 -0
- package/dist/modules/foundation/assets/AGENTS.template.md +2 -2
- package/dist/modules/foundation/assets/CLAUDE.template.md +4 -3
- package/dist/modules/foundation/assets/docs/compass.template.md +7 -7
- package/dist/modules/foundation/assets/docs/standards/lawbook.template.md +2 -2
- package/dist/modules/foundation/check.js +23 -5
- package/dist/modules/foundation/compass-nudge.js +189 -0
- package/dist/modules/foundation/hooks.js +18 -3
- package/dist/modules/lawbook/assets/agents/archiver.md +1 -1
- package/dist/modules/lawbook/assets/agents/explorer.md +7 -4
- package/dist/modules/lawbook/assets/agents/implementer.md +1 -1
- package/dist/modules/lawbook/assets/agents/planner.md +7 -4
- package/dist/modules/lawbook/assets/agents/reviewer.md +1 -1
- package/dist/modules/lawbook/assets/agents/tester.md +1 -1
- package/dist/modules/lawbook/assets/commands/draft.md +2 -2
- package/dist/modules/lawbook/assets/commands/quick.md +2 -2
- package/dist/modules/lawbook/assets/rules/spec-reports-disciplines.md +1 -1
- package/dist/modules/lawbook/assets/rules/spec-tasks-mandatory-steps.md +4 -4
- package/dist/modules/lawbook/assets/skills/archive/steps/02-reconcile.md +1 -1
- package/dist/modules/lawbook/assets/skills/build/steps/01-load-change.md +1 -1
- package/dist/modules/lawbook/assets/skills/cortex/steps/02-dispatch-loop.md +22 -6
- package/dist/modules/lawbook/assets/skills/draft/steps/01-ensure-workspace.md +1 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +7 -4
- package/dist/modules/lawbook/assets/skills/draft/steps/03-name-capabilities.md +3 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +12 -2
- package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +15 -6
- package/dist/modules/lawbook/assets/skills/explore/steps/02-summarize.md +2 -0
- package/dist/modules/lawbook/assets/skills/investigate/steps/02-hand-off.md +1 -1
- package/dist/modules/lawbook/assets/skills/quick/steps/02-implement.md +1 -1
- package/dist/modules/lawbook/assets/skills/sync/steps/01-confirm.md +1 -1
- package/dist/modules/lawbook/assets/skills/sync/steps/02-reconcile.md +1 -1
- package/dist/modules/lawbook/bugfix.js +22 -26
- package/dist/modules/lawbook/change-tool.js +15 -0
- package/dist/modules/lawbook/engine.js +11 -0
- package/dist/modules/lawbook/levels.js +2 -0
- package/dist/modules/lawbook/quick.js +14 -23
- package/dist/modules/lawbook/register.js +1 -1
- package/dist/modules/lawbook/scaffold-change.js +238 -0
- package/dist/shared/compass-calls.js +197 -0
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compass-first evidence gate for Cortex `advance`.
|
|
3
|
+
*
|
|
4
|
+
* Leaving `exploring` or `implementing` requires (strict) or recommends (warn)
|
|
5
|
+
* at least one Compass evidence call in the shared call log since the current
|
|
6
|
+
* stage started. Cortex must not import from the lawbook module, so the
|
|
7
|
+
* `compassGate` key is read with a local regex scan of `lawbook/config.yaml`.
|
|
8
|
+
*
|
|
9
|
+
* // Covers: req~compass-evidence-gate~1
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { EVIDENCE_TOOLS, countEvidence, readCompassCalls } from "../../shared/compass-calls.js";
|
|
14
|
+
/** Mode used when the key is missing or invalid. */
|
|
15
|
+
export const DEFAULT_COMPASS_GATE = "warn";
|
|
16
|
+
/** Stages whose exit is gated. */
|
|
17
|
+
export const GATED_STAGES = new Set(["exploring", "implementing"]);
|
|
18
|
+
const GATE_KEY = /^compassGate:[ \t]*(["']?)([A-Za-z]*)\1[ \t]*(?:#.*)?$/m;
|
|
19
|
+
/**
|
|
20
|
+
* Read the top-level `compassGate` key from `lawbook/config.yaml`.
|
|
21
|
+
*
|
|
22
|
+
* Quotes are optional and a trailing comment is allowed. A missing file or
|
|
23
|
+
* key, or any value other than `off`/`warn`/`strict`, yields `warn`.
|
|
24
|
+
*
|
|
25
|
+
* @param projectPath - Project root.
|
|
26
|
+
*/
|
|
27
|
+
export function readCompassGateMode(projectPath) {
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = fs.readFileSync(path.join(projectPath, "lawbook", "config.yaml"), "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return DEFAULT_COMPASS_GATE;
|
|
34
|
+
}
|
|
35
|
+
const value = GATE_KEY.exec(raw)?.[2];
|
|
36
|
+
return value === "off" || value === "warn" || value === "strict" ? value : DEFAULT_COMPASS_GATE;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* When the current stage started: the `at` of the newest history entry whose
|
|
40
|
+
* `to` is the current stage. `start` enters `exploring` and `rework` enters
|
|
41
|
+
* `implementing`, so a rework restarts the window. Null when none matches.
|
|
42
|
+
*
|
|
43
|
+
* @param state - Harness state.
|
|
44
|
+
*/
|
|
45
|
+
export function stageStartedAt(state) {
|
|
46
|
+
for (let i = state.history.length - 1; i >= 0; i--) {
|
|
47
|
+
const entry = state.history[i];
|
|
48
|
+
if (entry.to === state.stage)
|
|
49
|
+
return entry.at;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Count Compass evidence calls since the current stage started.
|
|
55
|
+
*
|
|
56
|
+
* Reads only the bounded call-log tail. With no matching history entry every
|
|
57
|
+
* log entry is eligible.
|
|
58
|
+
*
|
|
59
|
+
* @param projectPath - Project root.
|
|
60
|
+
* @param state - Harness state before the advance.
|
|
61
|
+
* @param mode - Gate mode; defaults to {@link readCompassGateMode}.
|
|
62
|
+
*/
|
|
63
|
+
export function evaluateCompassGate(projectPath, state, mode = readCompassGateMode(projectPath)) {
|
|
64
|
+
const since = stageStartedAt(state);
|
|
65
|
+
const sinceMs = since === null ? Number.NaN : Date.parse(since);
|
|
66
|
+
const calls = countEvidence(readCompassCalls(projectPath, Number.isNaN(sinceMs) ? {} : { sinceMs }));
|
|
67
|
+
return { mode, stage: state.stage, since, calls, satisfied: calls > 0 };
|
|
68
|
+
}
|
|
69
|
+
function evidenceToolList() {
|
|
70
|
+
return [...EVIDENCE_TOOLS].join(", ");
|
|
71
|
+
}
|
|
72
|
+
/** Error text for a strict rejection. */
|
|
73
|
+
export function compassGateError(result) {
|
|
74
|
+
return (`compass-first: cannot leave stage "${result.stage}" — no Compass evidence call ` +
|
|
75
|
+
`(${evidenceToolList()}) since ${result.since ?? "the stage started"}. ` +
|
|
76
|
+
`Locate and read the code with compass_find / compass_explore first, ` +
|
|
77
|
+
`or relax compassGate (off|warn) in lawbook/config.yaml.`);
|
|
78
|
+
}
|
|
79
|
+
/** Warning text for a warn-mode advance without evidence. */
|
|
80
|
+
export function compassGateWarning(result) {
|
|
81
|
+
return (`compass-first: left stage "${result.stage}" with no Compass evidence call ` +
|
|
82
|
+
`(${evidenceToolList()}) since ${result.since ?? "the stage started"}. ` +
|
|
83
|
+
`Set compassGate: strict in lawbook/config.yaml to enforce.`);
|
|
84
|
+
}
|
|
@@ -10,16 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"implementing",
|
|
18
|
-
"reviewing",
|
|
19
|
-
"testing",
|
|
20
|
-
"archiving",
|
|
21
|
-
"done",
|
|
22
|
-
];
|
|
13
|
+
import { GATED_STAGES, compassGateError, compassGateWarning, evaluateCompassGate, readCompassGateMode, } from "./compass-gate.js";
|
|
14
|
+
// Harness types live in the leaf `types.ts` so `compass-gate.ts` can use them
|
|
15
|
+
// without importing this file back (no file-level import cycle).
|
|
16
|
+
export { HARNESS_STAGES, } from "./types.js";
|
|
23
17
|
export const harnessOps = ["status", "start", "advance", "rework"];
|
|
24
18
|
const DEFAULT_MAX_REWORK = 3;
|
|
25
19
|
function specRoot(projectPath) {
|
|
@@ -186,6 +180,19 @@ export function handleHarness(args) {
|
|
|
186
180
|
return next;
|
|
187
181
|
}
|
|
188
182
|
const to = allowedAdvance(current.stage, current.level, args.verdict ?? undefined);
|
|
183
|
+
// Compass-first gate: runs before any write, so a strict rejection leaves
|
|
184
|
+
// harness.json byte-identical.
|
|
185
|
+
let compassEvidence;
|
|
186
|
+
const warnings = [];
|
|
187
|
+
const mode = GATED_STAGES.has(current.stage) ? readCompassGateMode(projectPath) : "off";
|
|
188
|
+
if (mode !== "off") {
|
|
189
|
+
const { satisfied, ...evidence } = evaluateCompassGate(projectPath, current, mode);
|
|
190
|
+
compassEvidence = evidence;
|
|
191
|
+
if (!satisfied && mode === "strict")
|
|
192
|
+
throw new Error(compassGateError(evidence));
|
|
193
|
+
if (!satisfied)
|
|
194
|
+
warnings.push(compassGateWarning(evidence));
|
|
195
|
+
}
|
|
189
196
|
const verdicts = { ...current.verdicts };
|
|
190
197
|
if (current.stage === "reviewing" && args.verdict)
|
|
191
198
|
verdicts.review = args.verdict;
|
|
@@ -208,7 +215,9 @@ export function handleHarness(args) {
|
|
|
208
215
|
],
|
|
209
216
|
};
|
|
210
217
|
writeHarness(projectPath, change, next);
|
|
211
|
-
|
|
218
|
+
if (!compassEvidence)
|
|
219
|
+
return next;
|
|
220
|
+
return warnings.length ? { ...next, compassEvidence, warnings } : { ...next, compassEvidence };
|
|
212
221
|
}
|
|
213
222
|
if (harnessOp === "rework") {
|
|
214
223
|
if (current.stage !== "reviewing" && current.stage !== "testing") {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness state types shared by `harness.ts` and `compass-gate.ts`. A leaf
|
|
3
|
+
* module: it imports nothing, so neither consumer forms an import cycle.
|
|
4
|
+
*/
|
|
5
|
+
export const HARNESS_STAGES = [
|
|
6
|
+
"exploring",
|
|
7
|
+
"planning",
|
|
8
|
+
"questions",
|
|
9
|
+
"implementing",
|
|
10
|
+
"reviewing",
|
|
11
|
+
"testing",
|
|
12
|
+
"archiving",
|
|
13
|
+
"done",
|
|
14
|
+
];
|
|
@@ -16,8 +16,8 @@ Claude-specific notes: [`CLAUDE.md`](CLAUDE.md). The law: [`LAWS.md`](LAWS.md).
|
|
|
16
16
|
standards below. Open the standard that governs your change before making
|
|
17
17
|
it. Conflicts resolve in favor of the standard; amendments go through a
|
|
18
18
|
spec change, never silent deviation.
|
|
19
|
-
2. **Compass first, always** — for any code question call `
|
|
20
|
-
`
|
|
19
|
+
2. **Compass first, always** — for any code question call `compass_find` /
|
|
20
|
+
`compass_explore` **before** any grep/sed/cat/Read,
|
|
21
21
|
including files you already know by name. Fall back to manual file tools only
|
|
22
22
|
after Compass returns nothing useful, the graph is missing (`compass_index`
|
|
23
23
|
first), or the target isn't indexed code (stylesheets, config, logs). Cheat
|
|
@@ -31,9 +31,10 @@ ignore it.
|
|
|
31
31
|
|
|
32
32
|
This repo is indexed by Compass, speclaw's local code graph (`.speclaw/`). For
|
|
33
33
|
**any** question about code — what a symbol is, what it uses, who calls it,
|
|
34
|
-
where it lives, how a value flows — call Compass **first**: `
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
where it lives, how a value flows — call Compass **first**: `compass_find` to
|
|
35
|
+
locate, `compass_explore` to read a symbol with its callers and callees (include
|
|
36
|
+
`blast_radius` for what could break, `to` for a call path), and
|
|
37
|
+
`compass_diff_context` for the context of a diff.
|
|
37
38
|
Run `compass_index` first if the graph is missing.
|
|
38
39
|
|
|
39
40
|
This includes files you already know the name of: to learn what `Foo` imports,
|
|
@@ -18,7 +18,7 @@ is nothing extra to install.
|
|
|
18
18
|
| Without Compass | With Compass |
|
|
19
19
|
|-----------------|--------------|
|
|
20
20
|
| Many `Grep` + `Read` round-trips (tokens spent scanning) | One `compass_explore` call returns just the relevant node |
|
|
21
|
-
| Guess which file matters | `
|
|
21
|
+
| Guess which file matters | `compass_find` finds code by name or meaning |
|
|
22
22
|
| Edit without knowing the blast radius | callers/callees returned with the node |
|
|
23
23
|
| Whole files dumped into context | verbatim source of the node + its neighbors only |
|
|
24
24
|
|
|
@@ -30,12 +30,12 @@ a request, not whole files.
|
|
|
30
30
|
| Tool | Use it to |
|
|
31
31
|
|------|-----------|
|
|
32
32
|
| `compass_index` | Build/refresh the graph (`.speclaw/index.db`). Incremental — unchanged files are skipped by hash. Run once after init and after significant edits. |
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
| `compass_find` | Find code: `mode: exact` by name/keyword, `mode: concept` by meaning in natural language. |
|
|
34
|
+
| `compass_explore` | Read a node's verbatim source plus its callers and callees. The default before editing. Add `include: ["blast_radius"]` for what could break, or `to: <symbol>` for the call path between two nodes. |
|
|
35
|
+
| `compass_diff_context` | Symbols, blast radius, and tests for a diff (working tree or a rev). |
|
|
36
|
+
|
|
37
|
+
`compass_index` also takes `action: start|stop|status` to keep the index fresh
|
|
38
|
+
automatically (a debounced incremental re-index on file change).
|
|
39
39
|
|
|
40
40
|
If the graph is missing (no `.speclaw/index.db`), run `compass_index` first —
|
|
41
41
|
a missing graph is not license to skip Compass. The only legitimate fallbacks
|
|
@@ -43,11 +43,11 @@ verdicts are complete.
|
|
|
43
43
|
- Scenario headers use exactly `#### Scenario:`.
|
|
44
44
|
- Acceptance criteria are testable without production integrations.
|
|
45
45
|
- The implemented code must match what the delta spec promises. Validate with
|
|
46
|
-
|
|
46
|
+
`lawbook_change` (action: validate) before syncing or archiving.
|
|
47
47
|
|
|
48
48
|
## Archiving discipline
|
|
49
49
|
|
|
50
|
-
Always archive with the `archive` command / `
|
|
50
|
+
Always archive with the `archive` command / `lawbook_change` (action: archive), never a manual
|
|
51
51
|
`mv`. The engine refuses archive while tasks are unchecked, reports are missing,
|
|
52
52
|
specs are out of sync when required, or harness review/test verdicts are not PASS.
|
|
53
53
|
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { performance } from "node:perf_hooks";
|
|
4
4
|
import { compileScope, hasBackend, isActiveLaw, manifestPath, matchCompiled, readLawManifest, } from "./laws.js";
|
|
5
|
+
import { compassNudge, isNudgeEvent } from "./compass-nudge.js";
|
|
5
6
|
const cache = new Map();
|
|
6
7
|
/**
|
|
7
8
|
* Load the project's compiled law index, using the in-process cache when the
|
|
@@ -79,18 +80,35 @@ function recordContextCoverage(projectPath, laws, file) {
|
|
|
79
80
|
* another (unimplemented) backend are skipped. The evaluator fails open: a
|
|
80
81
|
* missing or unparseable manifest, or any exception, yields `allow` with a
|
|
81
82
|
* diagnostic — an enforcement layer that blocks when its own checker crashes is
|
|
82
|
-
* worse than none.
|
|
83
|
+
* worse than none. On `PostToolUse` Read/Grep/Glob it evaluates no law and carries
|
|
84
|
+
* only the advisory Compass-first `nudge` (see `compass-nudge.ts`), which never
|
|
85
|
+
* alters the verdict.
|
|
83
86
|
*
|
|
84
87
|
* @param args - The project, hook event, optional tool name, and raw payload.
|
|
85
88
|
* @returns The verdict, the laws evaluated, an optional reason, and `elapsedMs`.
|
|
86
89
|
*/
|
|
87
90
|
export function checkAction(args) {
|
|
88
91
|
const start = performance.now();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
// Runs before the manifest load so the nudge works with no laws at all; it
|
|
93
|
+
// swallows its own errors and returns null.
|
|
94
|
+
const nudge = compassNudge(args);
|
|
95
|
+
const done = (r) => {
|
|
96
|
+
const reason = nudge ? (r.reason ? `${r.reason}\n${nudge}` : nudge) : r.reason;
|
|
97
|
+
return {
|
|
98
|
+
...r,
|
|
99
|
+
...(nudge ? { nudge, reason } : {}),
|
|
100
|
+
// Only PostToolUse: Stop / InstructionsLoaded keep their pre-change shape.
|
|
101
|
+
...(reason && args.event === "PostToolUse"
|
|
102
|
+
? { hookSpecificOutput: { hookEventName: args.event, additionalContext: reason } }
|
|
103
|
+
: {}),
|
|
104
|
+
elapsedMs: performance.now() - start,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
93
107
|
try {
|
|
108
|
+
// Laws govern mutations: a PostToolUse Read/Grep/Glob carries only the
|
|
109
|
+
// nudge and never evaluates a law, so reads stay as silent as on main.
|
|
110
|
+
if (isNudgeEvent(args))
|
|
111
|
+
return done({ verdict: "allow", evaluated: [] });
|
|
94
112
|
const laws = loadLaws(args.projectPath);
|
|
95
113
|
if (!laws) {
|
|
96
114
|
return done({
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { NUDGE_ENTRY, isEvidenceTool, readCompassCalls, recordCompassCall, } from "../../shared/compass-calls.js";
|
|
4
|
+
// Pure extension → language table: loads no grammar and opens no index DB.
|
|
5
|
+
import { langForPath } from "../compass/languages.js";
|
|
6
|
+
// Covers: req~compass-nudge~1
|
|
7
|
+
//
|
|
8
|
+
// The Compass-first nudge: on `PostToolUse` for Read/Grep/Glob of indexed code,
|
|
9
|
+
// remind the agent to locate and read through Compass when the call log shows
|
|
10
|
+
// no recent Compass evidence. Advisory context only — it never gates and never
|
|
11
|
+
// touches the verdict. It reads only the bounded call-log tail and the
|
|
12
|
+
// in-memory extension table, so it stays inside the hook latency budget.
|
|
13
|
+
/** Tools whose completed calls the nudge watches. */
|
|
14
|
+
export const NUDGE_TOOLS = new Set(["Read", "Grep", "Glob"]);
|
|
15
|
+
/** No nudge when a Compass evidence call landed within this window. */
|
|
16
|
+
export const NUDGE_EVIDENCE_WINDOW_MS = 10 * 60 * 1000;
|
|
17
|
+
/** At most one nudge per this window. */
|
|
18
|
+
export const NUDGE_RATE_LIMIT_MS = 5 * 60 * 1000;
|
|
19
|
+
/** Path segments that are never indexed source. */
|
|
20
|
+
const SKIP_SEGMENTS = new Set(["node_modules", ".git", ".speclaw", "dist"]);
|
|
21
|
+
/**
|
|
22
|
+
* True for a `PostToolUse` Read/Grep/Glob call. Those calls are the nudge's
|
|
23
|
+
* alone: laws govern mutations, so `checkAction` skips law evaluation for them.
|
|
24
|
+
*/
|
|
25
|
+
export function isNudgeEvent(args) {
|
|
26
|
+
if (args.event !== "PostToolUse")
|
|
27
|
+
return false;
|
|
28
|
+
const tool = usable(args.toolName) ?? usable(args.payload?.tool_name);
|
|
29
|
+
return !!tool && NUDGE_TOOLS.has(tool);
|
|
30
|
+
}
|
|
31
|
+
/** A usable string: non-empty and not an unsubstituted `${…}` hook placeholder. */
|
|
32
|
+
function usable(v) {
|
|
33
|
+
if (typeof v !== "string")
|
|
34
|
+
return null;
|
|
35
|
+
const s = v.trim();
|
|
36
|
+
return s && !s.includes("${") ? s : null;
|
|
37
|
+
}
|
|
38
|
+
/** Sentinel target for a repo-wide search: no path, or the project root. */
|
|
39
|
+
const ROOT = "";
|
|
40
|
+
/**
|
|
41
|
+
* The project-relative POSIX target, {@link ROOT} for the project root, or
|
|
42
|
+
* null when the nudge has nothing to say about it: outside the project, or
|
|
43
|
+
* under a never-indexed directory.
|
|
44
|
+
*/
|
|
45
|
+
function resolveTarget(projectPath, raw) {
|
|
46
|
+
const root = path.resolve(projectPath);
|
|
47
|
+
const rel = path.relative(root, path.resolve(root, raw));
|
|
48
|
+
if (!rel)
|
|
49
|
+
return ROOT;
|
|
50
|
+
if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel))
|
|
51
|
+
return null;
|
|
52
|
+
const segments = rel.split(path.sep);
|
|
53
|
+
if (segments.some((seg) => SKIP_SEGMENTS.has(seg)))
|
|
54
|
+
return null;
|
|
55
|
+
return segments.join("/");
|
|
56
|
+
}
|
|
57
|
+
/** ripgrep `--type` names whose extension differs from the name. */
|
|
58
|
+
const RG_TYPE_EXT = {
|
|
59
|
+
rust: "rs",
|
|
60
|
+
python: "py",
|
|
61
|
+
typescript: "ts",
|
|
62
|
+
javascript: "js",
|
|
63
|
+
ruby: "rb",
|
|
64
|
+
csharp: "cs",
|
|
65
|
+
kotlin: "kt",
|
|
66
|
+
markdown: "md",
|
|
67
|
+
golang: "go",
|
|
68
|
+
};
|
|
69
|
+
/** True when `ext` (with or without the dot) is a Compass-indexed extension. */
|
|
70
|
+
function indexedExt(ext) {
|
|
71
|
+
return langForPath(`x.${ext.replace(/^\./, "")}`) !== undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The extensions a glob restricts matches to, expanding `*.{ts,md}` braces;
|
|
75
|
+
* an empty list when the last segment names no extension (e.g. `**\/*`).
|
|
76
|
+
*/
|
|
77
|
+
function globExtensions(glob) {
|
|
78
|
+
const last = glob.split("/").pop() ?? glob;
|
|
79
|
+
const brace = /\.\{([^}]*)\}$/.exec(last);
|
|
80
|
+
if (brace)
|
|
81
|
+
return brace[1]
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((e) => e.trim())
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
const ext = path.extname(last);
|
|
86
|
+
return ext && !/[*?[]/.test(ext) ? [ext.slice(1)] : [];
|
|
87
|
+
}
|
|
88
|
+
/** A glob is code-targeting when it is extension-less or names an indexed extension. */
|
|
89
|
+
function globTargetsCode(glob) {
|
|
90
|
+
const exts = globExtensions(glob);
|
|
91
|
+
return exts.length === 0 || exts.some(indexedExt);
|
|
92
|
+
}
|
|
93
|
+
/** Grep is restricted to non-indexed files when its `glob` or `type` says so. */
|
|
94
|
+
function grepRestrictedAway(input) {
|
|
95
|
+
const glob = usable(input.glob);
|
|
96
|
+
if (glob && !globTargetsCode(glob))
|
|
97
|
+
return true;
|
|
98
|
+
const type = usable(input.type);
|
|
99
|
+
if (type && !indexedExt(RG_TYPE_EXT[type] ?? type))
|
|
100
|
+
return true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether a Read/Grep/Glob of `target` is code access the nudge cares about.
|
|
105
|
+
*
|
|
106
|
+
* - Read: the file has an indexed extension.
|
|
107
|
+
* - Grep: an indexed file, a directory, or a repo-wide search ({@link ROOT}),
|
|
108
|
+
* unless `glob`/`type` restrict it to non-indexed extensions.
|
|
109
|
+
* - Glob: an indexed file, a directory, or a repo-wide search, when `pattern`
|
|
110
|
+
* names an indexed extension or none at all (e.g. `**\/*`); a directory with
|
|
111
|
+
* no pattern stays eligible.
|
|
112
|
+
*/
|
|
113
|
+
function eligible(projectPath, tool, target, input) {
|
|
114
|
+
if (target === ROOT)
|
|
115
|
+
return tool !== "Read" && searchEligible(tool, "", input);
|
|
116
|
+
const dir = isDirectory(projectPath, target);
|
|
117
|
+
if (tool === "Read")
|
|
118
|
+
return dir !== true && indexedExt(path.extname(target) || ".");
|
|
119
|
+
// A directory has no extension, even when its name has a dot (`src/v1.2`).
|
|
120
|
+
const ext = dir === true ? "" : path.extname(target);
|
|
121
|
+
return searchEligible(tool, ext, input, target);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether the target is a directory: one cheap `statSync`, no index access.
|
|
125
|
+
* Undefined when it cannot be stat'ed (missing, permissions) — callers then
|
|
126
|
+
* fall back to the extension heuristic.
|
|
127
|
+
*/
|
|
128
|
+
function isDirectory(projectPath, target) {
|
|
129
|
+
try {
|
|
130
|
+
return fs.statSync(path.join(projectPath, target)).isDirectory();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Grep/Glob eligibility once the target's extension (`""` for a directory) is known. */
|
|
137
|
+
function searchEligible(tool, ext, input, target = ROOT) {
|
|
138
|
+
if (ext && !indexedExt(ext))
|
|
139
|
+
return false;
|
|
140
|
+
if (tool === "Grep")
|
|
141
|
+
return !grepRestrictedAway(input);
|
|
142
|
+
const pattern = usable(input.pattern);
|
|
143
|
+
if (!pattern)
|
|
144
|
+
return target !== ROOT;
|
|
145
|
+
return globTargetsCode(pattern);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Evaluate the Compass-first nudge for one check call.
|
|
149
|
+
*
|
|
150
|
+
* Returns the nudge text — after recording a `nudge` entry in the call log for
|
|
151
|
+
* the rate limit — or null when the event is not an eligible `PostToolUse`
|
|
152
|
+
* Read/Grep/Glob, a Compass evidence call landed in the last 10 minutes, or a
|
|
153
|
+
* nudge already fired in the last 5 minutes. Never throws.
|
|
154
|
+
*
|
|
155
|
+
* @param args - The check call's project, event, tool, and raw payload.
|
|
156
|
+
* @param now - Current epoch ms (injectable for tests).
|
|
157
|
+
*/
|
|
158
|
+
export function compassNudge(args, now = Date.now()) {
|
|
159
|
+
try {
|
|
160
|
+
if (args.event !== "PostToolUse")
|
|
161
|
+
return null;
|
|
162
|
+
const payload = args.payload ?? {};
|
|
163
|
+
const tool = usable(args.toolName) ?? usable(payload.tool_name);
|
|
164
|
+
if (!tool || !NUDGE_TOOLS.has(tool))
|
|
165
|
+
return null;
|
|
166
|
+
const input = (payload.tool_input ?? payload.toolInput ?? payload);
|
|
167
|
+
// No path on Grep/Glob means the cwd: a repo-wide search.
|
|
168
|
+
const raw = usable(input.file_path) ?? usable(input.path) ?? ".";
|
|
169
|
+
const target = resolveTarget(args.projectPath, raw);
|
|
170
|
+
if (target === null || !eligible(args.projectPath, tool, target, input))
|
|
171
|
+
return null;
|
|
172
|
+
const pattern = usable(input.pattern);
|
|
173
|
+
const recent = readCompassCalls(args.projectPath, { sinceMs: now - NUDGE_EVIDENCE_WINDOW_MS });
|
|
174
|
+
if (recent.some((c) => isEvidenceTool(c.tool)))
|
|
175
|
+
return null;
|
|
176
|
+
const rateFloor = now - NUDGE_RATE_LIMIT_MS;
|
|
177
|
+
if (recent.some((c) => c.tool === NUDGE_ENTRY && Date.parse(c.at) >= rateFloor))
|
|
178
|
+
return null;
|
|
179
|
+
recordCompassCall(args.projectPath, NUDGE_ENTRY, new Date(now));
|
|
180
|
+
const stem = target === ROOT ? "" : path.basename(target, path.extname(target));
|
|
181
|
+
const query = tool === "Grep" && pattern ? pattern : stem || pattern || "<concept>";
|
|
182
|
+
const symbol = stem || (tool === "Grep" && pattern ? pattern : "<symbol>");
|
|
183
|
+
return (`Compass first: no compass_find / compass_explore call in the last 10 min. ` +
|
|
184
|
+
`Try \`compass_explore ${symbol}\` or \`compass_find "${query}"\` before reading code.`);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -11,7 +11,13 @@ const SPECLAW_HOOK_INPUT = {
|
|
|
11
11
|
payload: {
|
|
12
12
|
hook_event_name: "${hook_event_name}",
|
|
13
13
|
tool_name: "${tool_name}",
|
|
14
|
-
tool_input: {
|
|
14
|
+
tool_input: {
|
|
15
|
+
file_path: "${tool_input.file_path}",
|
|
16
|
+
path: "${tool_input.path}",
|
|
17
|
+
pattern: "${tool_input.pattern}",
|
|
18
|
+
glob: "${tool_input.glob}",
|
|
19
|
+
type: "${tool_input.type}",
|
|
20
|
+
},
|
|
15
21
|
},
|
|
16
22
|
};
|
|
17
23
|
/** The speclaw hook object — its `{type, server}` pair is the merge identity. */
|
|
@@ -24,6 +30,12 @@ const SPECLAW_HOOK = {
|
|
|
24
30
|
};
|
|
25
31
|
/** Tool-name matcher for the file-mutating tools the `path` backend can evaluate. */
|
|
26
32
|
const MUTATION_MATCHER = "Write|Edit|MultiEdit|NotebookEdit";
|
|
33
|
+
/**
|
|
34
|
+
* Tool-name matcher for the code-reading tools the Compass-first nudge watches.
|
|
35
|
+
* Installed on `PostToolUse` only: there the result is context the agent reads
|
|
36
|
+
* and the event cannot gate the tool; a `PreToolUse` "allow" would auto-approve.
|
|
37
|
+
*/
|
|
38
|
+
export const NUDGE_MATCHER = "Read|Grep|Glob";
|
|
27
39
|
/** True when a hook object is one speclaw owns (safe to replace on merge). */
|
|
28
40
|
function isSpeclawHook(h) {
|
|
29
41
|
const o = h;
|
|
@@ -33,7 +45,8 @@ function isSpeclawHook(h) {
|
|
|
33
45
|
* Compile a law manifest into the hook groups speclaw contributes, one per event
|
|
34
46
|
* the laws demand: `PreToolUse` when any `bloqueo` law exists, `PostToolUse` for
|
|
35
47
|
* `feedback`, `Stop` for `gate`, and `InstructionsLoaded` whenever any law exists
|
|
36
|
-
* (the context-coverage audit).
|
|
48
|
+
* (the context-coverage audit). One `PostToolUse` group matching `Read|Grep|Glob`
|
|
49
|
+
* is always emitted for the Compass-first nudge, even with no laws. A law whose scope contains a malformed glob is
|
|
37
50
|
* excluded and reported, so a bad pattern fails loudly at generation rather than
|
|
38
51
|
* silently matching nothing at runtime.
|
|
39
52
|
*
|
|
@@ -56,8 +69,10 @@ export function compileHooks(manifest) {
|
|
|
56
69
|
const hasGate = valid.some((l) => l.enforcement === "gate");
|
|
57
70
|
if (hasBloqueo)
|
|
58
71
|
byEvent.PreToolUse = [{ matcher: MUTATION_MATCHER, hooks: [{ ...SPECLAW_HOOK }] }];
|
|
72
|
+
// The Compass-first nudge entry is always present, laws or not.
|
|
73
|
+
byEvent.PostToolUse = [{ matcher: NUDGE_MATCHER, hooks: [{ ...SPECLAW_HOOK }] }];
|
|
59
74
|
if (hasFeedback)
|
|
60
|
-
byEvent.PostToolUse
|
|
75
|
+
byEvent.PostToolUse.unshift({ matcher: MUTATION_MATCHER, hooks: [{ ...SPECLAW_HOOK }] });
|
|
61
76
|
if (hasGate)
|
|
62
77
|
byEvent.Stop = [{ hooks: [{ ...SPECLAW_HOOK }] }];
|
|
63
78
|
if (valid.length > 0)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: archiver
|
|
3
3
|
description: Reconcile specs, sync, and archive a change when Cortex stage is archiving. No new feature implementation.
|
|
4
|
-
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__lawbook_change,
|
|
4
|
+
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__lawbook_change, mcp__speclaw__cortex
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
You are the **archiver** role in speclaw's **Cortex** (One brain. Many agents.).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: explorer
|
|
3
3
|
description: Read-only codebase investigation for a lawbook change. Use when the Cortex stage is exploring (or investigate for bugs). Compass-first; writes nothing under lawbook/ or src/.
|
|
4
|
-
tools: Read, Grep, Glob, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find,
|
|
4
|
+
tools: Read, Grep, Glob, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find, mcp__speclaw__compass_diff_context, mcp__speclaw__compass_index, mcp__speclaw__lawbook_change, mcp__speclaw__lawbook_investigate
|
|
5
5
|
disallowedTools: Write, Edit, MultiEdit, Bash, NotebookEdit
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -9,9 +9,12 @@ You are the **explorer** role in speclaw's **Cortex** (One brain. Many agents.).
|
|
|
9
9
|
|
|
10
10
|
## Goal
|
|
11
11
|
|
|
12
|
-
Investigate the idea or bug with Compass **before** any grep/read
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
Investigate the idea or bug with Compass **before** any grep/read: locate with
|
|
13
|
+
`compass_find`, read with `compass_explore`, and run `compass_index` only when
|
|
14
|
+
find returns nothing or reports a missing or stale index. Read/Grep code only
|
|
15
|
+
after naming which Rule 1 fallback holds. Produce a short brief for the
|
|
16
|
+
planner: relevant symbols, blast radius, risks, open questions, and
|
|
17
|
+
`Compass calls made: N`. Do **not** draft change artifacts or edit source.
|
|
15
18
|
|
|
16
19
|
## Skill
|
|
17
20
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: implementer
|
|
3
3
|
description: Implement drafted lawbook tasks in source and tests. Use when Cortex stage is implementing. Does not run final quality gates, review, sync, or archive.
|
|
4
|
-
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find,
|
|
4
|
+
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find, mcp__speclaw__compass_diff_context, mcp__speclaw__compass_index, mcp__speclaw__lawbook_change, mcp__speclaw__cortex
|
|
5
5
|
disallowedTools: NotebookEdit
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: planner
|
|
3
3
|
description: Draft lawbook change artifacts and ceremony level. Use when Cortex stage is planning. Asks clarifying questions via the coordinator; never edits src/.
|
|
4
|
-
tools: Read, Grep, Glob, Write, Edit, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find, mcp__speclaw__lawbook_change
|
|
4
|
+
tools: Read, Grep, Glob, Write, Edit, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find, mcp__speclaw__lawbook_change
|
|
5
5
|
disallowedTools: Bash, NotebookEdit
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -11,7 +11,8 @@ You are the **planner** role in speclaw's **Cortex** (One brain. Many agents.).
|
|
|
11
11
|
|
|
12
12
|
Turn the explorer brief + user intent into a reviewable change under
|
|
13
13
|
`lawbook/changes/<name>/` at the confirmed ceremony level. Prefer `draft` /
|
|
14
|
-
`quick` skills
|
|
14
|
+
`quick` skills; scaffold with `lawbook_change` action `draft` (`change`,
|
|
15
|
+
`level`).
|
|
15
16
|
|
|
16
17
|
A complete explorer brief is the code map. Do not re-investigate it. Call
|
|
17
18
|
Compass only for a gap the brief names.
|
|
@@ -19,8 +20,10 @@ Compass only for a gap the brief names.
|
|
|
19
20
|
## Questions
|
|
20
21
|
|
|
21
22
|
If anything material is ambiguous, **stop**. Return numbered questions to the
|
|
22
|
-
coordinator (do not invent answers).
|
|
23
|
-
|
|
23
|
+
coordinator (do not invent answers). When dispatched for questions only, return
|
|
24
|
+
the questions and draft nothing. The coordinator batches them with the explorer
|
|
25
|
+
open questions and the level confirmation into one `pauseForQuestions` round.
|
|
26
|
+
After the human answers, draft at the level the coordinator recorded.
|
|
24
27
|
|
|
25
28
|
## Hard constraints
|
|
26
29
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Deep review of implemented change without patching code. Writes reports/review.md with PASS or FAIL. Use when Cortex stage is reviewing.
|
|
4
|
-
tools: Read, Grep, Glob, Write, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find,
|
|
4
|
+
tools: Read, Grep, Glob, Write, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find, mcp__speclaw__compass_diff_context, mcp__speclaw__lawbook_change, mcp__speclaw__cortex
|
|
5
5
|
disallowedTools: Edit, MultiEdit, Bash, NotebookEdit
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tester
|
|
3
3
|
description: Run quality gates and manual verification; write discipline reports. Use when Cortex stage is testing. May add missing tests only — not features.
|
|
4
|
-
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore,
|
|
4
|
+
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_diff_context, mcp__speclaw__lawbook_change, mcp__speclaw__cortex
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
You are the **tester** role in speclaw's **Cortex** (One brain. Many agents.).
|
|
@@ -4,8 +4,8 @@ description: Draft a new spec-driven change at the confirmed ceremony level befo
|
|
|
4
4
|
|
|
5
5
|
Draft a new change under `lawbook/changes/<name>/` for: $ARGUMENTS
|
|
6
6
|
|
|
7
|
-
Follow the `draft` skill: ensure `lawbook/` exists (`
|
|
8
|
-
with Compass, propose a ceremony level (`
|
|
7
|
+
Follow the `draft` skill: ensure `lawbook/` exists (`lawbook_change` action `init`), investigate
|
|
8
|
+
with Compass, propose a ceremony level (`lawbook_change` action `level`, mode `propose`) and
|
|
9
9
|
**confirm** it with the human (`set`), then scaffold only the artifacts that
|
|
10
10
|
level requires. For true one-liners use `speclaw quick` / the `quick` skill
|
|
11
11
|
instead. Finish by running `lawbook_change` (action: validate) and fixing every issue.
|
|
@@ -6,9 +6,9 @@ Scaffold a **ceremony level 0** change named `$ARGUMENTS` with `speclaw quick`
|
|
|
6
6
|
(or the equivalent `scaffoldQuick` path). Do **not** invent `proposal.md` /
|
|
7
7
|
`design.md` / delta specs for a true one-liner.
|
|
8
8
|
|
|
9
|
-
1. Ensure `lawbook/` exists (`
|
|
9
|
+
1. Ensure `lawbook/` exists (`lawbook_change` action `init` if needed).
|
|
10
10
|
2. Prefer passing `--path` / `--symbol` so the proposal rationale is measured.
|
|
11
11
|
3. Confirm with the human if the measured proposal is higher than 0 — promote
|
|
12
|
-
(`
|
|
12
|
+
(`lawbook_change` action `level`, mode `promote`) instead of staying at quick.
|
|
13
13
|
4. Implement, check the checklist in `record.md`, write a discipline report
|
|
14
14
|
under `reports/`, then archive (no sync required at level 0).
|
|
@@ -70,7 +70,7 @@ in.
|
|
|
70
70
|
|
|
71
71
|
## 4. Reports gate the archive
|
|
72
72
|
|
|
73
|
-
`
|
|
73
|
+
`lawbook_change` action `archive` refuses to archive while `reports/` holds no discipline report
|
|
74
74
|
(the `reports/README.md` scaffold does not count). Which disciplines a change
|
|
75
75
|
touched — and therefore which reports are owed, including `api.md` for any
|
|
76
76
|
API-touching change — is the agent's responsibility to judge and satisfy before
|