@esneiderbravo/speclaw 0.1.10 → 0.1.12
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 +17 -7
- package/dist/cli/commands/init.js +4 -4
- package/dist/cli/commands/update.js +57 -11
- package/dist/modules/foundation/assets/AGENTS.template.md +6 -3
- package/dist/modules/foundation/assets/CLAUDE.template.md +20 -7
- package/dist/modules/foundation/assets/LAWS.template.md +1 -1
- package/dist/modules/foundation/assets/docs/compass.template.md +10 -4
- package/dist/modules/foundation/assets/docs/standards/lawbook.template.md +31 -8
- package/dist/modules/foundation/ownership.js +32 -0
- package/dist/modules/foundation/scaffold.js +21 -8
- package/dist/modules/lawbook/assets/commands/archive.md +5 -4
- package/dist/modules/lawbook/assets/commands/sync.md +4 -3
- package/dist/modules/lawbook/assets/rules/spec-tasks-mandatory-steps.md +8 -0
- package/dist/modules/lawbook/assets/skills/archive/SKILL.md +33 -8
- package/dist/modules/lawbook/assets/skills/build/SKILL.md +15 -2
- package/dist/modules/lawbook/assets/skills/draft/SKILL.md +6 -2
- package/dist/modules/lawbook/assets/skills/sync/SKILL.md +24 -4
- package/dist/modules/lawbook/engine.js +57 -1
- package/dist/modules/lawbook/register.js +4 -4
- package/dist/modules/tools/packs.js +5 -3
- package/dist/shared/install.js +42 -10
- package/dist/shared/manifest.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,10 +106,10 @@ non-trivial lands without a spec change. It's a loop of five steps:
|
|
|
106
106
|
| Step | What happens |
|
|
107
107
|
| :-- | :-- |
|
|
108
108
|
| **explore** | Think an idea through *before* committing to it — should we do this, and how. Writes nothing. |
|
|
109
|
-
| **draft** | Capture the intent as a change under `lawbook/changes/<name>/` — four artifacts, always (see below). |
|
|
110
|
-
| **build** | Implement the tasks in order, keeping code and spec in agreement
|
|
111
|
-
| **sync** |
|
|
112
|
-
| **archive** |
|
|
109
|
+
| **draft** | Capture the intent as a change under `lawbook/changes/<name>/` — four artifacts plus a `reports/` folder, always (see below). |
|
|
110
|
+
| **build** | Implement the tasks in order, keeping code and spec in agreement, and record test results under `reports/`. |
|
|
111
|
+
| **sync** | Reconcile the delta specs against what was actually built, then promote them into the canonical `lawbook/specs/` — the always-true description of how the system behaves. |
|
|
112
|
+
| **archive** | Reconcile, then validate, promote, and move the change to `lawbook/changes/archive/` — **in the same PR**, never a post-merge chore. Gated: refused while any task is unchecked, `reports/` is empty, or the specs are unsynced. |
|
|
113
113
|
|
|
114
114
|
**Every `draft` writes four artifacts under `lawbook/changes/<name>/` — none optional:**
|
|
115
115
|
|
|
@@ -120,6 +120,8 @@ non-trivial lands without a spec change. It's a loop of five steps:
|
|
|
120
120
|
| `design.md` | The **how** — approach, alternatives weighed, and the trade-offs behind the decision. |
|
|
121
121
|
| `tasks.md` | The **plan** — ordered, checkable steps, including the mandatory ones from `config.yaml`. |
|
|
122
122
|
|
|
123
|
+
Plus a **`reports/`** folder — scaffolded at draft, filled at build with one report per discipline (`backend.md`, `frontend.md`, …) recording the real unit/integration/e2e results. Evidence of testing travels with the change, and `lawbook_archive` refuses to archive without it.
|
|
124
|
+
|
|
123
125
|
> [!NOTE]
|
|
124
126
|
> **Delta specs are normative and testable.** Requirements use `SHALL`/`MUST`
|
|
125
127
|
> under `### Requirement:` headers, each with one or more `#### Scenario:` blocks
|
|
@@ -183,9 +185,17 @@ speclaw update
|
|
|
183
185
|
```
|
|
184
186
|
|
|
185
187
|
`update` upgrades the global package **and** brings the current project up to date
|
|
186
|
-
without a re-init
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
without a re-init, splitting files by who owns them:
|
|
189
|
+
|
|
190
|
+
- **Managed files** (speclaw's workflow machinery — the skills, commands, rules,
|
|
191
|
+
and agent packs under `ai-specs/`) are **refreshed** to the new version, so
|
|
192
|
+
improvements actually reach your project. If you edited one locally, your copy
|
|
193
|
+
is saved as `<file>.bak` before the refresh — nothing is lost.
|
|
194
|
+
- **Personalized files** (your constitution and standards — `CLAUDE.md`,
|
|
195
|
+
`AGENTS.md`, `LAWS.md`, `docs/standards/*`, `docs/compass.md`,
|
|
196
|
+
`lawbook/config.yaml`) are **never auto-edited**. When a release changes their
|
|
197
|
+
speclaw-authored content, `update` prints a prompt for **the agent you're
|
|
198
|
+
using** to apply the change while preserving your project's specifics.
|
|
189
199
|
|
|
190
200
|
- `speclaw update --check` — report whether an update exists, change nothing.
|
|
191
201
|
- `NO_UPDATE_NOTIFIER=1` — silence the reminder.
|
|
@@ -113,12 +113,12 @@ export async function runInit(flags) {
|
|
|
113
113
|
c.bold(c.cream(String(stats.embeddings))) +
|
|
114
114
|
c.muted(" embeddings"));
|
|
115
115
|
}
|
|
116
|
-
// 3. Handoff prompt
|
|
117
|
-
//
|
|
118
|
-
|
|
116
|
+
// 3. Handoff prompt — printed as a single flush-left line so it copy-pastes
|
|
117
|
+
// cleanly (no borders, no wrapping artifacts). Agent-generic on purpose:
|
|
118
|
+
// speclaw supports any agent, so it addresses "the agent you're using".
|
|
119
119
|
ui.step("You're set — one last step");
|
|
120
120
|
ui.plain();
|
|
121
|
-
ui.info(`Copy this and paste it into ${c.cyan(
|
|
121
|
+
ui.info(`Copy this and paste it into ${c.cyan("the agent you're using")}:`);
|
|
122
122
|
ui.plain();
|
|
123
123
|
console.log(c.cream("Complete speclaw's foundation: analyze this repo and fill LAWS.md and " +
|
|
124
124
|
"docs/standards/* with its real architecture, quality gates and conventions. " +
|
|
@@ -5,12 +5,29 @@ import { ui, c } from "../lib/ui.js";
|
|
|
5
5
|
import { checkForUpdates, isNewer } from "../lib/update-check.js";
|
|
6
6
|
import { pkgName, pkgVersion } from "../../shared/version.js";
|
|
7
7
|
import { scaffold } from "../../modules/foundation/scaffold.js";
|
|
8
|
+
import { PERSONALIZED } from "../../modules/foundation/ownership.js";
|
|
8
9
|
import { detectConfiguredAgents } from "../../shared/agents.js";
|
|
9
10
|
import { readManifest } from "../../shared/manifest.js";
|
|
10
11
|
import { loadPacks } from "../../modules/tools/packs.js";
|
|
11
12
|
import { detectProjectName } from "./init.js";
|
|
13
|
+
// The first migration must be tagged at the version that introduces this
|
|
14
|
+
// mechanism (0.1.12): `isNewer` is strict, so an entry tagged at an already-
|
|
15
|
+
// shipped version (e.g. 0.1.11) would never fire for projects already on it.
|
|
12
16
|
const MIGRATIONS = [
|
|
13
|
-
|
|
17
|
+
{
|
|
18
|
+
version: "0.1.12",
|
|
19
|
+
describe: "Compass-first rule + reports mandatory step in personalized files",
|
|
20
|
+
agentPrompt: "- In CLAUDE.md and AGENTS.md, change the Compass rule to 'Compass first, always': " +
|
|
21
|
+
"call Compass (compass_explore/search/recall) before grep/sed/cat/Read for any code " +
|
|
22
|
+
"question — including files you already know by name — and fall back to manual file " +
|
|
23
|
+
"tools only after a Compass call returns nothing useful, the graph is missing, or the " +
|
|
24
|
+
"target isn't indexed code (CSS/JSON/logs). Update the matching Compass row in LAWS.md " +
|
|
25
|
+
"and the intro of docs/compass.md the same way.\n" +
|
|
26
|
+
"- In lawbook/config.yaml, add this mandatory task step before the docs/archive steps: " +
|
|
27
|
+
'"Produce the discipline reports under reports/ (unit/integration/e2e results for what ' +
|
|
28
|
+
'the feature touched)."\n' +
|
|
29
|
+
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
30
|
+
},
|
|
14
31
|
];
|
|
15
32
|
/**
|
|
16
33
|
* Update speclaw and bring the current project up to date without a full re-init:
|
|
@@ -84,23 +101,52 @@ function applyProjectMigrations(cwd) {
|
|
|
84
101
|
const agents = detectConfiguredAgents(cwd);
|
|
85
102
|
const known = loadPacks();
|
|
86
103
|
const packs = (readManifest(cwd)?.packs ?? []).filter((p) => p in known);
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
// Managed files (skills/commands/rules/agents) are refreshed to the current
|
|
105
|
+
// version; personalized files (constitution/standards/config) are never
|
|
106
|
+
// rewritten — those changes are handed to the user's agent below.
|
|
107
|
+
const report = scaffold(cwd, { project_name: detectProjectName(cwd) }, packs, agents, {
|
|
108
|
+
refreshManaged: true,
|
|
109
|
+
});
|
|
110
|
+
const changed = report.written.filter((w) => !w.includes(".gitignore"));
|
|
111
|
+
if (changed.length) {
|
|
112
|
+
for (const w of changed)
|
|
113
|
+
ui.ok(c.cream(path.relative(cwd, w)));
|
|
114
|
+
ui.info(`${changed.length} file(s) added/refreshed · ${report.skipped.length} left untouched.`);
|
|
95
115
|
}
|
|
96
116
|
else {
|
|
97
|
-
ui.ok("
|
|
117
|
+
ui.ok("Managed content already up to date — nothing to refresh.");
|
|
98
118
|
}
|
|
99
|
-
const
|
|
119
|
+
for (const b of report.backedUp) {
|
|
120
|
+
const rel = path.relative(cwd, b);
|
|
121
|
+
ui.warn(`${c.cream(rel)} had local edits — saved as ${rel}.bak before refreshing.`);
|
|
122
|
+
}
|
|
123
|
+
// A project several releases behind jumps straight to @latest, so apply EVERY
|
|
124
|
+
// migration newer than its recorded version — not just the next one — oldest
|
|
125
|
+
// first (sorted, so array order can't matter). Entries are cumulative: never
|
|
126
|
+
// delete a shipped migration, or a laggard crossing it later would miss it.
|
|
127
|
+
const pending = MIGRATIONS.filter((m) => isNewer(m.version, fromVersion)).sort((a, b) => isNewer(a.version, b.version) ? 1 : isNewer(b.version, a.version) ? -1 : 0);
|
|
100
128
|
for (const m of pending) {
|
|
129
|
+
if (!m.run)
|
|
130
|
+
continue;
|
|
101
131
|
ui.step(`Step for ${m.version}: ${m.describe}`);
|
|
102
132
|
m.run(cwd, report);
|
|
103
133
|
}
|
|
134
|
+
// Personalized files can't be auto-edited (they hold project specifics), so
|
|
135
|
+
// hand their changes to whatever agent the user runs — never a hardcoded one.
|
|
136
|
+
const prompt = pending
|
|
137
|
+
.map((m) => m.agentPrompt)
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.join("\n");
|
|
140
|
+
if (prompt) {
|
|
141
|
+
ui.plain();
|
|
142
|
+
ui.step("One step for the agent you're using");
|
|
143
|
+
ui.info(`Personalized files (${PERSONALIZED.join(", ")}) hold your project specifics, so ` +
|
|
144
|
+
`speclaw won't edit them. Paste this into the agent you're using to apply this ` +
|
|
145
|
+
`release's changes while keeping your content:`);
|
|
146
|
+
ui.plain();
|
|
147
|
+
console.log(c.cream(prompt));
|
|
148
|
+
ui.plain();
|
|
149
|
+
}
|
|
104
150
|
ui.plain();
|
|
105
151
|
ui.ok(`On ${c.cyan(pkgVersion())}. No re-init needed.`);
|
|
106
152
|
}
|
|
@@ -16,9 +16,12 @@ 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. **
|
|
20
|
-
`compass_search` / `compass_recall`
|
|
21
|
-
|
|
19
|
+
2. **Compass first, always** — for any code question call `compass_explore` /
|
|
20
|
+
`compass_search` / `compass_recall` **before** any grep/sed/cat/Read,
|
|
21
|
+
including files you already know by name. Fall back to manual file tools only
|
|
22
|
+
after Compass returns nothing useful, the graph is missing (`compass_index`
|
|
23
|
+
first), or the target isn't indexed code (stylesheets, config, logs). Cheat
|
|
24
|
+
sheet: [`docs/compass.md`](docs/compass.md).
|
|
22
25
|
3. **Follow the lawbook workflow** for every non-trivial change; archive
|
|
23
26
|
within the same PR. Rules:
|
|
24
27
|
[`docs/standards/lawbook.md`](docs/standards/lawbook.md).
|
|
@@ -27,13 +27,26 @@ When any instruction conflicts with a standard, **the standard wins** — and if
|
|
|
27
27
|
you believe it is wrong, propose an amendment via a spec change; never silently
|
|
28
28
|
ignore it.
|
|
29
29
|
|
|
30
|
-
## Rule 1 — Compass
|
|
31
|
-
|
|
32
|
-
This repo is indexed by Compass, speclaw's local code graph (`.speclaw/`).
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
## Rule 1 — Compass first, always
|
|
31
|
+
|
|
32
|
+
This repo is indexed by Compass, speclaw's local code graph (`.speclaw/`). For
|
|
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**: `compass_search` /
|
|
35
|
+
`compass_recall` to locate, `compass_explore` to read a symbol with its callers
|
|
36
|
+
and callees, `compass_impact` / `compass_trace` for blast radius and call paths.
|
|
37
|
+
Run `compass_index` first if the graph is missing.
|
|
38
|
+
|
|
39
|
+
This includes files you already know the name of: to learn what `Foo` imports,
|
|
40
|
+
uses, or depends on, run `compass_explore Foo` — do **not** `cat`/`sed`/`grep`/
|
|
41
|
+
Read the file to work it out by hand. "I know which file it is" is not an
|
|
42
|
+
exemption.
|
|
43
|
+
|
|
44
|
+
Fall back to Grep / Read / `sed` / `cat` **only after** you can name which of
|
|
45
|
+
these holds: (1) a Compass call actually ran and returned nothing useful for the
|
|
46
|
+
query, (2) the graph is missing and `compass_index` can't be run, or (3) the
|
|
47
|
+
target is not indexed code — stylesheets, JSON/config, markdown, logs,
|
|
48
|
+
generated files, lockfiles. Never skip Compass because grep "feels faster."
|
|
49
|
+
See [`docs/compass.md`](docs/compass.md).
|
|
37
50
|
|
|
38
51
|
## Rule 2 — Spec-driven, always
|
|
39
52
|
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
| Documentation | [`docs/standards/documentation.md`](docs/standards/documentation.md) | Docstring/API-comment convention per language |
|
|
23
23
|
| Conventions | [`docs/standards/conventions.md`](docs/standards/conventions.md) | Branches, PRs, tracker, versioning |
|
|
24
24
|
| Lawbook | [`docs/standards/lawbook.md`](docs/standards/lawbook.md) | Spec-driven workflow, mandatory task steps, archiving |
|
|
25
|
-
| Compass | [`docs/compass.md`](docs/compass.md) | Using the code knowledge graph before grep |
|
|
25
|
+
| Compass | [`docs/compass.md`](docs/compass.md) | Using the code knowledge graph first — before any grep/read |
|
|
26
26
|
|
|
27
27
|
## Binding rules
|
|
28
28
|
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
**Compass** is speclaw's local code graph: a pre-indexed map of every symbol
|
|
4
4
|
(node) and relationship (edge) in this workspace, plus a local vector store for
|
|
5
|
-
semantic recall. Agents MUST
|
|
6
|
-
|
|
5
|
+
semantic recall. Agents **MUST** call Compass first for any code question —
|
|
6
|
+
before any `grep`/`sed`/`cat`/Read, and before opening a file whose name they
|
|
7
|
+
already know. Manual file tools are a fallback used **only after** a Compass
|
|
8
|
+
call returns nothing useful, the graph is missing, or the target isn't indexed
|
|
9
|
+
code (stylesheets, config, logs). This is Rule 1 of the agent contract
|
|
10
|
+
(`AGENTS.md`).
|
|
7
11
|
|
|
8
12
|
It runs entirely on your machine, needs no LLM and no external service, and
|
|
9
13
|
stores everything in `.speclaw/` (gitignored). It ships inside speclaw — there
|
|
@@ -33,8 +37,10 @@ a request, not whole files.
|
|
|
33
37
|
| `compass_trace` | Trace a call path between two nodes — how an entrypoint reaches a sink. |
|
|
34
38
|
| `compass_watch` | Keep the index fresh automatically (start/stop a debounced incremental re-index on file change). |
|
|
35
39
|
|
|
36
|
-
If the graph is missing (no `.speclaw/index.db`), run `compass_index` first
|
|
37
|
-
|
|
40
|
+
If the graph is missing (no `.speclaw/index.db`), run `compass_index` first —
|
|
41
|
+
a missing graph is not license to skip Compass. The only legitimate fallbacks
|
|
42
|
+
to Grep/Read: a Compass call returned nothing useful for your query, or the
|
|
43
|
+
target isn't indexed code (stylesheets, JSON/config, markdown, logs).
|
|
38
44
|
|
|
39
45
|
## Project-specific starting points
|
|
40
46
|
|
|
@@ -10,20 +10,32 @@ No non-trivial change lands without a spec change:
|
|
|
10
10
|
|
|
11
11
|
1. **explore** — think an idea through before committing (writes nothing).
|
|
12
12
|
2. **draft** — create `lawbook/changes/<name>/`: `proposal.md`, delta specs under
|
|
13
|
-
`specs/<capability>/spec.md`, `design.md`,
|
|
13
|
+
`specs/<capability>/spec.md`, `design.md`, `tasks.md`, and a `reports/`
|
|
14
|
+
folder.
|
|
14
15
|
3. **build** — implement the tasks in order, keeping code and spec in
|
|
15
|
-
agreement
|
|
16
|
-
4. **sync** —
|
|
17
|
-
`lawbook/specs/` (`lawbook_sync`).
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
agreement, and write the discipline reports under `reports/`.
|
|
17
|
+
4. **sync** — reconcile the delta specs against what was actually built, then
|
|
18
|
+
promote them into the canonical `lawbook/specs/` (`lawbook_sync`). The tool
|
|
19
|
+
is a deterministic copy; the agent does the code↔spec reconciliation first.
|
|
20
|
+
5. **archive** — finalize: reconcile, sync, then move the change to
|
|
21
|
+
`lawbook/changes/archive/` (`lawbook_archive`), **within the same PR** —
|
|
22
|
+
never a post-merge chore. The archive is gated (see below).
|
|
20
23
|
|
|
21
24
|
## Mandatory task steps
|
|
22
25
|
|
|
23
26
|
`tasks.md` MUST include the steps defined in `lawbook/config.yaml` and the
|
|
24
27
|
`spec-tasks-mandatory-steps` rule: feature branch first, tests reviewed and
|
|
25
|
-
run, manual verification executed by the agent,
|
|
26
|
-
the PR. The agent performs the manual testing
|
|
28
|
+
run, manual verification executed by the agent, discipline reports produced,
|
|
29
|
+
docs updated, archive within the PR. The agent performs the manual testing
|
|
30
|
+
itself — never delegates it.
|
|
31
|
+
|
|
32
|
+
## Reports
|
|
33
|
+
|
|
34
|
+
Every change carries a `reports/` folder. `build` writes one report per
|
|
35
|
+
discipline it touched (`backend.md`, `frontend.md`, …) recording what was tested
|
|
36
|
+
and the real results — unit, integration, and end-to-end as applicable — with
|
|
37
|
+
the commands run and their output. It is evidence of testing that travels with
|
|
38
|
+
the change; the archive is blocked until at least one discipline report exists.
|
|
27
39
|
|
|
28
40
|
## Delta specs
|
|
29
41
|
|
|
@@ -39,6 +51,17 @@ the PR. The agent performs the manual testing itself — never delegates it.
|
|
|
39
51
|
Always archive with the `archive` command / `lawbook_archive` tool, never a manual
|
|
40
52
|
`mv` — the tool performs the spec promotion and validation a manual move skips.
|
|
41
53
|
|
|
54
|
+
Before archiving, the agent runs a reconciliation review: it compares what was
|
|
55
|
+
built against the delta specs and, when the code has drifted past the original
|
|
56
|
+
contracts, shows short insights and reconciles the delta specs.
|
|
57
|
+
|
|
58
|
+
`lawbook_archive` is then **gated in the engine** — it refuses to archive (and
|
|
59
|
+
reports the reason) while any task is unchecked, while `reports/` holds no
|
|
60
|
+
discipline report, or while the delta specs are not yet synced into the
|
|
61
|
+
canonical specs. Because the gate covers both the tool and the CLI, a change
|
|
62
|
+
reaches the archive only when it is genuinely complete: reconcile, `sync`, then
|
|
63
|
+
archive.
|
|
64
|
+
|
|
42
65
|
## Amendments to the law
|
|
43
66
|
|
|
44
67
|
The standards in `docs/standards/` are amended like code: through a spec change
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Which scaffolded files speclaw owns vs the user owns. `speclaw update` uses
|
|
2
|
+
// this split to decide what it may overwrite automatically (managed) and what it
|
|
3
|
+
// must leave to the user's agent via a prompt (personalized).
|
|
4
|
+
/**
|
|
5
|
+
* Project-relative trees that hold speclaw's workflow machinery. The user is not
|
|
6
|
+
* meant to edit these, so `speclaw update` overwrites them with the current
|
|
7
|
+
* version (backing up any local edits to `<file>.bak` first).
|
|
8
|
+
*/
|
|
9
|
+
export const MANAGED_TREES = [
|
|
10
|
+
"ai-specs/skills",
|
|
11
|
+
"ai-specs/commands",
|
|
12
|
+
"ai-specs/rules",
|
|
13
|
+
"ai-specs/agents",
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Files filled with project specifics at init. `speclaw update` never rewrites
|
|
17
|
+
* these; when a release changes their speclaw-authored content, update emits an
|
|
18
|
+
* agent prompt to apply the change while preserving the user's content.
|
|
19
|
+
*/
|
|
20
|
+
export const PERSONALIZED = [
|
|
21
|
+
"CLAUDE.md",
|
|
22
|
+
"AGENTS.md",
|
|
23
|
+
"LAWS.md",
|
|
24
|
+
"docs/standards",
|
|
25
|
+
"docs/compass.md",
|
|
26
|
+
"lawbook/config.yaml",
|
|
27
|
+
];
|
|
28
|
+
/** True if a project-relative path sits under a managed tree. */
|
|
29
|
+
export function isManaged(relPath) {
|
|
30
|
+
const norm = relPath.split("\\").join("/");
|
|
31
|
+
return MANAGED_TREES.some((t) => norm === t || norm.startsWith(t + "/"));
|
|
32
|
+
}
|
|
@@ -6,7 +6,7 @@ import { emptyReport, ensureGitignore } from "../../shared/install.js";
|
|
|
6
6
|
import { configureAgent } from "../../shared/agents.js";
|
|
7
7
|
import { installWorkflow } from "../lawbook/register.js";
|
|
8
8
|
import { installPack, loadPacks } from "../tools/packs.js";
|
|
9
|
-
import { writeManifest } from "../../shared/manifest.js";
|
|
9
|
+
import { readManifest, writeManifest } from "../../shared/manifest.js";
|
|
10
10
|
import { pkgVersion } from "../../shared/version.js";
|
|
11
11
|
const ASSETS = assetsDir(import.meta.url);
|
|
12
12
|
// Every {{var}} the foundation templates may reference. Ones the agent didn't
|
|
@@ -70,10 +70,13 @@ function renderFoundation(projectPath, vars, report) {
|
|
|
70
70
|
* @param profile - Project identity and conventions for template rendering.
|
|
71
71
|
* @param packNames - Tool pack names to install (the spec workflow is always installed).
|
|
72
72
|
* @param agents - Agent ids to configure with symlinks + MCP; empty writes content only.
|
|
73
|
+
* @param opts - `refreshManaged: true` overwrites the managed trees (skills,
|
|
74
|
+
* commands, rules, agents) with the current version, backing up local edits to
|
|
75
|
+
* `<file>.bak`. Default (init) is additive — existing files are kept.
|
|
73
76
|
* @returns The install report augmented with the ordered next steps to run.
|
|
74
77
|
* @throws If `projectPath` does not exist, or any pack name is unknown.
|
|
75
78
|
*/
|
|
76
|
-
export function scaffold(projectPath, profile, packNames, agents = []) {
|
|
79
|
+
export function scaffold(projectPath, profile, packNames, agents = [], opts = {}) {
|
|
77
80
|
if (!fs.existsSync(projectPath)) {
|
|
78
81
|
throw new Error(`projectPath does not exist: ${projectPath}`);
|
|
79
82
|
}
|
|
@@ -83,16 +86,26 @@ export function scaffold(projectPath, profile, packNames, agents = []) {
|
|
|
83
86
|
throw new Error(`Unknown packs: ${unknown.join(", ")}`);
|
|
84
87
|
const report = { ...emptyReport(), nextSteps: [] };
|
|
85
88
|
const vars = { ...FOUNDATION_DEFAULTS, ...profile };
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
// Managed trees (MANAGED_TREES) carry speclaw's workflow logic and may be
|
|
90
|
+
// overwritten on update; the foundation (personalized) is always additive.
|
|
91
|
+
const record = {};
|
|
92
|
+
const managedOpts = {
|
|
93
|
+
overwrite: Boolean(opts.refreshManaged),
|
|
94
|
+
projectPath,
|
|
95
|
+
baselines: readManifest(projectPath)?.baselines ?? {},
|
|
96
|
+
record,
|
|
97
|
+
};
|
|
98
|
+
renderFoundation(projectPath, vars, report); // personalized — never overwritten
|
|
99
|
+
installWorkflow(projectPath, vars, report, managedOpts); // managed
|
|
88
100
|
for (const name of packNames)
|
|
89
|
-
installPack(projectPath, name, vars, report); //
|
|
101
|
+
installPack(projectPath, name, vars, report, managedOpts); // managed
|
|
90
102
|
ensureGitignore(projectPath, ".speclaw/", "speclaw local code Compass (never commit)", report);
|
|
91
103
|
for (const id of agents)
|
|
92
104
|
configureAgent(projectPath, id, report); // only the chosen agents
|
|
93
|
-
// Record what was installed so `speclaw update` can re-apply
|
|
94
|
-
//
|
|
95
|
-
|
|
105
|
+
// Record what was installed so `speclaw update` can re-apply these packs and
|
|
106
|
+
// gate feature migrations by version, plus the managed-file baselines that let
|
|
107
|
+
// a later update tell user edits from stale files.
|
|
108
|
+
writeManifest(projectPath, pkgVersion(), packNames, record);
|
|
96
109
|
report.nextSteps = [
|
|
97
110
|
"Run the `lawbook_init` tool to set up the spec-driven workflow (creates lawbook/). No external CLI needed — it's built into speclaw.",
|
|
98
111
|
"Run the `compass_index` tool to build the local code graph (.speclaw/). No install, no LLM — it's built into speclaw. Re-run it after significant edits.",
|
|
@@ -4,7 +4,8 @@ description: Finalize a completed change — sync specs into canonical, then arc
|
|
|
4
4
|
|
|
5
5
|
Archive the completed change: $ARGUMENTS
|
|
6
6
|
|
|
7
|
-
Follow the `archive` skill: confirm every task is done and gates are green,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
the
|
|
7
|
+
Follow the `archive` skill: confirm every task is done and gates are green, run
|
|
8
|
+
the reconciliation review (recommend a sync with short insights if the code
|
|
9
|
+
drifted past the contracts), run `lawbook_validate`, then `lawbook_archive` with
|
|
10
|
+
today's date (YYYY-MM-DD). It syncs the specs and moves the change to
|
|
11
|
+
`lawbook/changes/archive/`. Never move the folder by hand.
|
|
@@ -4,6 +4,7 @@ description: Promote a change's delta specs into the canonical specs, without ar
|
|
|
4
4
|
|
|
5
5
|
Sync the change's specs into canonical: $ARGUMENTS
|
|
6
6
|
|
|
7
|
-
Follow the `sync` skill:
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Follow the `sync` skill: reconcile the delta specs against what was actually
|
|
8
|
+
built (branch diff + code graph), validate the change (`lawbook_validate`), then
|
|
9
|
+
run `lawbook_sync` to promote each delta spec into `lawbook/specs/`. Report what
|
|
10
|
+
you reconciled and what was promoted; leave the change active.
|
|
@@ -21,6 +21,9 @@ steps, branch convention, and testing/documentation requirements.
|
|
|
21
21
|
(see `docs/standards/testing-standards.md`).
|
|
22
22
|
- Perform manual verification of the behavior — **the agent executes this
|
|
23
23
|
itself, never the user.**
|
|
24
|
+
- Produce the discipline reports under `reports/` (`backend.md`, `frontend.md`,
|
|
25
|
+
… as relevant) with the unit/integration/e2e results for what the feature
|
|
26
|
+
touched.
|
|
24
27
|
- Update the technical documentation the change touches.
|
|
25
28
|
- Archive the change within the same PR (the `archive` command / `lawbook_archive`
|
|
26
29
|
tool).
|
|
@@ -35,3 +38,8 @@ not complete until the agent has verified it.
|
|
|
35
38
|
|
|
36
39
|
A change is not done until it is archived with `lawbook_archive` (never a manual
|
|
37
40
|
`mv`). The archive lands in the same PR that implements the change.
|
|
41
|
+
|
|
42
|
+
`lawbook_archive` is gated: it refuses to archive while any task is unchecked,
|
|
43
|
+
while `reports/` has no discipline report, or while the delta specs are not yet
|
|
44
|
+
synced into the canonical specs. Resolve those first — the gate is enforced in
|
|
45
|
+
the engine, so a manual `mv` only hides an incomplete change.
|
|
@@ -9,14 +9,39 @@ Close out a completed change: its delta specs become canonical and the change
|
|
|
9
9
|
folder moves to `lawbook/changes/archive/`. This is part of the PR that
|
|
10
10
|
implements the change, not a post-merge chore.
|
|
11
11
|
|
|
12
|
+
`lawbook_archive` is **gated** — the engine refuses to archive (and reports the
|
|
13
|
+
reason) while any task is unchecked, while `reports/` holds no discipline report,
|
|
14
|
+
or while the delta specs are not yet synced into the canonical specs. So archive
|
|
15
|
+
is the last step of a completed change: reconcile, sync, then archive.
|
|
16
|
+
|
|
12
17
|
## Steps
|
|
13
18
|
|
|
14
19
|
1. Confirm the change is truly done: every task in `tasks.md` checked, quality
|
|
15
|
-
gates green, behavior verified
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
gates green, behavior verified, and the discipline reports written under
|
|
21
|
+
`reports/`.
|
|
22
|
+
|
|
23
|
+
2. **Reconciliation review (agent-executed).** Run the reconciliation from the
|
|
24
|
+
`sync` skill: reconstruct what was built (branch diff since draft +
|
|
25
|
+
`compass_explore` / `compass_impact`) and compare it to the change's delta
|
|
26
|
+
specs.
|
|
27
|
+
- **If the code drifted past the contracts:** show short insights — a tight
|
|
28
|
+
bullet list of what was built outside the delta specs and why it matters
|
|
29
|
+
(e.g. "DB path renamed to `data/app.db` + auto-migration — infra behavior
|
|
30
|
+
absent from the spec") — and reconcile the delta specs (write the drift
|
|
31
|
+
in). Drift left unreconciled cannot be archived: the specs-synced gate will
|
|
32
|
+
block it.
|
|
33
|
+
- **If nothing drifted:** say so and continue.
|
|
34
|
+
|
|
35
|
+
3. Run `lawbook_validate`, then `lawbook_sync` to promote the delta specs into
|
|
36
|
+
`lawbook/specs/`. This is required, not optional: `lawbook_archive` refuses
|
|
37
|
+
unless the canonical specs already match the delta specs.
|
|
38
|
+
|
|
39
|
+
4. Run the `lawbook_archive` tool with the change name and today's date
|
|
40
|
+
(`YYYY-MM-DD`). It re-checks the gate deterministically and, if it passes,
|
|
41
|
+
moves `lawbook/changes/<name>/` to `lawbook/changes/archive/<date>-<name>/`.
|
|
42
|
+
If it refuses, resolve the reported blockers (unchecked tasks, missing
|
|
43
|
+
reports, unsynced specs) and retry.
|
|
44
|
+
|
|
45
|
+
5. Report the archive path, what you reconciled (or that nothing drifted), and
|
|
46
|
+
the promoted specs. Never move the folder by hand — a manual `mv` skips the
|
|
47
|
+
gate and hides an incomplete change.
|
|
@@ -43,7 +43,20 @@ Run them yourself and report real output. A red gate blocks completion.
|
|
|
43
43
|
Exercise the behavior (endpoint/UI/CLI) yourself where feasible — do not
|
|
44
44
|
delegate manual testing to the user. Record what you verified.
|
|
45
45
|
|
|
46
|
-
## Step 5 —
|
|
46
|
+
## Step 5 — Write the discipline reports (mandatory)
|
|
47
|
+
|
|
48
|
+
Record the evidence of testing under `lawbook/changes/<name>/reports/`, one file
|
|
49
|
+
per discipline the change touched (`backend.md`, `frontend.md`, …). Each report
|
|
50
|
+
states what was tested and the real results — unit, integration, and end-to-end
|
|
51
|
+
as applicable — with the commands run and their output and a one-line verdict.
|
|
52
|
+
If a test kind does not yet apply (e.g. no unit runner), say so and record the
|
|
53
|
+
gates and manual verification that stood in. Omit disciplines the change did not
|
|
54
|
+
touch. The archive is blocked until at least one discipline report exists.
|
|
55
|
+
|
|
56
|
+
## Step 6 — Hand off
|
|
47
57
|
|
|
48
58
|
When every task is checked and gates are green, tell the user the change is
|
|
49
|
-
ready to `sync` and `archive`.
|
|
59
|
+
ready to `sync` and `archive`. Keep the delta specs current as you build, but
|
|
60
|
+
know that `sync` formally reconciles the delta specs against what was actually
|
|
61
|
+
built — so behavior that drifted past the original spec is caught there, not
|
|
62
|
+
left to chance.
|
|
@@ -50,8 +50,12 @@ Create under `lawbook/changes/<name>/`:
|
|
|
50
50
|
behind the decision. For a small change, keep it short — but write it.
|
|
51
51
|
- **tasks.md** — ordered, checkable steps. MUST include the mandatory steps
|
|
52
52
|
from `lawbook/config.yaml` (feature branch first; tests reviewed and run;
|
|
53
|
-
manual verification executed by the agent;
|
|
54
|
-
PR).
|
|
53
|
+
manual verification executed by the agent; discipline reports produced; docs
|
|
54
|
+
updated; archive within the PR).
|
|
55
|
+
- **reports/** — create the folder with a short `reports/README.md` naming the
|
|
56
|
+
discipline reports (`backend.md`, `frontend.md`, … as relevant) that `build`
|
|
57
|
+
will fill with real test results. Every change ships this folder; archive is
|
|
58
|
+
blocked until it holds at least one discipline report.
|
|
55
59
|
|
|
56
60
|
## Step 4 — Validate
|
|
57
61
|
|
|
@@ -9,13 +9,33 @@ Update the project's canonical specifications (`lawbook/specs/`) with a change's
|
|
|
9
9
|
delta specs, without archiving the change. Use this when the specs should
|
|
10
10
|
become the source of truth but the change isn't finished (e.g. multi-PR work).
|
|
11
11
|
|
|
12
|
+
`lawbook_sync` is a deterministic copy — it is blind to the code. So before
|
|
13
|
+
promoting, YOU reconcile the delta specs against what was actually built, so the
|
|
14
|
+
specs that become canonical describe reality, not just the original draft.
|
|
15
|
+
|
|
12
16
|
## Steps
|
|
13
17
|
|
|
14
18
|
1. Confirm which change to sync (run `lawbook_list` if unsure).
|
|
15
|
-
|
|
19
|
+
|
|
20
|
+
2. **Reconcile code → delta specs (agent-executed).** Before promoting, compare
|
|
21
|
+
what was built against the change's delta specs:
|
|
22
|
+
- Reconstruct what shipped: `git diff <branch-point>...HEAD` for the change's
|
|
23
|
+
branch, then `compass_explore` / `compass_impact` on the touched symbols to
|
|
24
|
+
understand behavior, not just changed lines.
|
|
25
|
+
- Diff intent vs reality: list behavior that is implemented but missing from,
|
|
26
|
+
or contradicted by, `lawbook/changes/<name>/specs/**`.
|
|
27
|
+
- Write the gaps into the delta specs — normative `SHALL`/`MUST` requirements
|
|
28
|
+
under `### Requirement:` and `#### Scenario:` acceptance criteria — so the
|
|
29
|
+
contract matches what was built. Capture only behavior that actually
|
|
30
|
+
exists; never invent scope that was not implemented.
|
|
31
|
+
- If nothing drifted, make no edits and say so.
|
|
32
|
+
|
|
33
|
+
3. Run `lawbook_validate` for the change; do not sync a change whose specs are
|
|
16
34
|
invalid.
|
|
17
|
-
|
|
35
|
+
|
|
36
|
+
4. Run the `lawbook_sync` tool for the change. It copies each
|
|
18
37
|
`lawbook/changes/<name>/specs/<capability>/spec.md` over the canonical
|
|
19
38
|
`lawbook/specs/<capability>/spec.md` and reports what it promoted.
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
|
|
40
|
+
5. Report to the user what you reconciled (or that nothing drifted) and the
|
|
41
|
+
promoted files. The change stays active — `archive` it when it's fully done.
|
|
@@ -26,6 +26,7 @@ mandatory_task_steps:
|
|
|
26
26
|
- "Review and update the affected tests."
|
|
27
27
|
- "Run the quality gates and verify they pass (see docs/standards/testing-standards.md)."
|
|
28
28
|
- "Perform manual verification of the behavior — the agent executes this itself, never the user."
|
|
29
|
+
- "Produce the discipline reports under reports/ (unit/integration/e2e results for what the feature touched)."
|
|
29
30
|
- "Update the technical documentation touched by the change."
|
|
30
31
|
- "Archive the change within the same PR (lawbook:archive)."
|
|
31
32
|
|
|
@@ -185,6 +186,56 @@ export function specSync(projectPath, change) {
|
|
|
185
186
|
walk(changeSpecs);
|
|
186
187
|
return { change, promoted };
|
|
187
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Deterministic completeness checks that gate archiving a change. Returns the
|
|
191
|
+
* blocking reasons; an empty array means the change may be archived.
|
|
192
|
+
*
|
|
193
|
+
* A change is blocked when any task is still unchecked, when it has no discipline
|
|
194
|
+
* report under reports/, or when its delta specs are not synced — the canonical
|
|
195
|
+
* spec is missing for, or differs from, a delta (meaning sync was not run after
|
|
196
|
+
* the last spec edit). The reports/README.md scaffold does not count as a report.
|
|
197
|
+
*
|
|
198
|
+
* @param projectPath - Absolute path to the project root.
|
|
199
|
+
* @param change - Change name (folder under lawbook/changes/).
|
|
200
|
+
* @returns Human-readable blockers; empty when the change is ready to archive.
|
|
201
|
+
*/
|
|
202
|
+
export function specArchivePreconditions(projectPath, change) {
|
|
203
|
+
const root = specRoot(projectPath);
|
|
204
|
+
const changeDir = path.join(root, "changes", change);
|
|
205
|
+
if (!fs.existsSync(changeDir))
|
|
206
|
+
return [`change "${change}" not found under lawbook/changes/`];
|
|
207
|
+
const blockers = [];
|
|
208
|
+
// 1. Every task must be checked.
|
|
209
|
+
const tasksPath = path.join(changeDir, "tasks.md");
|
|
210
|
+
if (!fs.existsSync(tasksPath)) {
|
|
211
|
+
blockers.push("missing tasks.md");
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const unchecked = (fs.readFileSync(tasksPath, "utf8").match(/^\s*[-*]\s+\[ \]/gm) ?? []).length;
|
|
215
|
+
if (unchecked > 0)
|
|
216
|
+
blockers.push(`${unchecked} unchecked task(s) in tasks.md`);
|
|
217
|
+
}
|
|
218
|
+
// 2. At least one discipline report must exist (README.md scaffold aside).
|
|
219
|
+
const reportsDir = path.join(changeDir, "reports");
|
|
220
|
+
const reports = fs.existsSync(reportsDir)
|
|
221
|
+
? fs.readdirSync(reportsDir).filter((n) => n.endsWith(".md") && n.toLowerCase() !== "readme.md")
|
|
222
|
+
: [];
|
|
223
|
+
if (reports.length === 0) {
|
|
224
|
+
blockers.push("no discipline report under reports/ (build must record what was tested)");
|
|
225
|
+
}
|
|
226
|
+
// 3. Delta specs must already be synced into the canonical specs.
|
|
227
|
+
for (const file of deltaSpecFiles(changeDir)) {
|
|
228
|
+
const rel = path.relative(path.join(changeDir, "specs"), file);
|
|
229
|
+
const canonical = path.join(root, "specs", rel);
|
|
230
|
+
if (!fs.existsSync(canonical)) {
|
|
231
|
+
blockers.push(`spec not synced: lawbook/specs/${rel} missing (run sync first)`);
|
|
232
|
+
}
|
|
233
|
+
else if (fs.readFileSync(file, "utf8") !== fs.readFileSync(canonical, "utf8")) {
|
|
234
|
+
blockers.push(`spec not synced: lawbook/specs/${rel} differs from the delta (run sync first)`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return blockers;
|
|
238
|
+
}
|
|
188
239
|
/**
|
|
189
240
|
* Finalize a change: promote its delta specs (via {@link specSync}), then move
|
|
190
241
|
* it to changes/archive/<date>-<name>/.
|
|
@@ -193,13 +244,18 @@ export function specSync(projectPath, change) {
|
|
|
193
244
|
* @param change - Change name (folder under lawbook/changes/).
|
|
194
245
|
* @param date - Archive date prefix, formatted YYYY-MM-DD.
|
|
195
246
|
* @returns The promoted specs and the archive destination path.
|
|
196
|
-
* @throws If the change does not exist,
|
|
247
|
+
* @throws If the change does not exist, the archive target already exists, or
|
|
248
|
+
* any archive precondition (see {@link specArchivePreconditions}) is unmet.
|
|
197
249
|
*/
|
|
198
250
|
export function specArchive(projectPath, change, date) {
|
|
199
251
|
const root = specRoot(projectPath);
|
|
200
252
|
const changeDir = path.join(root, "changes", change);
|
|
201
253
|
if (!fs.existsSync(changeDir))
|
|
202
254
|
throw new Error(`change "${change}" not found`);
|
|
255
|
+
const blockers = specArchivePreconditions(projectPath, change);
|
|
256
|
+
if (blockers.length > 0) {
|
|
257
|
+
throw new Error(`cannot archive "${change}" — resolve first:\n${blockers.map((b) => ` - ${b}`).join("\n")}`);
|
|
258
|
+
}
|
|
203
259
|
const { promoted } = specSync(projectPath, change);
|
|
204
260
|
const archiveDir = path.join(root, "changes", "archive", `${date}-${change}`);
|
|
205
261
|
fs.mkdirSync(path.dirname(archiveDir), { recursive: true });
|
|
@@ -10,11 +10,11 @@ const ASSETS = assetsDir(import.meta.url);
|
|
|
10
10
|
* the draft/build/sync/archive/explore skills, the /spec commands, and the
|
|
11
11
|
* mandatory-task-steps rule. Always installed — it's the core workflow.
|
|
12
12
|
*/
|
|
13
|
-
export function installWorkflow(projectPath, vars, report) {
|
|
13
|
+
export function installWorkflow(projectPath, vars, report, opts) {
|
|
14
14
|
const aiSpecs = path.join(projectPath, "ai-specs");
|
|
15
|
-
copyRendered(path.join(ASSETS, "skills"), path.join(aiSpecs, "skills"), vars, report);
|
|
16
|
-
copyRendered(path.join(ASSETS, "commands"), path.join(aiSpecs, "commands", "lawbook"), vars, report);
|
|
17
|
-
copyRendered(path.join(ASSETS, "rules"), path.join(aiSpecs, "rules"), vars, report);
|
|
15
|
+
copyRendered(path.join(ASSETS, "skills"), path.join(aiSpecs, "skills"), vars, report, opts);
|
|
16
|
+
copyRendered(path.join(ASSETS, "commands"), path.join(aiSpecs, "commands", "lawbook"), vars, report, opts);
|
|
17
|
+
copyRendered(path.join(ASSETS, "rules"), path.join(aiSpecs, "rules"), vars, report, opts);
|
|
18
18
|
}
|
|
19
19
|
// ─── The spec module: speclaw's own spec-driven workflow (no external OpenSpec) ───
|
|
20
20
|
// Mechanical operations behind the draft/build/sync/archive/explore commands.
|
|
@@ -21,9 +21,11 @@ export function loadPacks() {
|
|
|
21
21
|
* @param name - Pack name to install (key in the manifest).
|
|
22
22
|
* @param vars - Template variables applied while copying the pack's assets.
|
|
23
23
|
* @param report - Mutated in place with the copy results.
|
|
24
|
+
* @param opts - Overwrite/baseline behavior forwarded to {@link copyRendered}
|
|
25
|
+
* (packs write into managed trees, so update refreshes them).
|
|
24
26
|
* @throws If no pack with the given name exists in the manifest.
|
|
25
27
|
*/
|
|
26
|
-
export function installPack(projectPath, name, vars, report) {
|
|
28
|
+
export function installPack(projectPath, name, vars, report, opts) {
|
|
27
29
|
const packs = loadPacks();
|
|
28
30
|
const def = packs[name];
|
|
29
31
|
if (!def) {
|
|
@@ -33,11 +35,11 @@ export function installPack(projectPath, name, vars, report) {
|
|
|
33
35
|
const aiSpecs = path.join(projectPath, "ai-specs");
|
|
34
36
|
for (const sub of fs.readdirSync(packRoot, { withFileTypes: true })) {
|
|
35
37
|
if (sub.isDirectory()) {
|
|
36
|
-
copyRendered(path.join(packRoot, sub.name), path.join(aiSpecs, sub.name), vars, report);
|
|
38
|
+
copyRendered(path.join(packRoot, sub.name), path.join(aiSpecs, sub.name), vars, report, opts);
|
|
37
39
|
}
|
|
38
40
|
else if (sub.name.endsWith(".md")) {
|
|
39
41
|
// loose .md at pack root = agent definitions
|
|
40
|
-
copyRendered(packRoot, path.join(aiSpecs, "agents"), vars, report);
|
|
42
|
+
copyRendered(packRoot, path.join(aiSpecs, "agents"), vars, report, opts);
|
|
41
43
|
break;
|
|
42
44
|
}
|
|
43
45
|
}
|
package/dist/shared/install.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { render } from "./render.js";
|
|
4
5
|
/** Create a fresh, empty {@link InstallReport} to accumulate results into. */
|
|
5
6
|
export function emptyReport() {
|
|
6
|
-
return { written: [], skipped: [], symlinks: [], unresolvedVars: [] };
|
|
7
|
+
return { written: [], skipped: [], backedUp: [], symlinks: [], unresolvedVars: [] };
|
|
8
|
+
}
|
|
9
|
+
/** SHA-256 of a file's intended content, used to track managed-file baselines. */
|
|
10
|
+
export function sha256(content) {
|
|
11
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
7
12
|
}
|
|
8
13
|
/**
|
|
9
14
|
* Recursively copy a source tree into a destination, rendering {{var}}
|
|
@@ -13,33 +18,60 @@ export function emptyReport() {
|
|
|
13
18
|
* @param srcDir - Source directory tree to copy from.
|
|
14
19
|
* @param destDir - Destination directory (created if missing).
|
|
15
20
|
* @param vars - Placeholder values used to render `.md`/`.mdc` files.
|
|
16
|
-
* @param report - Report mutated in place with written, skipped,
|
|
21
|
+
* @param report - Report mutated in place with written, skipped, backed-up, and
|
|
22
|
+
* unresolved-var entries.
|
|
23
|
+
* @param opts - Overwrite/baseline behavior; omitted means additive (skip existing).
|
|
17
24
|
*/
|
|
18
|
-
export function copyRendered(srcDir, destDir, vars, report) {
|
|
25
|
+
export function copyRendered(srcDir, destDir, vars, report, opts) {
|
|
19
26
|
fs.mkdirSync(destDir, { recursive: true });
|
|
20
27
|
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
21
28
|
const src = path.join(srcDir, entry.name);
|
|
22
29
|
const dest = path.join(destDir, entry.name);
|
|
23
30
|
if (entry.isDirectory()) {
|
|
24
|
-
copyRendered(src, dest, vars, report);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (fs.existsSync(dest)) {
|
|
28
|
-
report.skipped.push(dest);
|
|
31
|
+
copyRendered(src, dest, vars, report, opts);
|
|
29
32
|
continue;
|
|
30
33
|
}
|
|
34
|
+
// Compute the content speclaw wants at dest (rendered for md/mdc, raw otherwise).
|
|
35
|
+
let content;
|
|
31
36
|
if (entry.name.endsWith(".md") || entry.name.endsWith(".mdc")) {
|
|
32
37
|
const { output, unresolved } = render(fs.readFileSync(src, "utf8"), vars);
|
|
33
38
|
unresolved.forEach((v) => {
|
|
34
39
|
if (!report.unresolvedVars.includes(v))
|
|
35
40
|
report.unresolvedVars.push(v);
|
|
36
41
|
});
|
|
37
|
-
|
|
42
|
+
content = output;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
content = fs.readFileSync(src);
|
|
46
|
+
}
|
|
47
|
+
const rel = opts?.projectPath ? path.relative(opts.projectPath, dest) : dest;
|
|
48
|
+
const newSha = sha256(content);
|
|
49
|
+
if (fs.existsSync(dest)) {
|
|
50
|
+
if (!opts?.overwrite) {
|
|
51
|
+
report.skipped.push(dest);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const current = fs.readFileSync(dest);
|
|
55
|
+
if (sha256(current) === newSha) {
|
|
56
|
+
// Already the current version — nothing to write, but keep the baseline.
|
|
57
|
+
if (opts.record)
|
|
58
|
+
opts.record[rel] = newSha;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const baseline = opts.baselines?.[rel];
|
|
62
|
+
if (!baseline || sha256(current) !== baseline) {
|
|
63
|
+
// Diverged from what we last wrote (or unknown) — preserve the user's copy.
|
|
64
|
+
fs.copyFileSync(dest, dest + ".bak");
|
|
65
|
+
report.backedUp.push(dest);
|
|
66
|
+
}
|
|
67
|
+
fs.writeFileSync(dest, content);
|
|
38
68
|
}
|
|
39
69
|
else {
|
|
40
|
-
fs.
|
|
70
|
+
fs.writeFileSync(dest, content);
|
|
41
71
|
}
|
|
42
72
|
report.written.push(dest);
|
|
73
|
+
if (opts?.record)
|
|
74
|
+
opts.record[rel] = newSha;
|
|
43
75
|
}
|
|
44
76
|
}
|
|
45
77
|
/**
|
package/dist/shared/manifest.js
CHANGED
|
@@ -15,6 +15,9 @@ export function readManifest(projectPath) {
|
|
|
15
15
|
return {
|
|
16
16
|
version: String(m.version ?? "0.0.0"),
|
|
17
17
|
packs: Array.isArray(m.packs) ? m.packs.map(String) : [],
|
|
18
|
+
baselines: m.baselines && typeof m.baselines === "object"
|
|
19
|
+
? m.baselines
|
|
20
|
+
: {},
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
23
|
catch {
|
|
@@ -28,11 +31,13 @@ export function readManifest(projectPath) {
|
|
|
28
31
|
* @param projectPath - Project root to write into.
|
|
29
32
|
* @param version - The speclaw version doing the write.
|
|
30
33
|
* @param packs - Pack names installed in this run.
|
|
34
|
+
* @param baselines - Managed-file hashes to merge over the recorded ones.
|
|
31
35
|
*/
|
|
32
|
-
export function writeManifest(projectPath, version, packs) {
|
|
36
|
+
export function writeManifest(projectPath, version, packs, baselines = {}) {
|
|
33
37
|
const prev = readManifest(projectPath);
|
|
34
38
|
const merged = Array.from(new Set([...(prev?.packs ?? []), ...packs]));
|
|
39
|
+
const mergedBaselines = { ...(prev?.baselines ?? {}), ...baselines };
|
|
35
40
|
const p = manifestPath(projectPath);
|
|
36
41
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
37
|
-
fs.writeFileSync(p, JSON.stringify({ version, packs: merged }, null, 2) + "\n");
|
|
42
|
+
fs.writeFileSync(p, JSON.stringify({ version, packs: merged, baselines: mergedBaselines }, null, 2) + "\n");
|
|
38
43
|
}
|