simt-dev 0.6.2 → 0.7.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/CHANGELOG.md +83 -1
- package/README.md +46 -17
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -6
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
- package/docs/decisions/ADR-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md +94 -0
- package/docs/decisions/ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md +95 -0
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +49 -0
- package/docs/decisions/ADR-103-co-projected-duplicates-merge-down.md +74 -0
- package/docs/decisions/ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md +76 -0
- package/docs/decisions/ADR-105-the-json-ceiling-shrinks-arrays-and-never-cuts-bytes.md +44 -0
- package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- package/docs/decisions/README.md +53 -0
- package/package.json +4 -2
- package/rules/core.md +2 -2
- package/rules/mdc/preview-postflight.mdc +5 -6
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +10 -11
- package/rules/stacks/cloudflare-workers.md +1 -1
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +4 -3
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/react-native.md +3 -5
- package/rules/stacks/react-web.md +9 -9
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +238 -15
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/policy.mjs +81 -5
- package/src/agenthooks/prompt.mjs +6 -2
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shell.mjs +523 -0
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/argv.mjs +62 -9
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/mine.mjs +48 -1
- package/src/bench/probes.mjs +5 -6
- package/src/bench/report.mjs +243 -80
- package/src/bench/run.mjs +12 -11
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +6 -0
- package/src/ci/workflow.mjs +16 -7
- package/src/commands/agent.mjs +206 -0
- package/src/commands/agenthook.mjs +133 -17
- package/src/commands/bench.mjs +2 -1
- package/src/commands/ci.mjs +28 -3
- package/src/commands/compare.mjs +8 -7
- package/src/commands/completions.mjs +27 -0
- package/src/commands/decide.mjs +20 -6
- package/src/commands/doctor.mjs +367 -0
- package/src/commands/evidence.mjs +13 -3
- package/src/commands/explain.mjs +52 -0
- package/src/commands/gate.mjs +85 -19
- package/src/commands/graph.mjs +18 -5
- package/src/commands/help.mjs +96 -26
- package/src/commands/hooks.mjs +258 -67
- package/src/commands/ignore.mjs +152 -0
- package/src/commands/impact.mjs +4 -2
- package/src/commands/index.mjs +69 -102
- package/src/commands/init.mjs +239 -37
- package/src/commands/lease.mjs +304 -0
- package/src/commands/lessons.mjs +77 -9
- package/src/commands/map.mjs +33 -18
- package/src/commands/mcp.mjs +2 -2
- package/src/commands/pack.mjs +9 -0
- package/src/commands/plan.mjs +13 -1
- package/src/commands/plugin.mjs +17 -2
- package/src/commands/prbody.mjs +45 -0
- package/src/commands/repoindex.mjs +4 -0
- package/src/commands/rules.mjs +219 -105
- package/src/commands/shared.mjs +36 -5
- package/src/commands/skills.mjs +329 -206
- package/src/commands/skillscompile.mjs +8 -7
- package/src/commands/skillsinstall.mjs +112 -30
- package/src/commands/slice.mjs +712 -0
- package/src/commands/status.mjs +62 -0
- package/src/commands/sync.mjs +146 -36
- package/src/commands/tests.mjs +5 -2
- package/src/commands/undo.mjs +114 -0
- package/src/commands/upgrade.mjs +103 -9
- package/src/commands/why.mjs +10 -4
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -1
- package/src/completions.mjs +196 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +254 -0
- package/src/distill/distill.mjs +3 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +270 -0
- package/src/federation/config.mjs +44 -0
- package/src/graph/load.mjs +52 -24
- package/src/hooks/capability.mjs +14 -1
- package/src/hooks/dispatcher.mjs +441 -9
- package/src/hooks/gates.mjs +14 -3
- package/src/hooks/resolve.mjs +23 -3
- package/src/hooks/shim.mjs +12 -7
- package/src/identity.mjs +67 -14
- package/src/ignore.mjs +133 -0
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +372 -55
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +545 -0
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +195 -0
- package/src/ledger/index.mjs +91 -25
- package/src/mcp/resources.mjs +5 -3
- package/src/mcp/server.mjs +86 -13
- package/src/mcp/tools.mjs +340 -264
- package/src/memory/decide.mjs +98 -38
- package/src/memory/lessons.mjs +180 -33
- package/src/name.mjs +9 -0
- package/src/output/index.mjs +82 -12
- package/src/package-version.mjs +49 -1
- package/src/packs/manage.mjs +13 -4
- package/src/plan/index.mjs +37 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +33 -8
- package/src/prbody.mjs +253 -0
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +21 -14
- package/src/projection/manifest.mjs +82 -21
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +54 -3
- package/src/projection/shared.mjs +4 -1
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +69 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +5 -0
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +4 -2
- package/src/secrets.mjs +374 -0
- package/src/state.mjs +76 -0
- package/src/status.mjs +157 -0
- package/src/toolwords.mjs +5 -0
- package/src/tsconfig.mjs +39 -99
- package/templates/plan/tracker.json +1 -1
package/src/commands/init.mjs
CHANGED
|
@@ -17,21 +17,26 @@
|
|
|
17
17
|
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
|
-
import { mkdir, readFile, symlink
|
|
20
|
+
import { mkdir, readFile, symlink } from 'node:fs/promises';
|
|
21
|
+
import { commitJournal, ignoreJournal, journaledWriteFile } from '../journal.mjs';
|
|
22
|
+
import { invalidateCheckCache } from '../status.mjs';
|
|
21
23
|
import { dirname, join } from 'node:path';
|
|
22
24
|
|
|
23
|
-
import { EXIT, emit, emitJson, fail, isJson, note } from '../output/index.mjs';
|
|
25
|
+
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
24
26
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
25
27
|
import { rulePaths, scanDocument, summarise } from '../projection/conflicts.mjs';
|
|
26
|
-
import { MANIFEST, buildManifest, manifestPath, readManifest, writeManifest, planGates } from '../projection/manifest.mjs';
|
|
28
|
+
import { MANIFEST, buildManifest, manifestPath, manifestText, readManifest, writeManifest, planGates } from '../projection/manifest.mjs';
|
|
29
|
+
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
27
30
|
import { readCapability } from '../hooks/capability.mjs';
|
|
28
31
|
import { readHooksState } from '../compare/hooksstate.mjs';
|
|
29
32
|
import { planWorkflow, renderWorkflow, WORKFLOWS_DIR } from '../ci/workflow.mjs';
|
|
30
33
|
import { planDependabot, renderDependabot } from '../ci/dependabot.mjs';
|
|
31
|
-
import { planTsconfigBase
|
|
34
|
+
import { planTsconfigBase } from '../tsconfig.mjs';
|
|
32
35
|
import {
|
|
33
36
|
IDENTITY_FILE,
|
|
34
37
|
INCLUDE_KEY,
|
|
38
|
+
gitignoreWithIdentity,
|
|
39
|
+
identityIgnored,
|
|
35
40
|
ignoreIdentity,
|
|
36
41
|
includeOwnIdentity,
|
|
37
42
|
includesOwnIdentity,
|
|
@@ -41,10 +46,15 @@ import {
|
|
|
41
46
|
import { pointerState, pointerTargets } from '../projection/project.mjs';
|
|
42
47
|
import { surfacesLine } from '../projection/surfaces.mjs';
|
|
43
48
|
import { plan, provenanceDigest, PRIMARY_TARGET, LOCAL_PROFILE_DIR } from '../projection/project.mjs';
|
|
49
|
+
import { planAgents, writeAgentPlan, writeSkillFile } from '../agents/render.mjs';
|
|
50
|
+
import { computeLock } from '../projection/lock.mjs';
|
|
44
51
|
import { parse as parseRegion } from '../projection/markers.mjs';
|
|
52
|
+
import { reindex, SQLITE_FILE } from '../index/store.mjs';
|
|
45
53
|
import { CLI } from '../name.mjs';
|
|
46
54
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
47
55
|
import { packageVersion, hasNpm } from '../package-version.mjs';
|
|
56
|
+
import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
|
|
57
|
+
import { PACKAGE_NAME } from '../name.mjs';
|
|
48
58
|
|
|
49
59
|
const run = promisify(execFile);
|
|
50
60
|
|
|
@@ -65,6 +75,20 @@ export async function initCommand(positional, flags) {
|
|
|
65
75
|
// a repository without node_modules obtains the binary in CI.
|
|
66
76
|
const version = packageVersion();
|
|
67
77
|
const npm = await hasNpm(root);
|
|
78
|
+
// Q31: the CI install step and the plan row that names it read the same
|
|
79
|
+
// resolver as the card and the gates — lockfile first, `packageManager` next.
|
|
80
|
+
// `manager` is the Node manager id or null (no package.json: binary channel).
|
|
81
|
+
let manager = null;
|
|
82
|
+
if (npm) {
|
|
83
|
+
let packageManager = null;
|
|
84
|
+
try {
|
|
85
|
+
const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
86
|
+
if (typeof pkg.packageManager === 'string') packageManager = pkg.packageManager;
|
|
87
|
+
} catch {
|
|
88
|
+
// Unreadable here is reported by the manifest path; the default stays npm.
|
|
89
|
+
}
|
|
90
|
+
manager = resolveNodeRunner({ facts: detected.facts, packageManager }).id;
|
|
91
|
+
}
|
|
68
92
|
const existing = await readManifest(root);
|
|
69
93
|
const { manifest: current, exists, error } = existing;
|
|
70
94
|
if (error) {
|
|
@@ -84,9 +108,15 @@ export async function initCommand(positional, flags) {
|
|
|
84
108
|
const manifest = buildManifest({ root, stack: detected.stack, sha, version, existing: current, capability, facts: detected.facts });
|
|
85
109
|
const findings = await scanRepo(root);
|
|
86
110
|
const summary = summarise(findings);
|
|
111
|
+
// Every repo-file write is journaled, so `undo` can restore this run (R49).
|
|
112
|
+
// Created before `plan()` because the skill file must be on disk when
|
|
113
|
+
// `installedSkills` reads the directory for the region's disclosure section.
|
|
114
|
+
const journal = [];
|
|
115
|
+
await writeSkillFile(root, journal);
|
|
87
116
|
const projected = await plan(root, flags);
|
|
88
117
|
const region = projected.targets[0];
|
|
89
118
|
const mdcProblems = projected.mdcProblems ?? [];
|
|
119
|
+
const crossRefs = projected.crossRefs ?? [];
|
|
90
120
|
|
|
91
121
|
// Which editors this repo's rules reach. `--surfaces` wins for this run and is
|
|
92
122
|
// recorded in the manifest so the choice survives; with nothing declared the
|
|
@@ -102,12 +132,35 @@ export async function initCommand(positional, flags) {
|
|
|
102
132
|
// not a line to save.
|
|
103
133
|
else pointerPlan.push({ ...p, state: st.state });
|
|
104
134
|
}
|
|
105
|
-
|
|
135
|
+
// Record the choice only when it was made: a flag for this run, or a key the
|
|
136
|
+
// manifest already carries with a real value. A fresh adoption with neither gets
|
|
137
|
+
// no surfaces of its own - "absent" means the global standard applies, and writing
|
|
138
|
+
// today's default into every fresh manifest would freeze it for every release after
|
|
139
|
+
// (review 0003 Q30). With a declared array, `projected.surfaces` is that same array
|
|
140
|
+
// resolved, so the assignment records rather than rewrites.
|
|
141
|
+
if (flags.surfaces !== undefined || Array.isArray(existing?.surfaces)) {
|
|
142
|
+
manifest.surfaces = projected.surfaces;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// The lock's input digest includes the manifest this run writes. On a fresh adoption
|
|
146
|
+
// there is no manifest on disk yet, so the lock `plan` computed is the one for a repo
|
|
147
|
+
// that has none - and the adoption would be red on `check --frozen` the moment the
|
|
148
|
+
// manifest lands. Recompute it with the manifest about to write, so the bytes in the
|
|
149
|
+
// plan, the diff and the write are the same (Q29). A re-init preserves every lock
|
|
150
|
+
// input, so its `plan` bytes are already right and are left alone. After the
|
|
151
|
+
// surfaces line: that is the last mutation of the manifest, and the digest must
|
|
152
|
+
// cover the bytes that land on disk, not an earlier draft of them.
|
|
153
|
+
const lockTarget = projected.targets.find((t) => t.kind === 'lock');
|
|
154
|
+
if (lockTarget && !exists) {
|
|
155
|
+
const lock = await computeLock(root, flags, manifest);
|
|
156
|
+
lockTarget.text = `${JSON.stringify(lock, null, 2)}\n`;
|
|
157
|
+
lockTarget.bytes = Buffer.byteLength(lockTarget.text);
|
|
158
|
+
}
|
|
106
159
|
|
|
107
160
|
const adoption = exists || Boolean(region?.exists);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const apply =
|
|
161
|
+
// One decider for `--yes --dry-run`: the inline copy used to agree with it today,
|
|
162
|
+
// but a second copy of the same arithmetic is where the next bypass grows (Q09).
|
|
163
|
+
const { dryRun, apply } = writeMode(flags);
|
|
111
164
|
|
|
112
165
|
const manifestAction = exists ? 'update' : 'create';
|
|
113
166
|
const manifestFile = await manifestPath(root);
|
|
@@ -117,10 +170,25 @@ export async function initCommand(positional, flags) {
|
|
|
117
170
|
const ci = await planWorkflow(root);
|
|
118
171
|
const dependabot = await planDependabot(root);
|
|
119
172
|
const tsconfigBase = await planTsconfigBase(root);
|
|
173
|
+
// ADR-111. The one agent this tool ships, planned with the rest so the operator sees
|
|
174
|
+
// it in the same preview that lists everything else. Written in the apply path with
|
|
175
|
+
// everything else, journaled the same way.
|
|
176
|
+
const agentPlan = await planAgents(root);
|
|
120
177
|
// Per-developer, and planned with the rest so the operator sees it in the same
|
|
121
178
|
// preview that lists everything else. `exists` short-circuits the write: a
|
|
122
179
|
// repository that already has one keeps it exactly as it is.
|
|
123
180
|
const identity = await resolveIdentity(root);
|
|
181
|
+
// The .gitignore edit and the include repoint are writes too, so they get plan
|
|
182
|
+
// lines of their own: a line the run does not print is a write the reader
|
|
183
|
+
// approves from the diff afterwards (review 0003 Q30). The before-bytes are read
|
|
184
|
+
// once here and reused by the --diff pair below, so the preview's before side is
|
|
185
|
+
// the same bytes the plan's verb was decided from.
|
|
186
|
+
let gitignoreBefore = null;
|
|
187
|
+
let gitignoreAction = null;
|
|
188
|
+
if (!identity.exists && identity.from !== null && !(await identityIgnored(root))) {
|
|
189
|
+
gitignoreBefore = await readBefore(join(root, '.gitignore'));
|
|
190
|
+
gitignoreAction = gitignoreBefore === null ? 'create' : 'update';
|
|
191
|
+
}
|
|
124
192
|
const identityPlan = {
|
|
125
193
|
file: IDENTITY_FILE,
|
|
126
194
|
action: identity.exists ? 'unchanged' : identity.from === null ? 'skip' : 'created',
|
|
@@ -136,6 +204,9 @@ export async function initCommand(positional, flags) {
|
|
|
136
204
|
// pointing at another repository's identity file works here and breaks for the
|
|
137
205
|
// next person who clones without that sibling checkout.
|
|
138
206
|
include: await includesOwnIdentity(root),
|
|
207
|
+
// The .gitignore edit, planned like every other write: null when the entry is
|
|
208
|
+
// already there or no file is being created, 'create'/'update' otherwise.
|
|
209
|
+
gitignore: gitignoreAction,
|
|
139
210
|
};
|
|
140
211
|
|
|
141
212
|
const payload = {
|
|
@@ -179,8 +250,18 @@ export async function initCommand(positional, flags) {
|
|
|
179
250
|
bytes: tsconfigBase.bytes,
|
|
180
251
|
reason: tsconfigBase.reason,
|
|
181
252
|
},
|
|
253
|
+
// ADR-111. The agent files the apply path will write, one per harness that documents
|
|
254
|
+
// an agents directory. `text` is stripped from the emitted payload the way the other
|
|
255
|
+
// plans strip their rendered bytes; the apply path writes from `agentPlan`, which
|
|
256
|
+
// keeps it.
|
|
257
|
+
agents: agentPlan.map(({ text, ...rest }) => rest),
|
|
182
258
|
adoption,
|
|
183
259
|
manifest: { file: MANIFEST, action: manifestAction, path: manifestFile },
|
|
260
|
+
// Local state, not a repository file: gitignored, rebuildable from `git ls-files -s`,
|
|
261
|
+
// and not journaled. The plan names it because a file that appears after the run
|
|
262
|
+
// without being in the plan is the same silent write as any other. `build` means
|
|
263
|
+
// "built when --yes is applied"; the apply path replaces this with the result.
|
|
264
|
+
index: { file: SQLITE_FILE, action: 'build' },
|
|
184
265
|
region: region && {
|
|
185
266
|
file: region.file,
|
|
186
267
|
action: region.action,
|
|
@@ -242,6 +323,10 @@ export async function initCommand(positional, flags) {
|
|
|
242
323
|
);
|
|
243
324
|
}
|
|
244
325
|
|
|
326
|
+
// A dangling cross-reference does not stop the adoption: the region renders, and one
|
|
327
|
+
// unreachable reference is a defect to report, not a reason to withhold the rest.
|
|
328
|
+
for (const p of crossRefs) note(`PROBLEM ${p}`);
|
|
329
|
+
|
|
245
330
|
// Step 3: block on a real conflict before doing anything else. A duplicate is
|
|
246
331
|
// reported but never blocks - restating a ruling is a hygiene problem, while
|
|
247
332
|
// instructing an agent to do the forbidden thing is a correctness problem.
|
|
@@ -251,7 +336,7 @@ export async function initCommand(positional, flags) {
|
|
|
251
336
|
if (isJson(flags)) {
|
|
252
337
|
emitJson(payload);
|
|
253
338
|
} else {
|
|
254
|
-
emit(render(payload, manifest, { npm, version }));
|
|
339
|
+
emit(render(payload, manifest, { npm, manager, version }));
|
|
255
340
|
note('');
|
|
256
341
|
note(
|
|
257
342
|
`blocked ${summary.conflicts} conflict${summary.conflicts === 1 ? '' : 's'}; nothing written`,
|
|
@@ -272,7 +357,7 @@ export async function initCommand(positional, flags) {
|
|
|
272
357
|
payload.ok = false;
|
|
273
358
|
payload.blockedBy = 'malformed-region';
|
|
274
359
|
if (isJson(flags)) emitJson(payload);
|
|
275
|
-
else emit(render(payload, manifest, { npm, version }));
|
|
360
|
+
else emit(render(payload, manifest, { npm, manager, version }));
|
|
276
361
|
return fail(
|
|
277
362
|
`${malformed.length} target(s) have a malformed generated region (${malformed[0].file}: ${malformed[0].reason})`,
|
|
278
363
|
EXIT.FAIL,
|
|
@@ -280,11 +365,69 @@ export async function initCommand(positional, flags) {
|
|
|
280
365
|
);
|
|
281
366
|
}
|
|
282
367
|
|
|
368
|
+
// The `--diff` pairs, read before anything is written so the before side is
|
|
369
|
+
// true in preview and in apply alike (R50). The after-bytes reuse the exact
|
|
370
|
+
// render calls the apply path below writes, so the diff shows the write
|
|
371
|
+
// rather than a second opinion about it. Pointers carry no bytes, so the
|
|
372
|
+
// plan lines naming the link targets are already their whole story.
|
|
373
|
+
let diffs = [];
|
|
374
|
+
if (flags.diff === true) {
|
|
375
|
+
diffs = [
|
|
376
|
+
{
|
|
377
|
+
path: MANIFEST_FILE,
|
|
378
|
+
before: await readBefore(manifestFile),
|
|
379
|
+
after: manifestText(manifest),
|
|
380
|
+
},
|
|
381
|
+
];
|
|
382
|
+
if (payload.identity.action === 'created') {
|
|
383
|
+
diffs.push({
|
|
384
|
+
path: IDENTITY_FILE,
|
|
385
|
+
before: null,
|
|
386
|
+
after: renderIdentityFile(identity),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (payload.identity.gitignore !== null) {
|
|
390
|
+
diffs.push({
|
|
391
|
+
path: '.gitignore',
|
|
392
|
+
before: gitignoreBefore,
|
|
393
|
+
after: gitignoreWithIdentity(gitignoreBefore ?? ''),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (payload.ci.action === 'create') {
|
|
397
|
+
diffs.push({
|
|
398
|
+
path: payload.ci.file,
|
|
399
|
+
before: await readBefore(join(root, payload.ci.file)),
|
|
400
|
+
after: renderWorkflow({ own: payload.ci.own, npm, manager, version }),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (payload.dependabot.action === 'create') {
|
|
404
|
+
diffs.push({
|
|
405
|
+
path: payload.dependabot.file,
|
|
406
|
+
before: await readBefore(join(root, payload.dependabot.file)),
|
|
407
|
+
after: renderDependabot(payload.dependabot.facts, {
|
|
408
|
+
stack: payload.dependabot.stack,
|
|
409
|
+
own: payload.dependabot.own,
|
|
410
|
+
}),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
for (const target of projected.targets) {
|
|
414
|
+
if (target.action === 'unchanged' || target.action === 'malformed') continue;
|
|
415
|
+
diffs.push({
|
|
416
|
+
path: target.file,
|
|
417
|
+
before: await readBefore(target.path),
|
|
418
|
+
after: target.text,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
Object.assign(payload, diffField(diffs, flags));
|
|
422
|
+
}
|
|
423
|
+
|
|
283
424
|
// Step 4/5: report everything, then make changes only when told to.
|
|
284
425
|
if (isJson(flags)) {
|
|
285
426
|
if (!apply) emitJson(payload);
|
|
286
427
|
} else {
|
|
287
|
-
emit(render(payload, manifest, { npm, version }));
|
|
428
|
+
emit(render(payload, manifest, { npm, manager, version }));
|
|
429
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
430
|
+
emitDiffs(diffs, flags);
|
|
288
431
|
}
|
|
289
432
|
if (!apply) {
|
|
290
433
|
note('');
|
|
@@ -331,7 +474,9 @@ export async function initCommand(positional, flags) {
|
|
|
331
474
|
note(`branch switched to ${git.branch}; ${git.from} is untouched and \`git checkout ${git.from}\` returns to it`);
|
|
332
475
|
}
|
|
333
476
|
|
|
334
|
-
|
|
477
|
+
// Every repo-file write below is journaled, so `undo` can restore this run
|
|
478
|
+
// (R49). Pointer symlinks carry no bytes and are not journaled.
|
|
479
|
+
await writeManifest(root, manifest, journal);
|
|
335
480
|
note(`wrote ${MANIFEST_FILE} (${manifestAction})`);
|
|
336
481
|
|
|
337
482
|
// Identity last, and for the same reason the gate is written after the manifest:
|
|
@@ -345,8 +490,15 @@ export async function initCommand(positional, flags) {
|
|
|
345
490
|
// because replacing it when it is already right would be a no-op that reads as
|
|
346
491
|
// a change.
|
|
347
492
|
if (payload.identity.action === 'created') {
|
|
348
|
-
await
|
|
349
|
-
|
|
493
|
+
await journaledWriteFile(root, journal, join(root, IDENTITY_FILE), renderIdentityFile(identity));
|
|
494
|
+
// Both .gitignore edits are journaled, and the journal line lands first: the two
|
|
495
|
+
// entries fold to one (first before, last after), so undo restores the pre-run
|
|
496
|
+
// bytes — or removes the file — in one step. If the journal line landed after,
|
|
497
|
+
// unrecorded, the file would no longer match the recorded after-bytes and undo
|
|
498
|
+
// would refuse (review 0003 Q30). The `ignoreJournal` inside `commitJournal`
|
|
499
|
+
// below is then a no-op.
|
|
500
|
+
await ignoreJournal(root, journal);
|
|
501
|
+
await ignoreIdentity(root, journal);
|
|
350
502
|
note(`wrote ${IDENTITY_FILE} (${identity.from === 'last-commit' ? 'from the last commit in this repository' : 'from the identity configured on this machine'}); gitignored, because it is yours and not the repository's`);
|
|
351
503
|
} else if (payload.identity.action === 'unchanged') {
|
|
352
504
|
note(`kept ${IDENTITY_FILE} (${payload.identity.reason})`);
|
|
@@ -354,7 +506,7 @@ export async function initCommand(positional, flags) {
|
|
|
354
506
|
note(`skipped ${IDENTITY_FILE} ${payload.identity.reason}`);
|
|
355
507
|
}
|
|
356
508
|
if (!payload.identity.include) {
|
|
357
|
-
await includeOwnIdentity(root);
|
|
509
|
+
await includeOwnIdentity(root, journal);
|
|
358
510
|
note(`set ${INCLUDE_KEY} = ../${IDENTITY_FILE} this repository's own identity file`);
|
|
359
511
|
}
|
|
360
512
|
|
|
@@ -369,7 +521,7 @@ export async function initCommand(positional, flags) {
|
|
|
369
521
|
// guaranteed to be misled.
|
|
370
522
|
if (payload.ci.action === 'create') {
|
|
371
523
|
await mkdir(dirname(join(root, payload.ci.file)), { recursive: true });
|
|
372
|
-
await
|
|
524
|
+
await journaledWriteFile(root, journal, join(root, payload.ci.file), renderWorkflow({ own: payload.ci.own, npm, manager, version }));
|
|
373
525
|
note(`wrote ${payload.ci.file} (create; yours to edit from here, simt-dev will not rewrite it)`);
|
|
374
526
|
}
|
|
375
527
|
if (payload.dependabot.action === 'create') {
|
|
@@ -378,7 +530,9 @@ export async function initCommand(positional, flags) {
|
|
|
378
530
|
// in the plan below are the bytes written here, and re-detecting would make the
|
|
379
531
|
// printed entry list and the written one disagree the moment a manifest appears
|
|
380
532
|
// between the two calls.
|
|
381
|
-
await
|
|
533
|
+
await journaledWriteFile(
|
|
534
|
+
root,
|
|
535
|
+
journal,
|
|
382
536
|
join(root, payload.dependabot.file),
|
|
383
537
|
renderDependabot(payload.dependabot.facts, {
|
|
384
538
|
stack: payload.dependabot.stack,
|
|
@@ -389,13 +543,6 @@ export async function initCommand(positional, flags) {
|
|
|
389
543
|
`wrote ${payload.dependabot.file} (create; watches ${payload.dependabot.ecosystems.join(', ') || 'github actions'} — yours to edit, simt-dev will not rewrite it)`,
|
|
390
544
|
);
|
|
391
545
|
}
|
|
392
|
-
if (payload.tsconfigBase.action === 'create') {
|
|
393
|
-
await writeFile(join(root, payload.tsconfigBase.file), renderTsconfigBase());
|
|
394
|
-
note(
|
|
395
|
-
`wrote ${payload.tsconfigBase.file} (create; ${TSCONFIG_BASE_FILE} is JSON, so it cannot say this itself — add "extends": "./${TSCONFIG_BASE_FILE}" to your tsconfig.json or nothing reads it)`,
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
546
|
// Every planned target is written, not just the AGENTS.md region. An adoption that
|
|
400
547
|
// scaffolds the manifest and one region but leaves the `.mdc` rules for a later
|
|
401
548
|
// `sync` produces a repo whose `check` fails on files the user was never told
|
|
@@ -418,7 +565,7 @@ export async function initCommand(positional, flags) {
|
|
|
418
565
|
// no such configuration at all will not have. Creating them is part of adopting,
|
|
419
566
|
// not a detail of writing the file.
|
|
420
567
|
await mkdir(dirname(target.path), { recursive: true });
|
|
421
|
-
await
|
|
568
|
+
await journaledWriteFile(root, journal, target.path, target.text);
|
|
422
569
|
note(`wrote ${target.file} (${target.action})`);
|
|
423
570
|
}
|
|
424
571
|
|
|
@@ -441,8 +588,34 @@ export async function initCommand(positional, flags) {
|
|
|
441
588
|
for (const p of pointerBlocked) {
|
|
442
589
|
note(`BLOCKED ${p.file}: ${p.detail} (${p.bytes} bytes); left as it is`);
|
|
443
590
|
}
|
|
591
|
+
// ADR-111. The agent files, written with everything else and journaled the same way,
|
|
592
|
+
// so `undo` restores them with the rest of the adoption. Written from `agentPlan`,
|
|
593
|
+
// which keeps the rendered bytes the display payload strips.
|
|
594
|
+
const agentWritten = await writeAgentPlan(root, journal, agentPlan);
|
|
595
|
+
for (const f of agentWritten) note(`wrote ${f} (agent)`);
|
|
596
|
+
await commitJournal(root, 'init', journal);
|
|
597
|
+
// The tree changed, so the last `check` no longer describes it: the line
|
|
598
|
+
// goes back to "no cache" rather than reporting the tree as it was (R48).
|
|
599
|
+
await invalidateCheckCache(root);
|
|
444
600
|
payload.written = true;
|
|
445
601
|
|
|
602
|
+
// The index is a cache of git blobs (ADR-089): gitignored, rebuildable, and not
|
|
603
|
+
// journaled. Building it here means the first `simt map` or `simt brief` in a fresh
|
|
604
|
+
// adoption answers from real files instead of walking the tree on the reader's first
|
|
605
|
+
// prompt. A failure is a note, not a failed adoption: the next read rebuilds what
|
|
606
|
+
// this one could not.
|
|
607
|
+
try {
|
|
608
|
+
const built = await reindex(root);
|
|
609
|
+
payload.index = { file: SQLITE_FILE, action: 'built', files: built.files, symbols: built.symbols, ms: built.ms };
|
|
610
|
+
note(
|
|
611
|
+
`index ${built.files} files, ${built.symbols} symbols in ${built.ms} ms; gitignored cache, ` +
|
|
612
|
+
`refreshed on read by ${CLI} map, brief and slice`,
|
|
613
|
+
);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
payload.index = { file: SQLITE_FILE, action: 'failed', reason: error.message };
|
|
616
|
+
note(`index not built (${error.message}); ${CLI} index rebuilds it`);
|
|
617
|
+
}
|
|
618
|
+
|
|
446
619
|
if (isJson(flags)) emitJson(payload);
|
|
447
620
|
|
|
448
621
|
note('');
|
|
@@ -547,7 +720,7 @@ async function git(cwd, args) {
|
|
|
547
720
|
}
|
|
548
721
|
|
|
549
722
|
/** Every line is a decision with a reason. Nothing is silent. */
|
|
550
|
-
function render(payload, manifest, { npm = true, version } = {}) {
|
|
723
|
+
function render(payload, manifest, { npm = true, manager = null, version } = {}) {
|
|
551
724
|
const lines = [
|
|
552
725
|
`repo ${payload.root}`,
|
|
553
726
|
`stack ${payload.stack.length > 0 ? payload.stack.join(', ') : '(undetected)'}`,
|
|
@@ -579,6 +752,18 @@ function render(payload, manifest, { npm = true, version } = {}) {
|
|
|
579
752
|
? id.reason
|
|
580
753
|
: `${id.name} <${id.email}> from ${id.from === 'last-commit' ? "this repository's last commit" : 'the identity configured on this machine'}`;
|
|
581
754
|
lines.push(` ${verb.padEnd(9)} ${id.file}${detail ? `\n ${detail}` : ''}`);
|
|
755
|
+
// The two side writes of the same action, each with its own line: a dry run that
|
|
756
|
+
// says `created git.config` and stops is a plan missing two of its three writes
|
|
757
|
+
// (review 0003 Q30). The include line mirrors the apply note byte for byte, so
|
|
758
|
+
// preview and run cannot tell the reader two different stories.
|
|
759
|
+
if (id.gitignore !== null) {
|
|
760
|
+
lines.push(
|
|
761
|
+
` ${id.gitignore.padEnd(9)} .gitignore ${IDENTITY_FILE} is per-developer, and committing it publishes one identity to everyone`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
if (id.include === false) {
|
|
765
|
+
lines.push(` set ${INCLUDE_KEY} = ../${IDENTITY_FILE} this repository's own identity file`);
|
|
766
|
+
}
|
|
582
767
|
}
|
|
583
768
|
// Every gate the stack suggested and this repo cannot run is named, with the reason.
|
|
584
769
|
// A silently shorter gate list reads as "this repo needs no formatting check", when
|
|
@@ -617,7 +802,9 @@ function render(payload, manifest, { npm = true, version } = {}) {
|
|
|
617
802
|
? 'hand-written content outside the markers is never modified'
|
|
618
803
|
: target.kind === 'baseline'
|
|
619
804
|
? 'a region placed first, so your own settings below it still win (ADR-025)'
|
|
620
|
-
:
|
|
805
|
+
: target.kind === 'lock'
|
|
806
|
+
? 'the two digests a frozen check compares (ADR-086)'
|
|
807
|
+
: 'generated file: frontmatter kept, body replaced (ADR-016)';
|
|
621
808
|
lines.push(
|
|
622
809
|
` ${target.action.padEnd(9)} ${target.file} ${target.previousBytes} -> ${target.bytes} bytes ${caption}`,
|
|
623
810
|
);
|
|
@@ -635,17 +822,30 @@ function render(payload, manifest, { npm = true, version } = {}) {
|
|
|
635
822
|
lines.push(` remedy: move it aside, or delete it and rerun init`);
|
|
636
823
|
}
|
|
637
824
|
|
|
825
|
+
// ADR-111. The agent files, one per harness that documents an agents directory.
|
|
826
|
+
// Printed as their own kind: the file is owned whole by the tool, so the action is
|
|
827
|
+
// created, unchanged or updated and there is no byte delta to show.
|
|
828
|
+
for (const a of payload.agents ?? []) {
|
|
829
|
+
lines.push(` ${a.action.padEnd(9)} ${a.path} agent (ADR-111)`);
|
|
830
|
+
}
|
|
831
|
+
|
|
638
832
|
// The CI gate is listed as its own kind because it is the one plan entry whose
|
|
639
833
|
// absence is a decision rather than a default. Skipped states its reason inline so
|
|
640
834
|
// a reader can tell "this repo does not use GitHub Actions" from "this repo was
|
|
641
835
|
// not offered the gate".
|
|
642
836
|
if (payload.ci.action === 'create') {
|
|
643
|
-
lines.push(` ${'create'.padEnd(9)} ${payload.ci.file} ${renderWorkflow({ own: payload.ci.own, npm, version }).length} bytes`);
|
|
837
|
+
lines.push(` ${'create'.padEnd(9)} ${payload.ci.file} ${renderWorkflow({ own: payload.ci.own, npm, manager, version }).length} bytes`);
|
|
644
838
|
lines.push(
|
|
645
839
|
payload.ci.own
|
|
646
840
|
? ` this repository is simt-dev's own; ${CLI} ci --yes refreshes this copy when the gate changes`
|
|
647
841
|
: ` runs ${CLI} check where a pull request is merged; yours to edit after this`,
|
|
648
842
|
);
|
|
843
|
+
// Q31: the workflow installs simt-dev as a devDependency, so the plan states
|
|
844
|
+
// the install the reader must run — `<pm> add -D`, never a bare npm guess.
|
|
845
|
+
if (!payload.ci.own && manager) {
|
|
846
|
+
const addDev = { npm: `npm i -D ${PACKAGE_NAME}`, pnpm: `pnpm add -D ${PACKAGE_NAME}`, yarn: `yarn add -D ${PACKAGE_NAME}`, bun: `bun add -d ${PACKAGE_NAME}` }[manager];
|
|
847
|
+
if (addDev) lines.push(` ${'needs'.padEnd(9)} ${addDev} (CI installs the pinned devDependency before it can run ${CLI} check)`);
|
|
848
|
+
}
|
|
649
849
|
} else if (payload.ci.action === 'refresh') {
|
|
650
850
|
// init does not write it. `simt ci` owns the template's own copy, and a
|
|
651
851
|
// second writer would be a second answer to who owns it.
|
|
@@ -676,19 +876,21 @@ function render(payload, manifest, { npm = true, version } = {}) {
|
|
|
676
876
|
lines.push(` create ${WORKFLOWS_DIR}/ first if this repo is on GitHub Actions, then rerun init`);
|
|
677
877
|
}
|
|
678
878
|
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
879
|
+
// One state, and it is the point: init no longer creates the file (review 0003 Q28),
|
|
880
|
+
// so the plan line says what it will not write and why, instead of a floor appearing
|
|
881
|
+
// after the run without being in the plan. An existing base file is repo-owned and
|
|
882
|
+
// left exactly as it is, which needs no line because nothing changes.
|
|
682
883
|
const tb = payload.tsconfigBase;
|
|
683
|
-
if (tb && tb.action === '
|
|
684
|
-
lines.push(` ${'create'.padEnd(9)} ${tb.file} ${renderTsconfigBase().length} bytes`);
|
|
685
|
-
lines.push(
|
|
686
|
-
` the strictness flags these repositories already agree on; add "extends": "./${tb.file}" to your tsconfig.json to use it`,
|
|
687
|
-
);
|
|
688
|
-
} else if (tb && tb.action === 'skip') {
|
|
884
|
+
if (tb && tb.action === 'skip') {
|
|
689
885
|
lines.push(` ${'skip'.padEnd(9)} ${tb.file} ${tb.reason}`);
|
|
690
886
|
}
|
|
691
887
|
|
|
888
|
+
// The index is local state, not a repository file: gitignored and rebuildable, so the
|
|
889
|
+
// plan line describes what apply does rather than a file written into the tree.
|
|
890
|
+
lines.push(
|
|
891
|
+
` ${'build'.padEnd(9)} ${payload.index.file} gitignored cache of the git index; built on apply, refreshed on read by ${CLI} map, brief and slice (ADR-089)`,
|
|
892
|
+
);
|
|
893
|
+
|
|
692
894
|
const { findings, summary } = payload;
|
|
693
895
|
if (summary.conflicts > 0) {
|
|
694
896
|
lines.push('', `CONFLICTS ${summary.conflicts} - blocking without --accept-conflicts`);
|