simt-dev 0.7.0 → 0.8.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 +191 -870
- package/README.md +188 -213
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-014-workspace-is-configuration.md +2 -2
- package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
- package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
- package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.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/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
- package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
- package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
- package/ecosystems/bun.json +6 -0
- package/ecosystems/cargo.json +25 -0
- package/ecosystems/composer.json +6 -0
- package/ecosystems/dart.json +24 -0
- package/ecosystems/deno.json +21 -0
- package/ecosystems/devcontainers.json +6 -0
- package/ecosystems/docker-compose.json +6 -0
- package/ecosystems/docker.json +6 -0
- package/ecosystems/dotnet.json +25 -0
- package/ecosystems/elixir.json +23 -0
- package/ecosystems/gitsubmodule.json +6 -0
- package/ecosystems/go.json +25 -0
- package/ecosystems/gradle.json +13 -0
- package/ecosystems/maven.json +23 -0
- package/ecosystems/node.json +25 -0
- package/ecosystems/python.json +20 -0
- package/ecosystems/ruby.json +23 -0
- package/ecosystems/swift.json +25 -0
- package/ecosystems/terraform.json +6 -0
- package/ecosystems/uv.json +6 -0
- package/harnesses/antigravity.json +162 -0
- package/harnesses/claude.json +206 -0
- package/harnesses/codex.json +136 -0
- package/harnesses/copilot.json +88 -0
- package/harnesses/cursor.json +140 -0
- package/harnesses/deepseek.json +99 -0
- package/harnesses/gemini.json +122 -0
- package/harnesses/opencode.json +173 -0
- package/package.json +12 -2
- package/rules/core.md +7 -0
- package/rules/mdc/intake-questions.mdc +8 -0
- package/rules/mdc/slice-commit.mdc +57 -4
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/prompt.mjs +5 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agenthooks/shim.mjs +1 -1
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +31 -0
- package/src/argv.mjs +48 -3
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +163 -105
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +33 -73
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +121 -475
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +79 -1064
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +120 -962
- package/src/bench/run.mjs +185 -802
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +48 -15
- package/src/ci/dependabot.mjs +25 -55
- package/src/ci/workflow.mjs +30 -7
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +4 -13
- package/src/commands/agenthook.mjs +70 -40
- package/src/commands/bench.mjs +191 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +13 -14
- 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/docs.mjs +88 -0
- 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 +26 -25
- package/src/commands/graph.mjs +2 -3
- package/src/commands/harness.mjs +6 -5
- package/src/commands/help.mjs +168 -19
- package/src/commands/hooks.mjs +119 -49
- package/src/commands/ignore.mjs +13 -8
- package/src/commands/impact.mjs +8 -15
- package/src/commands/index.mjs +318 -193
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +124 -105
- package/src/commands/lease.mjs +73 -25
- package/src/commands/lessons.mjs +10 -9
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +24 -14
- package/src/commands/mcp.mjs +2 -18
- 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 +12 -11
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +37 -16
- package/src/commands/shared.mjs +14 -19
- package/src/commands/skills.mjs +33 -32
- package/src/commands/skillscompile.mjs +7 -8
- package/src/commands/skillsinstall.mjs +7 -6
- package/src/commands/slice.mjs +220 -74
- package/src/commands/spec.mjs +167 -0
- package/src/commands/status.mjs +27 -18
- package/src/commands/sync.mjs +27 -25
- package/src/commands/tests.mjs +18 -21
- package/src/commands/tokens.mjs +21 -26
- package/src/commands/undo.mjs +5 -15
- package/src/commands/upgrade.mjs +15 -7
- package/src/commands/why.mjs +15 -21
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/compare/row.mjs +0 -2
- package/src/context.mjs +64 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +3 -2
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +14 -1
- package/src/distill/engine.mjs +5 -1
- package/src/docs/commands.mjs +65 -0
- package/src/ecosystem/records.mjs +219 -0
- package/src/explain.mjs +31 -0
- package/src/gates/index.mjs +385 -0
- package/src/graph/compile.mjs +8 -4
- package/src/graph/load.mjs +6 -2
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +7 -5
- package/src/hooks/dispatcher.mjs +209 -60
- package/src/hooks/gates.mjs +10 -43
- package/src/hooks/resolve.mjs +4 -14
- package/src/hooks/shim.mjs +5 -2
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +31 -12
- package/src/index/coverage.mjs +58 -0
- package/src/index/extract.mjs +6 -16
- package/src/index/gate.mjs +1 -1
- package/src/index/govern.mjs +26 -9
- package/src/index/impact.mjs +4 -8
- package/src/index/map.mjs +14 -15
- package/src/index/open.mjs +173 -0
- package/src/index/persist.mjs +224 -57
- package/src/index/store.mjs +82 -39
- package/src/index/tests.mjs +2 -6
- package/src/journal.mjs +94 -20
- package/src/kernel/card.mjs +37 -63
- package/src/kernel/checks.mjs +4 -10
- package/src/lease.mjs +102 -7
- package/src/ledger/index.mjs +150 -14
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/resources.mjs +7 -10
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +119 -25
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -967
- package/src/measure/corpus.mjs +150 -0
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- package/src/measure/session.mjs +3 -2
- package/src/measure/stats.mjs +538 -0
- package/src/measure/task-schema.mjs +125 -0
- package/src/measure/tasks.mjs +69 -0
- package/src/memory/decide.mjs +6 -15
- package/src/memory/lesson-text.mjs +61 -0
- package/src/memory/lessons.mjs +29 -82
- package/src/name.mjs +7 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +209 -10
- package/src/packs/manage.mjs +32 -12
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +1 -1
- package/src/plugins.mjs +22 -16
- package/src/prbody.mjs +7 -11
- package/src/projection/loaders.mjs +7 -692
- package/src/projection/lock.mjs +11 -1
- package/src/projection/manifest.mjs +29 -468
- package/src/projection/mdc.mjs +7 -8
- package/src/projection/project.mjs +126 -221
- package/src/projection/region.mjs +561 -0
- package/src/projection/rules.mjs +20 -550
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +31 -0
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +19 -22
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +10 -13
- package/src/run.mjs +98 -0
- package/src/secrets.mjs +14 -22
- package/src/state/events.mjs +129 -0
- package/src/status-snapshot.mjs +233 -0
- package/src/status.mjs +2 -6
- package/src/sys/git.mjs +82 -0
- package/src/sys/proc.mjs +151 -0
- package/src/testing/parsers.mjs +265 -0
- package/src/testing/patches.mjs +23 -0
- package/src/testing/paths.mjs +49 -0
- package/src/testing/runners.mjs +186 -0
- package/src/testing/vault.mjs +68 -0
- package/src/text/frontmatter.mjs +49 -0
- package/src/text/tokens.mjs +52 -0
- package/src/toolwords.mjs +3 -0
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +58 -11
- package/src/write/plan.mjs +82 -0
- package/templates/plan/tracker.json +1 -0
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
- package/docs/decisions/README.md +0 -600
- package/src/bench/tasks.mjs +0 -36
|
@@ -53,9 +53,16 @@
|
|
|
53
53
|
// unverified assumption into every repo that adopted it, which is the exact
|
|
54
54
|
// failure this table exists to prevent.
|
|
55
55
|
|
|
56
|
+
import { harnessPointers, harnessScopeKeys, harnessSurfaces } from '../harness/records.mjs';
|
|
57
|
+
|
|
56
58
|
/**
|
|
57
59
|
* Surfaces, in the order the writer emits them.
|
|
58
60
|
*
|
|
61
|
+
* The rows are data records under `harnesses/` (Q71): a surface is a fact about a vendor,
|
|
62
|
+
* and a new one arrives as a new file, not a table edit (ADR-013). The engine attaches
|
|
63
|
+
* the one facet a record cannot carry — `ruleFrontmatter`, a function — by id below,
|
|
64
|
+
* and nothing else.
|
|
65
|
+
*
|
|
59
66
|
* `kind`:
|
|
60
67
|
* - `region` a generated region inside a file the repo may also write by hand
|
|
61
68
|
* - `file` a wholly generated file; hand-written body text is retired
|
|
@@ -80,180 +87,35 @@
|
|
|
80
87
|
* `ruleFrontmatter` returns the keys that editor reads BEFORE any rule text. A
|
|
81
88
|
* surface that needs none returns '' and the projected file starts at the body.
|
|
82
89
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
rule:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// publish no such limit.
|
|
113
|
-
readLimits: [
|
|
114
|
-
{
|
|
115
|
-
reader: 'Codex',
|
|
116
|
-
bytes: 32768,
|
|
117
|
-
verified: 'Codex project_doc_max_bytes defaults to 32 KiB',
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
verified:
|
|
121
|
-
'OpenCode v2 instructions and skills docs (opencode.ai/v2/docs, read 2026-10-14) — AGENTS.md only, combined from the workspace up to the project root; CLAUDE.md is not a fallback; skills at .opencode/skills/, .claude/skills/ and .agents/skills/<name>/SKILL.md, with the model shown name and description',
|
|
122
|
-
note:
|
|
123
|
-
'.agents/skills is one of the three skill locations opencode documents, so the same directory serves the standard surface without a symlink.',
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
id: 'cursor',
|
|
127
|
-
label: 'Cursor rules',
|
|
128
|
-
readers: 'Cursor',
|
|
129
|
-
primary: null,
|
|
130
|
-
kind: 'file',
|
|
131
|
-
rule: '.cursor/rules/{name}.mdc',
|
|
132
|
-
readsAgentsMd: 'all',
|
|
133
|
-
configDirs: ['.cursor'],
|
|
134
|
-
// `documentedKeys` is the set the vendor's own docs name for this surface, and
|
|
135
|
-
// `ruleFrontmatter` must emit nothing outside it. A key invented here would be
|
|
136
|
-
// inert at best; on Antigravity an unrecognised value silently discards the
|
|
137
|
-
// rule. The guard lives in test/shipped.test.mjs, and this row is its evidence.
|
|
138
|
-
documentedKeys: ['description', 'alwaysApply', 'globs'],
|
|
139
|
-
// ADR-021 ruled that "a Cursor reader getting .mdc files does not serve them",
|
|
140
|
-
// because Cursor had no documented channel for AGENTS.md. It does, and has:
|
|
141
|
-
// cursor.com/docs/context/rules calls AGENTS.md "a simple alternative to
|
|
142
|
-
// .cursor/rules" and documents support in the project root and subdirectories.
|
|
143
|
-
// So a folded rule reaches a Cursor reader twice - once inside the region, once
|
|
144
|
-
// in the .mdc - and the second copy is 28 KB of pure repetition across the five
|
|
145
|
-
// repos that select both surfaces.
|
|
146
|
-
//
|
|
147
|
-
// The caveat belongs here rather than in the ADR body, because it is a fact about
|
|
148
|
-
// a product and products move: a Cursor bug report records AGENTS.md NOT being
|
|
149
|
-
// read in worktree agents by default, fixed in 3.9. A reader below that version
|
|
150
|
-
// gets the .mdc copy and a reader above it does not need it.
|
|
151
|
-
readsPrimary: true,
|
|
152
|
-
// Filled into a substitution only when every selected surface names the same
|
|
153
|
-
// value. One body is written for all of them, so a Cursor name must not land
|
|
154
|
-
// in a file Claude Code also reads.
|
|
155
|
-
substitutions: { askTool: 'Use the interactive `AskQuestion` tool' },
|
|
156
|
-
ruleFrontmatter: (rule) => [
|
|
157
|
-
`description: ${rule.description ?? `Canonical ${rule.id} rule managed by simt-dev`}`,
|
|
158
|
-
// `globs` only appears when the rule declares a scope. Cursor attaches a rule
|
|
159
|
-
// with no globs on the strength of `alwaysApply` alone, so emitting either
|
|
160
|
-
// unquoted patterns on an eager rule or no globs at all on a scoped one is
|
|
161
|
-
// asking for the wrong loading behaviour.
|
|
162
|
-
...(rule.scope ? [`globs: ${rule.scope.map((g) => `"${g}"`).join(', ')}`] : []),
|
|
163
|
-
`alwaysApply: ${rule.alwaysApply ? 'true' : 'false'}`,
|
|
164
|
-
],
|
|
165
|
-
verified:
|
|
166
|
-
'Cursor project rules (.cursor/rules/*.mdc, description + alwaysApply + globs); alwaysApply: false with globs is "auto-attached when a matching file is in context", and the docs warn that alwaysApply: false with no description is manual @-mention only',
|
|
167
|
-
note: 'The only surface that reads `.mdc`. Every other row here is a plain `.md` or a symlink.',
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
id: 'claude',
|
|
171
|
-
label: 'Claude Code rules',
|
|
172
|
-
readers: 'Claude Code',
|
|
173
|
-
primary: null,
|
|
174
|
-
kind: 'file',
|
|
175
|
-
rule: '.claude/rules/{name}.md',
|
|
176
|
-
// `some`, not `none`. It reads AGENTS.md - the same docs say CLAUDE.md is used only
|
|
177
|
-
// when no AGENTS.md exists - but only on that condition, so a repository that has a
|
|
178
|
-
// hand-written CLAUDE.md is read by neither. That is why this row still needs its
|
|
179
|
-
// own channel and a pointer: the fallback is not a channel, it is the absence of
|
|
180
|
-
// one, and a reader who relied on it would be relying on the reader deleting a
|
|
181
|
-
// file they wrote.
|
|
182
|
-
readsAgentsMd: 'some',
|
|
183
|
-
readsAgentsMdNote:
|
|
184
|
-
'CLAUDE.md is read first; AGENTS.md is read only when there is no CLAUDE.md, which is why the pointer exists',
|
|
185
|
-
configDirs: ['.claude'],
|
|
186
|
-
// Intentionally no `paths:`. See decision 3 in the header.
|
|
187
|
-
documentedKeys: ['paths', 'description'],
|
|
188
|
-
ruleFrontmatter: () => [],
|
|
189
|
-
// The claude-md pointer makes CLAUDE.md a symlink to AGENTS.md (ADR-021 ruling 1),
|
|
190
|
-
// and Claude reads CLAUDE.md in preference to AGENTS.md. So the folded region
|
|
191
|
-
// arrives through the pointer and a rule file alongside it is the same text twice.
|
|
192
|
-
readsPrimary: true,
|
|
193
|
-
substitutions: { askTool: 'Use the interactive `AskUserQuestion` tool' },
|
|
194
|
-
verified: 'Claude Code, "Organize rules with .claude/rules/" — recursive discovery, symlinks supported, no `paths` loads at session start',
|
|
195
|
-
note:
|
|
196
|
-
'CLAUDE.md is a separate pointer surface (see POINTERS). A rule file without frontmatter loads eagerly, which is what an always-on rule wants.',
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
id: 'antigravity',
|
|
200
|
-
label: 'Antigravity workspace rules',
|
|
201
|
-
readers: 'Antigravity IDE, Antigravity CLI',
|
|
202
|
-
primary: null,
|
|
203
|
-
kind: 'file',
|
|
204
|
-
rule: '.agents/rules/{name}.md',
|
|
205
|
-
readsAgentsMd: 'all',
|
|
206
|
-
configDirs: ['.agents'],
|
|
207
|
-
// `trigger` is REQUIRED here and an unrecognised value discards the rule with no
|
|
208
|
-
// message, which is why the value is mapped rather than passed through.
|
|
209
|
-
documentedKeys: ['trigger', 'description', 'globs', 'glob'],
|
|
210
|
-
// The gemini-md pointer's own `verified` cites the same Antigravity docs: "workspace
|
|
211
|
-
// rules are AGENTS.md or GEMINI.md in the root or any subdirectory". Either name
|
|
212
|
-
// reaches the reader, so a .agents/rules file repeats text the region already
|
|
213
|
-
// carries. Both channels are documented as alternatives to each other, which is
|
|
214
|
-
// what makes them duplicates rather than a fallback.
|
|
215
|
-
readsPrimary: true,
|
|
216
|
-
ruleFrontmatter: (rule) => [
|
|
217
|
-
// `trigger: glob` carries the patterns, and they are quoted because YAML reads a
|
|
218
|
-
// value starting with `*` as an alias anchor — the Antigravity docs require it, and
|
|
219
|
-
// an invalid frontmatter block is discarded with no message at all.
|
|
220
|
-
`trigger: ${rule.scope ? 'glob' : rule.alwaysApply ? 'always_on' : 'model_decision'}`,
|
|
221
|
-
...(rule.scope ? [`globs: ${rule.scope.map((g) => `"${g}"`).join(', ')}`] : []),
|
|
222
|
-
`description: ${rule.description ?? `Canonical ${rule.id} rule managed by simt-dev`}`,
|
|
223
|
-
],
|
|
224
|
-
verified:
|
|
225
|
-
'Google Antigravity docs, "Rules" — .agents/rules/*.md, YAML frontmatter with trigger: always_on | model_decision | glob | manual; missing or invalid frontmatter silently discards the rule; trigger: glob takes comma-separated globs, which the docs require to be quoted so YAML does not read a leading * as an alias anchor; a 24 KB per-file limit and a 20,000-token budget for always_on rules, with automatic demotion to pointers past it',
|
|
226
|
-
note:
|
|
227
|
-
'This is why frontmatter is mandatory here and why the trigger value is not guessed: an unrecognised trigger means the rule is dropped with no message.',
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
id: 'copilot',
|
|
231
|
-
label: 'GitHub Copilot instructions',
|
|
232
|
-
readers: 'GitHub Copilot chat, Copilot CLI, Copilot code review, Copilot cloud agent',
|
|
233
|
-
primary: null,
|
|
234
|
-
kind: 'file',
|
|
235
|
-
rule: '.github/instructions/{name}.instructions.md',
|
|
236
|
-
// Copilot is why the column is three values and not two. Its CLI reads AGENTS.md
|
|
237
|
-
// and its editor and code review do not, so the row both needs its own channel and
|
|
238
|
-
// cannot be described as "reads AGENTS.md" without a reader reading that claim as
|
|
239
|
-
// "this repo is covered for Copilot", which for two of its three readers it is not.
|
|
240
|
-
readsAgentsMd: 'some',
|
|
241
|
-
readsAgentsMdNote:
|
|
242
|
-
'Copilot CLI reads AGENTS.md; the editor, code review and the cloud agent do not',
|
|
243
|
-
configDirs: ['.github'],
|
|
244
|
-
documentedKeys: ['applyTo', 'excludeAgent'],
|
|
245
|
-
ruleFrontmatter: (rule) => [`applyTo: ${rule.scope ? `"${rule.scope.join(',')}"` : '"**"'}`],
|
|
246
|
-
verified:
|
|
247
|
-
'GitHub docs, "Adding repository custom instructions" — .github/instructions/NAME.instructions.md with applyTo globs; `applyTo: "**"` is the documented example, and a comma-separated pattern list is how more than one is named',
|
|
248
|
-
// NOT marked readsPrimary, and this row is the reason the flag is per-surface
|
|
249
|
-
// rather than global. The reader is split: Copilot CLI reads AGENTS.md, the
|
|
250
|
-
// editor and code review do not. A per-rule file here is not duplication for two
|
|
251
|
-
// of the three, and dropping it would remove the process rules from a chat that
|
|
252
|
-
// had no other channel to them.
|
|
253
|
-
note:
|
|
254
|
-
'Copilot reads AGENTS.md itself through Copilot CLI, but the editor and code review do not; this is the channel they use.',
|
|
255
|
-
},
|
|
256
|
-
];
|
|
90
|
+
// `ruleFrontmatter` is the one facet a record cannot carry: it is a function, and JSON
|
|
91
|
+
// carries data. Attached by id, so a surface that needs none (opencode) keeps the row
|
|
92
|
+
// exactly as the record wrote it.
|
|
93
|
+
const RULE_FRONTMATTER = {
|
|
94
|
+
cursor: (rule) => [
|
|
95
|
+
`description: ${rule.description ?? `Canonical ${rule.id} rule managed by simt-dev`}`,
|
|
96
|
+
// `globs` only appears when the rule declares a scope. Cursor attaches a rule
|
|
97
|
+
// with no globs on the strength of `alwaysApply` alone, so emitting either
|
|
98
|
+
// unquoted patterns on an eager rule or no globs at all on a scoped one is
|
|
99
|
+
// asking for the wrong loading behaviour.
|
|
100
|
+
...(rule.scope ? [`globs: ${rule.scope.map((g) => `"${g}"`).join(', ')}`] : []),
|
|
101
|
+
`alwaysApply: ${rule.alwaysApply ? 'true' : 'false'}`,
|
|
102
|
+
],
|
|
103
|
+
// Intentionally no `paths:`. See decision 3 in the header.
|
|
104
|
+
claude: () => [],
|
|
105
|
+
antigravity: (rule) => [
|
|
106
|
+
// `trigger: glob` carries the patterns, and they are quoted because YAML reads a
|
|
107
|
+
// value starting with `*` as an alias anchor — the Antigravity docs require it, and
|
|
108
|
+
// an invalid frontmatter block is discarded with no message at all.
|
|
109
|
+
`trigger: ${rule.scope ? 'glob' : rule.alwaysApply ? 'always_on' : 'model_decision'}`,
|
|
110
|
+
...(rule.scope ? [`globs: ${rule.scope.map((g) => `"${g}"`).join(', ')}`] : []),
|
|
111
|
+
`description: ${rule.description ?? `Canonical ${rule.id} rule managed by simt-dev`}`,
|
|
112
|
+
],
|
|
113
|
+
copilot: (rule) => [`applyTo: ${rule.scope ? `"${rule.scope.join(',')}"` : '"**"'}`],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const SURFACES = harnessSurfaces().map((row) =>
|
|
117
|
+
RULE_FRONTMATTER[row.id] ? { ...row, ruleFrontmatter: RULE_FRONTMATTER[row.id] } : row,
|
|
118
|
+
);
|
|
257
119
|
|
|
258
120
|
/**
|
|
259
121
|
* Pointers: files that must exist and must carry no content.
|
|
@@ -263,37 +125,7 @@ export const SURFACES = [
|
|
|
263
125
|
* that fails loudly, whereas a stale copy is a file full of confidently wrong
|
|
264
126
|
* rules that nobody notices for a release.
|
|
265
127
|
*/
|
|
266
|
-
export const POINTERS =
|
|
267
|
-
{
|
|
268
|
-
id: 'claude-md',
|
|
269
|
-
// A pointer belongs to the surface that needs it, never to every repo.
|
|
270
|
-
// ADR-020 wrote both of these unconditionally, which meant that narrowing to
|
|
271
|
-
// one surface under ADR-021 still created a CLAUDE.md nobody asked for — the
|
|
272
|
-
// same over-writing that ruling set out to stop, one file down.
|
|
273
|
-
surface: 'claude',
|
|
274
|
-
path: 'CLAUDE.md',
|
|
275
|
-
to: 'AGENTS.md',
|
|
276
|
-
readers: 'Claude Code',
|
|
277
|
-
verified:
|
|
278
|
-
'Claude Code memory docs: "A symlink also works" — `ln -s AGENTS.md CLAUDE.md`; Claude reads CLAUDE.md by default and AGENTS.md only as a fallback',
|
|
279
|
-
note:
|
|
280
|
-
'Claude Code reads CLAUDE.md and ignores AGENTS.md when both exist. A pointer is what makes one file answer for both without a second copy.',
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
id: 'gemini-md',
|
|
284
|
-
surface: 'antigravity',
|
|
285
|
-
path: 'GEMINI.md',
|
|
286
|
-
to: 'AGENTS.md',
|
|
287
|
-
readers: 'Gemini CLI, Antigravity',
|
|
288
|
-
verified:
|
|
289
|
-
'Google Antigravity docs, "Rules": workspace rules are AGENTS.md or GEMINI.md in the root or any subdirectory; Copilot CLI also discovers GEMINI.md. ' +
|
|
290
|
-
'Gemini CLI, read today: its own docs (core memory index, and the configuration reference) say the memory service loads GEMINI.md hierarchically from the working directory up to the project root and the home directory and also into subdirectories, and that `context.fileName` in settings.json "accepts either a single string or an array of strings" naming the context file(s) to load, default undefined',
|
|
291
|
-
note:
|
|
292
|
-
'Covers Gemini CLI and gives Antigravity a second, equally-native name for the same file. ' +
|
|
293
|
-
'A repository can also point Gemini CLI at AGENTS.md itself with `context.fileName: ["AGENTS.md"]` and need no pointer at all, ' +
|
|
294
|
-
'which is the reason there is no separate Gemini CLI row here: this pointer and a native row would be two ways to hand one reader the same rule.',
|
|
295
|
-
},
|
|
296
|
-
];
|
|
128
|
+
export const POINTERS = harnessPointers();
|
|
297
129
|
|
|
298
130
|
/**
|
|
299
131
|
* The global standard: what a repo gets when it has not chosen.
|
|
@@ -362,7 +194,7 @@ export function canScope(surface) {
|
|
|
362
194
|
}
|
|
363
195
|
|
|
364
196
|
/** Surface ids whose scoping key was read in the vendor's own documentation. */
|
|
365
|
-
const SCOPE_KEYS =
|
|
197
|
+
const SCOPE_KEYS = harnessScopeKeys();
|
|
366
198
|
|
|
367
199
|
/**
|
|
368
200
|
* Surfaces whose reader already loads the primary document, so a per-rule file for
|
|
@@ -488,10 +320,11 @@ export function surface(id) {
|
|
|
488
320
|
|
|
489
321
|
/** The absolute-ish path a rule takes on a surface, given the rule's bare name. */
|
|
490
322
|
/** Assemble YAML frontmatter lines into a block. Lives here because this is the
|
|
491
|
-
* lowest module that owns frontmatter: surfaces.mjs imports
|
|
492
|
-
* the `.mdc` renderer and the
|
|
493
|
-
*
|
|
494
|
-
*
|
|
323
|
+
* lowest module that owns frontmatter: surfaces.mjs imports neither of its consumers
|
|
324
|
+
* (its only import is the data records), so both the `.mdc` renderer and the
|
|
325
|
+
* per-surface projector can share one implementation without either importing the
|
|
326
|
+
* other. Two copies of these four lines is the "third copy of the same logic is a
|
|
327
|
+
* defect" case with a third-copy count of two. */
|
|
495
328
|
export function frontmatterBlock(lines) {
|
|
496
329
|
if (lines.length === 0) return '';
|
|
497
330
|
return ['---', ...lines, '---'].join('\n');
|
|
@@ -19,6 +19,7 @@ import { diff, parse, inject } from './markers.mjs';
|
|
|
19
19
|
import { ruleName } from './mdc.mjs';
|
|
20
20
|
import { foldsRules, frontmatterBlock, POINTERS, rulePath, SURFACES } from './surfaces.mjs';
|
|
21
21
|
import { foldChecks } from '../kernel/checks.mjs';
|
|
22
|
+
import { frontmatterBlock as extractBlock, stripFrontmatter } from '../text/frontmatter.mjs';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* A managed rule file is a GENERATED FILE, unlike AGENTS.md.
|
|
@@ -276,8 +277,7 @@ export async function pointerState(root, pointer) {
|
|
|
276
277
|
export { rulePath };
|
|
277
278
|
|
|
278
279
|
export function extractFrontmatter(text) {
|
|
279
|
-
|
|
280
|
-
return m ? m[0].trimEnd() : null;
|
|
280
|
+
return extractBlock(text);
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
/**
|
|
@@ -296,10 +296,10 @@ export function extractFrontmatter(text) {
|
|
|
296
296
|
*/
|
|
297
297
|
function swapFrontmatter(text, frontmatter) {
|
|
298
298
|
if (frontmatter === '') return text;
|
|
299
|
-
const
|
|
300
|
-
return
|
|
299
|
+
const block = extractBlock(text);
|
|
300
|
+
return block === null ? `${frontmatter}\n${text}` : `${frontmatter}${text.slice(block.length)}`;
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
function bodyAfterFrontmatter(text) {
|
|
304
|
-
return text
|
|
304
|
+
return stripFrontmatter(text);
|
|
305
305
|
}
|
package/src/repo-root.mjs
CHANGED
|
@@ -50,6 +50,37 @@ export function repoRoot(cwd) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The directory a command works on: `--root`, else the first positional when the command
|
|
55
|
+
* takes one, else the current directory.
|
|
56
|
+
*
|
|
57
|
+
* This precedence is a user-visible contract, not an implementation detail: it decides
|
|
58
|
+
* which directory `--root .` and a bare positional mean, and thirty-four commands had
|
|
59
|
+
* their own copy of it. The copies drifted - some resolved the answer, some did not; some
|
|
60
|
+
* read `args[0]`, some `positional[0]` - so the same argv could name two directories
|
|
61
|
+
* depending on which command read it (review 0003 Q65).
|
|
62
|
+
*
|
|
63
|
+
* The answer is returned verbatim, not resolved: a caller that wants an absolute path
|
|
64
|
+
* says so, and a command that reports the root it was given still reports what the reader
|
|
65
|
+
* typed. `repoRoot` answers the different question of which repository that directory
|
|
66
|
+
* belongs to.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* The root a caller named, or undefined when they named none.
|
|
70
|
+
*
|
|
71
|
+
* Absent is not the current directory here. `upgrade` walks a workspace and only limits
|
|
72
|
+
* itself to one project when the caller said which; collapsing "no root" into cwd made a
|
|
73
|
+
* bare `simt upgrade` plan a single project instead of the workspace, which is the defect
|
|
74
|
+
* the two-step resolution exists to prevent.
|
|
75
|
+
*/
|
|
76
|
+
export function namedRoot(flags = {}, positional = []) {
|
|
77
|
+
return flags.root ?? positional[0];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function commandRoot(flags = {}, positional = []) {
|
|
81
|
+
return namedRoot(flags, positional) ?? process.cwd();
|
|
82
|
+
}
|
|
83
|
+
|
|
53
84
|
/**
|
|
54
85
|
* Whether git on this repository compares paths case-insensitively, which is
|
|
55
86
|
* the one filesystem fact a path comparison must honour: on a case-insensitive
|
package/src/resolve/eval.mjs
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// disagree about the score - `simt skills eval` reports it to a reader, and
|
|
7
7
|
// test/eval.test.mjs asserts it in CI - so the arithmetic lives here once (ADR-002).
|
|
8
8
|
|
|
9
|
-
import { rankSkills
|
|
9
|
+
import { rankSkills } from './resolve.mjs';
|
|
10
|
+
import { tokenize } from '../text/tokens.mjs';
|
|
10
11
|
|
|
11
12
|
/** The skill names the resolver selects for one case's declared signals and task. */
|
|
12
13
|
function selectForCase(registry, c) {
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
|
|
20
20
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
21
21
|
import { spawn } from 'node:child_process';
|
|
22
|
-
import {
|
|
22
|
+
import { join } from 'node:path';
|
|
23
23
|
|
|
24
24
|
import { matchesGlob } from './resolve.mjs';
|
|
25
25
|
import { MANIFEST_FILE, MANIFEST_LEGACY } from '../name.mjs';
|
|
26
|
+
import { loadManifest } from '../manifest/schema.mjs';
|
|
26
27
|
|
|
27
28
|
/** tag -> human-readable marker(s) that imply it. First match wins per tag. */
|
|
28
29
|
const MARKERS = [
|
|
@@ -225,6 +226,12 @@ export async function fingerprint(root) {
|
|
|
225
226
|
}
|
|
226
227
|
const names = new Set(entries);
|
|
227
228
|
|
|
229
|
+
// One walk answers every `glob` fact. Each candidate used to walk the tree again, so
|
|
230
|
+
// a repository with seven globs paid for seven identical walks - the same directories,
|
|
231
|
+
// in the same order, for the same answer (review 0003 Q67).
|
|
232
|
+
let nested = null;
|
|
233
|
+
const nestedPaths = async () => (nested ??= await listNested(root));
|
|
234
|
+
|
|
228
235
|
for (const [tag, markers] of MARKERS) {
|
|
229
236
|
const hit = markers.find((m) => names.has(m));
|
|
230
237
|
if (hit) {
|
|
@@ -249,7 +256,7 @@ export async function fingerprint(root) {
|
|
|
249
256
|
break;
|
|
250
257
|
}
|
|
251
258
|
if (fact.kind === 'glob') {
|
|
252
|
-
const hit = (await
|
|
259
|
+
const hit = findNestedIn(await nestedPaths(), candidate)[0];
|
|
253
260
|
if (hit) {
|
|
254
261
|
facts.add(fact.name);
|
|
255
262
|
factEvidence[fact.name] ??= hit;
|
|
@@ -306,12 +313,6 @@ export async function fingerprint(root) {
|
|
|
306
313
|
}
|
|
307
314
|
}
|
|
308
315
|
|
|
309
|
-
// two repositories both carry a wear/ module; only one of them is the wearable.
|
|
310
|
-
if (names.has('wear') && (await isDir(join(root, 'wear')))) {
|
|
311
|
-
stack.add('wear-os');
|
|
312
|
-
evidence.push('wear/');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
316
|
// Either spelling: a repository that adopted this tool has one of the two on disk,
|
|
316
317
|
// and a fingerprint that missed the manifest would silently lose the repo's own
|
|
317
318
|
// declared stack - which is the one input no amount of marker reading can replace.
|
|
@@ -320,9 +321,9 @@ export async function fingerprint(root) {
|
|
|
320
321
|
: names.has(MANIFEST_LEGACY)
|
|
321
322
|
? MANIFEST_LEGACY
|
|
322
323
|
: null;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
324
|
+
// One reader, so a file that does not parse is a missing declaration rather than an
|
|
325
|
+
// object whose only key is "error" pretending to be this repo manifest (Q69).
|
|
326
|
+
const manifest = manifestName === null ? null : (await loadManifest(root)).manifest;
|
|
326
327
|
|
|
327
328
|
const stackList = [...stack].sort();
|
|
328
329
|
const factList = [...facts].sort();
|
|
@@ -339,14 +340,6 @@ export async function fingerprint(root) {
|
|
|
339
340
|
};
|
|
340
341
|
}
|
|
341
342
|
|
|
342
|
-
async function readManifest(path) {
|
|
343
|
-
try {
|
|
344
|
-
return JSON.parse(await readFile(path, 'utf8'));
|
|
345
|
-
} catch {
|
|
346
|
-
return { error: `${basename(path)} unparseable` };
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
343
|
async function isDir(path) {
|
|
351
344
|
try {
|
|
352
345
|
return (await stat(path)).isDirectory();
|
|
@@ -364,7 +357,7 @@ async function isDir(path) {
|
|
|
364
357
|
* are skipped for the reason the extension sample skips them: detection is not a
|
|
365
358
|
* scan of vendored output.
|
|
366
359
|
*/
|
|
367
|
-
async function
|
|
360
|
+
async function listNested(root) {
|
|
368
361
|
const found = [];
|
|
369
362
|
const walk = async (dir, prefix, depth) => {
|
|
370
363
|
let items;
|
|
@@ -378,7 +371,7 @@ async function findNested(root, pattern) {
|
|
|
378
371
|
const rel = prefix ? `${prefix}/${item.name}` : item.name;
|
|
379
372
|
if (item.isDirectory()) {
|
|
380
373
|
if (depth < GLOB_DEPTH) await walk(join(dir, item.name), rel, depth + 1);
|
|
381
|
-
} else
|
|
374
|
+
} else {
|
|
382
375
|
found.push(rel);
|
|
383
376
|
}
|
|
384
377
|
}
|
|
@@ -387,6 +380,11 @@ async function findNested(root, pattern) {
|
|
|
387
380
|
return found.sort();
|
|
388
381
|
}
|
|
389
382
|
|
|
383
|
+
/** The paths in the one walk that match a shape. */
|
|
384
|
+
function findNestedIn(paths, pattern) {
|
|
385
|
+
return paths.filter((rel) => matchesGlob(rel, pattern)).sort();
|
|
386
|
+
}
|
|
387
|
+
|
|
390
388
|
/**
|
|
391
389
|
* Which extensions this repository is written in.
|
|
392
390
|
*
|
|
@@ -543,7 +541,6 @@ export const STACK_TAGS = new Set([
|
|
|
543
541
|
...MARKERS.map(([tag]) => tag),
|
|
544
542
|
...EXTENSION_TAGS.values(),
|
|
545
543
|
...DEP_TAGS.values(),
|
|
546
|
-
'wear-os', // derived from a wear/ module, not a marker file
|
|
547
544
|
]);
|
|
548
545
|
|
|
549
546
|
/** Every fact `fingerprint()` can emit, derived for the same reason as above. */
|
package/src/resolve/resolve.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { matchesGlob as pathMatchesGlob } from 'node:path';
|
|
12
12
|
import { STACK_TAGS } from './fingerprint.mjs';
|
|
13
|
+
import { STOPWORDS, tokenize } from '../text/tokens.mjs';
|
|
13
14
|
|
|
14
15
|
/** Signal weights. Deliberately few, so the ranking is explainable. */
|
|
15
16
|
const W = {
|
|
@@ -30,47 +31,6 @@ const W = {
|
|
|
30
31
|
*/
|
|
31
32
|
export const MIN_SCORE = W.capability;
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
-
* English function words, which appear in every summary this registry has and identify
|
|
35
|
-
* none of them.
|
|
36
|
-
*
|
|
37
|
-
* Applied to the summary signal ONLY. A keyword is an explicit declaration and a name is
|
|
38
|
-
* an identity, so suppressing those would hide something a person wrote on purpose;
|
|
39
|
-
* a summary is prose, and prose is full of words that are there for grammar. Measured:
|
|
40
|
-
* the task "fix a typo in the README" selected four skills on `summary:the` and
|
|
41
|
-
* `summary:in` alone. Hand-authored rather than derived, for the same reason the
|
|
42
|
-
* capability aliases are: a resolver nobody can review is not a resolver.
|
|
43
|
-
*/
|
|
44
|
-
const STOPWORDS = new Set([
|
|
45
|
-
'about', 'after', 'all', 'also', 'and', 'an', 'any', 'are', 'as', 'at', 'be', 'because',
|
|
46
|
-
'been', 'before', 'being', 'both', 'but', 'by', 'can', 'could', 'did', 'do', 'does', 'each',
|
|
47
|
-
'for', 'from', 'had', 'has', 'have', 'he', 'her', 'here', 'his', 'how', 'if', 'in', 'into',
|
|
48
|
-
'is', 'it', 'its', 'just', 'like', 'make', 'many', 'may', 'me', 'more', 'most', 'must', 'my',
|
|
49
|
-
'no', 'not', 'now', 'of', 'off', 'on', 'one', 'only', 'onto', 'or', 'other', 'our', 'out',
|
|
50
|
-
'over', 'own', 'per', 'same', 'see', 'set', 'she', 'should', 'so', 'some', 'such', 'than',
|
|
51
|
-
'that', 'the', 'their', 'them', 'then', 'there', 'these', 'they', 'this', 'those', 'to', 'too',
|
|
52
|
-
'two', 'under', 'up', 'us', 'use', 'used', 'using', 'very', 'was', 'way', 'we', 'were', 'what',
|
|
53
|
-
'when', 'where', 'which', 'while', 'who', 'why', 'will', 'with', 'within', 'without', 'would',
|
|
54
|
-
'you', 'your',
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Split a task string into comparable tokens.
|
|
59
|
-
*
|
|
60
|
-
* Length 2 is kept. The floor used to be 3, and it dropped the most specific words in
|
|
61
|
-
* this domain: `d1`, `r2`, `kv`, `ci` are two-letter vocabulary, not noise, and a task
|
|
62
|
-
* saying "fix d1 migration" lost the one word that identified the change. The cost is
|
|
63
|
-
* that six stopwords of two letters (`in on of or an so`) now reach the summary signal,
|
|
64
|
-
* which is S04's finding and not something this change can resolve on its own - the
|
|
65
|
-
* remedy there is a stopword list, not a longer minimum.
|
|
66
|
-
*/
|
|
67
|
-
export function tokenize(text) {
|
|
68
|
-
return String(text ?? '')
|
|
69
|
-
.toLowerCase()
|
|
70
|
-
.split(/[^a-z0-9]+/)
|
|
71
|
-
.filter((t) => t.length > 1);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
34
|
/**
|
|
75
35
|
* Whether `phrase` occurs in `tokens` as consecutive words.
|
|
76
36
|
*
|
package/src/review/critic.mjs
CHANGED
|
@@ -16,23 +16,21 @@
|
|
|
16
16
|
// earned, the same honesty ADR-076 asked of R38's metric note.
|
|
17
17
|
|
|
18
18
|
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
19
|
-
import { execFile } from 'node:child_process';
|
|
20
19
|
import { join, resolve } from 'node:path';
|
|
21
|
-
import { promisify } from 'node:util';
|
|
22
20
|
|
|
23
21
|
import { EXIT, fail } from '../output/index.mjs';
|
|
24
22
|
import { CLI } from '../name.mjs';
|
|
25
23
|
import { readLocalConfig, chatEndpoint } from '../local/config.mjs';
|
|
26
24
|
import { localChat } from '../local/client.mjs';
|
|
27
25
|
import { decisionRecords, loadCorpus, tracked } from '../commands/why.mjs';
|
|
28
|
-
import {
|
|
26
|
+
import { governs, sourceRoots } from '../index/govern.mjs';
|
|
29
27
|
// `digestOf`, `promptHash` and `proposalFrontmatter` landed with R38's engine and
|
|
30
28
|
// the critic is their second real user; sharing them beats a third copy (ADR-002).
|
|
31
29
|
import { digestOf, promptHash, proposalFrontmatter } from '../distill/engine.mjs';
|
|
32
|
-
|
|
33
|
-
const exec = promisify(execFile);
|
|
30
|
+
import { git } from '../sys/git.mjs';
|
|
34
31
|
|
|
35
32
|
import { LOCAL_DIR } from '../state.mjs';
|
|
33
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
36
34
|
|
|
37
35
|
// Locked on-disk beneath machine-local state, like the distilled cache (R38) —
|
|
38
36
|
// proposals keyed by content digest, never projected and never committed.
|
|
@@ -118,8 +116,7 @@ async function governingItems(root, files, commands = []) {
|
|
|
118
116
|
const ctx = { files: fileSet, commands: new Set(commands), roots: sourceRoots(files) };
|
|
119
117
|
const items = new Map();
|
|
120
118
|
for (const file of files) {
|
|
121
|
-
const
|
|
122
|
-
for (const hit of sortHits([...hits, ...scopeHits(file, corpus)])) {
|
|
119
|
+
for (const hit of governs(file, { records, ...corpus, ctx })) {
|
|
123
120
|
if (!items.has(hit.id)) items.set(hit.id, hit);
|
|
124
121
|
}
|
|
125
122
|
}
|
|
@@ -216,7 +213,7 @@ export async function reviewTarget(args, flags, { write = true, commands = [] }
|
|
|
216
213
|
if (args.length > 0) {
|
|
217
214
|
return fail(`unknown review argument ${args[0]}`, EXIT.USAGE, `usage: ${CLI} review [--local]`);
|
|
218
215
|
}
|
|
219
|
-
const root = resolve(flags
|
|
216
|
+
const root = resolve(commandRoot(flags));
|
|
220
217
|
const listed = await tracked(root);
|
|
221
218
|
if (listed.error) return listed.error;
|
|
222
219
|
|
|
@@ -224,11 +221,11 @@ export async function reviewTarget(args, flags, { write = true, commands = [] }
|
|
|
224
221
|
let names;
|
|
225
222
|
try {
|
|
226
223
|
const [diff, changed] = await Promise.all([
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
git(root, ['diff', '--staged'], { maxBuffer: 64 << 20 }),
|
|
225
|
+
git(root, ['diff', '--staged', '--name-only'], { maxBuffer: 8 << 20 }),
|
|
229
226
|
]);
|
|
230
|
-
patch = diff
|
|
231
|
-
names = changed.
|
|
227
|
+
patch = diff;
|
|
228
|
+
names = changed.split('\n').filter(Boolean);
|
|
232
229
|
} catch (error) {
|
|
233
230
|
return fail(
|
|
234
231
|
`could not read the staged diff here (${error.message})`,
|
|
@@ -442,7 +439,7 @@ export async function criticBench(root, { labelsPath, flags, commands = [] }) {
|
|
|
442
439
|
async function peer(root, sha, flags, endpoint, commands) {
|
|
443
440
|
let diff;
|
|
444
441
|
try {
|
|
445
|
-
diff =
|
|
442
|
+
diff = await git(root, ['show', '--unified=2', sha], { maxBuffer: 64 << 20 });
|
|
446
443
|
} catch (error) {
|
|
447
444
|
return { failed: true, exitMessage: error.message };
|
|
448
445
|
}
|