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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The read-only half of `init`: the conflict scan and the pre-write escape hatch.
|
|
2
|
+
//
|
|
3
|
+
// Split out of init.mjs when it reached the repository ceiling. Neither function writes
|
|
4
|
+
// the manifest; both answer a question the command asks before it does.
|
|
5
|
+
|
|
6
|
+
import { mkdir, readFile, symlink } from 'node:fs/promises';
|
|
7
|
+
import { git as sysGit } from '../sys/git.mjs';
|
|
8
|
+
import { rulePaths, scanDocument, summarise } from '../projection/conflicts.mjs';
|
|
9
|
+
import { parse as parseRegion } from '../projection/markers.mjs';
|
|
10
|
+
|
|
11
|
+
const BACKUP_TAG = 'backup-before-simt-dev';
|
|
12
|
+
const ADOPT_BRANCH = 'simt-dev/adopt';
|
|
13
|
+
|
|
14
|
+
async function scanRepo(root) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
for (const path of await rulePaths(root)) {
|
|
17
|
+
let text;
|
|
18
|
+
try {
|
|
19
|
+
text = await readFile(path, 'utf8');
|
|
20
|
+
} catch {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const region = parseRegion(text);
|
|
24
|
+
const handWritten = region.status === 'ok' ? `${region.before}\n${region.after}` : text;
|
|
25
|
+
findings.push(...scanDocument(handWritten, path));
|
|
26
|
+
}
|
|
27
|
+
return findings;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The opt-in escape hatch before the first write, taken when --backup is passed
|
|
32
|
+
* (ADR-063 amended plan 0002 §8.2 item 3 from "always" to on request). Both operations
|
|
33
|
+
* are best-effort: a repo that is not a git repo, or has no commits yet, must still be
|
|
34
|
+
* adoptable. Failure is reported as a note rather than blocking, because refusing
|
|
35
|
+
* to initialise an unborn repo would be worse than proceeding without a backup.
|
|
36
|
+
*/
|
|
37
|
+
async function establishEscapeHatch(root) {
|
|
38
|
+
const result = { branch: null, tag: null, from: null, notes: [] };
|
|
39
|
+
const isRepo = await git(root, ['rev-parse', '--is-inside-work-tree']);
|
|
40
|
+
if (!isRepo.ok) {
|
|
41
|
+
result.notes.push('not a git work tree; no backup branch or tag created');
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dirty = await git(root, ['status', '--porcelain']);
|
|
46
|
+
if (dirty.ok && dirty.stdout.trim() !== '') {
|
|
47
|
+
result.notes.push('worktree already dirty; left untouched, existing changes are not staged');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const head = await git(root, ['rev-parse', '--verify', 'HEAD']);
|
|
51
|
+
if (!head.ok) {
|
|
52
|
+
result.notes.push('no commits yet; nothing to tag');
|
|
53
|
+
} else {
|
|
54
|
+
const hasTag = await git(root, ['rev-parse', '--verify', `refs/tags/${BACKUP_TAG}`]);
|
|
55
|
+
if (hasTag.ok) {
|
|
56
|
+
result.tag = BACKUP_TAG;
|
|
57
|
+
} else {
|
|
58
|
+
const tag = await git(root, ['tag', BACKUP_TAG]);
|
|
59
|
+
if (tag.ok) result.tag = BACKUP_TAG;
|
|
60
|
+
else result.notes.push(`could not create tag ${BACKUP_TAG}: ${tag.stderr.trim()}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const current = await git(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
65
|
+
if (current.ok && current.stdout.trim() !== ADOPT_BRANCH) {
|
|
66
|
+
// Recorded rather than re-read by the caller: only this function knows what the
|
|
67
|
+
// branch was before it moved it, and a second `rev-parse` would answer with the new
|
|
68
|
+
// value and make every "you were on X" line read as "you were on Y".
|
|
69
|
+
result.from = current.stdout.trim();
|
|
70
|
+
const branch = await git(root, ['checkout', '-b', ADOPT_BRANCH]);
|
|
71
|
+
if (branch.ok) result.branch = ADOPT_BRANCH;
|
|
72
|
+
else result.notes.push(`could not create branch ${ADOPT_BRANCH}: ${branch.stderr.trim()}`);
|
|
73
|
+
} else if (current.ok) {
|
|
74
|
+
result.branch = ADOPT_BRANCH;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function git(cwd, args) {
|
|
81
|
+
try {
|
|
82
|
+
const stdout = await sysGit(cwd, args);
|
|
83
|
+
return { ok: true, stdout, stderr: '' };
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return { ok: false, stdout: error.stdout ?? '', stderr: error.stderr ?? '' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
export { ADOPT_BRANCH, BACKUP_TAG, establishEscapeHatch, git, scanRepo };
|
package/src/commands/init.mjs
CHANGED
|
@@ -15,10 +15,9 @@
|
|
|
15
15
|
// 6. write only with --yes
|
|
16
16
|
// Never commits, never pushes (ADR-001).
|
|
17
17
|
|
|
18
|
-
import { execFile } from 'node:child_process';
|
|
19
|
-
import { promisify } from 'node:util';
|
|
20
18
|
import { mkdir, readFile, symlink } from 'node:fs/promises';
|
|
21
19
|
import { commitJournal, ignoreJournal, journaledWriteFile } from '../journal.mjs';
|
|
20
|
+
import { applyPlan, change } from '../write/plan.mjs';
|
|
22
21
|
import { invalidateCheckCache } from '../status.mjs';
|
|
23
22
|
import { dirname, join } from 'node:path';
|
|
24
23
|
|
|
@@ -47,6 +46,7 @@ import { pointerState, pointerTargets } from '../projection/project.mjs';
|
|
|
47
46
|
import { surfacesLine } from '../projection/surfaces.mjs';
|
|
48
47
|
import { plan, provenanceDigest, PRIMARY_TARGET, LOCAL_PROFILE_DIR } from '../projection/project.mjs';
|
|
49
48
|
import { planAgents, writeAgentPlan, writeSkillFile } from '../agents/render.mjs';
|
|
49
|
+
import { applyAgentHooks, planAgentHooks } from './agenthook.mjs';
|
|
50
50
|
import { computeLock } from '../projection/lock.mjs';
|
|
51
51
|
import { parse as parseRegion } from '../projection/markers.mjs';
|
|
52
52
|
import { reindex, SQLITE_FILE } from '../index/store.mjs';
|
|
@@ -56,11 +56,9 @@ import { packageVersion, hasNpm } from '../package-version.mjs';
|
|
|
56
56
|
import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
|
|
57
57
|
import { PACKAGE_NAME } from '../name.mjs';
|
|
58
58
|
import { commandRoot } from '../repo-root.mjs';
|
|
59
|
+
import { git as sysGit } from '../sys/git.mjs';
|
|
60
|
+
import { ADOPT_BRANCH, BACKUP_TAG, establishEscapeHatch, git, scanRepo } from './init-scan.mjs';
|
|
59
61
|
|
|
60
|
-
const run = promisify(execFile);
|
|
61
|
-
|
|
62
|
-
const BACKUP_TAG = 'backup-before-simt-dev';
|
|
63
|
-
const ADOPT_BRANCH = 'simt-dev/adopt';
|
|
64
62
|
|
|
65
63
|
export async function initCommand(positional, flags) {
|
|
66
64
|
const root = commandRoot(flags, positional);
|
|
@@ -171,10 +169,29 @@ export async function initCommand(positional, flags) {
|
|
|
171
169
|
const ci = await planWorkflow(root);
|
|
172
170
|
const dependabot = await planDependabot(root);
|
|
173
171
|
const tsconfigBase = await planTsconfigBase(root);
|
|
172
|
+
// `--no-agents` opts out of the agent files and the agent-hooks in one flag: they are
|
|
173
|
+
// the two halves of the same decision — which harnesses this adoption speaks to — and
|
|
174
|
+
// splitting them into two flags would let a reader ask for the files without the hooks
|
|
175
|
+
// and end up with an agent the repository's own gate does not enforce. The parser's
|
|
176
|
+
// `no-` convention delivers it as the positive `agents` set to false.
|
|
177
|
+
const noAgents = flags.agents === false;
|
|
174
178
|
// ADR-111. The one agent this tool ships, planned with the rest so the operator sees
|
|
175
179
|
// it in the same preview that lists everything else. Written in the apply path with
|
|
176
|
-
// everything else, journaled the same way.
|
|
177
|
-
|
|
180
|
+
// everything else, journaled the same way. `--harness` narrows it to one reader, the
|
|
181
|
+
// same axis as the agent-hooks below, so the files and the hooks speak to the same set
|
|
182
|
+
// of harnesses (the rule surfaces are a separate axis: `--surfaces`).
|
|
183
|
+
const agentPlan = noAgents ? [] : await planAgents(root, { harness: flags.harness });
|
|
184
|
+
// The agent-hooks entry, absorbed into this run so the adoption enforces its own
|
|
185
|
+
// projection while the agent works. Planned with the rest for the same reason, and
|
|
186
|
+
// written into this run's journal (committed with the rest, not a second journal).
|
|
187
|
+
// `--harness` narrows it to the same reader the agent files are narrowed to.
|
|
188
|
+
let agentHooks = null;
|
|
189
|
+
if (!noAgents) {
|
|
190
|
+
agentHooks = await planAgentHooks(root, { harness: flags.harness });
|
|
191
|
+
if (agentHooks.error) {
|
|
192
|
+
return fail(agentHooks.error, EXIT.USAGE, agentHooks.remedy);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
178
195
|
// Per-developer, and planned with the rest so the operator sees it in the same
|
|
179
196
|
// preview that lists everything else. `exists` short-circuits the write: a
|
|
180
197
|
// repository that already has one keeps it exactly as it is.
|
|
@@ -256,6 +273,18 @@ export async function initCommand(positional, flags) {
|
|
|
256
273
|
// plans strip their rendered bytes; the apply path writes from `agentPlan`, which
|
|
257
274
|
// keeps it.
|
|
258
275
|
agents: agentPlan.map(({ text, ...rest }) => rest),
|
|
276
|
+
// The agent-hooks entry the apply path will write, absorbed into this run so the
|
|
277
|
+
// adoption enforces its own projection while the agent works. `text` is stripped the
|
|
278
|
+
// same way; the apply path writes from `agentHooks`, which keeps it. `null` when
|
|
279
|
+
// `--no-agents` opted out of the hooks (and the agent files with them).
|
|
280
|
+
agentHooks: agentHooks
|
|
281
|
+
? {
|
|
282
|
+
plan: agentHooks.plan.map(({ text, ...rest }) => rest),
|
|
283
|
+
shim: agentHooks.shim.row,
|
|
284
|
+
notWritten: agentHooks.others,
|
|
285
|
+
refused: agentHooks.refused.map(({ text, ...rest }) => rest),
|
|
286
|
+
}
|
|
287
|
+
: null,
|
|
259
288
|
adoption,
|
|
260
289
|
manifest: { file: MANIFEST, action: manifestAction, path: manifestFile },
|
|
261
290
|
// Local state, not a repository file: gitignored, rebuildable from `git ls-files -s`,
|
|
@@ -350,6 +379,28 @@ export async function initCommand(positional, flags) {
|
|
|
350
379
|
);
|
|
351
380
|
}
|
|
352
381
|
|
|
382
|
+
// An unreadable hooks config is a refusal, not a failure to try harder: init inherits
|
|
383
|
+
// the standalone command's refusal and blocks the whole run, because writing the other
|
|
384
|
+
// hooks while one is unreadable would leave the adoption in a state where the report it
|
|
385
|
+
// just printed is no longer true. The remedy names the opt-out, because a repository
|
|
386
|
+
// that cannot be read by one harness can still be adopted without the agent layer.
|
|
387
|
+
if (agentHooks !== null && agentHooks.refused.length > 0) {
|
|
388
|
+
payload.ok = false;
|
|
389
|
+
payload.blockedBy = 'agent-hooks-refused';
|
|
390
|
+
if (isJson(flags)) {
|
|
391
|
+
emitJson(payload);
|
|
392
|
+
} else {
|
|
393
|
+
emit(render(payload, manifest, { npm, manager, version }));
|
|
394
|
+
note('');
|
|
395
|
+
note(`refused ${agentHooks.refused.length} hooks config(s) could not be read; nothing written`);
|
|
396
|
+
}
|
|
397
|
+
return fail(
|
|
398
|
+
`${root} has a hooks config init cannot read: ${agentHooks.refused.map((p) => `${p.path} (${p.error})`).join('; ')}`,
|
|
399
|
+
EXIT.FAIL,
|
|
400
|
+
`fix or remove the named config file, then rerun: ${CLI} init --yes | or opt out of the agent layer: ${CLI} init --no-agents --yes`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
353
404
|
// One malformed target refuses the whole adoption, not just that file. Writing the
|
|
354
405
|
// others first would leave the repo half-adopted with the half that failed having
|
|
355
406
|
// produced no change at all — indistinguishable, in the diff, from never running.
|
|
@@ -408,6 +459,7 @@ export async function initCommand(positional, flags) {
|
|
|
408
459
|
after: renderDependabot(payload.dependabot.facts, {
|
|
409
460
|
stack: payload.dependabot.stack,
|
|
410
461
|
own: payload.dependabot.own,
|
|
462
|
+
root: payload.dependabot.root,
|
|
411
463
|
}),
|
|
412
464
|
});
|
|
413
465
|
}
|
|
@@ -538,6 +590,7 @@ export async function initCommand(positional, flags) {
|
|
|
538
590
|
renderDependabot(payload.dependabot.facts, {
|
|
539
591
|
stack: payload.dependabot.stack,
|
|
540
592
|
own: payload.dependabot.own,
|
|
593
|
+
root: payload.dependabot.root,
|
|
541
594
|
}),
|
|
542
595
|
);
|
|
543
596
|
note(
|
|
@@ -548,26 +601,32 @@ export async function initCommand(positional, flags) {
|
|
|
548
601
|
// scaffolds the manifest and one region but leaves the `.mdc` rules for a later
|
|
549
602
|
// `sync` produces a repo whose `check` fails on files the user was never told
|
|
550
603
|
// about — which reads as a broken adoption rather than an unfinished one.
|
|
604
|
+
//
|
|
605
|
+
// The malformed guard runs before anything is applied. A target this run cannot write
|
|
606
|
+
// has to stop the run, not leave the earlier targets written and the late one reported.
|
|
607
|
+
const badTarget = projected.targets.find((t) => t.action === 'malformed');
|
|
608
|
+
if (badTarget) {
|
|
609
|
+
// Checked before any write above; reaching here means the guard moved.
|
|
610
|
+
return fail(
|
|
611
|
+
`${badTarget.file}: ${badTarget.reason}`,
|
|
612
|
+
EXIT.FAIL,
|
|
613
|
+
`fix the markers at ${badTarget.file}:${badTarget.line}, then rerun: ${CLI} init --yes`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
// `before` is read, not inferred from `target.action`: the engine derives create from
|
|
617
|
+
// missing before-bytes, and a writer that passes its own action is the bug it removes.
|
|
618
|
+
const targetChanges = await Promise.all(
|
|
619
|
+
projected.targets
|
|
620
|
+
.filter((t) => t.action !== 'unchanged')
|
|
621
|
+
.map(async (t) =>
|
|
622
|
+
change({ path: t.path, before: await readBefore(t.path), after: t.text, owner: 'init', reason: t.action }),
|
|
623
|
+
),
|
|
624
|
+
);
|
|
625
|
+
// The caller's journal, committed once below with the manifest and identity writes.
|
|
626
|
+
await applyPlan(root, targetChanges, { mode: { apply: true }, journal, commit: false });
|
|
551
627
|
for (const target of projected.targets) {
|
|
552
|
-
if (target.action === 'unchanged') {
|
|
553
|
-
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
if (target.action === 'malformed') {
|
|
557
|
-
// Checked before any write above; reaching here means the guard moved.
|
|
558
|
-
return fail(
|
|
559
|
-
`${target.file}: ${target.reason}`,
|
|
560
|
-
EXIT.FAIL,
|
|
561
|
-
`fix the markers at ${target.file}:${target.line}, then rerun: ${CLI} init --yes`,
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
// A per-rule target lives in `.cursor/rules/`, `.claude/rules/`,
|
|
565
|
-
// `.agents/rules/` or `.github/instructions/` — four directories a repo that had
|
|
566
|
-
// no such configuration at all will not have. Creating them is part of adopting,
|
|
567
|
-
// not a detail of writing the file.
|
|
568
|
-
await mkdir(dirname(target.path), { recursive: true });
|
|
569
|
-
await journaledWriteFile(root, journal, target.path, target.text);
|
|
570
|
-
note(`wrote ${target.file} (${target.action})`);
|
|
628
|
+
if (target.action === 'unchanged') note(`current ${target.file} (already up to date)`);
|
|
629
|
+
else note(`wrote ${target.file} (${target.action})`);
|
|
571
630
|
}
|
|
572
631
|
|
|
573
632
|
// Pointers, after the file they point at exists. A regular file already at one of
|
|
@@ -594,6 +653,20 @@ export async function initCommand(positional, flags) {
|
|
|
594
653
|
// which keeps the rendered bytes the display payload strips.
|
|
595
654
|
const agentWritten = await writeAgentPlan(root, journal, agentPlan);
|
|
596
655
|
for (const f of agentWritten) note(`wrote ${f} (agent)`);
|
|
656
|
+
// The agent-hooks entry, written into this run's journal and committed with the rest of
|
|
657
|
+
// the adoption. It is part of the adoption, not a second run: a second journal would
|
|
658
|
+
// make `undo` restore the hooks separately from the files they enforce, and the reader
|
|
659
|
+
// would not know the two moves belong together.
|
|
660
|
+
if (agentHooks !== null) {
|
|
661
|
+
const { written, shimWritten } = await applyAgentHooks(root, journal, {
|
|
662
|
+
plan: agentHooks.plan,
|
|
663
|
+
shim: agentHooks.shim,
|
|
664
|
+
owner: 'init',
|
|
665
|
+
});
|
|
666
|
+
for (const p of written) note(`wrote ${p.path} (agent-hooks)`);
|
|
667
|
+
if (shimWritten) note(`wrote ${agentHooks.shim.row.path} (agent-hooks shim)`);
|
|
668
|
+
if (written.length === 0 && !shimWritten) note('agent-hooks every documented config already carried this tools entry');
|
|
669
|
+
}
|
|
597
670
|
await commitJournal(root, 'init', journal);
|
|
598
671
|
// The tree changed, so the last `check` no longer describes it: the line
|
|
599
672
|
// goes back to "no cache" rather than reporting the tree as it was (R48).
|
|
@@ -645,81 +718,6 @@ export async function initCommand(positional, flags) {
|
|
|
645
718
|
/** Scan whichever rule files exist, named or globbed. A missing file is not a
|
|
646
719
|
* finding. Generated regions are skipped: quoting a ruling is not a conflict with
|
|
647
720
|
* it, and the region is rewritten by `sync` anyway. */
|
|
648
|
-
async function scanRepo(root) {
|
|
649
|
-
const findings = [];
|
|
650
|
-
for (const path of await rulePaths(root)) {
|
|
651
|
-
let text;
|
|
652
|
-
try {
|
|
653
|
-
text = await readFile(path, 'utf8');
|
|
654
|
-
} catch {
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
const region = parseRegion(text);
|
|
658
|
-
const handWritten = region.status === 'ok' ? `${region.before}\n${region.after}` : text;
|
|
659
|
-
findings.push(...scanDocument(handWritten, path));
|
|
660
|
-
}
|
|
661
|
-
return findings;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* The opt-in escape hatch before the first write, taken when --backup is passed
|
|
666
|
-
* (ADR-063 amended plan 0002 §8.2 item 3 from "always" to on request). Both operations
|
|
667
|
-
* are best-effort: a repo that is not a git repo, or has no commits yet, must still be
|
|
668
|
-
* adoptable. Failure is reported as a note rather than blocking, because refusing
|
|
669
|
-
* to initialise an unborn repo would be worse than proceeding without a backup.
|
|
670
|
-
*/
|
|
671
|
-
async function establishEscapeHatch(root) {
|
|
672
|
-
const result = { branch: null, tag: null, from: null, notes: [] };
|
|
673
|
-
const isRepo = await git(root, ['rev-parse', '--is-inside-work-tree']);
|
|
674
|
-
if (!isRepo.ok) {
|
|
675
|
-
result.notes.push('not a git work tree; no backup branch or tag created');
|
|
676
|
-
return result;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const dirty = await git(root, ['status', '--porcelain']);
|
|
680
|
-
if (dirty.ok && dirty.stdout.trim() !== '') {
|
|
681
|
-
result.notes.push('worktree already dirty; left untouched, existing changes are not staged');
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
const head = await git(root, ['rev-parse', '--verify', 'HEAD']);
|
|
685
|
-
if (!head.ok) {
|
|
686
|
-
result.notes.push('no commits yet; nothing to tag');
|
|
687
|
-
} else {
|
|
688
|
-
const hasTag = await git(root, ['rev-parse', '--verify', `refs/tags/${BACKUP_TAG}`]);
|
|
689
|
-
if (hasTag.ok) {
|
|
690
|
-
result.tag = BACKUP_TAG;
|
|
691
|
-
} else {
|
|
692
|
-
const tag = await git(root, ['tag', BACKUP_TAG]);
|
|
693
|
-
if (tag.ok) result.tag = BACKUP_TAG;
|
|
694
|
-
else result.notes.push(`could not create tag ${BACKUP_TAG}: ${tag.stderr.trim()}`);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
const current = await git(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
699
|
-
if (current.ok && current.stdout.trim() !== ADOPT_BRANCH) {
|
|
700
|
-
// Recorded rather than re-read by the caller: only this function knows what the
|
|
701
|
-
// branch was before it moved it, and a second `rev-parse` would answer with the new
|
|
702
|
-
// value and make every "you were on X" line read as "you were on Y".
|
|
703
|
-
result.from = current.stdout.trim();
|
|
704
|
-
const branch = await git(root, ['checkout', '-b', ADOPT_BRANCH]);
|
|
705
|
-
if (branch.ok) result.branch = ADOPT_BRANCH;
|
|
706
|
-
else result.notes.push(`could not create branch ${ADOPT_BRANCH}: ${branch.stderr.trim()}`);
|
|
707
|
-
} else if (current.ok) {
|
|
708
|
-
result.branch = ADOPT_BRANCH;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
return result;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
async function git(cwd, args) {
|
|
715
|
-
try {
|
|
716
|
-
const { stdout, stderr } = await run('git', args, { cwd });
|
|
717
|
-
return { ok: true, stdout, stderr };
|
|
718
|
-
} catch (error) {
|
|
719
|
-
return { ok: false, stdout: error.stdout ?? '', stderr: error.stderr ?? '' };
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
721
|
/** Every line is a decision with a reason. Nothing is silent. */
|
|
724
722
|
function render(payload, manifest, { npm = true, manager = null, version } = {}) {
|
|
725
723
|
const lines = [
|
|
@@ -830,6 +828,26 @@ function render(payload, manifest, { npm = true, manager = null, version } = {})
|
|
|
830
828
|
lines.push(` ${a.action.padEnd(9)} ${a.path} agent (ADR-111)`);
|
|
831
829
|
}
|
|
832
830
|
|
|
831
|
+
// The agent-hooks entry, absorbed into the run so the adoption enforces its own
|
|
832
|
+
// projection while the agent works. Printed with the same shape the standalone command
|
|
833
|
+
// uses: one line per config the tool writes, the shim, and the readers it narrows past.
|
|
834
|
+
// `--no-agents` suppresses the section and says so, because a line that appears after
|
|
835
|
+
// the run without being in the plan is the same silent write as any other.
|
|
836
|
+
if (payload.agentHooks === null) {
|
|
837
|
+
lines.push(` ${'skip'.padEnd(9)} agent-hooks opted out with --no-agents; the agent files are skipped with it`);
|
|
838
|
+
} else {
|
|
839
|
+
for (const p of payload.agentHooks.plan) {
|
|
840
|
+
lines.push(` ${p.action.padEnd(9)} ${p.path} agent-hooks${p.error ? ` (${p.error})` : ''}`);
|
|
841
|
+
}
|
|
842
|
+
const shim = payload.agentHooks.shim;
|
|
843
|
+
lines.push(` ${shim.action.padEnd(9)} ${shim.path} agent-hooks shim`);
|
|
844
|
+
for (const o of payload.agentHooks.notWritten ?? []) {
|
|
845
|
+
if (o.state === 'skipped') {
|
|
846
|
+
lines.push(` ${'skip'.padEnd(9)} ${o.id} ${o.note}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
833
851
|
// The CI gate is listed as its own kind because it is the one plan entry whose
|
|
834
852
|
// absence is a decision rather than a default. Skipped states its reason inline so
|
|
835
853
|
// a reader can tell "this repo does not use GitHub Actions" from "this repo was
|
|
@@ -863,7 +881,7 @@ function render(payload, manifest, { npm = true, manager = null, version } = {})
|
|
|
863
881
|
// produced by two hand-written renderers.
|
|
864
882
|
const db = payload.dependabot;
|
|
865
883
|
if (db && db.action === 'create') {
|
|
866
|
-
lines.push(` ${'create'.padEnd(9)} ${db.file} ${renderDependabot(db.facts, { stack: db.stack, own: db.own }).length} bytes`);
|
|
884
|
+
lines.push(` ${'create'.padEnd(9)} ${db.file} ${renderDependabot(db.facts, { stack: db.stack, own: db.own, root: db.root }).length} bytes`);
|
|
867
885
|
lines.push(
|
|
868
886
|
db.own
|
|
869
887
|
? ` watches ${db.ecosystems.join(', ')}; ${CLI} ci --yes refreshes this copy when the template changes`
|
|
@@ -912,4 +930,4 @@ function render(payload, manifest, { npm = true, manager = null, version } = {})
|
|
|
912
930
|
}
|
|
913
931
|
if (summary.total === 0) lines.push('', 'CONFLICTS none detected');
|
|
914
932
|
return lines.filter((l) => l !== undefined).join('\n');
|
|
915
|
-
}
|
|
933
|
+
}
|
package/src/commands/lease.mjs
CHANGED
|
@@ -20,11 +20,14 @@ import {
|
|
|
20
20
|
DEFAULT_TTL_SECONDS,
|
|
21
21
|
activeLeases,
|
|
22
22
|
canonicalLeasePath,
|
|
23
|
+
flattenLease,
|
|
24
|
+
leaseCovers,
|
|
23
25
|
readLeases,
|
|
24
26
|
withLeaseLock,
|
|
25
27
|
writeLeases,
|
|
26
28
|
} from '../lease.mjs';
|
|
27
29
|
import { coreIgnoreCase } from '../repo-root.mjs';
|
|
30
|
+
import { noteEvent } from '../state/events.mjs';
|
|
28
31
|
import { SESSION_ENV } from '../agenthooks/policy.mjs';
|
|
29
32
|
import { CLI } from '../name.mjs';
|
|
30
33
|
import { commandRoot } from '../repo-root.mjs';
|
|
@@ -60,6 +63,8 @@ export async function leaseCommand(args, flags) {
|
|
|
60
63
|
|
|
61
64
|
const wanted = [];
|
|
62
65
|
const seen = new Set();
|
|
66
|
+
// A trailing slash asks for the directory (Q74): one claim covering every path under it.
|
|
67
|
+
const kinds = new Map();
|
|
63
68
|
// The claim is stored in the one spelling the hook's comparison will produce:
|
|
64
69
|
// a claim taken as `src/Keep.mjs` and an edit that spells `src/keep.mjs` must
|
|
65
70
|
// be the same claim on a case-insensitive volume, so the stored path is
|
|
@@ -73,19 +78,36 @@ export async function leaseCommand(args, flags) {
|
|
|
73
78
|
if (!seen.has(rel)) {
|
|
74
79
|
seen.add(rel);
|
|
75
80
|
wanted.push(rel);
|
|
81
|
+
kinds.set(rel, String(arg).endsWith('/') ? 'dir' : 'path');
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
// Older claim files hold the pre-canonical spelling, so a stored path is
|
|
80
86
|
// canonicalised before it is compared: a claim written before the resolver
|
|
81
87
|
// existed must still conflict with a claim taken for the same file.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
// A claim and a target overlap when either covers the other: claiming `src/` while
|
|
89
|
+
// `src/a.mjs` is held is the conflict the directory claim exists to catch (Q74).
|
|
90
|
+
const overlaps = (l) =>
|
|
91
|
+
wanted.some(
|
|
92
|
+
(rel) =>
|
|
93
|
+
leaseCovers(l, rel) ||
|
|
94
|
+
(kinds.get(rel) === 'dir' && (l.path === rel || l.path.startsWith(`${rel}/`))),
|
|
95
|
+
);
|
|
96
|
+
const newClaim = (expiresAt, held) =>
|
|
97
|
+
wanted.map((path) => {
|
|
98
|
+
const fences = held.filter((l) => leaseCovers(l, path)).map((l) => Number(l.fence) || 0);
|
|
99
|
+
return flattenLease({
|
|
100
|
+
resource: { kind: kinds.get(path) ?? 'path', id: path },
|
|
101
|
+
holder: { session, harness },
|
|
102
|
+
fence: 1 + (fences.length === 0 ? 0 : Math.max(...fences)),
|
|
103
|
+
acquiredAt: Date.now(),
|
|
104
|
+
expiresAt,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
85
107
|
|
|
86
108
|
const mode = writeMode(flags);
|
|
87
109
|
if (!mode.apply) {
|
|
88
|
-
const mine = newClaim(Date.now() + ttl * 1000);
|
|
110
|
+
const mine = newClaim(Date.now() + ttl * 1000, []);
|
|
89
111
|
const apply = `run: ${CLI} lease ${wanted.map((p) => JSON.stringify(p)).join(' ')} --session ${session} --yes`;
|
|
90
112
|
if (isJson(flags)) {
|
|
91
113
|
emitJson({ ok: true, command: 'lease', root, dryRun: true, session, ttl, leases: mine, remedy: apply });
|
|
@@ -109,12 +131,12 @@ export async function leaseCommand(args, flags) {
|
|
|
109
131
|
const read = await readLeases(root);
|
|
110
132
|
if (read.error !== null) return { error: read.error, path: read.path };
|
|
111
133
|
const held = activeLeases(read);
|
|
112
|
-
const conflicts = held.filter((l) =>
|
|
134
|
+
const conflicts = held.filter((l) => overlaps(l) && l.session !== session);
|
|
113
135
|
if (conflicts.length > 0) return { conflicts };
|
|
114
136
|
// Keep every other live claim, refresh ours, and let an expired one fall out:
|
|
115
137
|
// the write is also the only sweep the file ever gets.
|
|
116
|
-
const others = held.filter((l) => !
|
|
117
|
-
const mine = newClaim(Date.now() + ttl * 1000);
|
|
138
|
+
const others = held.filter((l) => !overlaps(l));
|
|
139
|
+
const mine = newClaim(Date.now() + ttl * 1000, held);
|
|
118
140
|
const path = await writeLeases(root, [...others, ...mine]);
|
|
119
141
|
return { mine, path };
|
|
120
142
|
});
|
|
@@ -129,6 +151,15 @@ export async function leaseCommand(args, flags) {
|
|
|
129
151
|
);
|
|
130
152
|
}
|
|
131
153
|
const { mine, path } = claimed;
|
|
154
|
+
// Coordination state a hook acts on belongs in the log a UI can tail, not only in a
|
|
155
|
+
// file a person has to open (Q73).
|
|
156
|
+
await noteEvent(root, {
|
|
157
|
+
kind: 'lease.acquired',
|
|
158
|
+
session,
|
|
159
|
+
harness: harness ?? null,
|
|
160
|
+
subject: mine.map((l) => l.path).join(','),
|
|
161
|
+
data: { ttl, fence: mine[0]?.fence ?? null },
|
|
162
|
+
});
|
|
132
163
|
|
|
133
164
|
if (isJson(flags)) {
|
|
134
165
|
emitJson({ ok: true, command: 'lease', root, session, ttl, dryRun: false, leases: mine, file: path });
|
|
@@ -152,6 +183,7 @@ function resolveTargets(root, args, session, verb) {
|
|
|
152
183
|
const ignoreCase = coreIgnoreCase(root);
|
|
153
184
|
const wanted = [];
|
|
154
185
|
const seen = new Set();
|
|
186
|
+
const kinds = new Map();
|
|
155
187
|
for (const arg of args) {
|
|
156
188
|
const rel = canonicalLeasePath(root, arg, { ignoreCase });
|
|
157
189
|
if (rel === null) {
|
|
@@ -160,9 +192,10 @@ function resolveTargets(root, args, session, verb) {
|
|
|
160
192
|
if (!seen.has(rel)) {
|
|
161
193
|
seen.add(rel);
|
|
162
194
|
wanted.push(rel);
|
|
195
|
+
kinds.set(rel, String(arg).endsWith('/') ? 'dir' : 'path');
|
|
163
196
|
}
|
|
164
197
|
}
|
|
165
|
-
return { wanted, seen, ignoreCase };
|
|
198
|
+
return { wanted, seen, ignoreCase, kinds };
|
|
166
199
|
}
|
|
167
200
|
|
|
168
201
|
function claimSession(flags) {
|
|
@@ -178,23 +211,28 @@ async function releaseLeases(root, args, flags) {
|
|
|
178
211
|
if (sessionError !== undefined) return sessionError;
|
|
179
212
|
const resolved = resolveTargets(root, args, session, 'release');
|
|
180
213
|
if (resolved.error !== undefined) return resolved.error;
|
|
181
|
-
const { wanted, seen, ignoreCase } = resolved;
|
|
182
|
-
const
|
|
214
|
+
const { wanted, seen, ignoreCase, kinds } = resolved;
|
|
215
|
+
const overlaps = (l) =>
|
|
216
|
+
wanted.some(
|
|
217
|
+
(rel) =>
|
|
218
|
+
leaseCovers(l, rel) ||
|
|
219
|
+
(kinds.get(rel) === 'dir' && (l.path === rel || l.path.startsWith(`${rel}/`))),
|
|
220
|
+
);
|
|
183
221
|
const mode = writeMode(flags);
|
|
184
222
|
|
|
185
223
|
const released = await withLeaseLock(root, async () => {
|
|
186
224
|
const read = await readLeases(root);
|
|
187
225
|
if (read.error !== null) return { error: read.error, path: read.path };
|
|
188
226
|
const held = activeLeases(read);
|
|
189
|
-
const foreign = held.filter((l) =>
|
|
227
|
+
const foreign = held.filter((l) => overlaps(l) && l.session !== session);
|
|
190
228
|
if (foreign.length > 0) {
|
|
191
229
|
return {
|
|
192
230
|
conflicts: foreign.map((c) => `${c.path} is leased by ${c.session} until ${ISO(c.expiresAt)}`).join('; '),
|
|
193
231
|
};
|
|
194
232
|
}
|
|
195
|
-
const dropped = held.filter((l) =>
|
|
233
|
+
const dropped = held.filter((l) => overlaps(l));
|
|
196
234
|
if (!mode.apply) return { preview: dropped };
|
|
197
|
-
const kept = held.filter((l) => !
|
|
235
|
+
const kept = held.filter((l) => !overlaps(l));
|
|
198
236
|
const path = await writeLeases(root, kept);
|
|
199
237
|
return { dropped, path };
|
|
200
238
|
});
|
|
@@ -216,6 +254,12 @@ async function releaseLeases(root, args, flags) {
|
|
|
216
254
|
else emit(lines.join('\n'));
|
|
217
255
|
return EXIT.OK;
|
|
218
256
|
}
|
|
257
|
+
await noteEvent(root, {
|
|
258
|
+
kind: 'lease.released',
|
|
259
|
+
session,
|
|
260
|
+
subject: released.dropped.map((l) => l.path).join(','),
|
|
261
|
+
data: { released: released.dropped.length },
|
|
262
|
+
});
|
|
219
263
|
for (const l of released.dropped) note(`note released ${l.path} (was held by ${l.session})`);
|
|
220
264
|
if (isJson(flags)) emitJson({ ok: true, command: 'lease release', root, dryRun: false, session, leases: released.dropped });
|
|
221
265
|
else {
|
|
@@ -236,27 +280,32 @@ async function renewLeases(root, args, flags) {
|
|
|
236
280
|
if (sessionError !== undefined) return sessionError;
|
|
237
281
|
const resolved = resolveTargets(root, args, session, 'renew');
|
|
238
282
|
if (resolved.error !== undefined) return resolved.error;
|
|
239
|
-
const { wanted, seen, ignoreCase } = resolved;
|
|
283
|
+
const { wanted, seen, ignoreCase, kinds } = resolved;
|
|
240
284
|
const ttl = flags.ttl === undefined ? DEFAULT_TTL_SECONDS : Number(flags.ttl);
|
|
241
285
|
if (!Number.isInteger(ttl) || ttl < 1) {
|
|
242
286
|
return fail(`--ttl must be a positive number of seconds, received ${flags.ttl}`, EXIT.USAGE, `example: run: ${CLI} lease renew <paths> --ttl 900`);
|
|
243
287
|
}
|
|
244
|
-
const
|
|
288
|
+
const overlaps = (l) =>
|
|
289
|
+
wanted.some(
|
|
290
|
+
(rel) =>
|
|
291
|
+
leaseCovers(l, rel) ||
|
|
292
|
+
(kinds.get(rel) === 'dir' && (l.path === rel || l.path.startsWith(`${rel}/`))),
|
|
293
|
+
);
|
|
245
294
|
const mode = writeMode(flags);
|
|
246
295
|
|
|
247
296
|
const renewed = await withLeaseLock(root, async () => {
|
|
248
297
|
const read = await readLeases(root);
|
|
249
298
|
if (read.error !== null) return { error: read.error, path: read.path };
|
|
250
299
|
const held = activeLeases(read);
|
|
251
|
-
const foreign = held.filter((l) =>
|
|
300
|
+
const foreign = held.filter((l) => overlaps(l) && l.session !== session);
|
|
252
301
|
if (foreign.length > 0) {
|
|
253
302
|
return {
|
|
254
303
|
conflicts: foreign.map((c) => `${c.path} is leased by ${c.session} until ${ISO(c.expiresAt)}`).join('; '),
|
|
255
304
|
};
|
|
256
305
|
}
|
|
257
306
|
const expiresAt = Date.now() + ttl * 1000;
|
|
258
|
-
const mine = held.map((l) => (
|
|
259
|
-
const touched = held.filter((l) =>
|
|
307
|
+
const mine = held.map((l) => (overlaps(l) ? { ...l, expiresAt } : l));
|
|
308
|
+
const touched = held.filter((l) => overlaps(l));
|
|
260
309
|
if (!mode.apply) return { preview: touched.map((l) => ({ ...l, expiresAt })) };
|
|
261
310
|
const path = await writeLeases(root, mine);
|
|
262
311
|
return { touched: touched.map((l) => ({ ...l, expiresAt })), path };
|
package/src/commands/map.mjs
CHANGED
|
@@ -32,9 +32,9 @@ export async function mapCommand(args, flags) {
|
|
|
32
32
|
if (!Number.isInteger(budget) || budget < 0) {
|
|
33
33
|
return fail(`budget must be a non-negative integer`, EXIT.USAGE, `run: ${CLI} map --budget 2048`);
|
|
34
34
|
}
|
|
35
|
-
const loaded = await loadGraph(root);
|
|
36
|
-
if (loaded.error) return loaded.error;
|
|
37
35
|
const task = typeof flags.task === 'string' ? flags.task : '';
|
|
36
|
+
const loaded = await loadGraph(root, { task, embed: flags.embed === true });
|
|
37
|
+
if (loaded.error) return loaded.error;
|
|
38
38
|
const bm25 = task.trim().length > 0 ? await searchFtsBm25(root, task, { fts: loaded.fts }) : [];
|
|
39
39
|
const queryVector =
|
|
40
40
|
flags.embed === true && loaded.embeddings.length > 0
|
|
@@ -109,8 +109,12 @@ async function recall(root, flags) {
|
|
|
109
109
|
* first prompt in a fresh clone wait, and a prompt that takes a minute to appear is
|
|
110
110
|
* how a hook gets uninstalled. The next prompt gets the packet, and a command a
|
|
111
111
|
* person typed at a terminal always builds what it needs.
|
|
112
|
+
*
|
|
113
|
+
* FTS bodies and stored vectors load only when the caller names them (review 0003
|
|
114
|
+
* Q76): BM25 runs in SQLite, so the graph read pays for files, symbols and edges
|
|
115
|
+
* and nothing more.
|
|
112
116
|
*/
|
|
113
|
-
export async function loadGraph(root, { build = true } = {}) {
|
|
117
|
+
export async function loadGraph(root, { build = true, task = '', embed = false } = {}) {
|
|
114
118
|
if (build) {
|
|
115
119
|
try {
|
|
116
120
|
await reindex(root);
|
|
@@ -124,12 +128,13 @@ export async function loadGraph(root, { build = true } = {}) {
|
|
|
124
128
|
};
|
|
125
129
|
}
|
|
126
130
|
}
|
|
131
|
+
const wantFts = String(task ?? '').trim().length > 0;
|
|
127
132
|
const [files, symbols, edges, fts, embeddings] = await Promise.all([
|
|
128
133
|
readFiles(root),
|
|
129
134
|
readSymbols(root),
|
|
130
135
|
readEdges(root),
|
|
131
|
-
readFts(root),
|
|
132
|
-
readEmbeddings(root),
|
|
136
|
+
wantFts ? readFts(root) : Promise.resolve([]),
|
|
137
|
+
embed === true ? readEmbeddings(root) : Promise.resolve([]),
|
|
133
138
|
]);
|
|
134
139
|
// A present index that cannot be read is named, not served as empty
|
|
135
140
|
// (review 0003 Q44): `cold` is "nothing here", `broken` is "something is
|