simt-dev 0.7.1 → 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 +36 -1
- package/README.md +169 -80
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- 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-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 +3 -1
- package/rules/mdc/intake-questions.mdc +8 -0
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +7 -5
- package/src/argv.mjs +19 -0
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +155 -113
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +12 -26
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +17 -535
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +15 -1037
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +116 -971
- package/src/bench/run.mjs +164 -881
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +11 -13
- package/src/ci/dependabot.mjs +25 -55
- package/src/commands/agent.mjs +1 -9
- package/src/commands/agenthook.mjs +66 -35
- package/src/commands/bench.mjs +2 -2
- package/src/commands/ci.mjs +11 -13
- package/src/commands/docs.mjs +88 -0
- package/src/commands/gate.mjs +10 -13
- package/src/commands/harness.mjs +3 -3
- package/src/commands/help.mjs +44 -3
- package/src/commands/hooks.mjs +20 -20
- package/src/commands/ignore.mjs +11 -7
- package/src/commands/impact.mjs +4 -8
- package/src/commands/index.mjs +49 -9
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +122 -104
- package/src/commands/lease.mjs +67 -18
- package/src/commands/map.mjs +10 -5
- package/src/commands/mcp.mjs +2 -17
- package/src/commands/repoindex.mjs +8 -8
- package/src/commands/rules.mjs +8 -4
- package/src/commands/shared.mjs +12 -16
- package/src/commands/skills.mjs +5 -6
- package/src/commands/skillscompile.mjs +3 -3
- package/src/commands/skillsinstall.mjs +5 -4
- package/src/commands/slice.mjs +199 -64
- package/src/commands/spec.mjs +78 -62
- package/src/commands/status.mjs +24 -12
- package/src/commands/sync.mjs +14 -14
- package/src/commands/tests.mjs +12 -12
- package/src/commands/tokens.mjs +19 -24
- package/src/commands/upgrade.mjs +8 -4
- package/src/commands/why.mjs +6 -13
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/decisions/index.mjs +3 -2
- 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 +2 -1
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +5 -3
- package/src/hooks/dispatcher.mjs +26 -32
- package/src/hooks/gates.mjs +12 -2
- package/src/hooks/resolve.mjs +2 -7
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +2 -11
- 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 +16 -11
- 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 +124 -17
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +49 -9
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -1005
- package/src/{bench → measure}/corpus.mjs +1 -1
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- 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/output/index.mjs +122 -31
- package/src/packs/manage.mjs +15 -9
- 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/manifest.mjs +10 -435
- 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 +16 -547
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +13 -1
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +0 -7
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +8 -12
- 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/{bench/process.mjs → sys/proc.mjs} +28 -5
- 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 +1 -1
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +3 -1
- 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/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
|
@@ -65,8 +65,20 @@ export function repoRoot(cwd) {
|
|
|
65
65
|
* typed. `repoRoot` answers the different question of which repository that directory
|
|
66
66
|
* belongs to.
|
|
67
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
|
+
|
|
68
80
|
export function commandRoot(flags = {}, positional = []) {
|
|
69
|
-
return flags
|
|
81
|
+
return namedRoot(flags, positional) ?? process.cwd();
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
/**
|
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) {
|
|
@@ -313,12 +313,6 @@ export async function fingerprint(root) {
|
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
// two repositories both carry a wear/ module; only one of them is the wearable.
|
|
317
|
-
if (names.has('wear') && (await isDir(join(root, 'wear')))) {
|
|
318
|
-
stack.add('wear-os');
|
|
319
|
-
evidence.push('wear/');
|
|
320
|
-
}
|
|
321
|
-
|
|
322
316
|
// Either spelling: a repository that adopted this tool has one of the two on disk,
|
|
323
317
|
// and a fingerprint that missed the manifest would silently lose the repo's own
|
|
324
318
|
// declared stack - which is the one input no amount of marker reading can replace.
|
|
@@ -547,7 +541,6 @@ export const STACK_TAGS = new Set([
|
|
|
547
541
|
...MARKERS.map(([tag]) => tag),
|
|
548
542
|
...EXTENSION_TAGS.values(),
|
|
549
543
|
...DEP_TAGS.values(),
|
|
550
|
-
'wear-os', // derived from a wear/ module, not a marker file
|
|
551
544
|
]);
|
|
552
545
|
|
|
553
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,21 +16,18 @@
|
|
|
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';
|
|
36
33
|
import { commandRoot } from '../repo-root.mjs';
|
|
@@ -119,8 +116,7 @@ async function governingItems(root, files, commands = []) {
|
|
|
119
116
|
const ctx = { files: fileSet, commands: new Set(commands), roots: sourceRoots(files) };
|
|
120
117
|
const items = new Map();
|
|
121
118
|
for (const file of files) {
|
|
122
|
-
const
|
|
123
|
-
for (const hit of sortHits([...hits, ...scopeHits(file, corpus)])) {
|
|
119
|
+
for (const hit of governs(file, { records, ...corpus, ctx })) {
|
|
124
120
|
if (!items.has(hit.id)) items.set(hit.id, hit);
|
|
125
121
|
}
|
|
126
122
|
}
|
|
@@ -225,11 +221,11 @@ export async function reviewTarget(args, flags, { write = true, commands = [] }
|
|
|
225
221
|
let names;
|
|
226
222
|
try {
|
|
227
223
|
const [diff, changed] = await Promise.all([
|
|
228
|
-
|
|
229
|
-
|
|
224
|
+
git(root, ['diff', '--staged'], { maxBuffer: 64 << 20 }),
|
|
225
|
+
git(root, ['diff', '--staged', '--name-only'], { maxBuffer: 8 << 20 }),
|
|
230
226
|
]);
|
|
231
|
-
patch = diff
|
|
232
|
-
names = changed.
|
|
227
|
+
patch = diff;
|
|
228
|
+
names = changed.split('\n').filter(Boolean);
|
|
233
229
|
} catch (error) {
|
|
234
230
|
return fail(
|
|
235
231
|
`could not read the staged diff here (${error.message})`,
|
|
@@ -443,7 +439,7 @@ export async function criticBench(root, { labelsPath, flags, commands = [] }) {
|
|
|
443
439
|
async function peer(root, sha, flags, endpoint, commands) {
|
|
444
440
|
let diff;
|
|
445
441
|
try {
|
|
446
|
-
diff =
|
|
442
|
+
diff = await git(root, ['show', '--unified=2', sha], { maxBuffer: 64 << 20 });
|
|
447
443
|
} catch (error) {
|
|
448
444
|
return { failed: true, exitMessage: error.message };
|
|
449
445
|
}
|
package/src/run.mjs
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// `run(command, input)` - one command, as a result (review 0003 Q63).
|
|
2
|
+
//
|
|
3
|
+
// The CLI prints and returns an exit code; that is right for a terminal and useless for a
|
|
4
|
+
// library, an MCP tool or a harness that wants the answer. Rather than rewrite seventy
|
|
5
|
+
// command modules to return values they do not currently compute, this runs the same
|
|
6
|
+
// handler through the output sink: the handler is unchanged, and what it would have
|
|
7
|
+
// written comes back as data.
|
|
8
|
+
//
|
|
9
|
+
// That is a boundary, not a second implementation. `dispatch` and `run` resolve the same
|
|
10
|
+
// registry, apply the same flag guard rails and call the same function, so a command
|
|
11
|
+
// cannot work in one and not the other - the failure would be invisible in the CLI, which
|
|
12
|
+
// is why `test/run.test.mjs` drives both surfaces.
|
|
13
|
+
//
|
|
14
|
+
// What a caller gets is not a rendering decision made for them: `text` and `documents` are
|
|
15
|
+
// both there when the command produced both, and `renderText`/`renderJson` are the two
|
|
16
|
+
// answers the CLI had. A caller that wants neither reads the fields a command exported
|
|
17
|
+
// (`src/api.mjs`).
|
|
18
|
+
|
|
19
|
+
import { EXIT, clearWarnings, jsonCommand, jsonFailMode, jsonIndentDefault, maxBytesLimit, warnings, withOutputState, withSink } from './output/index.mjs';
|
|
20
|
+
import { CLI } from './name.mjs';
|
|
21
|
+
import { GLOBAL_FLAGS, resolveCommand } from './commands/index.mjs';
|
|
22
|
+
|
|
23
|
+
/** `--some-flag` for a flag key the parser handed back as `someFlag`. */
|
|
24
|
+
function spelled(key) {
|
|
25
|
+
return key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function failure(command, code, message, remedy) {
|
|
29
|
+
return {
|
|
30
|
+
command,
|
|
31
|
+
ok: false,
|
|
32
|
+
code,
|
|
33
|
+
text: '',
|
|
34
|
+
documents: [],
|
|
35
|
+
notes: [],
|
|
36
|
+
warnings: [],
|
|
37
|
+
error: { message, remedy: remedy ?? `run: ${CLI} --help`, exitCode: code },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Run one command path and answer with what it produced.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} command the dispatch key: `status`, `plan next`, `hooks install`
|
|
45
|
+
* @param {{args?: string[], flags?: object, root?: string}} [input]
|
|
46
|
+
*/
|
|
47
|
+
export async function run(command, { args = [], flags = {}, root } = {}) {
|
|
48
|
+
const resolved = await resolveCommand(command);
|
|
49
|
+
if (resolved === null) return failure(command, EXIT.USAGE, `unknown command \`${command}\``);
|
|
50
|
+
|
|
51
|
+
// The guard rails the dispatcher applies, applied here for the same reason: a library
|
|
52
|
+
// caller that passes `--frozen` to `status` must be told, not quietly served a default.
|
|
53
|
+
for (const key of Object.keys(flags)) {
|
|
54
|
+
if (GLOBAL_FLAGS.has(key) || resolved.declared.has(spelled(key))) continue;
|
|
55
|
+
return failure(command, EXIT.USAGE, `--${spelled(key)} is not a flag of \`${command}\``);
|
|
56
|
+
}
|
|
57
|
+
for (const [flag, allowed] of Object.entries(resolved.values)) {
|
|
58
|
+
const value = flags[flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase())];
|
|
59
|
+
if (value !== undefined && !allowed.includes(value)) {
|
|
60
|
+
return failure(command, EXIT.USAGE, `--${flag} takes ${allowed.join(', ')}; received ${value}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clearWarnings();
|
|
65
|
+
jsonCommand(command);
|
|
66
|
+
jsonIndentDefault(flags.pretty === true);
|
|
67
|
+
maxBytesLimit(flags.maxBytes);
|
|
68
|
+
jsonFailMode(flags.json === true);
|
|
69
|
+
|
|
70
|
+
const effective = root === undefined ? flags : { root, ...flags };
|
|
71
|
+
const produced = await withOutputState(() => withSink(() => resolved.run(args, effective)));
|
|
72
|
+
return {
|
|
73
|
+
command,
|
|
74
|
+
ok: produced.code === EXIT.OK,
|
|
75
|
+
code: produced.code,
|
|
76
|
+
text: produced.text,
|
|
77
|
+
documents: produced.documents,
|
|
78
|
+
notes: produced.notes,
|
|
79
|
+
warnings: warnings(),
|
|
80
|
+
error: produced.error,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The text the command printed, as one string. Empty when it answered in JSON. */
|
|
85
|
+
export function renderText(result) {
|
|
86
|
+
return result.text;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The document the command emitted, or null when it answered as text.
|
|
91
|
+
*
|
|
92
|
+
* A command that emits a stream (NDJSON) has more than one document and gets an array:
|
|
93
|
+
* collapsing a stream to its first element is how a caller silently loses rows.
|
|
94
|
+
*/
|
|
95
|
+
export function renderJson(result) {
|
|
96
|
+
if (result.documents.length === 0) return null;
|
|
97
|
+
return result.documents.length === 1 ? result.documents[0] : result.documents;
|
|
98
|
+
}
|
package/src/secrets.mjs
CHANGED
|
@@ -23,10 +23,7 @@
|
|
|
23
23
|
// is documentation, and a gate that blocks it teaches the exact `--no-verify`
|
|
24
24
|
// habit the dispatcher's fail-closed rule exists to prevent.
|
|
25
25
|
|
|
26
|
-
import {
|
|
27
|
-
import { promisify } from 'node:util';
|
|
28
|
-
|
|
29
|
-
const exec = promisify(execFile);
|
|
26
|
+
import { git } from './sys/git.mjs';
|
|
30
27
|
|
|
31
28
|
/** The length at which a token is worth measuring, and the entropy it must reach. */
|
|
32
29
|
const ENTROPY_MIN_LENGTH = 32;
|
|
@@ -294,24 +291,19 @@ export async function scanStaged(root, options = {}) {
|
|
|
294
291
|
// program that prints nothing, `-diff` in `.gitattributes` and genuinely binary
|
|
295
292
|
// files make git print no text at all, a textconv rewrites the bytes on the way
|
|
296
293
|
// out, `diff.mnemonicPrefix` renames the header prefixes the attribution below
|
|
297
|
-
// reads
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
'
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
'--src-prefix=a/',
|
|
311
|
-
'--dst-prefix=b/',
|
|
312
|
-
],
|
|
313
|
-
{ cwd: root, maxBuffer: 32 * 1024 * 1024 },
|
|
314
|
-
));
|
|
294
|
+
// reads. The scan asks git for the staged bytes with its own flags and ignores
|
|
295
|
+
// what the repository asked for.
|
|
296
|
+
stdout = await git(root, [
|
|
297
|
+
'diff',
|
|
298
|
+
'--cached',
|
|
299
|
+
'--no-ext-diff',
|
|
300
|
+
'--no-textconv',
|
|
301
|
+
'--text',
|
|
302
|
+
'--unified=0',
|
|
303
|
+
'--no-color',
|
|
304
|
+
'--src-prefix=a/',
|
|
305
|
+
'--dst-prefix=b/',
|
|
306
|
+
]);
|
|
315
307
|
} catch (error) {
|
|
316
308
|
const stderr = String(error.stderr ?? '').trim();
|
|
317
309
|
const reason = (stderr || String(error.message ?? error)).split('\n')[0];
|