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
package/src/bench/variants.mjs
CHANGED
|
@@ -18,20 +18,19 @@ import { plan } from '../projection/project.mjs';
|
|
|
18
18
|
import { isEligible, loadRules, readDeclared, select } from '../projection/rules.mjs';
|
|
19
19
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
20
20
|
import { CANARY_ALPHA, DEFAULT_CANARY_SIZE, DEL_R_TOLERANCE, shouldEscalateCanary } from './ablate.mjs';
|
|
21
|
+
import { evaluateRunRecord, loadPrices } from '../measure/metrics.mjs';
|
|
22
|
+
import { loadRunRecords } from '../measure/records.mjs';
|
|
21
23
|
import {
|
|
22
24
|
DEFAULT_BOOTSTRAP_SAMPLES,
|
|
23
25
|
DEFAULT_BOOTSTRAP_SEED,
|
|
24
26
|
collapseArmByTask,
|
|
25
27
|
compareArms,
|
|
26
|
-
evaluateRunRecord,
|
|
27
28
|
holmAdjust,
|
|
28
|
-
|
|
29
|
-
loadRunRecords,
|
|
30
|
-
} from './report.mjs';
|
|
29
|
+
} from '../measure/stats.mjs';
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
32
|
* Harnesses that grade, not answer: `gold` applies the oracle patch and `noop` does
|
|
34
|
-
* nothing (`src/bench/adapters.json`, `kind: "
|
|
33
|
+
* nothing (`src/bench/adapters.json`, `kind: "oracle"`). Their runs measure the
|
|
35
34
|
* grader, not the rule text, so they must never close a variant experiment by
|
|
36
35
|
* writing a lock entry (Q45). A test asserts this set matches the adapter table.
|
|
37
36
|
*/
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Isolated workspaces for a benchmarked run.
|
|
2
|
+
//
|
|
3
|
+
// One base tree is exported per task and copied per run (review 0003 Q78): every arm and
|
|
4
|
+
// repetition used to unpack the same `git archive` again, which made the cost of the
|
|
5
|
+
// harness include the cost of exporting the repository. The copy is a fresh directory
|
|
6
|
+
// with no `.git` of its own until the arm has materialised, so two runs of one task can
|
|
7
|
+
// never see each other's edits.
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { STATE_DIR } from '../name.mjs';
|
|
14
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
15
|
+
import { sanitizedEnv, spawnBounded } from '../sys/proc.mjs';
|
|
16
|
+
import { materialiseArm } from './arms.mjs';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_GIT_TIMEOUT_MS = 120_000;
|
|
19
|
+
|
|
20
|
+
/** Run one bounded child process with a sanitised environment. */
|
|
21
|
+
export function runCmd(cmd, args, { cwd, env, input = null, timeoutMs = DEFAULT_GIT_TIMEOUT_MS } = {}) {
|
|
22
|
+
return spawnBounded(cmd, args, { cwd, env: sanitizedEnv(env), input, timeoutMs });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Apply a bounded, order-preserving pool over `items`.
|
|
27
|
+
*
|
|
28
|
+
* @template T, R
|
|
29
|
+
* @param {T[]} items
|
|
30
|
+
* @param {number} concurrency
|
|
31
|
+
* @param {(item: T, index: number) => Promise<R>} fn
|
|
32
|
+
* @returns {Promise<R[]>} results in input order
|
|
33
|
+
*/
|
|
34
|
+
export async function mapPool(items, concurrency, fn) {
|
|
35
|
+
const results = new Array(items.length);
|
|
36
|
+
let cursor = 0;
|
|
37
|
+
async function worker() {
|
|
38
|
+
while (true) {
|
|
39
|
+
const index = cursor;
|
|
40
|
+
cursor += 1;
|
|
41
|
+
if (index >= items.length) return;
|
|
42
|
+
results[index] = await fn(items[index], index);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const count = Math.max(1, Math.min(concurrency, items.length));
|
|
46
|
+
await Promise.all(Array.from({ length: count }, () => worker()));
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Export one commit into a fresh directory: the tree every run of a task starts from.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} root
|
|
54
|
+
* @param {string} baseSha
|
|
55
|
+
* @param {string} parentDir
|
|
56
|
+
* @returns {Promise<string>} the directory holding the unpacked tree
|
|
57
|
+
*/
|
|
58
|
+
export async function prepareBaseTree(root, baseSha, parentDir) {
|
|
59
|
+
const baseDir = await mkdtemp(join(parentDir, 'base-'));
|
|
60
|
+
const archiveRes = await runCmd('git', ['archive', '--format=tar', baseSha], { cwd: root });
|
|
61
|
+
if (archiveRes.code !== 0) {
|
|
62
|
+
await rm(baseDir, { recursive: true, force: true }).catch(() => {});
|
|
63
|
+
throw new Error(`git archive failed for ${baseSha}: ${archiveRes.stderr.toString('utf8').trim()}`);
|
|
64
|
+
}
|
|
65
|
+
const tarRes = await runCmd('tar', ['-xf', '-', '-C', baseDir], {
|
|
66
|
+
cwd: baseDir,
|
|
67
|
+
input: archiveRes.stdout,
|
|
68
|
+
});
|
|
69
|
+
if (tarRes.code !== 0) {
|
|
70
|
+
await rm(baseDir, { recursive: true, force: true }).catch(() => {});
|
|
71
|
+
throw new Error(`tar unpack failed for ${baseSha}: ${tarRes.stderr.toString('utf8').trim()}`);
|
|
72
|
+
}
|
|
73
|
+
return baseDir;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create the workspace one run is measured in: the base tree, the arm materialised over
|
|
78
|
+
* it, one commit, and no tool state.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} root
|
|
81
|
+
* @param {string} baseSha
|
|
82
|
+
* @param {object} armSpec
|
|
83
|
+
* @param {object} [task]
|
|
84
|
+
* @param {{parentDir?: string, baseDir?: string}} [options]
|
|
85
|
+
* @returns {Promise<{slotDir: string, workDir: string, initialHead: string,
|
|
86
|
+
* env: Record<string, string>, promptPrefix: string}>}
|
|
87
|
+
*/
|
|
88
|
+
export async function prepareIsolatedWorkspace(root, baseSha, armSpec, task = {}, { parentDir, baseDir } = {}) {
|
|
89
|
+
const slotDir = await mkdtemp(join(parentDir ?? tmpdir(), 'bench-run-'));
|
|
90
|
+
const workDir = join(slotDir, 'workspace');
|
|
91
|
+
await mkdir(workDir, { recursive: true });
|
|
92
|
+
|
|
93
|
+
if (baseDir) {
|
|
94
|
+
const copied = await runCmd('cp', ['-a', `${baseDir}/.`, workDir], { cwd: workDir });
|
|
95
|
+
if (copied.code !== 0) {
|
|
96
|
+
await rm(slotDir, { recursive: true, force: true }).catch(() => {});
|
|
97
|
+
throw new Error(`copying the base tree failed for ${baseSha}: ${copied.stderr.toString('utf8').trim()}`);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
const archiveRes = await runCmd('git', ['archive', '--format=tar', baseSha], { cwd: root });
|
|
101
|
+
if (archiveRes.code !== 0) {
|
|
102
|
+
await rm(slotDir, { recursive: true, force: true }).catch(() => {});
|
|
103
|
+
throw new Error(`git archive failed for ${baseSha}: ${archiveRes.stderr.toString('utf8').trim()}`);
|
|
104
|
+
}
|
|
105
|
+
const tarRes = await runCmd('tar', ['-xf', '-', '-C', workDir], { cwd: workDir, input: archiveRes.stdout });
|
|
106
|
+
if (tarRes.code !== 0) {
|
|
107
|
+
await rm(slotDir, { recursive: true, force: true }).catch(() => {});
|
|
108
|
+
throw new Error(`tar unpack failed for ${baseSha}: ${tarRes.stderr.toString('utf8').trim()}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Lessons and local profiles live outside the archived commit, so an adopted
|
|
113
|
+
// repository's memory reaches the arm without being written into its base tree.
|
|
114
|
+
const rootLessonsDir = join(root, 'docs', 'lessons');
|
|
115
|
+
const rootLessonFiles = (await readdir(rootLessonsDir).catch(() => [])).filter((f) => f.endsWith('.md'));
|
|
116
|
+
if (rootLessonFiles.length > 0) {
|
|
117
|
+
const destLessonsDir = join(workDir, 'docs', 'lessons');
|
|
118
|
+
await mkdir(destLessonsDir, { recursive: true });
|
|
119
|
+
for (const file of rootLessonFiles) {
|
|
120
|
+
const target = join(destLessonsDir, file);
|
|
121
|
+
if (!existsSync(target)) {
|
|
122
|
+
await writeFile(target, await readFile(join(rootLessonsDir, file), 'utf8'), 'utf8');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const rootProfilesDir = join(root, STATE_DIR, 'profiles');
|
|
128
|
+
const rootProfileFiles = (await readdir(rootProfilesDir).catch(() => [])).filter((f) => f.endsWith('.md'));
|
|
129
|
+
if (rootProfileFiles.length > 0) {
|
|
130
|
+
const destProfilesDir = join(workDir, STATE_DIR, 'profiles');
|
|
131
|
+
await mkdir(destProfilesDir, { recursive: true });
|
|
132
|
+
for (const file of rootProfileFiles) {
|
|
133
|
+
await writeFile(join(destProfilesDir, file), await readFile(join(rootProfilesDir, file), 'utf8'), 'utf8');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await runCmd('git', ['init', '-q'], { cwd: workDir });
|
|
138
|
+
await runCmd('git', ['config', 'user.name', 'bench'], { cwd: workDir });
|
|
139
|
+
await runCmd('git', ['config', 'user.email', 'bench@local'], { cwd: workDir });
|
|
140
|
+
await runCmd('git', ['config', 'commit.gpgsign', 'false'], { cwd: workDir });
|
|
141
|
+
|
|
142
|
+
// The repository exists before an arm materialises: the `brief` arm assembles its
|
|
143
|
+
// packet from git's answer, which a directory that is not yet a repository cannot give.
|
|
144
|
+
const { env, promptPrefix } = await materialiseArm(workDir, armSpec, task);
|
|
145
|
+
// Assembling a packet writes tool state. It belongs to no arm, and leaving it in the
|
|
146
|
+
// base commit would make the `brief` workspace differ by an index as well as by context.
|
|
147
|
+
await rm(join(workDir, LOCAL_DIR), { recursive: true, force: true }).catch(() => {});
|
|
148
|
+
|
|
149
|
+
await runCmd('git', ['add', '-A'], { cwd: workDir });
|
|
150
|
+
const commitRes = await runCmd('git', ['commit', '-qm', 'base', '--allow-empty', '--no-verify'], {
|
|
151
|
+
cwd: workDir,
|
|
152
|
+
});
|
|
153
|
+
if (commitRes.code !== 0) {
|
|
154
|
+
await rm(slotDir, { recursive: true, force: true }).catch(() => {});
|
|
155
|
+
throw new Error(`git commit failed in isolated workspace: ${commitRes.stderr.toString('utf8').trim()}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const headRes = await runCmd('git', ['rev-parse', 'HEAD'], { cwd: workDir });
|
|
159
|
+
return { slotDir, workDir, initialHead: headRes.stdout.toString('utf8').trim(), env, promptPrefix };
|
|
160
|
+
}
|
package/src/brief/assemble.mjs
CHANGED
|
@@ -45,15 +45,17 @@ import { commandCard } from '../kernel/card.mjs';
|
|
|
45
45
|
import { decisionHits, scopeHits, sourceRoots } from '../index/govern.mjs';
|
|
46
46
|
import { embedTaskQuery, rankFiles, renderMap } from '../index/map.mjs';
|
|
47
47
|
import { searchFtsBm25 } from '../index/store.mjs';
|
|
48
|
-
import {
|
|
48
|
+
import { coverage } from '../index/coverage.mjs';
|
|
49
49
|
import { loadRegistry } from '../graph/load.mjs';
|
|
50
50
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
51
|
-
import { closure, rankSkills, selectWithinBudget
|
|
51
|
+
import { closure, rankSkills, selectWithinBudget } from '../resolve/resolve.mjs';
|
|
52
|
+
import { tokenize } from '../text/tokens.mjs';
|
|
52
53
|
import { loadGraph } from '../commands/map.mjs';
|
|
53
54
|
import { dirtyPaths, parsePathList } from '../commands/skills.mjs';
|
|
54
55
|
import { decisionRecords, lessonScopes } from '../commands/why.mjs';
|
|
55
56
|
import { readFederation } from '../federation/config.mjs';
|
|
56
57
|
import { consult } from '../federation/query.mjs';
|
|
58
|
+
import { gitText } from '../sys/git.mjs';
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
61
|
* How many ranked files the packet may consider as candidates for other sections.
|
|
@@ -93,17 +95,11 @@ const MAP_FACTOR = 8;
|
|
|
93
95
|
* only the candidate list; the region parser decides.
|
|
94
96
|
*/
|
|
95
97
|
async function generatedRegions(root) {
|
|
96
|
-
const { execFile } = await import('node:child_process');
|
|
97
|
-
const { promisify } = await import('node:util');
|
|
98
98
|
const { readFile } = await import('node:fs/promises');
|
|
99
99
|
let candidates;
|
|
100
100
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
['grep', '-lF', '--', `${'simt'}:begin`],
|
|
104
|
-
{ cwd: root, maxBuffer: 8 << 20 },
|
|
105
|
-
);
|
|
106
|
-
candidates = stdout.split('\n').filter((line) => line.length > 0).sort();
|
|
101
|
+
const stdout = await gitText(root, ['grep', '-lF', '--', `${'simt'}:begin`], { maxBuffer: 8 << 20 });
|
|
102
|
+
candidates = (stdout ?? '').split('\n').filter((line) => line.length > 0).sort();
|
|
107
103
|
} catch {
|
|
108
104
|
// No match is exit 1, and a directory that is not a repository fails too. Both mean
|
|
109
105
|
// the same thing to a reader: no generated region was found to warn about.
|
|
@@ -152,7 +148,7 @@ export async function assembleBrief({
|
|
|
152
148
|
const dirty = (await dirtyPaths(root)).filter((path) => !path.startsWith(`${STATE_DIR}/`));
|
|
153
149
|
const scope = named ? parsePathList(paths) : dirty;
|
|
154
150
|
|
|
155
|
-
const graph = await loadGraph(root, { build });
|
|
151
|
+
const graph = await loadGraph(root, { build, task, embed: flags?.embed === true });
|
|
156
152
|
if (graph.error) return { error: graph.error };
|
|
157
153
|
if (graph.cold) return { cold: true };
|
|
158
154
|
const graphStale = graph.stale === true;
|
|
@@ -251,13 +247,15 @@ export async function assembleBrief({
|
|
|
251
247
|
}
|
|
252
248
|
|
|
253
249
|
const likely = ranked.slice(0, LIKELY_FILES).map((row) => row.path);
|
|
254
|
-
const known = testEdges({ files, imports: edges.filter((edge) => edge.kind === 'import') });
|
|
255
250
|
const untested = [];
|
|
256
251
|
packet.add('TESTS', `the index knows direct importers; \`${CLI} tests <path>\` is the authority for multi-line and dynamic imports`, {
|
|
257
252
|
why: ['a cheap packet and an exact answer are different questions'],
|
|
258
253
|
});
|
|
259
254
|
for (const path of likely) {
|
|
260
|
-
|
|
255
|
+
// The packet's own edges, walked by the same function `tests` uses (Q77).
|
|
256
|
+
const found = coverage({ files, imports: edges.filter((edge) => edge.kind === 'import') }, [path], {
|
|
257
|
+
transitive: false,
|
|
258
|
+
});
|
|
261
259
|
if (found.length === 0) untested.push(path);
|
|
262
260
|
else packet.add('TESTS', `${path} -> ${found.join(' ')}`, { why: ['the test imports it, or sits beside it'] });
|
|
263
261
|
}
|
package/src/ci/dependabot.mjs
CHANGED
|
@@ -40,61 +40,30 @@ import { join } from 'node:path';
|
|
|
40
40
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
41
41
|
import { isOwnRepo, WORKFLOWS_DIR } from './workflow.mjs';
|
|
42
42
|
import { CLI } from '../name.mjs';
|
|
43
|
+
// Q70: the Dependabot entries are the records' `dependabot` facet, not a table in this
|
|
44
|
+
// file. Adding an ecosystem is a new file under `ecosystems/`, not a row here.
|
|
45
|
+
import { dependabotEntries } from '../ecosystem/records.mjs';
|
|
43
46
|
|
|
44
47
|
/** The one file this module writes. */
|
|
45
48
|
export const DEPENDABOT_FILE = '.github/dependabot.yml';
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
|
-
* fact -> the `package-ecosystem` value Dependabot documents for it.
|
|
51
|
+
* fact/stack -> the `package-ecosystem` value Dependabot documents for it.
|
|
49
52
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* about the estate rather than a code detail.
|
|
53
|
+
* Read from the ecosystem records (ADR-013: content is data, the engine is code), not
|
|
54
|
+
* written here. `dependabotEntries(root)` returns the rows in their declared order; `root`
|
|
55
|
+
* lets a repository's own `.simt/ecosystems/` supply an entry the shipped set does not have,
|
|
56
|
+
* so adding an ecosystem is a new file, not a row in a table the engine edits.
|
|
55
57
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* a dependency name, so a repo's package manager is either of those and adding
|
|
65
|
-
* `has-node-manifest` would be a third name for one observation - the exact
|
|
66
|
-
* duplication that table's own header rules out. `gradle` is the same shape:
|
|
67
|
-
* the stack tag already names the build file. Every other entry below is a
|
|
68
|
-
* real `has-*` fact.
|
|
69
|
-
*
|
|
70
|
-
* `has-build-system` is deliberately NOT mapped: Makefile, CMakeLists and
|
|
71
|
-
* meson.build are build files, not dependency manifests, and Dependabot has no
|
|
72
|
-
* ecosystem that reads them. Mapping it to `maven` would be a lie of exactly the
|
|
73
|
-
* kind the detection-driven ruling exists to prevent.
|
|
58
|
+
* The fact names are already in the fingerprint vocabulary and already carry evidence, so
|
|
59
|
+
* the records add no new observation vocabulary: each entry says which Dependabot value
|
|
60
|
+
* watches which observable manifest. Several facts map to one value on purpose — `has-pyproject`
|
|
61
|
+
* and `has-requirements` are two ways of declaring python dependencies and Dependabot reads
|
|
62
|
+
* both under `pip`; emitting two `pip` entries would be a duplicate the config rejects. A
|
|
63
|
+
* `stack` tag stands in where no fact exists (`node`, `gradle`). `has-build-system` is
|
|
64
|
+
* deliberately NOT mapped: Makefile, CMakeLists and meson.build are build files, not
|
|
65
|
+
* dependency manifests, and Dependabot has no ecosystem that reads them.
|
|
74
66
|
*/
|
|
75
|
-
const ECOSYSTEMS = [
|
|
76
|
-
{ stack: 'node', value: 'npm', manifest: 'package.json' },
|
|
77
|
-
{ stack: 'typescript', value: 'npm', manifest: 'package.json' },
|
|
78
|
-
{ fact: 'has-pyproject', value: 'pip', manifest: 'pyproject.toml' },
|
|
79
|
-
{ fact: 'has-requirements', value: 'pip', manifest: 'requirements.txt' },
|
|
80
|
-
{ fact: 'has-go-mod', value: 'gomod', manifest: 'go.mod' },
|
|
81
|
-
{ fact: 'has-cargo-toml', value: 'cargo', manifest: 'Cargo.toml' },
|
|
82
|
-
{ fact: 'has-gemfile', value: 'bundler', manifest: 'Gemfile' },
|
|
83
|
-
{ fact: 'has-composer', value: 'composer', manifest: 'composer.json' },
|
|
84
|
-
{ fact: 'has-maven-pom', value: 'maven', manifest: 'pom.xml' },
|
|
85
|
-
{ fact: 'has-dockerfile', value: 'docker', manifest: 'Dockerfile' },
|
|
86
|
-
{ fact: 'has-compose', value: 'docker-compose', manifest: 'compose file' },
|
|
87
|
-
{ fact: 'has-terraform', value: 'terraform', manifest: '*.tf' },
|
|
88
|
-
{ stack: 'gradle', value: 'gradle', manifest: 'build.gradle' },
|
|
89
|
-
{ fact: 'has-submodules', value: 'gitsubmodule', manifest: '.gitmodules' },
|
|
90
|
-
{ fact: 'has-nuget', value: 'nuget', manifest: '*.csproj' },
|
|
91
|
-
{ fact: 'has-pubspec', value: 'pub', manifest: 'pubspec.yaml' },
|
|
92
|
-
{ fact: 'has-swift-package', value: 'swift', manifest: 'Package.swift' },
|
|
93
|
-
{ fact: 'has-mix', value: 'mix', manifest: 'mix.exs' },
|
|
94
|
-
{ fact: 'has-uv-lock', value: 'uv', manifest: 'uv.lock' },
|
|
95
|
-
{ fact: 'has-bun-lock', value: 'bun', manifest: 'bun.lock' },
|
|
96
|
-
{ fact: 'has-devcontainer', value: 'devcontainers', manifest: 'devcontainer.json' },
|
|
97
|
-
];
|
|
98
67
|
|
|
99
68
|
/**
|
|
100
69
|
* The ecosystems this tree has a manifest for, in a stable order.
|
|
@@ -108,11 +77,11 @@ const ECOSYSTEMS = [
|
|
|
108
77
|
* @param {Iterable<string>} [stack] emitted stack tags
|
|
109
78
|
* @returns {{value: string, manifest: string}[]}
|
|
110
79
|
*/
|
|
111
|
-
export function ecosystems(facts, stack = []) {
|
|
80
|
+
export function ecosystems(facts, stack = [], root = null) {
|
|
112
81
|
const seenFacts = new Set(facts);
|
|
113
82
|
const seenStack = new Set(stack);
|
|
114
83
|
const out = [];
|
|
115
|
-
for (const row of
|
|
84
|
+
for (const row of dependabotEntries(root)) {
|
|
116
85
|
const found = row.fact ? seenFacts.has(row.fact) : seenStack.has(row.stack);
|
|
117
86
|
if (!found || out.some((e) => e.value === row.value)) continue;
|
|
118
87
|
out.push({ value: row.value, manifest: row.manifest });
|
|
@@ -174,8 +143,8 @@ function entry({ value }) {
|
|
|
174
143
|
* @param {{stack?: Iterable<string>, own?: boolean}} [options]
|
|
175
144
|
* @returns {string} YAML, LF-terminated
|
|
176
145
|
*/
|
|
177
|
-
export function renderDependabot(facts, { stack = [], own = false } = {}) {
|
|
178
|
-
const found = ecosystems(facts, stack);
|
|
146
|
+
export function renderDependabot(facts, { stack = [], own = false, root = null } = {}) {
|
|
147
|
+
const found = ecosystems(facts, stack, root);
|
|
179
148
|
const entries = [entry({ value: 'github-actions' }), ...found.map(entry)];
|
|
180
149
|
return `${header(own)}version: 2
|
|
181
150
|
updates:
|
|
@@ -221,18 +190,19 @@ export async function planDependabot(root) {
|
|
|
221
190
|
// "the file differs" instead of "the file differs from what this repo should get",
|
|
222
191
|
// and those are different questions when a manifest appeared since.
|
|
223
192
|
const { facts, stack } = await fingerprint(root);
|
|
224
|
-
const watched = ecosystems(facts, stack).map((e) => e.value);
|
|
193
|
+
const watched = ecosystems(facts, stack, root).map((e) => e.value);
|
|
225
194
|
// Carried on the plan so the writer renders from the same observation this
|
|
226
195
|
// comparison used. Re-detecting at write time would make `ci` and `check`
|
|
227
196
|
// disagree whenever a manifest appeared between the two calls, and the file
|
|
228
|
-
// would be written from facts nobody ever saw in the plan.
|
|
229
|
-
|
|
197
|
+
// would be written from facts nobody ever saw in the plan. `root` rides along so
|
|
198
|
+
// the writer renders the records a repository's own `.simt/ecosystems/` overrides.
|
|
199
|
+
const detection = { facts, stack, root };
|
|
230
200
|
|
|
231
201
|
if (exists) {
|
|
232
202
|
// Both sides of the comparison, for the same reason as the workflow plan: a reader
|
|
233
203
|
// told the file differs can be shown how, and re-rendering for a diff would let
|
|
234
204
|
// the two answers disagree.
|
|
235
|
-
const desired = renderDependabot(facts, { stack, own: true });
|
|
205
|
+
const desired = renderDependabot(facts, { stack, own: true, root });
|
|
236
206
|
const current = own ? text === desired : true;
|
|
237
207
|
return {
|
|
238
208
|
file: DEPENDABOT_FILE,
|
package/src/commands/agent.mjs
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
// parse that can fail and no partial state, which is what makes this command
|
|
11
11
|
// shorter than `agent-hooks install`.
|
|
12
12
|
|
|
13
|
-
import { readFile } from 'node:fs/promises';
|
|
14
13
|
import { join } from 'node:path';
|
|
15
14
|
|
|
16
15
|
import { commitJournal } from '../journal.mjs';
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
writeAgentPlan,
|
|
29
28
|
} from '../agents/render.mjs';
|
|
30
29
|
import { commandRoot } from '../repo-root.mjs';
|
|
30
|
+
import { readIfExists } from '../util/fs.mjs';
|
|
31
31
|
|
|
32
32
|
export async function agentsCommand(sub, args, flags) {
|
|
33
33
|
const root = commandRoot(flags, args);
|
|
@@ -195,11 +195,3 @@ async function agentsStatus(root, flags) {
|
|
|
195
195
|
emit(lines.join('\n'));
|
|
196
196
|
return problems.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
197
197
|
}
|
|
198
|
-
|
|
199
|
-
async function readIfExists(path) {
|
|
200
|
-
try {
|
|
201
|
-
return await readFile(path, 'utf8');
|
|
202
|
-
} catch {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
import { existsSync } from 'node:fs';
|
|
26
26
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
27
27
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
28
|
-
import { commitJournal
|
|
28
|
+
import { commitJournal } from '../journal.mjs';
|
|
29
|
+
import { applyPlan, change } from '../write/plan.mjs';
|
|
29
30
|
import { AGENT_HOOK_SHIM, renderAgentHookShim } from '../agenthooks/shim.mjs';
|
|
30
31
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
31
32
|
|
|
@@ -179,18 +180,26 @@ export async function agentHooksCommand(sub, args, flags) {
|
|
|
179
180
|
// `skipped` was an adapter the caller did not ask for (ADR-107).
|
|
180
181
|
const OTHER_LABEL = { none: 'NO FILE ', other: 'NOT OURS', skipped: 'SKIPPED ' };
|
|
181
182
|
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
/**
|
|
184
|
+
* The agent-hooks install, split from `installHooks` into the plan and the apply so
|
|
185
|
+
* `init` can run the same writes inside its own journal (ADR-002: a second real use).
|
|
186
|
+
*
|
|
187
|
+
* The plan is pure: it reads each documented config, renders the entry, and returns the
|
|
188
|
+
* shim the same way, so a preview and an apply describe the same set of files. A config
|
|
189
|
+
* this tool cannot parse lands in `refused` with its action set to `refused`, and the
|
|
190
|
+
* caller decides what that means — this command blocks on it, and `init` inherits the
|
|
191
|
+
* refusal because writing the other configs while one is unreadable would leave the
|
|
192
|
+
* repository in a state where the report it printed is no longer true.
|
|
193
|
+
*/
|
|
194
|
+
export async function planAgentHooks(root, { harness }) {
|
|
184
195
|
// `--harness` narrows the install to one reader (ADR-107). The id is validated
|
|
185
196
|
// against the same table the loop reads, so a typo fails before anything is
|
|
186
197
|
// planned; the remedy names the ids the way `agent-hook` does.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`--harness <id>, one of: ${adapters().map((a) => a.id).join(', ')}`,
|
|
193
|
-
);
|
|
198
|
+
if (harness !== undefined && adapter(harness) === null) {
|
|
199
|
+
return {
|
|
200
|
+
error: `unknown harness \`${harness}\``,
|
|
201
|
+
remedy: `--harness <id>, one of: ${adapters().map((a) => a.id).join(', ')}`,
|
|
202
|
+
};
|
|
194
203
|
}
|
|
195
204
|
// The delivery is a second entry in the same config, and a repository asks for it by
|
|
196
205
|
// declaring a `brief` key rather than by having the entry. Read through a dynamic
|
|
@@ -202,7 +211,7 @@ async function installHooks(root, flags) {
|
|
|
202
211
|
listPromotedLessons(root).catch(() => []),
|
|
203
212
|
]);
|
|
204
213
|
const hasLessons = promotedLessons.length > 0;
|
|
205
|
-
const rows =
|
|
214
|
+
const rows = harness === undefined ? adapters() : adapters().filter((r) => r.id === harness);
|
|
206
215
|
const plan = [];
|
|
207
216
|
for (const row of rows) {
|
|
208
217
|
const path = hookPath(row.id);
|
|
@@ -232,7 +241,7 @@ async function installHooks(root, flags) {
|
|
|
232
241
|
for (const r of hookRows()) {
|
|
233
242
|
if (r.path === null) others.push({ id: r.id, path: null, state: 'none', note: r.note });
|
|
234
243
|
else if (adapter(r.id) === null) others.push({ id: r.id, path: r.path, state: 'other', note: r.note });
|
|
235
|
-
else if (
|
|
244
|
+
else if (harness !== undefined && r.id !== harness) {
|
|
236
245
|
others.push({ id: r.id, path: r.path, state: 'skipped', note: 'not requested; install without --harness to write every harness' });
|
|
237
246
|
}
|
|
238
247
|
}
|
|
@@ -240,13 +249,48 @@ async function installHooks(root, flags) {
|
|
|
240
249
|
const refused = plan.filter((p) => p.error);
|
|
241
250
|
|
|
242
251
|
// The shim every harness config points at: without it the entries above name a
|
|
243
|
-
// binary no harness has on PATH (review 0003 Q21).
|
|
244
|
-
// dependency the same way the git shims' does — a devDependency where the
|
|
245
|
-
// repository has a package.json, the release binary where it does not.
|
|
252
|
+
// binary no harness has on PATH (review 0003 Q21).
|
|
246
253
|
const shimText = renderAgentHookShim({ npm: (await readIfExists(join(root, 'package.json'))) !== null });
|
|
247
254
|
const shimBefore = await readBefore(join(root, AGENT_HOOK_SHIM));
|
|
248
255
|
const shimAction = shimBefore === null ? 'create' : shimBefore === shimText ? 'unchanged' : 'update';
|
|
249
|
-
const
|
|
256
|
+
const shim = { text: shimText, before: shimBefore, action: shimAction, row: { id: 'shim', path: AGENT_HOOK_SHIM, action: shimAction } };
|
|
257
|
+
|
|
258
|
+
return { plan, shim, others, promptLine, refused };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Write the planned agent-hooks entries and the shim into the caller's journal. Nothing
|
|
263
|
+
* is committed here: the caller commits its own journal under its own label, which is
|
|
264
|
+
* what separates this command's run (a standalone `agent-hooks install`) from `init`'s
|
|
265
|
+
* absorbed one (part of the adoption, committed with the rest of it).
|
|
266
|
+
*/
|
|
267
|
+
export async function applyAgentHooks(root, journal, { plan, shim, owner }) {
|
|
268
|
+
const written = [];
|
|
269
|
+
const hookChanges = [];
|
|
270
|
+
for (const p of plan.filter((x) => x.action !== 'unchanged')) {
|
|
271
|
+
const path = join(root, p.path);
|
|
272
|
+
hookChanges.push(change({ path, before: await readBefore(path), after: p.text, owner, reason: p.action }));
|
|
273
|
+
written.push(p);
|
|
274
|
+
}
|
|
275
|
+
// The shim is a file like the configs: unchanged installs leave it alone, and a
|
|
276
|
+
// changed one is journaled so `undo` removes or restores it with the configs.
|
|
277
|
+
let shimWritten = false;
|
|
278
|
+
if (shim.action !== 'unchanged') {
|
|
279
|
+
const shimPath = join(root, AGENT_HOOK_SHIM);
|
|
280
|
+
hookChanges.push(change({ path: shimPath, before: await readBefore(shimPath), after: shim.text, owner, reason: shim.action, mode: 0o755 }));
|
|
281
|
+
shimWritten = true;
|
|
282
|
+
}
|
|
283
|
+
await applyPlan(root, hookChanges, { mode: { apply: true }, journal, commit: false });
|
|
284
|
+
return { written, shimWritten };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function installHooks(root, flags) {
|
|
288
|
+
const mode = writeMode(flags);
|
|
289
|
+
const planned = await planAgentHooks(root, { harness: flags.harness });
|
|
290
|
+
if (planned.error) {
|
|
291
|
+
return fail(planned.error, EXIT.USAGE, planned.remedy);
|
|
292
|
+
}
|
|
293
|
+
const { plan, shim, others, promptLine, refused } = planned;
|
|
250
294
|
|
|
251
295
|
// The `--diff` pairs, read before anything is written so the before side is
|
|
252
296
|
// true in preview and in apply alike (R50).
|
|
@@ -256,7 +300,7 @@ async function installHooks(root, flags) {
|
|
|
256
300
|
plan
|
|
257
301
|
.filter((p) => p.action !== 'unchanged')
|
|
258
302
|
.map(async (p) => ({ path: p.path, before: await readBefore(join(root, p.path)), after: p.text }))
|
|
259
|
-
.concat(
|
|
303
|
+
.concat(shim.action === 'unchanged' ? [] : [{ path: AGENT_HOOK_SHIM, before: shim.before, after: shim.text }]),
|
|
260
304
|
)
|
|
261
305
|
: [];
|
|
262
306
|
|
|
@@ -268,7 +312,7 @@ async function installHooks(root, flags) {
|
|
|
268
312
|
root,
|
|
269
313
|
dryRun: true,
|
|
270
314
|
plan: plan.map(({ text, ...rest }) => rest),
|
|
271
|
-
shim:
|
|
315
|
+
shim: shim.row,
|
|
272
316
|
notWritten: others,
|
|
273
317
|
filesWritten: false,
|
|
274
318
|
...diffField(diffs, flags),
|
|
@@ -279,7 +323,7 @@ async function installHooks(root, flags) {
|
|
|
279
323
|
[
|
|
280
324
|
`repo ${root}`,
|
|
281
325
|
...plan.map((p) => `${p.action.toUpperCase().padEnd(9)} ${p.id.padEnd(12)} ${p.path}${p.error ? ` (${p.error})` : ''}`),
|
|
282
|
-
`${
|
|
326
|
+
`${shim.action.toUpperCase().padEnd(9)} shim ${AGENT_HOOK_SHIM}`,
|
|
283
327
|
...others.map((o) => `${OTHER_LABEL[o.state]} ${o.id.padEnd(12)} ${o.path ?? '(no hooks file is documented)'}`),
|
|
284
328
|
...promptLine,
|
|
285
329
|
'',
|
|
@@ -302,20 +346,7 @@ async function installHooks(root, flags) {
|
|
|
302
346
|
|
|
303
347
|
// Journaled so `undo` restores this run (R49).
|
|
304
348
|
const journal = [];
|
|
305
|
-
const written =
|
|
306
|
-
for (const p of plan.filter((x) => x.action !== 'unchanged')) {
|
|
307
|
-
await mkdir(join(root, dirname(p.path)), { recursive: true });
|
|
308
|
-
await journaledWriteFile(root, journal, join(root, p.path), p.text);
|
|
309
|
-
written.push(p);
|
|
310
|
-
}
|
|
311
|
-
// The shim is a file like the configs: unchanged installs leave it alone, and a
|
|
312
|
-
// changed one is journaled so `undo` removes or restores it with the configs.
|
|
313
|
-
let shimWritten = false;
|
|
314
|
-
if (shimAction !== 'unchanged') {
|
|
315
|
-
await mkdir(join(root, dirname(AGENT_HOOK_SHIM)), { recursive: true });
|
|
316
|
-
await journaledWriteFile(root, journal, join(root, AGENT_HOOK_SHIM), shimText, { mode: 0o755 });
|
|
317
|
-
shimWritten = true;
|
|
318
|
-
}
|
|
349
|
+
const { written, shimWritten } = await applyAgentHooks(root, journal, { plan, shim, owner: 'agent-hooks install' });
|
|
319
350
|
await commitJournal(root, 'agent-hooks install', journal);
|
|
320
351
|
|
|
321
352
|
const notes = [];
|
|
@@ -331,7 +362,7 @@ async function installHooks(root, flags) {
|
|
|
331
362
|
command: 'agent-hooks install',
|
|
332
363
|
root,
|
|
333
364
|
plan: plan.map(({ text, ...rest }) => rest),
|
|
334
|
-
shim: { ...
|
|
365
|
+
shim: { ...shim.row, written: shimWritten },
|
|
335
366
|
notWritten: others,
|
|
336
367
|
written: written.map((p) => p.path),
|
|
337
368
|
filesWritten: true,
|
|
@@ -344,7 +375,7 @@ async function installHooks(root, flags) {
|
|
|
344
375
|
[
|
|
345
376
|
`repo ${root}`,
|
|
346
377
|
...plan.map((p) => `${p.action.toUpperCase().padEnd(9)} ${p.id.padEnd(12)} ${p.path}`),
|
|
347
|
-
`${
|
|
378
|
+
`${shim.action.toUpperCase().padEnd(9)} shim ${AGENT_HOOK_SHIM}`,
|
|
348
379
|
...others.map((o) => `${OTHER_LABEL[o.state]} ${o.id.padEnd(12)} ${o.path ?? '(no hooks file is documented)'}`),
|
|
349
380
|
...promptLine,
|
|
350
381
|
'',
|
package/src/commands/bench.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
11
11
|
import { CLI } from '../name.mjs';
|
|
12
12
|
import { EXIT, emit, emitJson, fail, isJson, writeMode } from '../output/index.mjs';
|
|
13
13
|
import { DEFAULT_MINE_TARGET, mineTasks } from '../bench/mine.mjs';
|
|
14
|
-
import { buildCorpusDocument, verifyCorpusDocument, writeCorpusDocument } from '../
|
|
14
|
+
import { buildCorpusDocument, verifyCorpusDocument, writeCorpusDocument } from '../measure/corpus.mjs';
|
|
15
15
|
import { regradeRun, runBench } from '../bench/run.mjs';
|
|
16
16
|
import { buildBenchReport } from '../bench/report.mjs';
|
|
17
17
|
import { formatRecall, measureRecall } from '../index/map.mjs';
|
|
@@ -233,7 +233,7 @@ export async function benchReportCommand(positional, flags) {
|
|
|
233
233
|
);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
const root = resolve(flags
|
|
236
|
+
const root = resolve(commandRoot(flags, pos));
|
|
237
237
|
const arms = listFlag(flags.arm);
|
|
238
238
|
|
|
239
239
|
let report;
|