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/commands/slice.mjs
CHANGED
|
@@ -26,10 +26,8 @@
|
|
|
26
26
|
// slice's code: editing the same row in both places would make every merge of a
|
|
27
27
|
// finished slice a conflict on the one line that records it.
|
|
28
28
|
|
|
29
|
-
import {
|
|
30
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
29
|
+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
31
30
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
32
|
-
import { promisify } from 'node:util';
|
|
33
31
|
|
|
34
32
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
35
33
|
import { planDone, planNext } from '../plan/index.mjs';
|
|
@@ -53,8 +51,7 @@ import { readManifest } from '../projection/manifest.mjs';
|
|
|
53
51
|
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
54
52
|
import { LOCAL_DIR } from '../state.mjs';
|
|
55
53
|
import { commandRoot } from '../repo-root.mjs';
|
|
56
|
-
|
|
57
|
-
const exec = promisify(execFile);
|
|
54
|
+
import { git, gitText } from '../sys/git.mjs';
|
|
58
55
|
|
|
59
56
|
/**
|
|
60
57
|
* The state a started slice moves to: the template's `started`-role state, not
|
|
@@ -77,11 +74,6 @@ async function startedState(main) {
|
|
|
77
74
|
*/
|
|
78
75
|
const TYPE = /^[a-z]+(?:\([a-z0-9][a-z0-9._-]*\))?!?$/;
|
|
79
76
|
|
|
80
|
-
async function git(root, args) {
|
|
81
|
-
const { stdout } = await exec('git', ['-C', root, ...args], { maxBuffer: 32 << 20 });
|
|
82
|
-
return stdout.trim();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
77
|
/**
|
|
86
78
|
* The checkout that owns the tracker: the main worktree when `root` is a linked one.
|
|
87
79
|
*
|
|
@@ -93,7 +85,7 @@ async function git(root, args) {
|
|
|
93
85
|
*/
|
|
94
86
|
async function mainCheckout(root) {
|
|
95
87
|
try {
|
|
96
|
-
const common = await
|
|
88
|
+
const common = await gitText(root, ['rev-parse', '--git-common-dir']);
|
|
97
89
|
return dirname(resolve(root, common));
|
|
98
90
|
} catch {
|
|
99
91
|
return root;
|
|
@@ -143,6 +135,47 @@ async function resolveSlice(main, id) {
|
|
|
143
135
|
return { slice: found.next };
|
|
144
136
|
}
|
|
145
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The id for a prompt slice: a slug of the prompt text, capped so the branch and
|
|
140
|
+
* worktree names it feeds stay short. Whitespace and punctuation fold to one hyphen,
|
|
141
|
+
* and empty text falls back to `slice` rather than naming the slice after nothing.
|
|
142
|
+
*/
|
|
143
|
+
function slugify(text) {
|
|
144
|
+
const slug = String(text ?? '')
|
|
145
|
+
.toLowerCase()
|
|
146
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
147
|
+
.replace(/^-+|-+$/g, '')
|
|
148
|
+
.slice(0, 48)
|
|
149
|
+
.replace(/-+$/g, '');
|
|
150
|
+
return slug === '' ? 'slice' : slug;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The record a prompt slice writes in place of a tracker row. A prompt slice is not in
|
|
155
|
+
* a plan, so `planDone` has no row to flip and `planNext` has no section to read; the
|
|
156
|
+
* record is the only place its existence is stated, and it is what `finish` and
|
|
157
|
+
* `abandon` fall back to when the tracker has no row for the id. It lives beside the
|
|
158
|
+
* briefs, under the local state directory, not in the tree the slice commits.
|
|
159
|
+
*/
|
|
160
|
+
function recordPath(main, id) {
|
|
161
|
+
return join(main, LOCAL_DIR, 'slices', `${id.toLowerCase()}.json`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function writeSliceRecord(main, id, record) {
|
|
165
|
+
const path = recordPath(main, id);
|
|
166
|
+
await mkdir(dirname(path), { recursive: true });
|
|
167
|
+
await writeFile(path, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
168
|
+
return path;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function readSliceRecord(main, id) {
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(await readFile(recordPath(main, id), 'utf8'));
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
146
179
|
/** The session holding the slice, or null when the caller named none: a person
|
|
147
180
|
* at a terminal has no session, and starting without one proceeds unleased
|
|
148
181
|
* rather than refusing the work. */
|
|
@@ -221,9 +254,20 @@ async function releaseSlice(main, id, session) {
|
|
|
221
254
|
export async function sliceCommand(sub, args, flags) {
|
|
222
255
|
const root = commandRoot(flags);
|
|
223
256
|
if (sub === 'start') {
|
|
257
|
+
// Prompt mode: `simt slice start -- "<text>"`. The `--` is the marker, so the
|
|
258
|
+
// positional after it is the prompt, not a tracker id — there is no row to start,
|
|
259
|
+
// and the id is derived from the text (or pinned with --id) rather than read.
|
|
260
|
+
if (flags.dashdash === true) {
|
|
261
|
+
const prompt = args[0];
|
|
262
|
+
if (prompt === undefined || prompt === '') {
|
|
263
|
+
return fail(`${CLI} slice start -- needs a prompt after --`, EXIT.USAGE, `run: ${CLI} slice start -- "<text>"`);
|
|
264
|
+
}
|
|
265
|
+
const id = flags.id ?? slugify(prompt);
|
|
266
|
+
return startSlice(root, id, flags, prompt);
|
|
267
|
+
}
|
|
224
268
|
const id = args[0];
|
|
225
269
|
if (!id) return fail(`${CLI} slice start needs a slice id`, EXIT.USAGE, `run: ${CLI} plan next`);
|
|
226
|
-
return startSlice(root, id, flags);
|
|
270
|
+
return startSlice(root, id, flags, null);
|
|
227
271
|
}
|
|
228
272
|
if (sub === 'finish') {
|
|
229
273
|
const id = args[0];
|
|
@@ -242,12 +286,22 @@ export async function sliceCommand(sub, args, flags) {
|
|
|
242
286
|
);
|
|
243
287
|
}
|
|
244
288
|
|
|
245
|
-
async function startSlice(root, id, flags) {
|
|
289
|
+
async function startSlice(root, id, flags, prompt) {
|
|
246
290
|
const mode = writeMode(flags);
|
|
247
291
|
const main = await mainCheckout(root);
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
292
|
+
const promptMode = prompt !== null;
|
|
293
|
+
// A prompt slice has no tracker row: the title is the prompt, and the record file —
|
|
294
|
+
// not `planDone` — is what `finish` and `abandon` will read.
|
|
295
|
+
let title;
|
|
296
|
+
let doc;
|
|
297
|
+
if (promptMode) {
|
|
298
|
+
title = prompt;
|
|
299
|
+
doc = prompt;
|
|
300
|
+
} else {
|
|
301
|
+
const resolved = await resolveSlice(main, id);
|
|
302
|
+
if (resolved.error) return fail(resolved.error, EXIT.USAGE, resolved.remedy);
|
|
303
|
+
({ title, doc } = resolved.slice);
|
|
304
|
+
}
|
|
251
305
|
const { branch, worktree } = places(main, id);
|
|
252
306
|
// The brief lives beside the worktree, not in it: a file inside the worktree
|
|
253
307
|
// is untracked content in the slice's own tree, and the tree id must name
|
|
@@ -256,17 +310,29 @@ async function startSlice(root, id, flags) {
|
|
|
256
310
|
const leaseRel = sliceLeaseRel(main, worktree);
|
|
257
311
|
const session = sliceSession(flags);
|
|
258
312
|
const task = `${id}: ${String(title ?? '').trim()}`;
|
|
259
|
-
const apply =
|
|
313
|
+
const apply = promptMode
|
|
314
|
+
? `run: ${CLI} slice start -- ${JSON.stringify(prompt)} --yes`
|
|
315
|
+
: `run: ${CLI} slice start ${id} --yes`;
|
|
316
|
+
const record = recordPath(main, id);
|
|
260
317
|
|
|
261
|
-
const started = await startedState(main);
|
|
262
|
-
if (started.error) return fail(started.error, EXIT.USAGE, started.remedy);
|
|
263
318
|
// The row first, dry, against the main checkout: it is the only part of `start`
|
|
264
319
|
// that can refuse for a reason about the tracker rather than about git, and a
|
|
265
320
|
// preview that discovers it late is a preview that half-described the work.
|
|
266
|
-
|
|
267
|
-
|
|
321
|
+
// A prompt slice has no row to flip, so there is nothing to preview here.
|
|
322
|
+
const started = promptMode ? { state: null } : await startedState(main);
|
|
323
|
+
if (started.error) return fail(started.error, EXIT.USAGE, started.remedy);
|
|
324
|
+
const preview = promptMode
|
|
325
|
+
? null
|
|
326
|
+
: await planDone(main, id, { state: started.state, dryRun: true });
|
|
327
|
+
if (preview !== null && preview.error) return fail(preview.error, EXIT.USAGE, preview.remedy ?? `run: ${CLI} plan status`);
|
|
268
328
|
|
|
269
329
|
const leaseLine = session === null ? 'lease (no session: pass --session or set SIMT_SESSION to lease the slice)' : `lease slice ${id} as ${session} (claimed on apply)`;
|
|
330
|
+
const stateLine = promptMode
|
|
331
|
+
? `state (no tracker row; recorded at ${relative(main, record)})`
|
|
332
|
+
: `state ${preview.from} -> ${preview.to}`;
|
|
333
|
+
const stateJson = promptMode
|
|
334
|
+
? { row: false, record: relative(main, record) }
|
|
335
|
+
: { from: preview.from, to: preview.to, changed: preview.wouldChange };
|
|
270
336
|
|
|
271
337
|
if (!mode.apply) {
|
|
272
338
|
if (isJson(flags)) {
|
|
@@ -281,7 +347,7 @@ async function startSlice(root, id, flags) {
|
|
|
281
347
|
worktree: relative(main, worktree),
|
|
282
348
|
brief: { path: relative(main, briefPath), bytes: null },
|
|
283
349
|
lease: session === null ? null : { slice: id, path: leaseRel, session },
|
|
284
|
-
state:
|
|
350
|
+
state: stateJson,
|
|
285
351
|
remedy: apply,
|
|
286
352
|
});
|
|
287
353
|
return EXIT.OK;
|
|
@@ -293,7 +359,7 @@ async function startSlice(root, id, flags) {
|
|
|
293
359
|
`worktree ${relative(main, worktree)}`,
|
|
294
360
|
`brief ${relative(main, briefPath)} (assembled on apply)`,
|
|
295
361
|
leaseLine,
|
|
296
|
-
|
|
362
|
+
stateLine,
|
|
297
363
|
`apply ${apply}`,
|
|
298
364
|
];
|
|
299
365
|
emit(lines.join('\n'));
|
|
@@ -342,9 +408,17 @@ async function startSlice(root, id, flags) {
|
|
|
342
408
|
await mkdir(dirname(briefPath), { recursive: true });
|
|
343
409
|
await writeFile(briefPath, packet.packed.text, 'utf8');
|
|
344
410
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
411
|
+
// Normal slices flip the tracker row; prompt slices write the record file that stands
|
|
412
|
+
// in for it. Both are the "started" mark, and both land after the worktree exists so a
|
|
413
|
+
// failed `worktree add` leaves neither behind.
|
|
414
|
+
let row = null;
|
|
415
|
+
if (promptMode) {
|
|
416
|
+
await writeSliceRecord(main, id, { id, title, kind: 'prompt', createdAt: new Date().toISOString() });
|
|
417
|
+
} else {
|
|
418
|
+
row = await planDone(main, id, { state: started.state, dryRun: false });
|
|
419
|
+
if (row.error) {
|
|
420
|
+
return fail(`${branch} exists with its brief, but the tracker row was not updated: ${row.error}`, EXIT.FAIL, row.remedy ?? `run: ${CLI} plan status`);
|
|
421
|
+
}
|
|
348
422
|
}
|
|
349
423
|
|
|
350
424
|
const bytes = Buffer.byteLength(packet.packed.text);
|
|
@@ -360,7 +434,9 @@ async function startSlice(root, id, flags) {
|
|
|
360
434
|
worktree: relative(main, worktree),
|
|
361
435
|
brief: { path: relative(main, briefPath), bytes },
|
|
362
436
|
lease,
|
|
363
|
-
state:
|
|
437
|
+
state: promptMode
|
|
438
|
+
? { row: false, record: relative(main, record) }
|
|
439
|
+
: { from: row.from, to: row.to, changed: row.changed },
|
|
364
440
|
});
|
|
365
441
|
return EXIT.OK;
|
|
366
442
|
}
|
|
@@ -371,7 +447,9 @@ async function startSlice(root, id, flags) {
|
|
|
371
447
|
`worktree ${relative(main, worktree)}`,
|
|
372
448
|
`brief ${relative(main, briefPath)} ${bytes} bytes`,
|
|
373
449
|
lease === null ? 'lease (none)' : `lease slice ${id} as ${session}`,
|
|
374
|
-
|
|
450
|
+
promptMode
|
|
451
|
+
? `state (no tracker row; recorded at ${relative(main, record)})`
|
|
452
|
+
: `state ${row.from} -> ${row.to}`,
|
|
375
453
|
];
|
|
376
454
|
emit(lines.join('\n'));
|
|
377
455
|
return EXIT.OK;
|
|
@@ -423,8 +501,16 @@ async function finishSlice(root, id, flags) {
|
|
|
423
501
|
const mode = writeMode(flags);
|
|
424
502
|
const main = await mainCheckout(root);
|
|
425
503
|
const resolved = await resolveSlice(main, id);
|
|
426
|
-
|
|
427
|
-
|
|
504
|
+
// A prompt slice has no tracker row: the record file stands in for it, and the
|
|
505
|
+
// finish commits and releases the lease without flipping a row.
|
|
506
|
+
const record = resolved.error ? await readSliceRecord(main, id) : null;
|
|
507
|
+
if (resolved.error && record === null) {
|
|
508
|
+
return fail(resolved.error, EXIT.USAGE, resolved.remedy);
|
|
509
|
+
}
|
|
510
|
+
const promptMode = record !== null;
|
|
511
|
+
const title = promptMode ? record.title : resolved.slice.title;
|
|
512
|
+
const doc = promptMode ? record.title : resolved.slice.doc;
|
|
513
|
+
const recRel = relative(main, recordPath(main, id));
|
|
428
514
|
const { branch, worktree } = places(main, id);
|
|
429
515
|
|
|
430
516
|
const type = String(flags.type ?? 'feat');
|
|
@@ -448,8 +534,13 @@ async function finishSlice(root, id, flags) {
|
|
|
448
534
|
const split = await splitGates(root, declared, manifest.manifest);
|
|
449
535
|
|
|
450
536
|
if (!mode.apply) {
|
|
451
|
-
|
|
452
|
-
if (
|
|
537
|
+
let row = null;
|
|
538
|
+
if (!promptMode) {
|
|
539
|
+
row = await planDone(main, id, { sha: null, test, dryRun: true });
|
|
540
|
+
if (row.error) return fail(row.error, EXIT.USAGE, row.remedy ?? `run: ${CLI} plan status`);
|
|
541
|
+
}
|
|
542
|
+
const stateLine = promptMode ? `state (no tracker row; recorded at ${recRel})` : `state ${row.from} -> ${row.to}`;
|
|
543
|
+
const stateJson = promptMode ? { row: false, record: recRel } : { from: row.from, to: row.to, changed: row.wouldChange };
|
|
453
544
|
const gateLine =
|
|
454
545
|
declared.length === 0
|
|
455
546
|
? '(none declared)'
|
|
@@ -464,7 +555,7 @@ async function finishSlice(root, id, flags) {
|
|
|
464
555
|
subject,
|
|
465
556
|
staged,
|
|
466
557
|
gates: { event: 'pre-commit', declared, skipped: split.skipped.map((s) => s.name) },
|
|
467
|
-
state:
|
|
558
|
+
state: stateJson,
|
|
468
559
|
remedy: apply,
|
|
469
560
|
});
|
|
470
561
|
return EXIT.OK;
|
|
@@ -475,7 +566,7 @@ async function finishSlice(root, id, flags) {
|
|
|
475
566
|
`gates pre-commit ${gateLine} (run on apply)`,
|
|
476
567
|
`staged ${staged.length} file(s)${staged.length === 0 ? ' (finish refuses without one)' : ''}`,
|
|
477
568
|
`subject ${subject}`,
|
|
478
|
-
|
|
569
|
+
stateLine,
|
|
479
570
|
`apply ${apply}`,
|
|
480
571
|
];
|
|
481
572
|
emit(lines.join('\n'));
|
|
@@ -485,7 +576,7 @@ async function finishSlice(root, id, flags) {
|
|
|
485
576
|
// The commit lands on the slice branch or not at all: finishing from `main`
|
|
486
577
|
// commits main's tree under the slice's subject, which is the defect this
|
|
487
578
|
// guard closes (review 0003 Q23).
|
|
488
|
-
const head = await
|
|
579
|
+
const head = await gitText(root, ['rev-parse', '--abbrev-ref', 'HEAD']).catch(() => null);
|
|
489
580
|
if (head !== branch) {
|
|
490
581
|
return fail(
|
|
491
582
|
`not on the slice branch: HEAD is ${head ?? 'unknown'}, expected ${branch}`,
|
|
@@ -563,15 +654,18 @@ async function finishSlice(root, id, flags) {
|
|
|
563
654
|
const message = `${subject}\n\n${CO_AUTHOR}`;
|
|
564
655
|
try {
|
|
565
656
|
await git(root, ['commit', '-m', message]);
|
|
566
|
-
sha = await
|
|
657
|
+
sha = await gitText(root, ['rev-parse', 'HEAD']);
|
|
567
658
|
} catch (error) {
|
|
568
659
|
const detail = String(error.stderr ?? error.message ?? error).trim().split('\n')[0];
|
|
569
660
|
return fail(`the gates passed but the commit failed: ${detail}`, EXIT.FAIL, `run: git -C ${root} commit -m ${JSON.stringify(message)}`);
|
|
570
661
|
}
|
|
571
662
|
|
|
572
|
-
|
|
573
|
-
if (
|
|
574
|
-
|
|
663
|
+
let row = null;
|
|
664
|
+
if (!promptMode) {
|
|
665
|
+
row = await planDone(main, id, { sha, test, dryRun: false });
|
|
666
|
+
if (row.error) {
|
|
667
|
+
return fail(`${sha} is committed, but the tracker row could not record it: ${row.error}`, EXIT.FAIL, row.remedy ?? `run: ${CLI} plan status`);
|
|
668
|
+
}
|
|
575
669
|
}
|
|
576
670
|
|
|
577
671
|
// The slice is done: its lease goes with it. A failure here is a note — the
|
|
@@ -590,8 +684,12 @@ async function finishSlice(root, id, flags) {
|
|
|
590
684
|
commit: { sha, subject },
|
|
591
685
|
gates: gates.gates,
|
|
592
686
|
staged,
|
|
593
|
-
|
|
594
|
-
|
|
687
|
+
...(promptMode
|
|
688
|
+
? { state: { row: false, record: recRel } }
|
|
689
|
+
: {
|
|
690
|
+
evidence: { recorded: row.evidenceRecorded, sha: row.sha, test: row.test, note: row.evidenceNote },
|
|
691
|
+
row: { file: relative(main, row.file), line: row.line, from: row.from, to: row.to, changed: row.changed },
|
|
692
|
+
}),
|
|
595
693
|
});
|
|
596
694
|
return EXIT.OK;
|
|
597
695
|
}
|
|
@@ -602,11 +700,15 @@ async function finishSlice(root, id, flags) {
|
|
|
602
700
|
`slice ${id} ${doc}`,
|
|
603
701
|
...renderGateReport('pre-commit', declared, gates),
|
|
604
702
|
`commit ${sha} ${subject}`,
|
|
605
|
-
`state ${row.from} -> ${row.to}`,
|
|
606
|
-
`record ${relative(main, row.file)} line ${row.line} (edited, not staged: commit it with the merge)`,
|
|
607
703
|
];
|
|
608
|
-
if (
|
|
609
|
-
|
|
704
|
+
if (promptMode) {
|
|
705
|
+
lines.push(`state (no tracker row; recorded at ${recRel})`);
|
|
706
|
+
} else {
|
|
707
|
+
lines.push(`state ${row.from} -> ${row.to}`);
|
|
708
|
+
lines.push(`record ${relative(main, row.file)} line ${row.line} (edited, not staged: commit it with the merge)`);
|
|
709
|
+
if (row.evidenceRecorded) lines.push(`evidence ${sha}${test ? ` test: ${test}` : ''}`);
|
|
710
|
+
else if (row.evidenceNote) lines.push(`note ${row.evidenceNote}`);
|
|
711
|
+
}
|
|
610
712
|
emit(lines.join('\n'));
|
|
611
713
|
return EXIT.OK;
|
|
612
714
|
}
|
|
@@ -622,17 +724,29 @@ async function abandonSlice(root, id, flags) {
|
|
|
622
724
|
const mode = writeMode(flags);
|
|
623
725
|
const main = await mainCheckout(root);
|
|
624
726
|
const resolved = await resolveSlice(main, id);
|
|
625
|
-
|
|
626
|
-
|
|
727
|
+
// A prompt slice has no tracker row: the record file stands in for it, and the
|
|
728
|
+
// abandon removes the record file where a tracked abandon would reset the row.
|
|
729
|
+
const record = resolved.error ? await readSliceRecord(main, id) : null;
|
|
730
|
+
if (resolved.error && record === null) {
|
|
731
|
+
return fail(resolved.error, EXIT.USAGE, resolved.remedy);
|
|
732
|
+
}
|
|
733
|
+
const promptMode = record !== null;
|
|
734
|
+
const doc = promptMode ? record.title : resolved.slice.doc;
|
|
735
|
+
const recRel = relative(main, recordPath(main, id));
|
|
627
736
|
const { branch, worktree } = places(main, id);
|
|
628
737
|
const session = sliceSession(flags);
|
|
629
738
|
const apply = `run: ${CLI} slice abandon ${id} --yes`;
|
|
630
739
|
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
740
|
+
let preview = null;
|
|
741
|
+
if (!promptMode) {
|
|
742
|
+
const loaded = await loadFormat(main);
|
|
743
|
+
if (loaded.error) return fail(loaded.error, EXIT.USAGE, loaded.remedy);
|
|
744
|
+
const readyDisplay = loaded.format.canonical(loaded.format.ready) ?? 'Queued';
|
|
745
|
+
preview = await planDone(main, id, { state: readyDisplay, dryRun: true });
|
|
746
|
+
if (preview.error) return fail(preview.error, EXIT.USAGE, preview.remedy ?? `run: ${CLI} plan status`);
|
|
747
|
+
}
|
|
748
|
+
const stateLine = promptMode ? `state (no tracker row; removing ${recRel})` : `state ${preview.from} -> ${preview.to}`;
|
|
749
|
+
const stateJson = promptMode ? { row: false, record: recRel } : { from: preview.from, to: preview.to, changed: preview.wouldChange };
|
|
636
750
|
|
|
637
751
|
if (!mode.apply) {
|
|
638
752
|
if (isJson(flags)) {
|
|
@@ -644,7 +758,7 @@ async function abandonSlice(root, id, flags) {
|
|
|
644
758
|
dryRun: true,
|
|
645
759
|
branch,
|
|
646
760
|
worktree: relative(main, worktree),
|
|
647
|
-
state:
|
|
761
|
+
state: stateJson,
|
|
648
762
|
remedy: apply,
|
|
649
763
|
});
|
|
650
764
|
return EXIT.OK;
|
|
@@ -653,7 +767,7 @@ async function abandonSlice(root, id, flags) {
|
|
|
653
767
|
`repo ${root}`,
|
|
654
768
|
`slice ${id} ${doc}`,
|
|
655
769
|
`remove ${relative(main, worktree)} and branch ${branch} (on apply)`,
|
|
656
|
-
|
|
770
|
+
stateLine,
|
|
657
771
|
`apply ${apply}`,
|
|
658
772
|
];
|
|
659
773
|
emit(lines.join('\n'));
|
|
@@ -663,7 +777,7 @@ async function abandonSlice(root, id, flags) {
|
|
|
663
777
|
// Abandoning from inside the worktree would pull the floor out: the shell's
|
|
664
778
|
// own directory is the thing being removed. Compared through git, not through
|
|
665
779
|
// strings, because the caller's spelling of the path may differ in symlinks.
|
|
666
|
-
const top = await
|
|
780
|
+
const top = await gitText(root, ['rev-parse', '--show-toplevel']).catch(() => null);
|
|
667
781
|
if (top !== null && (top === worktree || top.startsWith(`${worktree}/`))) {
|
|
668
782
|
return fail(
|
|
669
783
|
`cannot abandon ${id} from inside its worktree`,
|
|
@@ -688,13 +802,27 @@ async function abandonSlice(root, id, flags) {
|
|
|
688
802
|
removed.push(`kept ${branch} (${detail})`);
|
|
689
803
|
}
|
|
690
804
|
|
|
691
|
-
|
|
692
|
-
if (
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
805
|
+
let row = null;
|
|
806
|
+
if (promptMode) {
|
|
807
|
+
// No row to reset; the record file is the only mark of the slice, and it goes.
|
|
808
|
+
try {
|
|
809
|
+
await unlink(recordPath(main, id));
|
|
810
|
+
} catch {
|
|
811
|
+
// A record that is already gone is not a second failure: the worktree and
|
|
812
|
+
// branch are what abandonment exists to take down.
|
|
813
|
+
}
|
|
814
|
+
} else {
|
|
815
|
+
const loaded = await loadFormat(main);
|
|
816
|
+
if (loaded.error) return fail(loaded.error, EXIT.USAGE, loaded.remedy);
|
|
817
|
+
const readyDisplay = loaded.format.canonical(loaded.format.ready) ?? 'Queued';
|
|
818
|
+
row = await planDone(main, id, { state: readyDisplay, dryRun: false });
|
|
819
|
+
if (row.error) {
|
|
820
|
+
return fail(
|
|
821
|
+
`the worktree is down, but the tracker row was not updated: ${row.error}`,
|
|
822
|
+
EXIT.FAIL,
|
|
823
|
+
row.remedy ?? `run: ${CLI} plan status`,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
698
826
|
}
|
|
699
827
|
|
|
700
828
|
const released = await releaseSlice(main, id, session);
|
|
@@ -704,6 +832,7 @@ async function abandonSlice(root, id, flags) {
|
|
|
704
832
|
: released.error
|
|
705
833
|
? `the slice lease could not be released (${released.error}); the TTL reaps it`
|
|
706
834
|
: 'no slice lease was held';
|
|
835
|
+
const abandonStateJson = promptMode ? { row: false, record: recRel, removed: true } : { from: row.from, to: row.to, changed: row.changed };
|
|
707
836
|
if (isJson(flags)) {
|
|
708
837
|
emitJson({
|
|
709
838
|
ok: true,
|
|
@@ -713,11 +842,17 @@ async function abandonSlice(root, id, flags) {
|
|
|
713
842
|
branch,
|
|
714
843
|
worktree: relative(main, worktree),
|
|
715
844
|
lease: released.released === true ? { slice: id, session } : null,
|
|
716
|
-
state:
|
|
845
|
+
state: abandonStateJson,
|
|
717
846
|
});
|
|
718
847
|
return EXIT.OK;
|
|
719
848
|
}
|
|
720
|
-
const lines = [
|
|
849
|
+
const lines = [
|
|
850
|
+
`repo ${root}`,
|
|
851
|
+
`slice ${id} ${doc}`,
|
|
852
|
+
...removed,
|
|
853
|
+
promptMode ? `state (no tracker row; removed ${recRel})` : `state ${row.from} -> ${row.to}`,
|
|
854
|
+
`lease ${leaseLine}`,
|
|
855
|
+
];
|
|
721
856
|
emit(lines.join('\n'));
|
|
722
857
|
return EXIT.OK;
|
|
723
858
|
}
|