simt-dev 0.2.2 → 0.3.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 +100 -0
- package/README.md +129 -75
- package/bin/simt-dev.mjs +21 -2
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +1 -1
- package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
- package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
- package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
- package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
- package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
- package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
- package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
- package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
- package/docs/decisions/ADR-083-a-budget-is-met-by-cutting-the-kernel-and-hard-is-the-floor.md +153 -0
- package/docs/decisions/ADR-084-a-workspace-carries-its-repeated-rules-once.md +100 -0
- package/docs/decisions/ADR-085-the-region-ends-with-the-names-it-kept-out.md +72 -0
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +69 -0
- package/docs/decisions/ADR-087-a-run-is-evidence-for-one-tree.md +67 -0
- package/docs/decisions/ADR-088-a-marked-line-is-enforced-by-a-checker.md +59 -0
- package/docs/decisions/ADR-089-the-local-index-is-a-cache-of-git-blobs.md +52 -0
- package/docs/decisions/ADR-090-an-optional-adapter-is-off-until-named.md +55 -0
- package/docs/decisions/ADR-091-the-map-ranks-files-a-task-mentions.md +60 -0
- package/docs/decisions/ADR-092-tests-are-imports-conventions-and-coverage.md +60 -0
- package/docs/decisions/ADR-093-why-names-what-governs-a-file.md +58 -0
- package/docs/decisions/ADR-094-a-change-runs-the-tests-that-import-it.md +44 -0
- package/docs/decisions/ADR-095-gate-output-keeps-the-failures.md +31 -0
- package/docs/decisions/ADR-096-impact-names-who-a-change-reaches.md +38 -0
- package/package.json +2 -1
- package/rules/core.md +1 -1
- package/src/agenthooks/decide.mjs +123 -0
- package/src/agenthooks/policy.mjs +78 -0
- package/src/agenthooks/render.mjs +240 -0
- package/src/argv.mjs +54 -1
- package/src/bench/ablate.mjs +630 -0
- package/src/bench/adapters.json +149 -0
- package/src/bench/grade.mjs +229 -0
- package/src/bench/mine.mjs +811 -0
- package/src/bench/probes.mjs +1350 -0
- package/src/bench/report.mjs +899 -0
- package/src/bench/run.mjs +1002 -0
- package/src/ci/workflow.mjs +11 -1
- package/src/commands/agenthook.mjs +282 -0
- package/src/commands/bench.mjs +371 -0
- package/src/commands/evidence.mjs +96 -0
- package/src/commands/gate.mjs +165 -0
- package/src/commands/harness.mjs +156 -5
- package/src/commands/help.mjs +89 -1
- package/src/commands/hooks.mjs +45 -5
- package/src/commands/impact.mjs +205 -0
- package/src/commands/index.mjs +60 -1
- package/src/commands/map.mjs +91 -0
- package/src/commands/plan.mjs +99 -0
- package/src/commands/repoindex.mjs +52 -0
- package/src/commands/shared.mjs +398 -0
- package/src/commands/sync.mjs +72 -3
- package/src/commands/tests.mjs +192 -0
- package/src/commands/tokens.mjs +61 -1
- package/src/commands/why.mjs +177 -0
- package/src/decisions/index.mjs +23 -5
- package/src/graph/spec.mjs +1 -1
- package/src/hooks/dispatcher.mjs +15 -0
- package/src/hooks/shim.mjs +31 -0
- package/src/index/compact.mjs +228 -0
- package/src/index/extract.mjs +331 -0
- package/src/index/gate.mjs +101 -0
- package/src/index/govern.mjs +222 -0
- package/src/index/impact.mjs +186 -0
- package/src/index/map.mjs +393 -0
- package/src/index/persist.mjs +172 -0
- package/src/index/store.mjs +301 -0
- package/src/index/tests.mjs +294 -0
- package/src/kernel/budget.mjs +100 -0
- package/src/kernel/card.mjs +254 -0
- package/src/kernel/checks.mjs +378 -0
- package/src/kernel/inferability.mjs +188 -0
- package/src/ledger/index.mjs +112 -0
- package/src/mcp/resources.mjs +157 -0
- package/src/mcp/server.mjs +37 -6
- package/src/mcp/tools.mjs +84 -0
- package/src/measure/context.mjs +184 -11
- package/src/measure/loaderView.mjs +435 -0
- package/src/measure/session.mjs +267 -0
- package/src/plan/index.mjs +650 -0
- package/src/plan/template.mjs +331 -0
- package/src/projection/disclosure.mjs +92 -0
- package/src/projection/loaders.mjs +689 -0
- package/src/projection/lock.mjs +93 -0
- package/src/projection/manifest.mjs +78 -3
- package/src/projection/markers.mjs +56 -17
- package/src/projection/project.mjs +24 -17
- package/src/projection/rules.mjs +452 -43
- package/src/projection/shared.mjs +428 -0
- package/src/projection/surfaces.mjs +22 -8
- package/src/projection/targets.mjs +2 -1
- package/src/toolwords.mjs +71 -0
- package/templates/plan/tracker.json +36 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// One renderer per harness: the payload in, the decision out, and the config written.
|
|
2
|
+
//
|
|
3
|
+
// Four harnesses read a hook and no two agree on anything. The config path, the event
|
|
4
|
+
// name, the shape of the document, the tool name for a shell command, the key holding
|
|
5
|
+
// the command, and the JSON a hook answers with are all different, and none of them is
|
|
6
|
+
// the same fact. So this module owns the differences and `policy.mjs` owns none of
|
|
7
|
+
// them:
|
|
8
|
+
//
|
|
9
|
+
// claude .claude/settings.json PreToolUse {"hooks":{E:[{matcher,hooks:[h]}]}}
|
|
10
|
+
// codex .codex/hooks.json PreToolUse {"hooks":{E:[{matcher,hooks:[h]}]}}
|
|
11
|
+
// cursor .cursor/hooks.json preToolUse {"version":1,"hooks":{E:[h]}}
|
|
12
|
+
// antigravity .agents/hooks.json PreToolUse {"<name>":{E:[{matcher,hooks:[h]}]}}
|
|
13
|
+
//
|
|
14
|
+
// The paths are not written here. They come from the reader table (R14) because that is
|
|
15
|
+
// where each one was verified against its vendor page, and a second copy of a verified
|
|
16
|
+
// fact is a copy that drifts the first time a vendor moves the file.
|
|
17
|
+
//
|
|
18
|
+
// Two things are recorded per row that a reader of the code cannot get from the code:
|
|
19
|
+
// the page the shape was read from, and whether anything about the row is inferred.
|
|
20
|
+
// An inferred key is marked as inferred rather than presented as documented, because
|
|
21
|
+
// the failure that matters is not a missing policy — it is a policy that silently never
|
|
22
|
+
// fires in a harness whose payload we guessed wrong.
|
|
23
|
+
|
|
24
|
+
import { CLI } from '../name.mjs';
|
|
25
|
+
import { LOADERS } from '../projection/loaders.mjs';
|
|
26
|
+
import { EVENTS } from './policy.mjs';
|
|
27
|
+
|
|
28
|
+
/** The seconds a hook is allowed before the harness gives up on it. */
|
|
29
|
+
const TIMEOUT_SECONDS = 10;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The hook's own entry point, as it appears inside every harness's config.
|
|
33
|
+
*
|
|
34
|
+
* A repository that has this tool has its binary on PATH, and `--harness` is explicit
|
|
35
|
+
* rather than detected from the payload: a hook that guessed would answer in the wrong
|
|
36
|
+
* harness's dialect when a payload arrived in an unexpected shape, and the harness would
|
|
37
|
+
* read a decision it does not understand as no decision at all.
|
|
38
|
+
*/
|
|
39
|
+
export function hookCommand(id) {
|
|
40
|
+
return `${CLI} agent-hook ${EVENTS[0]} --harness ${id}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The path a config file lives at, from the reader table rather than a second list. */
|
|
44
|
+
export function hookPath(id) {
|
|
45
|
+
const row = LOADERS.find((l) => l.id === id);
|
|
46
|
+
return row?.hooks?.path ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The reader rows that document a hooks file, and the ones that document none. */
|
|
50
|
+
export function hookRows() {
|
|
51
|
+
return LOADERS.map((l) => ({
|
|
52
|
+
id: l.id,
|
|
53
|
+
label: l.label,
|
|
54
|
+
path: l.hooks?.path ?? null,
|
|
55
|
+
note: l.hooks ? l.hooks.verified : (l.hooksNote ?? 'no hooks file is documented'),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ADAPTERS = {
|
|
60
|
+
claude: {
|
|
61
|
+
id: 'claude',
|
|
62
|
+
event: 'PreToolUse',
|
|
63
|
+
matcher: 'Bash|Edit|Write|MultiEdit|NotebookEdit',
|
|
64
|
+
denyExit: 2,
|
|
65
|
+
verified: 'docs.claude.com/en/docs/claude-code/hooks, read 2026-10-16',
|
|
66
|
+
root: (p) => p.cwd,
|
|
67
|
+
extract: (p) => ({
|
|
68
|
+
command: p.tool_name === 'Bash' ? (p.tool_input?.command ?? null) : null,
|
|
69
|
+
writes: ['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(p.tool_name)
|
|
70
|
+
? [p.tool_input?.file_path].filter(Boolean)
|
|
71
|
+
: [],
|
|
72
|
+
}),
|
|
73
|
+
deny: (message) => ({
|
|
74
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: message },
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
codex: {
|
|
78
|
+
id: 'codex',
|
|
79
|
+
event: 'PreToolUse',
|
|
80
|
+
matcher: 'Bash|apply_patch',
|
|
81
|
+
denyExit: 2,
|
|
82
|
+
verified: 'developers.openai.com/codex/hooks, read 2026-10-16',
|
|
83
|
+
// Codex is the one harness that will not run a hook until a person has approved the
|
|
84
|
+
// exact definition, keyed by its hash — so an install that does not say this leaves
|
|
85
|
+
// the repository with a hook that never fires and no way to tell why.
|
|
86
|
+
trust: 'Codex skips a hook nobody has trusted: review it with /hooks, or the entry stays inert.',
|
|
87
|
+
root: (p) => p.cwd,
|
|
88
|
+
extract: (p) => ({
|
|
89
|
+
command: p.tool_name === 'Bash' ? (p.tool_input?.command ?? null) : null,
|
|
90
|
+
// apply_patch carries its targets inside the patch text, one header line per file.
|
|
91
|
+
writes:
|
|
92
|
+
p.tool_name === 'apply_patch'
|
|
93
|
+
? [...String(p.tool_input?.command ?? '').matchAll(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/gm)].map((m) => m[1].trim())
|
|
94
|
+
: [],
|
|
95
|
+
}),
|
|
96
|
+
deny: (message) => ({
|
|
97
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: message },
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
cursor: {
|
|
101
|
+
id: 'cursor',
|
|
102
|
+
event: 'preToolUse',
|
|
103
|
+
matcher: 'Shell|Write|Edit',
|
|
104
|
+
denyExit: 2,
|
|
105
|
+
verified: 'cursor.com/docs/agent/hooks, read 2026-10-16',
|
|
106
|
+
root: (p) => p.working_directory ?? p.cwd,
|
|
107
|
+
extract: (p) => ({
|
|
108
|
+
command: p.tool_name === 'Shell' ? (p.tool_input?.command ?? null) : null,
|
|
109
|
+
writes: ['Write', 'Edit'].includes(p.tool_name)
|
|
110
|
+
? [p.tool_input?.file_path ?? p.tool_input?.path].filter(Boolean)
|
|
111
|
+
: [],
|
|
112
|
+
}),
|
|
113
|
+
deny: (message) => ({ permission: 'deny', user_message: message, agent_message: message }),
|
|
114
|
+
},
|
|
115
|
+
antigravity: {
|
|
116
|
+
id: 'antigravity',
|
|
117
|
+
event: 'PreToolUse',
|
|
118
|
+
matcher: 'run_command|write_to_file|replace_file_content|multi_replace_file_content',
|
|
119
|
+
// The page documents the decision JSON and not an exit code, so the denial travels
|
|
120
|
+
// in the JSON only: a non-zero exit whose meaning the vendor has not stated is not
|
|
121
|
+
// a block, it is a guess that happens to be loud.
|
|
122
|
+
denyExit: 0,
|
|
123
|
+
verified: 'antigravity.google/docs/hooks, read 2026-10-16',
|
|
124
|
+
root: (p) => p.workspacePaths?.[0] ?? p.cwd,
|
|
125
|
+
extract: (p) => {
|
|
126
|
+
const call = p.toolCall ?? {};
|
|
127
|
+
const args = call.args ?? {};
|
|
128
|
+
return {
|
|
129
|
+
command: call.name === 'run_command' ? (args.CommandLine ?? null) : null,
|
|
130
|
+
writes: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(call.name)
|
|
131
|
+
? [args.TargetFile].filter(Boolean)
|
|
132
|
+
: [],
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
deny: (message) => ({ decision: 'deny', reason: message }),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** The four adapters, in the order the reader table lists them. */
|
|
140
|
+
export function adapters() {
|
|
141
|
+
return hookRows()
|
|
142
|
+
.filter((r) => r.path !== null)
|
|
143
|
+
.map((r) => ADAPTERS[r.id])
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function adapter(id) {
|
|
148
|
+
const row = ADAPTERS[id];
|
|
149
|
+
return row && hookPath(id) !== null ? row : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The facts a payload carries, and the root it names. An unknown shape yields none. */
|
|
153
|
+
export function factsFrom(row, payload) {
|
|
154
|
+
return { root: row.root(payload) ?? process.cwd(), ...row.extract(payload ?? {}) };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The document to print for a decision, or null when there is nothing to say. */
|
|
158
|
+
export function decisionJson(row, decision) {
|
|
159
|
+
if (decision.decision !== 'deny') return null;
|
|
160
|
+
return row.deny(decision.messages.join('\n'));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// The config file: reading ours out of somebody else's JSON, and putting it back.
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/** The name antigravity's document keys its hooks by; its events live under a name. */
|
|
168
|
+
const NAMED_HOOK = 'simt-dev-policies';
|
|
169
|
+
|
|
170
|
+
/** Where a document keeps its event keys. Antigravity is the odd one: by name. */
|
|
171
|
+
function container(row, config, { create = false } = {}) {
|
|
172
|
+
if (row.id === 'antigravity') {
|
|
173
|
+
if (config[NAMED_HOOK] === undefined && create) config[NAMED_HOOK] = {};
|
|
174
|
+
return config[NAMED_HOOK] ?? null;
|
|
175
|
+
}
|
|
176
|
+
// Before `hooks`, so a file this tool creates reads the way the vendor's own example
|
|
177
|
+
// does rather than with the version key appended wherever it happened to land.
|
|
178
|
+
if (row.id === 'cursor' && config.version === undefined && create) config.version = 1;
|
|
179
|
+
if (config.hooks === undefined && create) config.hooks = {};
|
|
180
|
+
return config.hooks ?? null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The group this tool owns for one event, in that harness's shape. */
|
|
184
|
+
function group(row) {
|
|
185
|
+
const command = hookCommand(row.id);
|
|
186
|
+
if (row.id === 'cursor') {
|
|
187
|
+
// failClosed is set because a hooks file this tool wrote should not silently
|
|
188
|
+
// degrade to fail-open on its own crash: the vendor's default turns a broken hook
|
|
189
|
+
// into an absent one, and the whole point of writing it is that it runs.
|
|
190
|
+
return { command, timeout: TIMEOUT_SECONDS, matcher: row.matcher, failClosed: true };
|
|
191
|
+
}
|
|
192
|
+
return { matcher: row.matcher, hooks: [{ type: 'command', command, timeout: TIMEOUT_SECONDS }] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Whether a group is this tool's, judged by the command it runs and not by position. */
|
|
196
|
+
function ours(entry) {
|
|
197
|
+
return JSON.stringify(entry).includes(`${CLI} agent-hook`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function entries(row, config) {
|
|
201
|
+
const box = container(row, config);
|
|
202
|
+
const list = box?.[row.event];
|
|
203
|
+
return Array.isArray(list) ? list : [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Our entries in this document, for `status`; the document is not modified. */
|
|
207
|
+
export function installedIn(row, config) {
|
|
208
|
+
return entries(row, config).filter(ours);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The document with this tool's hook added, everything else left alone.
|
|
213
|
+
*
|
|
214
|
+
* Written as JSON with two spaces and a trailing newline because that is what a file in
|
|
215
|
+
* a repository reads like; a config that is rewritten with different indentation on
|
|
216
|
+
* every run is a diff nobody can review. Keys this tool does not own are preserved
|
|
217
|
+
* exactly, including their order, because the file belongs to the repository.
|
|
218
|
+
*/
|
|
219
|
+
function withHook(row, config) {
|
|
220
|
+
const box = container(row, config, { create: true });
|
|
221
|
+
const kept = entries(row, config).filter((e) => !ours(e));
|
|
222
|
+
box[row.event] = [...kept, group(row)];
|
|
223
|
+
return config;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The text `install` would write, or the reason the existing file cannot be read. */
|
|
227
|
+
export function rendered(row, existing) {
|
|
228
|
+
if (existing === null) return { text: `${JSON.stringify(withHook(row, {}), null, 2)}\n`, action: 'created' };
|
|
229
|
+
let config;
|
|
230
|
+
try {
|
|
231
|
+
config = JSON.parse(existing);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
return { error: `not valid JSON (${e.message}); left alone` };
|
|
234
|
+
}
|
|
235
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
236
|
+
return { error: 'not a JSON object; left alone' };
|
|
237
|
+
}
|
|
238
|
+
const text = `${JSON.stringify(withHook(row, config), null, 2)}\n`;
|
|
239
|
+
return { text, action: text === existing ? 'unchanged' : 'updated' };
|
|
240
|
+
}
|
package/src/argv.mjs
CHANGED
|
@@ -51,6 +51,29 @@ export const BOOL_FLAGS = new Set([
|
|
|
51
51
|
// command whose output changes because something was installed elsewhere is one a
|
|
52
52
|
// reader stops trusting.
|
|
53
53
|
'user',
|
|
54
|
+
// harness loaders: print the vendor facts (order, caps, imports, hooks and MCP
|
|
55
|
+
// config) without measuring a repository. The measurement is the other mode; this is
|
|
56
|
+
// the one that answers "what does this reader do", which does not depend on the repo.
|
|
57
|
+
'table',
|
|
58
|
+
// sync: name every line the inferability filter dropped and the evidence behind it
|
|
59
|
+
// (plan 0003 R09). Off by default because a count is enough to act on; the per-line
|
|
60
|
+
// detail is what an author needs, and a reader is not an author.
|
|
61
|
+
'explain',
|
|
62
|
+
// check: the lock must match. Off by default because a repository that has not
|
|
63
|
+
// written one yet is not drift, and a gate that failed for the absence would be
|
|
64
|
+
// red on every repository this package already adopted.
|
|
65
|
+
'frozen',
|
|
66
|
+
// map: report gold-file recall@10 instead of rendering the ranking. Off by
|
|
67
|
+
// default because the ranking is the answer a task asked for, and the recall
|
|
68
|
+
// figure needs the hidden benchmark patches.
|
|
69
|
+
'recall',
|
|
70
|
+
// tests: also list files that only changed in the same commits as a test.
|
|
71
|
+
// Off by default because a shared commit is not a test.
|
|
72
|
+
'cochange',
|
|
73
|
+
// gate: read the working-tree diff instead of the paths named after the command.
|
|
74
|
+
// Off by default because a path is the change the caller named, and the diff
|
|
75
|
+
// is a different question.
|
|
76
|
+
'changed',
|
|
54
77
|
]);
|
|
55
78
|
|
|
56
79
|
export const VALUE_FLAGS = new Set([
|
|
@@ -71,6 +94,30 @@ export const VALUE_FLAGS = new Set([
|
|
|
71
94
|
// silently applied an unnamed set of rules is the decoration this repository exists
|
|
72
95
|
// to refuse. One is on offer today, so the command rejects any other by name.
|
|
73
96
|
'spec',
|
|
97
|
+
// harness loaders: which reader to report. A value rather than a boolean because
|
|
98
|
+
// "--loader" would not say WHICH loader, and a report that silently applied to all
|
|
99
|
+
// of them would be decoration that misdescribes the artefact.
|
|
100
|
+
'reader',
|
|
101
|
+
// harness loaders: measure as if the agent were standing here. The chain rows walk
|
|
102
|
+
// the project root down to the working directory, so this is what makes them mean
|
|
103
|
+
// anything beyond the root document.
|
|
104
|
+
'at',
|
|
105
|
+
// tokens: split a local Claude Code or Codex session transcript into rules, file
|
|
106
|
+
// reads and tool output. Local paths only (ADR-075).
|
|
107
|
+
'session',
|
|
108
|
+
// bench run / report: arms, harness adapter, model id, repetition count, regrade target and arm comparison.
|
|
109
|
+
'arm',
|
|
110
|
+
'harness',
|
|
111
|
+
'model',
|
|
112
|
+
'reps',
|
|
113
|
+
'regrade',
|
|
114
|
+
'compare',
|
|
115
|
+
// plan done: the test command that proves the slice. Plan 0003 R31.
|
|
116
|
+
'test',
|
|
117
|
+
'state',
|
|
118
|
+
// map: outline (paths), names (exported symbols) or signatures (the line).
|
|
119
|
+
// A boolean would not say which of the three the caller wanted.
|
|
120
|
+
'level',
|
|
74
121
|
]);
|
|
75
122
|
|
|
76
123
|
/**
|
|
@@ -86,7 +133,7 @@ export const VALUE_FLAGS = new Set([
|
|
|
86
133
|
* is a superset of the old behaviour, not a replacement: `--skill a,b` parsed before it
|
|
87
134
|
* parses now.
|
|
88
135
|
*/
|
|
89
|
-
export const REPEATABLE_FLAGS = new Set(['skill']);
|
|
136
|
+
export const REPEATABLE_FLAGS = new Set(['skill', 'arm', 'compare']);
|
|
90
137
|
|
|
91
138
|
/**
|
|
92
139
|
* @param {string[]} argv raw arguments, excluding node and the script path
|
|
@@ -140,6 +187,12 @@ export function parse(argv) {
|
|
|
140
187
|
return { ok: false, code: usage(`--${raw} must be a non-negative integer, received ${value}`) };
|
|
141
188
|
}
|
|
142
189
|
flags[camel(raw)] = n;
|
|
190
|
+
} else if (raw === 'reps') {
|
|
191
|
+
const n = Number(value);
|
|
192
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
193
|
+
return { ok: false, code: usage(`--reps must be a positive integer, received ${value}`) };
|
|
194
|
+
}
|
|
195
|
+
flags.reps = n;
|
|
143
196
|
} else {
|
|
144
197
|
const key = camel(raw);
|
|
145
198
|
flags[key] = REPEATABLE_FLAGS.has(raw)
|