simt-dev 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +158 -872
- package/README.md +19 -133
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/package.json +10 -2
- package/rules/core.md +7 -0
- package/rules/mdc/slice-commit.mdc +57 -4
- package/src/agenthooks/prompt.mjs +5 -2
- package/src/agenthooks/shim.mjs +1 -1
- package/src/api.mjs +29 -0
- package/src/argv.mjs +29 -3
- package/src/bench/adapters.json +16 -0
- package/src/bench/corpus.mjs +150 -0
- package/src/bench/grade.mjs +23 -49
- package/src/bench/mine.mjs +199 -35
- package/src/bench/probes.mjs +99 -62
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +17 -4
- package/src/bench/run.mjs +242 -142
- package/src/brief/assemble.mjs +38 -3
- package/src/ci/workflow.mjs +30 -7
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +3 -4
- package/src/commands/agenthook.mjs +4 -5
- package/src/commands/bench.mjs +190 -17
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +42 -42
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +4 -4
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +6 -9
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +16 -12
- package/src/commands/graph.mjs +2 -3
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +125 -17
- package/src/commands/hooks.mjs +100 -30
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +4 -7
- package/src/commands/index.mjs +273 -188
- package/src/commands/init.mjs +2 -1
- package/src/commands/lease.mjs +6 -7
- package/src/commands/lessons.mjs +10 -9
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +14 -9
- package/src/commands/mcp.mjs +0 -1
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +2 -2
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +4 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +33 -16
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +28 -26
- package/src/commands/skillscompile.mjs +4 -5
- package/src/commands/skillsinstall.mjs +2 -2
- package/src/commands/slice.mjs +21 -10
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +13 -11
- package/src/commands/tests.mjs +6 -9
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +5 -15
- package/src/commands/upgrade.mjs +7 -3
- package/src/commands/why.mjs +9 -8
- package/src/compare/row.mjs +0 -2
- package/src/context.mjs +64 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +14 -1
- package/src/distill/engine.mjs +5 -1
- package/src/graph/load.mjs +4 -1
- package/src/hooks/capability.mjs +3 -3
- package/src/hooks/dispatcher.mjs +198 -43
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +2 -7
- package/src/hooks/shim.mjs +5 -2
- package/src/identity.mjs +29 -1
- package/src/journal.mjs +78 -9
- package/src/ledger/index.mjs +30 -1
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/resources.mjs +7 -10
- package/src/mcp/server.mjs +70 -16
- package/src/mcp/tools.mjs +45 -7
- package/src/measure/session.mjs +3 -2
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +111 -3
- package/src/packs/manage.mjs +17 -3
- package/src/packs.mjs +20 -2
- package/src/projection/lock.mjs +11 -1
- package/src/projection/manifest.mjs +20 -34
- package/src/projection/rules.mjs +4 -3
- package/src/repo-root.mjs +19 -0
- package/src/resolve/fingerprint.mjs +19 -15
- package/src/review/critic.mjs +2 -1
- package/src/toolwords.mjs +3 -0
- package/src/workspace.mjs +55 -10
- package/docs/decisions/README.md +0 -600
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-affects: ["src/hooks/shim.mjs", "src/commands/hooks.mjs", "src/journal.mjs", "src/agenthooks/shim.mjs", "test/hooks.test.mjs", "test/agenthooks.test.mjs"]
|
|
3
|
+
simt-supersedes: []
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ADR-122 — the shims move into the state directory, and the legacy directory removes itself
|
|
7
|
+
|
|
8
|
+
Status: **accepted** · 2026-10-31
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
The git shims live in a top-level `.simt-hooks/` directory, and `core.hooksPath` points
|
|
13
|
+
git at it. Every other piece of per-repo state — the journal, the ledger, the evidence,
|
|
14
|
+
the index — lives under the state directory `.simt/`, which the repository ignores as a
|
|
15
|
+
single line. The top-level split has no technical reason: `core.hooksPath` accepts any
|
|
16
|
+
path, and nothing about the shims requires a location of its own. It does have a cost —
|
|
17
|
+
two roots for what is one body of state, and an ignore line that exists only for the
|
|
18
|
+
shims.
|
|
19
|
+
|
|
20
|
+
The move has one genuine collision. The journal's stated boundary is that nothing under
|
|
21
|
+
the state directory is journaled: the journal recording itself is recursion, and the
|
|
22
|
+
ledger, index and candidates are evidence and caches whose byte-shape must not be
|
|
23
|
+
rolled back. Review 0003 Q22 ruled the opposite for the shims: `undo` restores
|
|
24
|
+
`core.hooksPath` and the shims together, because a routing restored without its shims is
|
|
25
|
+
a hook that no longer runs. Shims that land under the state directory unjournaled would
|
|
26
|
+
recreate Q22 exactly — the undo restores the routing and the shims stay. The move is
|
|
27
|
+
therefore not a rename; it is a rename plus a named exception to a recorded boundary.
|
|
28
|
+
|
|
29
|
+
Repositories installed before the move carry the legacy directory. The move must be
|
|
30
|
+
self-healing in those repositories, and the cleanup must obey the rule that already
|
|
31
|
+
governs the hooks directory: a file this tool cannot recognise as its own is never
|
|
32
|
+
deleted on its way.
|
|
33
|
+
|
|
34
|
+
## Ruling
|
|
35
|
+
|
|
36
|
+
### 1. The shims live in the state directory, and everything derives from one constant
|
|
37
|
+
|
|
38
|
+
`HOOKS_DIR` is `.simt/hooks`. The shim file names (`fileFor`), the agent-hook shim
|
|
39
|
+
(`.simt/hooks/agent-hook`), the `core.hooksPath` value a write installs, the hooks
|
|
40
|
+
status, and the agent-hooks `rm` policy all derive from that one constant, so the
|
|
41
|
+
location has exactly one home. The preceding location is `HOOKS_DIR_LEGACY`
|
|
42
|
+
(`.simt-hooks`), named because it is read during cleanup and cited in the plan.
|
|
43
|
+
|
|
44
|
+
### 2. The legacy directory removes itself on install, judged in the plan
|
|
45
|
+
|
|
46
|
+
On `hooks install` (and `doctor --fix`, which shares the plan), a present legacy
|
|
47
|
+
directory is scanned and judged removable in the plan: every entry in it must be a
|
|
48
|
+
regular file carrying a simt-dev marker — the shim marker or the chain marker. Removable
|
|
49
|
+
means the plan announces the removal and the apply removes each file journaled and then
|
|
50
|
+
takes the directory down, so `undo` re-creates the files and their directory. A single
|
|
51
|
+
entry the tool cannot recognise as its own — a foreign file, or anything that is not a
|
|
52
|
+
regular file — leaves the directory in place, unannounced and untouched. The judgment
|
|
53
|
+
lives in the plan and the deletion in the apply, so preview and apply agree by
|
|
54
|
+
construction, the way every other write in this command does.
|
|
55
|
+
|
|
56
|
+
### 3. The shim directory is the one named exception to the unjournaled state directory
|
|
57
|
+
|
|
58
|
+
`journaledWriteFile` and `journaledRemoveFile` record writes under the state directory
|
|
59
|
+
only when the path is the shim directory. The exception is named, not broadened: the
|
|
60
|
+
recursion and evidence arguments for the rest of the directory are untouched, and the
|
|
61
|
+
module header states the exception alongside the rule. Without it, `undo` after an
|
|
62
|
+
install restores `core.hooksPath` to unset and leaves the shims behind — the Q22
|
|
63
|
+
defect, relocated.
|
|
64
|
+
|
|
65
|
+
## Consequences
|
|
66
|
+
|
|
67
|
+
- `src/hooks/shim.mjs` exports both constants; `src/commands/hooks.mjs` gains
|
|
68
|
+
`legacyHooksScan` (the plan-side judgment) and the apply-side removal; `doctor --fix`
|
|
69
|
+
inherits both through the shared plan.
|
|
70
|
+
- `src/journal.mjs` imports the shim location from `src/hooks/shim.mjs` — the
|
|
71
|
+
dependency direction is the journal learning one named path, not the location module
|
|
72
|
+
learning about the journal.
|
|
73
|
+
- The repository's ignore file drops the `.simt-hooks/` line: the state directory line
|
|
74
|
+
already covers the shims.
|
|
75
|
+
- A repository installed before the move self-heals on its next `hooks install --yes`:
|
|
76
|
+
the routing re-points, the new shims land, and the legacy directory is gone — journaled,
|
|
77
|
+
so an `undo` brings it back.
|
|
78
|
+
- `src/toolwords.mjs` keeps `simt-hooks` as a tool word: the legacy name is still a
|
|
79
|
+
reference to the tool in remedies and history, and the estate pattern that exempts it
|
|
80
|
+
is unchanged.
|
|
81
|
+
- The agent-hook shim is re-rendered at its new location by the agent-hooks installer,
|
|
82
|
+
which reads the same constant.
|
|
83
|
+
|
|
84
|
+
## Rejected
|
|
85
|
+
|
|
86
|
+
- **Leaving the shims at the top level.** The split buys nothing and costs an ignore
|
|
87
|
+
line, a second state root, and a location that the state-directory story does not
|
|
88
|
+
explain.
|
|
89
|
+
- **Removing the legacy directory without the marker check.** A file a person put there
|
|
90
|
+
is not the tool's to delete, and the hooks directory already carries exactly this rule
|
|
91
|
+
for its own files; the cleanup is the same principle applied to the directory.
|
|
92
|
+
- **Journaled everything under the state directory.** The recursion and evidence
|
|
93
|
+
arguments are load-bearing for the ledger and the index; the exception must be the one
|
|
94
|
+
directory git's routing points at, and no wider.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-affects: ["src/commands/index.mjs", "src/commands/spec.mjs", "test/argv-repeat.test.mjs"]
|
|
3
|
+
simt-supersedes: []
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ADR-123 — a surplus positional is not refused, because a command's arity is not in its source
|
|
7
|
+
|
|
8
|
+
Status: **accepted** · 2026-10-31
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
Review 0003 Q62 asked dispatch to validate a command's positionals the way it validates
|
|
13
|
+
its flags, and the cheapest place to read the maximum looked like the module that
|
|
14
|
+
receives `args`: a command that reads `args[0]` takes one, a command that reads nothing
|
|
15
|
+
takes none, and a command that joins or spreads `args` has no maximum at all.
|
|
16
|
+
|
|
17
|
+
Measured against the tree, that rule is wrong for five of the 76 rows. `init`, `tokens`,
|
|
18
|
+
`explain`, `graph lint` and `bench report` all accept a positional that their own module
|
|
19
|
+
does not index — it is forwarded to a helper in another module, or deliberately ignored.
|
|
20
|
+
Enforcing the derived maximum failed 27 tests, 11 of them on `init` alone (`init` a path
|
|
21
|
+
is the command's most ordinary call). The source tells you where an argument is *read*;
|
|
22
|
+
it does not tell you what the command *accepts*, and for the five commands whose reading
|
|
23
|
+
happens elsewhere the two answers differ.
|
|
24
|
+
|
|
25
|
+
## Ruling
|
|
26
|
+
|
|
27
|
+
### 1. Dispatch does not enforce a maximum number of positionals
|
|
28
|
+
|
|
29
|
+
Dispatch enforces what a spec row can state truthfully: the flags a command reads, the values a closed set accepts, and that a single-valued flag is given once. It does not enforce a positional maximum, and a surplus positional is accepted exactly as it was before the rule was attempted.
|
|
30
|
+
|
|
31
|
+
### 2. A command that refuses a surplus positional says so itself, in its own words
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### 3. The withdrawn arity is recorded here rather than carried as a spec field
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Consequences
|
|
40
|
+
|
|
41
|
+
- Q62's surplus-positional clause is withdrawn. The arity it would have needed (29 rows
|
|
42
|
+
with a maximum, 47 unbounded) is not shipped, and no row carries an `args` field.
|
|
43
|
+
- `test/argv-repeat.test.mjs` keeps the refusal of a repeated flag and the closed set;
|
|
44
|
+
the surplus-positional test is gone with the rule that would have made it pass.
|
|
45
|
+
- The source-reading heuristic is recorded here rather than kept in a comment, because
|
|
46
|
+
the next agent to read Q62 will propose it again, and it will fail the same 27 tests.
|
|
47
|
+
|
|
48
|
+
## Rejected
|
|
49
|
+
|
|
50
|
+
- **Reading arity from the module, as written.** A command's accepted positionals are a
|
|
51
|
+
contract, and the source is an implementation of it; `init` proves they diverge. A
|
|
52
|
+
check that refuses `init <path>` is worse than the silence it replaces.
|
|
53
|
+
- **Declaring arity by hand for all 76 rows.** A number nobody derives and no caller
|
|
54
|
+
checks drifts silently, and a wrong maximum refuses a real call — the same failure,
|
|
55
|
+
arriving a release later and without a test to find it.
|
|
56
|
+
- **Making every surplus positional an error.** The forwarding commands would need a
|
|
57
|
+
per-command exception list, which is the manual declaration above with extra steps.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-affects: ["src/manifest/schema.mjs", "src/resolve/fingerprint.mjs", "test/manifest-schema.test.mjs"]
|
|
3
|
+
simt-supersedes: []
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ADR-124 — the manifest has a schema in the tool, and its unknown keys stay the repository's
|
|
7
|
+
|
|
8
|
+
Status: **accepted** · 2026-10-31
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
`simt.json` is read in 29 places and 41 keys are read ad hoc across the tree. There was
|
|
13
|
+
no list of the keys, so `"mode": "kernal"` was accepted by every reader and reached the
|
|
14
|
+
projection as a mode nothing had heard of, and one reader (`fingerprint`) returned
|
|
15
|
+
`{error}` as the manifest, so a file that did not parse looked like a declaration of a
|
|
16
|
+
key called `error`.
|
|
17
|
+
|
|
18
|
+
Writing that list is where this repository's characteristic mistake is easiest to make:
|
|
19
|
+
the first draft was written from memory and called `limits.fileLines` a positive integer
|
|
20
|
+
and `surfaces` an object. In the only manifest the tool has to hand, the first is an
|
|
21
|
+
object of two tiers and the second is a list.
|
|
22
|
+
|
|
23
|
+
## Ruling
|
|
24
|
+
|
|
25
|
+
### 1. One table declares the manifest keys, their owner and their check
|
|
26
|
+
|
|
27
|
+
One table in `src/manifest/schema.mjs` declares every key with its owner (`tool` or
|
|
28
|
+
`repo`), the version it arrived in, and a check that returns a sentence naming what is
|
|
29
|
+
wrong. One loader, `loadManifest(root)`, returns the manifest, whether a file exists,
|
|
30
|
+
the version, and the problems. The table is held to this repository's own `simt.json` by
|
|
31
|
+
a test, because a schema written from memory fails that test in the same run that proves
|
|
32
|
+
the loader works.
|
|
33
|
+
|
|
34
|
+
### 2. An unparseable file is no manifest, and an absent version field is version 1
|
|
35
|
+
|
|
36
|
+
A caller that receives a half-read object cannot tell it from a declaration, so a file
|
|
37
|
+
that does not parse yields no manifest and one problem naming the parse error. `version`
|
|
38
|
+
arrived after every manifest now on disk, so absent means 1 rather than a problem.
|
|
39
|
+
|
|
40
|
+
### 3. An unknown key is not a problem, because the file invites the repository's own
|
|
41
|
+
|
|
42
|
+
The manifest comment tells a repository that a value it adds survives a re-init. A
|
|
43
|
+
validator that refused an unknown key would make the tool the owner of a file it calls
|
|
44
|
+
the repository's, so only a known key with an impossible value is reported.
|
|
45
|
+
|
|
46
|
+
## Consequences
|
|
47
|
+
|
|
48
|
+
- `fingerprint` reads through the loader, so a broken manifest no longer arrives as an
|
|
49
|
+
object with an `error` key; a dependent read sees no declaration.
|
|
50
|
+
- The 29 direct `JSON.parse` call sites remain. Migrating them is the rest of Q69 and
|
|
51
|
+
changes no behaviour this table already answers for a single call.
|
|
52
|
+
- `GENERATED_KEYS` is not yet derived from the table; the table distinguishes `tool`
|
|
53
|
+
from `repo` keys, so the derivation is possible and is not taken here.
|
|
54
|
+
|
|
55
|
+
## Rejected
|
|
56
|
+
|
|
57
|
+
- **A JSON Schema file and a validator dependency.** `package.json` declares an empty
|
|
58
|
+
dependency set (ADR-012), and a schema in a second language would be a second place
|
|
59
|
+
the keys are stated.
|
|
60
|
+
- **Refusing unknown keys.** The file comment invites a repository to keep its own
|
|
61
|
+
keys, and refusing them breaks every repository that already does.
|
|
62
|
+
- **Throwing on a bad value.** A manifest is read on the path of every command; a throw
|
|
63
|
+
turns a typo into a crash with no remedy, where a named problem can be printed beside
|
|
64
|
+
the answer.
|
package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-affects: ["src/context.mjs", "src/commands/hooks.mjs", "test/hooks-context.test.mjs"]
|
|
3
|
+
simt-supersedes: []
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ADR-125 — a shared context belongs to a read, and a writer reads directly
|
|
7
|
+
|
|
8
|
+
Status: **accepted** · 2026-10-31
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
`repoContext` holds one pending promise per question for the length of a command. It
|
|
13
|
+
was threaded through the reads — `check`, the four skills reads, `diagnose`, the rules
|
|
14
|
+
catalogue — and Q67's last clause was to finish the job, which raised the question the
|
|
15
|
+
row did not answer: should a command that *writes* hold a context too?
|
|
16
|
+
|
|
17
|
+
The two cases are not the same. A reader asks several questions about an unchanging
|
|
18
|
+
tree and pays once. A writer changes the tree between its questions: `init` writes a
|
|
19
|
+
manifest and then renders against it, `upgrade` replaces files and then reports drift,
|
|
20
|
+
`sync` rewrites the region and then computes the lock. A context that survived those
|
|
21
|
+
writes would answer the second question with the first answer, and the command would
|
|
22
|
+
report a repository it had already changed.
|
|
23
|
+
|
|
24
|
+
## Ruling
|
|
25
|
+
|
|
26
|
+
### 1. A read command may hold one context for its whole run
|
|
27
|
+
|
|
28
|
+
A command that only reads creates one `repoContext` at its entry and passes it to
|
|
29
|
+
every reader beneath it. Two questions with the same answer cost one computation, and
|
|
30
|
+
the cache reports its own hits so a context that is wired but never used cannot pass
|
|
31
|
+
for a fast machine.
|
|
32
|
+
|
|
33
|
+
### 2. A writer reads its repository directly
|
|
34
|
+
|
|
35
|
+
A writer calls the readers as it always has. It is not an optimisation left undone: a
|
|
36
|
+
shared context would make a stale answer the reward for a correct write, and the
|
|
37
|
+
failure would surface as a drifted manifest in a repository that had just been fixed.
|
|
38
|
+
|
|
39
|
+
### 3. The hook path keeps its dynamic import
|
|
40
|
+
|
|
41
|
+
`planHooksInstall` resolves the node runner from a fingerprint, and it imports the
|
|
42
|
+
fingerprint module dynamically so that loading the hooks command does not load it.
|
|
43
|
+
Static-importing a context into `src/commands/hooks.mjs` costs about 3 ms of module
|
|
44
|
+
graph on the shim path, which Q64 measured and removed once. A caller that holds a
|
|
45
|
+
context passes it and shares the walk; a caller that does not keeps the light path.
|
|
46
|
+
|
|
47
|
+
## Consequences
|
|
48
|
+
|
|
49
|
+
- Q67 closes with the reads threaded and the writers explained rather than converted.
|
|
50
|
+
- The 3 ms module cost is paid only when a command actually uses a context; `hooks
|
|
51
|
+
run` keeps the dynamic path.
|
|
52
|
+
- A future writer that wants a context has this record to argue against, instead of
|
|
53
|
+
discovering the staleness as a bug report.
|
|
54
|
+
|
|
55
|
+
## Rejected
|
|
56
|
+
|
|
57
|
+
- **Giving every writer a context and invalidating entries after each write.** The
|
|
58
|
+
invalidation is the whole bug surface: forget one entry and the failure is a stale
|
|
59
|
+
answer that looks like a correct one, in the command whose job is to make the tree
|
|
60
|
+
correct.
|
|
61
|
+
- **A process-lifetime context.** `bench` writes manifests mid-run; a process-wide
|
|
62
|
+
memo would answer a benchmark for a repository it had just changed.
|
|
63
|
+
- **Making the context required on every reader signature.** It would put the cache in
|
|
64
|
+
the contract of functions that do not need one, and a caller with no context would
|
|
65
|
+
have to invent an empty one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simt-dev",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "An intelligence amplifier for AI coding agents: rules and dual gates adopted by one command, plus local retrieval, task briefs, benches and repo memory — deterministic by default, with local-model adapters that propose and never select.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Said Elimam",
|
|
@@ -44,6 +44,14 @@
|
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
46
|
"test": "node --test --test-concurrency=$(node -p 'os.cpus().length') test/*.test.mjs",
|
|
47
|
-
"lint": "node bin/simt.mjs
|
|
47
|
+
"lint": "node bin/simt.mjs graph lint",
|
|
48
|
+
"verify:shipped": "node --test test/shipped.test.mjs",
|
|
49
|
+
"build:sea": "node scripts/build-sea.mjs",
|
|
50
|
+
"verify:wheel": "node scripts/verify-wheel.mjs"
|
|
51
|
+
},
|
|
52
|
+
"exports": {
|
|
53
|
+
".": "./src/api.mjs",
|
|
54
|
+
"./package.json": "./package.json",
|
|
55
|
+
"./src/*": "./src/*"
|
|
48
56
|
}
|
|
49
57
|
}
|
package/rules/core.md
CHANGED
|
@@ -24,9 +24,16 @@ commands belongs in this repository, not here.
|
|
|
24
24
|
|
|
25
25
|
## Git
|
|
26
26
|
|
|
27
|
+
The commit and push procedure - order, message format, worktrees - is the `slice-commit`
|
|
28
|
+
rule; follow it for every commit and push.
|
|
29
|
+
|
|
27
30
|
- Commit locally, report the SHA. **Push only on explicit approval.** (ADR-001)
|
|
28
31
|
- Before asking for push approval, state whether the push triggers a deployment, a migration
|
|
29
32
|
or any remote mutation.
|
|
33
|
+
- Once approved, you may push directly without the pre-push gate re-running the full suite
|
|
34
|
+
when you already ran that suite green on the current state and changed nothing since: cite
|
|
35
|
+
the already-passing run as the pass, and the gate recognizes the evidence and skips the
|
|
36
|
+
re-run rather than paying for it again. (ADR-120)
|
|
30
37
|
- Never use `--no-verify`, amend, rebase, reset or rewrite history unless explicitly asked.
|
|
31
38
|
- Never commit failing, partial, secret-bearing or unrelated work.
|
|
32
39
|
|
|
@@ -15,6 +15,35 @@ one is a mechanical procedure with a fixed order, because every step after it de
|
|
|
15
15
|
on the one before: you cannot review a diff you have not staged, and you cannot claim
|
|
16
16
|
evidence for a test you have not run.
|
|
17
17
|
|
|
18
|
+
## Commit message
|
|
19
|
+
|
|
20
|
+
A message is read by the next agent before they are told what to do. It says what the
|
|
21
|
+
commit is, what it answers, and what it changed, so a reader never reconstructs that
|
|
22
|
+
from the diff.
|
|
23
|
+
|
|
24
|
+
The title is Conventional Commits: imperative, no trailing period, a short scope slug
|
|
25
|
+
that names the subsystem — not a ticket id.
|
|
26
|
+
|
|
27
|
+
feat(dispatcher): skip the test gate on a push that already passed
|
|
28
|
+
|
|
29
|
+
The body is plain sections in this order. Drop any section that has nothing to say; a
|
|
30
|
+
one-line commit needs none of them.
|
|
31
|
+
|
|
32
|
+
What: <one sentence — what this commit is>
|
|
33
|
+
Why: <what it answers — the task, defect, or decision that asked for it>
|
|
34
|
+
Before: <the state this replaces, briefly>
|
|
35
|
+
After: <the state it leaves, briefly>
|
|
36
|
+
|
|
37
|
+
"Before" and "After" are a short state summary, not a diff: enough to see what moved,
|
|
38
|
+
not enough to need the patch. Cite a decision record where one settles the change, and
|
|
39
|
+
never claim a test in the body that was not run on this tree.
|
|
40
|
+
|
|
41
|
+
The message ends with a trailer on its own line, after the body - attribution to the
|
|
42
|
+
tool, in the form git renders as a co-author. It is part of the message, not a
|
|
43
|
+
section, so it stays even when every section is dropped:
|
|
44
|
+
|
|
45
|
+
Co-Authored-By: Simt Dev <noreply@simt.space>
|
|
46
|
+
|
|
18
47
|
## Order
|
|
19
48
|
|
|
20
49
|
1. **Run the focused test the change needs**, through the repository's own runner,
|
|
@@ -24,10 +53,8 @@ evidence for a test you have not run.
|
|
|
24
53
|
alone. Report what you left alone; silence about it reads as "I reverted it".
|
|
25
54
|
3. **Read the staged diff.** Not the working tree — the diff. It is what will be
|
|
26
55
|
committed, and it is the last chance to notice a file that does not belong.
|
|
27
|
-
4. **Write the message.**
|
|
28
|
-
|
|
29
|
-
The subject says what changed; the body says why, and what was deliberately not
|
|
30
|
-
changed.
|
|
56
|
+
4. **Write the message in the format above.** The title names what changed; the body
|
|
57
|
+
says why, and what was deliberately not changed.
|
|
31
58
|
5. **Commit.** `git commit`, non-interactive. Report the SHA.
|
|
32
59
|
6. **Stop.**
|
|
33
60
|
|
|
@@ -44,6 +71,32 @@ Never use `--no-verify`. A hook that fails is telling you something about the ch
|
|
|
44
71
|
skipping it turns a gate into a suggestion and the next person has no way to know it
|
|
45
72
|
was skipped.
|
|
46
73
|
|
|
74
|
+
Once the push is approved, you may push directly without the pre-push gate re-running
|
|
75
|
+
the full suite when you already ran that suite green on the current state and changed
|
|
76
|
+
nothing since: the gate records every run against the tree it ran on, recognises that
|
|
77
|
+
evidence, and skips the re-run, reporting the skip and the record that earned it.
|
|
78
|
+
Cite the already-passing run as the pass — do not claim a re-run that did not happen.
|
|
79
|
+
(ADR-120)
|
|
80
|
+
|
|
81
|
+
## Worktrees
|
|
82
|
+
|
|
83
|
+
A worktree is the unit of isolation for concurrent work. The rules that keep it honest:
|
|
84
|
+
|
|
85
|
+
- **One agent, one worktree, one slice.** Never direct two agents at the same files
|
|
86
|
+
concurrently; give each its own worktree and a slice whose files do not overlap.
|
|
87
|
+
- **Start from the record, not from memory.** `simt slice start <id>` creates the
|
|
88
|
+
worktree for the tracker row and returns its path; work in that path, not the main
|
|
89
|
+
checkout.
|
|
90
|
+
- **Finish where you started.** `simt slice finish <id>` runs the declared gates and
|
|
91
|
+
commits from the worktree; `simt plan done <id> <sha>` records the SHA in the main
|
|
92
|
+
checkout's tracker. A slice is not done when its worktree is green — it is done
|
|
93
|
+
when the tracker row is complete and the SHA is recorded.
|
|
94
|
+
- **Leave the worktree when the slice is done.** A finished worktree that still holds
|
|
95
|
+
a dirty tree is a claim about work that is not committed; remove it, and report what
|
|
96
|
+
you left behind if there was any.
|
|
97
|
+
- **Never rewrite history across worktrees.** A rebase, reset or amend in one worktree
|
|
98
|
+
changes the base every other worktree branched from; coordinate it, or do not do it.
|
|
99
|
+
|
|
47
100
|
## What never belongs in a commit
|
|
48
101
|
|
|
49
102
|
- Failing, partial, or unrun work. A commit is a claim; a partial one is a false one.
|
|
@@ -60,7 +60,7 @@ export async function promptOptIn(root) {
|
|
|
60
60
|
* a minute while a repository is walked — is how the hook gets uninstalled, and the
|
|
61
61
|
* packet is not worth a prompt that cannot be typed.
|
|
62
62
|
*/
|
|
63
|
-
export async function deliverPrompt(row, payload) {
|
|
63
|
+
export async function deliverPrompt(row, payload, { root: flagRoot } = {}) {
|
|
64
64
|
const task = promptText(row, payload);
|
|
65
65
|
if (task === null) return { skipped: 'the payload carries no prompt this harness documents' };
|
|
66
66
|
|
|
@@ -68,7 +68,10 @@ export async function deliverPrompt(row, payload) {
|
|
|
68
68
|
// and resolved to the repository's top level the same way: the manifest, like the
|
|
69
69
|
// lease file, is a repository-relative fact, and a prompt submitted from a
|
|
70
70
|
// subdirectory must find it (review 0003 Q16).
|
|
71
|
-
|
|
71
|
+
// `--root` outranks the payload here as it does on the decision path: a packaged hook
|
|
72
|
+
// runs from the plugin directory, so the payload's cwd would name the wrong repository
|
|
73
|
+
// and find no manifest, which delivered nothing and looked like an opt-out.
|
|
74
|
+
const root = repoRoot(factsFrom(row, payload, { root: flagRoot }).root);
|
|
72
75
|
const optIn = await promptOptIn(root);
|
|
73
76
|
if (!optIn.optedIn) return { skipped: optIn.why };
|
|
74
77
|
|
package/src/agenthooks/shim.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// harness's PATH. A harness config naming the bare `simt` binary therefore
|
|
5
5
|
// runs nothing on Claude (a non-2 exit is silent: no enforcement) and blocks
|
|
6
6
|
// every tool call on Cursor (its hook fails closed). The fix is a shim beside
|
|
7
|
-
// the git shims, resolved the same way: `.simt
|
|
7
|
+
// the git shims, resolved the same way: `.simt/hooks/agent-hook` locates the
|
|
8
8
|
// dispatcher with the shared `renderLocator` block and hands over, so every
|
|
9
9
|
// harness config names the shim and never the bare binary (review 0003 Q21).
|
|
10
10
|
//
|
package/src/api.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The library surface: the read commands as data (review 0003 Q63).
|
|
2
|
+
//
|
|
3
|
+
// Every function here is the one the CLI and the MCP tools already call. Nothing in
|
|
4
|
+
// this file renders: a caller that wants text, JSON or an MCP result renders these
|
|
5
|
+
// results itself, which is the whole point - a UI, a harness and a terminal asking the
|
|
6
|
+
// same question must not get three answers, and they cannot disagree if they are
|
|
7
|
+
// reading one function's return value.
|
|
8
|
+
//
|
|
9
|
+
// This is the read half only. The commands that write (`init`, `sync`, `hooks
|
|
10
|
+
// install`, `skills install`, `upgrade`) still print their plans from the command
|
|
11
|
+
// module, because their output is a diff a person reviews before it is applied
|
|
12
|
+
// (ADR-001). They migrate in the next slice, when `run(ctx, input)` exists to carry
|
|
13
|
+
// a plan back to a caller that can show it.
|
|
14
|
+
//
|
|
15
|
+
// The exports map in package.json points `import 'simt-dev'` here, and keeps
|
|
16
|
+
// `./src/*` because the plugin runtime and the harness shims import deep paths.
|
|
17
|
+
// Removing that subpath would break them to make the surface look tidier.
|
|
18
|
+
|
|
19
|
+
export { checkRepo, checkTargets, checkTarget, frozenFindings } from './commands/sync.mjs';
|
|
20
|
+
export { diagnose, serialisedFindings } from './commands/doctor.mjs';
|
|
21
|
+
export { rulesCatalogue, rulesWhyFacts } from './commands/rules.mjs';
|
|
22
|
+
export { skillsBriefFacts, skillsFindFacts, skillsShowFacts, skillsWhyFacts } from './commands/skills.mjs';
|
|
23
|
+
export { assembleBrief } from './brief/assemble.mjs';
|
|
24
|
+
export { loadRegistry } from './graph/load.mjs';
|
|
25
|
+
export { discover } from './compare/discover.mjs';
|
|
26
|
+
export { buildRows, resolveConcurrency, summarise } from './compare/row.mjs';
|
|
27
|
+
export { planNext } from './plan/index.mjs';
|
|
28
|
+
export { buildPrBody } from './prbody.mjs';
|
|
29
|
+
export { federationAccess, readFederation } from './federation/config.mjs';
|
package/src/argv.mjs
CHANGED
|
@@ -26,6 +26,11 @@ import { EXIT } from './output/index.mjs';
|
|
|
26
26
|
// mean something, which it could not while three accepted flags had no description.
|
|
27
27
|
|
|
28
28
|
export const BOOL_FLAGS = new Set([
|
|
29
|
+
// bench ablate: compare across harness, model and experiment groups. The command
|
|
30
|
+
// read `flags.crossGroup` while the parser accepted no such flag, so the override
|
|
31
|
+
// existed only in the error message that told the reader to pass it (review 0003
|
|
32
|
+
// Q62, found by the spec-vs-parser drift test).
|
|
33
|
+
'cross-group',
|
|
29
34
|
'json',
|
|
30
35
|
'all',
|
|
31
36
|
'dry-run',
|
|
@@ -89,6 +94,10 @@ export const BOOL_FLAGS = new Set([
|
|
|
89
94
|
// bench.json marked unmeasured, and a lint that failed it on arrival would make
|
|
90
95
|
// every new pack red before its author wrote the rule (plan 0003 R45).
|
|
91
96
|
'require-measured',
|
|
97
|
+
// bench mine: restore the old "reflect exactly this history" semantics by
|
|
98
|
+
// deleting task files the current history does not reproduce. Off by default
|
|
99
|
+
// because mining is append-only (review 0003 Q48).
|
|
100
|
+
'replace',
|
|
92
101
|
// distill: propose the summary from the configured local chat model instead of
|
|
93
102
|
// the deterministic outline. Off by default: no model in the default path (P1),
|
|
94
103
|
// and a model that is not asked for should not be opened against (ADR-090).
|
|
@@ -166,6 +175,9 @@ export const VALUE_FLAGS = new Set([
|
|
|
166
175
|
// lease: how long the claim lasts, in seconds. A value, because "a lease is short"
|
|
167
176
|
// is not a number any reader can act on, and an expiry nobody chose is an expiry
|
|
168
177
|
// nobody can plan around (plan 0003 R54, ADR-101).
|
|
178
|
+
// lessons capture: how long a captured candidate stays eligible. A value, because a
|
|
179
|
+
// default would be a deadline nobody chose (review 0003 Q62, found by the drift test).
|
|
180
|
+
'expires',
|
|
169
181
|
'ttl',
|
|
170
182
|
// map: outline (paths), names (exported symbols) or signatures (the line).
|
|
171
183
|
// A boolean would not say which of the three the caller wanted.
|
|
@@ -281,9 +293,23 @@ export function parse(argv) {
|
|
|
281
293
|
flags.reps = n;
|
|
282
294
|
} else {
|
|
283
295
|
const key = camel(raw);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
296
|
+
if (REPEATABLE_FLAGS.has(raw)) {
|
|
297
|
+
flags[key] = [...(Array.isArray(flags[key]) ? flags[key] : []), value];
|
|
298
|
+
} else if (flags[key] !== undefined) {
|
|
299
|
+
// `--root a --root b` kept `b`, so half of what the caller asked for vanished
|
|
300
|
+
// with no error and no way to tell which half. Only a flag declared repeatable
|
|
301
|
+
// accumulates (review 0003 Q62); a second value for any other is a usage error.
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
error: usage(
|
|
305
|
+
`--${raw} was given twice; it takes one value (repeatable: ${[...REPEATABLE_FLAGS]
|
|
306
|
+
.map((name) => `--${name}`)
|
|
307
|
+
.join(', ')})`,
|
|
308
|
+
),
|
|
309
|
+
};
|
|
310
|
+
} else {
|
|
311
|
+
flags[key] = value;
|
|
312
|
+
}
|
|
287
313
|
}
|
|
288
314
|
continue;
|
|
289
315
|
}
|
package/src/bench/adapters.json
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"args": ["-p", "{prompt}", "--output-format", "json", "--max-turns", "{turns}"],
|
|
11
11
|
"modelArgs": ["--model", "{model}"],
|
|
12
12
|
"format": "json",
|
|
13
|
+
"isolation": "env",
|
|
14
|
+
"env": ["ANTHROPIC_API_KEY"],
|
|
13
15
|
"usageSource": "usage.*, total_cost_usd, num_turns",
|
|
14
16
|
"fields": {
|
|
15
17
|
"input": ["usage.input_tokens", "usage.input"],
|
|
@@ -30,6 +32,8 @@
|
|
|
30
32
|
"args": ["exec", "--json", "{prompt}"],
|
|
31
33
|
"modelArgs": ["--model", "{model}"],
|
|
32
34
|
"format": "jsonl",
|
|
35
|
+
"isolation": "env",
|
|
36
|
+
"env": ["OPENAI_API_KEY"],
|
|
33
37
|
"usageSource": "token events in JSONL",
|
|
34
38
|
"fields": {
|
|
35
39
|
"input": ["usage.input_tokens", "input_tokens", "usage.input"],
|
|
@@ -50,6 +54,8 @@
|
|
|
50
54
|
"args": ["-p", "{prompt}", "--output-format", "json"],
|
|
51
55
|
"modelArgs": ["--model", "{model}"],
|
|
52
56
|
"format": "json",
|
|
57
|
+
"isolation": "env",
|
|
58
|
+
"env": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
53
59
|
"usageSource": "stats block",
|
|
54
60
|
"fields": {
|
|
55
61
|
"input": ["stats.inputTokens", "stats.tokens.prompt", "stats.prompt_tokens", "usage.input_tokens"],
|
|
@@ -70,6 +76,8 @@
|
|
|
70
76
|
"args": ["run", "--format", "json", "{prompt}"],
|
|
71
77
|
"modelArgs": ["--model", "{model}"],
|
|
72
78
|
"format": "jsonl",
|
|
79
|
+
"isolation": "env",
|
|
80
|
+
"env": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
|
|
73
81
|
"usageSource": "session export",
|
|
74
82
|
"fields": {
|
|
75
83
|
"input": ["tokens.input", "usage.input_tokens", "part.tokens.input"],
|
|
@@ -90,6 +98,8 @@
|
|
|
90
98
|
"args": ["-p", "{prompt}", "--output-format", "json"],
|
|
91
99
|
"modelArgs": ["--model", "{model}"],
|
|
92
100
|
"format": "json",
|
|
101
|
+
"isolation": "env",
|
|
102
|
+
"env": ["CURSOR_API_KEY"],
|
|
93
103
|
"usageSource": "result JSON",
|
|
94
104
|
"fields": {
|
|
95
105
|
"input": ["usage.input_tokens", "usage.input", "stats.inputTokens"],
|
|
@@ -110,6 +120,8 @@
|
|
|
110
120
|
"args": ["--message", "{prompt}", "--yes-always", "--analytics-log", "{analyticsLog}"],
|
|
111
121
|
"modelArgs": ["--model", "{model}"],
|
|
112
122
|
"format": "analytics-log",
|
|
123
|
+
"isolation": "env",
|
|
124
|
+
"env": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
|
|
113
125
|
"usageSource": "--analytics-log file",
|
|
114
126
|
"fields": {
|
|
115
127
|
"input": ["prompt_tokens", "properties.prompt_tokens", "usage.input_tokens"],
|
|
@@ -130,6 +142,8 @@
|
|
|
130
142
|
"args": [],
|
|
131
143
|
"modelArgs": [],
|
|
132
144
|
"format": "builtin",
|
|
145
|
+
"isolation": "none",
|
|
146
|
+
"env": [],
|
|
133
147
|
"usageSource": "vault gold.patch",
|
|
134
148
|
"fields": {}
|
|
135
149
|
},
|
|
@@ -142,6 +156,8 @@
|
|
|
142
156
|
"args": [],
|
|
143
157
|
"modelArgs": [],
|
|
144
158
|
"format": "builtin",
|
|
159
|
+
"isolation": "none",
|
|
160
|
+
"env": [],
|
|
145
161
|
"usageSource": "none",
|
|
146
162
|
"fields": {}
|
|
147
163
|
}
|