simt-dev 0.7.0 → 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 +191 -870
- package/README.md +188 -213
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-014-workspace-is-configuration.md +2 -2
- 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-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- 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 +12 -2
- package/rules/core.md +7 -0
- package/rules/mdc/intake-questions.mdc +8 -0
- package/rules/mdc/slice-commit.mdc +57 -4
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/prompt.mjs +5 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agenthooks/shim.mjs +1 -1
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +31 -0
- package/src/argv.mjs +48 -3
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +163 -105
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +33 -73
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +121 -475
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +79 -1064
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +120 -962
- package/src/bench/run.mjs +185 -802
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +48 -15
- package/src/ci/dependabot.mjs +25 -55
- package/src/ci/workflow.mjs +30 -7
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +4 -13
- package/src/commands/agenthook.mjs +70 -40
- package/src/commands/bench.mjs +191 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +13 -14
- package/src/commands/compare.mjs +42 -42
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +4 -4
- package/src/commands/distill.mjs +4 -3
- package/src/commands/docs.mjs +88 -0
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +6 -9
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +26 -25
- package/src/commands/graph.mjs +2 -3
- package/src/commands/harness.mjs +6 -5
- package/src/commands/help.mjs +168 -19
- package/src/commands/hooks.mjs +119 -49
- package/src/commands/ignore.mjs +13 -8
- package/src/commands/impact.mjs +8 -15
- package/src/commands/index.mjs +318 -193
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +124 -105
- package/src/commands/lease.mjs +73 -25
- package/src/commands/lessons.mjs +10 -9
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +24 -14
- package/src/commands/mcp.mjs +2 -18
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +2 -2
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +12 -11
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +37 -16
- package/src/commands/shared.mjs +14 -19
- package/src/commands/skills.mjs +33 -32
- package/src/commands/skillscompile.mjs +7 -8
- package/src/commands/skillsinstall.mjs +7 -6
- package/src/commands/slice.mjs +220 -74
- package/src/commands/spec.mjs +167 -0
- package/src/commands/status.mjs +27 -18
- package/src/commands/sync.mjs +27 -25
- package/src/commands/tests.mjs +18 -21
- package/src/commands/tokens.mjs +21 -26
- package/src/commands/undo.mjs +5 -15
- package/src/commands/upgrade.mjs +15 -7
- package/src/commands/why.mjs +15 -21
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/compare/row.mjs +0 -2
- package/src/context.mjs +64 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +3 -2
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +14 -1
- package/src/distill/engine.mjs +5 -1
- 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 +6 -2
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +7 -5
- package/src/hooks/dispatcher.mjs +209 -60
- package/src/hooks/gates.mjs +10 -43
- package/src/hooks/resolve.mjs +4 -14
- package/src/hooks/shim.mjs +5 -2
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +31 -12
- 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 +94 -20
- 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 +150 -14
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/resources.mjs +7 -10
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +119 -25
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -967
- package/src/measure/corpus.mjs +150 -0
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- package/src/measure/session.mjs +3 -2
- 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/node-version.mjs +37 -0
- package/src/output/index.mjs +209 -10
- package/src/packs/manage.mjs +32 -12
- package/src/packs.mjs +20 -2
- 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/lock.mjs +11 -1
- package/src/projection/manifest.mjs +29 -468
- 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 +20 -550
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +31 -0
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +19 -22
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +10 -13
- 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/sys/proc.mjs +151 -0
- 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 +3 -0
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +58 -11
- 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/docs/decisions/README.md +0 -600
- 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';
|
|
@@ -52,8 +50,8 @@ import { coreIgnoreCase } from '../repo-root.mjs';
|
|
|
52
50
|
import { readManifest } from '../projection/manifest.mjs';
|
|
53
51
|
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
54
52
|
import { LOCAL_DIR } from '../state.mjs';
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
54
|
+
import { git, gitText } from '../sys/git.mjs';
|
|
57
55
|
|
|
58
56
|
/**
|
|
59
57
|
* The state a started slice moves to: the template's `started`-role state, not
|
|
@@ -76,11 +74,6 @@ async function startedState(main) {
|
|
|
76
74
|
*/
|
|
77
75
|
const TYPE = /^[a-z]+(?:\([a-z0-9][a-z0-9._-]*\))?!?$/;
|
|
78
76
|
|
|
79
|
-
async function git(root, args) {
|
|
80
|
-
const { stdout } = await exec('git', ['-C', root, ...args], { maxBuffer: 32 << 20 });
|
|
81
|
-
return stdout.trim();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
77
|
/**
|
|
85
78
|
* The checkout that owns the tracker: the main worktree when `root` is a linked one.
|
|
86
79
|
*
|
|
@@ -92,7 +85,7 @@ async function git(root, args) {
|
|
|
92
85
|
*/
|
|
93
86
|
async function mainCheckout(root) {
|
|
94
87
|
try {
|
|
95
|
-
const common = await
|
|
88
|
+
const common = await gitText(root, ['rev-parse', '--git-common-dir']);
|
|
96
89
|
return dirname(resolve(root, common));
|
|
97
90
|
} catch {
|
|
98
91
|
return root;
|
|
@@ -125,6 +118,15 @@ function subjectFor(type, id, title) {
|
|
|
125
118
|
return `${type}(${id.toLowerCase()}): ${description}`;
|
|
126
119
|
}
|
|
127
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The trailer of every commit this tool writes: attribution to the tool, in the
|
|
123
|
+
* form git renders as a co-author. The slice-commit rule ends every message it
|
|
124
|
+
* defines with the same line, so a commit the tool writes and one an agent
|
|
125
|
+
* writes from the rule agree on attribution. Module-local: one caller needs no
|
|
126
|
+
* export, and an export is a promise to a caller (deadcode).
|
|
127
|
+
*/
|
|
128
|
+
const CO_AUTHOR = 'Co-Authored-By: Simt Dev <noreply@simt.space>';
|
|
129
|
+
|
|
128
130
|
/** Resolve one named slice through `plan next`, which already knows missing and ambiguous. */
|
|
129
131
|
async function resolveSlice(main, id) {
|
|
130
132
|
const found = await planNext(main, id);
|
|
@@ -133,6 +135,47 @@ async function resolveSlice(main, id) {
|
|
|
133
135
|
return { slice: found.next };
|
|
134
136
|
}
|
|
135
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
|
+
|
|
136
179
|
/** The session holding the slice, or null when the caller named none: a person
|
|
137
180
|
* at a terminal has no session, and starting without one proceeds unleased
|
|
138
181
|
* rather than refusing the work. */
|
|
@@ -209,11 +252,22 @@ async function releaseSlice(main, id, session) {
|
|
|
209
252
|
}
|
|
210
253
|
|
|
211
254
|
export async function sliceCommand(sub, args, flags) {
|
|
212
|
-
const root = flags
|
|
255
|
+
const root = commandRoot(flags);
|
|
213
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
|
+
}
|
|
214
268
|
const id = args[0];
|
|
215
269
|
if (!id) return fail(`${CLI} slice start needs a slice id`, EXIT.USAGE, `run: ${CLI} plan next`);
|
|
216
|
-
return startSlice(root, id, flags);
|
|
270
|
+
return startSlice(root, id, flags, null);
|
|
217
271
|
}
|
|
218
272
|
if (sub === 'finish') {
|
|
219
273
|
const id = args[0];
|
|
@@ -232,12 +286,22 @@ export async function sliceCommand(sub, args, flags) {
|
|
|
232
286
|
);
|
|
233
287
|
}
|
|
234
288
|
|
|
235
|
-
async function startSlice(root, id, flags) {
|
|
289
|
+
async function startSlice(root, id, flags, prompt) {
|
|
236
290
|
const mode = writeMode(flags);
|
|
237
291
|
const main = await mainCheckout(root);
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|
|
241
305
|
const { branch, worktree } = places(main, id);
|
|
242
306
|
// The brief lives beside the worktree, not in it: a file inside the worktree
|
|
243
307
|
// is untracked content in the slice's own tree, and the tree id must name
|
|
@@ -246,17 +310,29 @@ async function startSlice(root, id, flags) {
|
|
|
246
310
|
const leaseRel = sliceLeaseRel(main, worktree);
|
|
247
311
|
const session = sliceSession(flags);
|
|
248
312
|
const task = `${id}: ${String(title ?? '').trim()}`;
|
|
249
|
-
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);
|
|
250
317
|
|
|
251
|
-
const started = await startedState(main);
|
|
252
|
-
if (started.error) return fail(started.error, EXIT.USAGE, started.remedy);
|
|
253
318
|
// The row first, dry, against the main checkout: it is the only part of `start`
|
|
254
319
|
// that can refuse for a reason about the tracker rather than about git, and a
|
|
255
320
|
// preview that discovers it late is a preview that half-described the work.
|
|
256
|
-
|
|
257
|
-
|
|
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`);
|
|
258
328
|
|
|
259
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 };
|
|
260
336
|
|
|
261
337
|
if (!mode.apply) {
|
|
262
338
|
if (isJson(flags)) {
|
|
@@ -271,7 +347,7 @@ async function startSlice(root, id, flags) {
|
|
|
271
347
|
worktree: relative(main, worktree),
|
|
272
348
|
brief: { path: relative(main, briefPath), bytes: null },
|
|
273
349
|
lease: session === null ? null : { slice: id, path: leaseRel, session },
|
|
274
|
-
state:
|
|
350
|
+
state: stateJson,
|
|
275
351
|
remedy: apply,
|
|
276
352
|
});
|
|
277
353
|
return EXIT.OK;
|
|
@@ -283,10 +359,10 @@ async function startSlice(root, id, flags) {
|
|
|
283
359
|
`worktree ${relative(main, worktree)}`,
|
|
284
360
|
`brief ${relative(main, briefPath)} (assembled on apply)`,
|
|
285
361
|
leaseLine,
|
|
286
|
-
|
|
362
|
+
stateLine,
|
|
287
363
|
`apply ${apply}`,
|
|
288
364
|
];
|
|
289
|
-
emit(lines.join('\n')
|
|
365
|
+
emit(lines.join('\n'));
|
|
290
366
|
return EXIT.OK;
|
|
291
367
|
}
|
|
292
368
|
|
|
@@ -332,9 +408,17 @@ async function startSlice(root, id, flags) {
|
|
|
332
408
|
await mkdir(dirname(briefPath), { recursive: true });
|
|
333
409
|
await writeFile(briefPath, packet.packed.text, 'utf8');
|
|
334
410
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
}
|
|
338
422
|
}
|
|
339
423
|
|
|
340
424
|
const bytes = Buffer.byteLength(packet.packed.text);
|
|
@@ -350,7 +434,9 @@ async function startSlice(root, id, flags) {
|
|
|
350
434
|
worktree: relative(main, worktree),
|
|
351
435
|
brief: { path: relative(main, briefPath), bytes },
|
|
352
436
|
lease,
|
|
353
|
-
state:
|
|
437
|
+
state: promptMode
|
|
438
|
+
? { row: false, record: relative(main, record) }
|
|
439
|
+
: { from: row.from, to: row.to, changed: row.changed },
|
|
354
440
|
});
|
|
355
441
|
return EXIT.OK;
|
|
356
442
|
}
|
|
@@ -361,9 +447,11 @@ async function startSlice(root, id, flags) {
|
|
|
361
447
|
`worktree ${relative(main, worktree)}`,
|
|
362
448
|
`brief ${relative(main, briefPath)} ${bytes} bytes`,
|
|
363
449
|
lease === null ? 'lease (none)' : `lease slice ${id} as ${session}`,
|
|
364
|
-
|
|
450
|
+
promptMode
|
|
451
|
+
? `state (no tracker row; recorded at ${relative(main, record)})`
|
|
452
|
+
: `state ${row.from} -> ${row.to}`,
|
|
365
453
|
];
|
|
366
|
-
emit(lines.join('\n')
|
|
454
|
+
emit(lines.join('\n'));
|
|
367
455
|
return EXIT.OK;
|
|
368
456
|
}
|
|
369
457
|
|
|
@@ -413,8 +501,16 @@ async function finishSlice(root, id, flags) {
|
|
|
413
501
|
const mode = writeMode(flags);
|
|
414
502
|
const main = await mainCheckout(root);
|
|
415
503
|
const resolved = await resolveSlice(main, id);
|
|
416
|
-
|
|
417
|
-
|
|
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));
|
|
418
514
|
const { branch, worktree } = places(main, id);
|
|
419
515
|
|
|
420
516
|
const type = String(flags.type ?? 'feat');
|
|
@@ -438,8 +534,13 @@ async function finishSlice(root, id, flags) {
|
|
|
438
534
|
const split = await splitGates(root, declared, manifest.manifest);
|
|
439
535
|
|
|
440
536
|
if (!mode.apply) {
|
|
441
|
-
|
|
442
|
-
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 };
|
|
443
544
|
const gateLine =
|
|
444
545
|
declared.length === 0
|
|
445
546
|
? '(none declared)'
|
|
@@ -454,7 +555,7 @@ async function finishSlice(root, id, flags) {
|
|
|
454
555
|
subject,
|
|
455
556
|
staged,
|
|
456
557
|
gates: { event: 'pre-commit', declared, skipped: split.skipped.map((s) => s.name) },
|
|
457
|
-
state:
|
|
558
|
+
state: stateJson,
|
|
458
559
|
remedy: apply,
|
|
459
560
|
});
|
|
460
561
|
return EXIT.OK;
|
|
@@ -465,17 +566,17 @@ async function finishSlice(root, id, flags) {
|
|
|
465
566
|
`gates pre-commit ${gateLine} (run on apply)`,
|
|
466
567
|
`staged ${staged.length} file(s)${staged.length === 0 ? ' (finish refuses without one)' : ''}`,
|
|
467
568
|
`subject ${subject}`,
|
|
468
|
-
|
|
569
|
+
stateLine,
|
|
469
570
|
`apply ${apply}`,
|
|
470
571
|
];
|
|
471
|
-
emit(lines.join('\n')
|
|
572
|
+
emit(lines.join('\n'));
|
|
472
573
|
return EXIT.OK;
|
|
473
574
|
}
|
|
474
575
|
|
|
475
576
|
// The commit lands on the slice branch or not at all: finishing from `main`
|
|
476
577
|
// commits main's tree under the slice's subject, which is the defect this
|
|
477
578
|
// guard closes (review 0003 Q23).
|
|
478
|
-
const head = await
|
|
579
|
+
const head = await gitText(root, ['rev-parse', '--abbrev-ref', 'HEAD']).catch(() => null);
|
|
479
580
|
if (head !== branch) {
|
|
480
581
|
return fail(
|
|
481
582
|
`not on the slice branch: HEAD is ${head ?? 'unknown'}, expected ${branch}`,
|
|
@@ -545,22 +646,26 @@ async function finishSlice(root, id, flags) {
|
|
|
545
646
|
return EXIT.FAIL;
|
|
546
647
|
}
|
|
547
648
|
for (const n of gates.notes ?? []) note(`note ${n}`);
|
|
548
|
-
emit(renderGateReport('pre-commit', declared, gates).join('\n')
|
|
649
|
+
emit(renderGateReport('pre-commit', declared, gates).join('\n'));
|
|
549
650
|
return EXIT.FAIL;
|
|
550
651
|
}
|
|
551
652
|
|
|
552
653
|
let sha = null;
|
|
654
|
+
const message = `${subject}\n\n${CO_AUTHOR}`;
|
|
553
655
|
try {
|
|
554
|
-
await git(root, ['commit', '-m',
|
|
555
|
-
sha = await
|
|
656
|
+
await git(root, ['commit', '-m', message]);
|
|
657
|
+
sha = await gitText(root, ['rev-parse', 'HEAD']);
|
|
556
658
|
} catch (error) {
|
|
557
659
|
const detail = String(error.stderr ?? error.message ?? error).trim().split('\n')[0];
|
|
558
|
-
return fail(`the gates passed but the commit failed: ${detail}`, EXIT.FAIL, `run: git -C ${root} commit -m ${JSON.stringify(
|
|
660
|
+
return fail(`the gates passed but the commit failed: ${detail}`, EXIT.FAIL, `run: git -C ${root} commit -m ${JSON.stringify(message)}`);
|
|
559
661
|
}
|
|
560
662
|
|
|
561
|
-
|
|
562
|
-
if (
|
|
563
|
-
|
|
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
|
+
}
|
|
564
669
|
}
|
|
565
670
|
|
|
566
671
|
// The slice is done: its lease goes with it. A failure here is a note — the
|
|
@@ -579,8 +684,12 @@ async function finishSlice(root, id, flags) {
|
|
|
579
684
|
commit: { sha, subject },
|
|
580
685
|
gates: gates.gates,
|
|
581
686
|
staged,
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
}),
|
|
584
693
|
});
|
|
585
694
|
return EXIT.OK;
|
|
586
695
|
}
|
|
@@ -591,12 +700,16 @@ async function finishSlice(root, id, flags) {
|
|
|
591
700
|
`slice ${id} ${doc}`,
|
|
592
701
|
...renderGateReport('pre-commit', declared, gates),
|
|
593
702
|
`commit ${sha} ${subject}`,
|
|
594
|
-
`state ${row.from} -> ${row.to}`,
|
|
595
|
-
`record ${relative(main, row.file)} line ${row.line} (edited, not staged: commit it with the merge)`,
|
|
596
703
|
];
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
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
|
+
}
|
|
712
|
+
emit(lines.join('\n'));
|
|
600
713
|
return EXIT.OK;
|
|
601
714
|
}
|
|
602
715
|
|
|
@@ -611,17 +724,29 @@ async function abandonSlice(root, id, flags) {
|
|
|
611
724
|
const mode = writeMode(flags);
|
|
612
725
|
const main = await mainCheckout(root);
|
|
613
726
|
const resolved = await resolveSlice(main, id);
|
|
614
|
-
|
|
615
|
-
|
|
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));
|
|
616
736
|
const { branch, worktree } = places(main, id);
|
|
617
737
|
const session = sliceSession(flags);
|
|
618
738
|
const apply = `run: ${CLI} slice abandon ${id} --yes`;
|
|
619
739
|
|
|
620
|
-
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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 };
|
|
625
750
|
|
|
626
751
|
if (!mode.apply) {
|
|
627
752
|
if (isJson(flags)) {
|
|
@@ -633,7 +758,7 @@ async function abandonSlice(root, id, flags) {
|
|
|
633
758
|
dryRun: true,
|
|
634
759
|
branch,
|
|
635
760
|
worktree: relative(main, worktree),
|
|
636
|
-
state:
|
|
761
|
+
state: stateJson,
|
|
637
762
|
remedy: apply,
|
|
638
763
|
});
|
|
639
764
|
return EXIT.OK;
|
|
@@ -642,17 +767,17 @@ async function abandonSlice(root, id, flags) {
|
|
|
642
767
|
`repo ${root}`,
|
|
643
768
|
`slice ${id} ${doc}`,
|
|
644
769
|
`remove ${relative(main, worktree)} and branch ${branch} (on apply)`,
|
|
645
|
-
|
|
770
|
+
stateLine,
|
|
646
771
|
`apply ${apply}`,
|
|
647
772
|
];
|
|
648
|
-
emit(lines.join('\n')
|
|
773
|
+
emit(lines.join('\n'));
|
|
649
774
|
return EXIT.OK;
|
|
650
775
|
}
|
|
651
776
|
|
|
652
777
|
// Abandoning from inside the worktree would pull the floor out: the shell's
|
|
653
778
|
// own directory is the thing being removed. Compared through git, not through
|
|
654
779
|
// strings, because the caller's spelling of the path may differ in symlinks.
|
|
655
|
-
const top = await
|
|
780
|
+
const top = await gitText(root, ['rev-parse', '--show-toplevel']).catch(() => null);
|
|
656
781
|
if (top !== null && (top === worktree || top.startsWith(`${worktree}/`))) {
|
|
657
782
|
return fail(
|
|
658
783
|
`cannot abandon ${id} from inside its worktree`,
|
|
@@ -677,13 +802,27 @@ async function abandonSlice(root, id, flags) {
|
|
|
677
802
|
removed.push(`kept ${branch} (${detail})`);
|
|
678
803
|
}
|
|
679
804
|
|
|
680
|
-
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
+
}
|
|
687
826
|
}
|
|
688
827
|
|
|
689
828
|
const released = await releaseSlice(main, id, session);
|
|
@@ -693,6 +832,7 @@ async function abandonSlice(root, id, flags) {
|
|
|
693
832
|
: released.error
|
|
694
833
|
? `the slice lease could not be released (${released.error}); the TTL reaps it`
|
|
695
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 };
|
|
696
836
|
if (isJson(flags)) {
|
|
697
837
|
emitJson({
|
|
698
838
|
ok: true,
|
|
@@ -702,11 +842,17 @@ async function abandonSlice(root, id, flags) {
|
|
|
702
842
|
branch,
|
|
703
843
|
worktree: relative(main, worktree),
|
|
704
844
|
lease: released.released === true ? { slice: id, session } : null,
|
|
705
|
-
state:
|
|
845
|
+
state: abandonStateJson,
|
|
706
846
|
});
|
|
707
847
|
return EXIT.OK;
|
|
708
848
|
}
|
|
709
|
-
const lines = [
|
|
710
|
-
|
|
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
|
+
];
|
|
856
|
+
emit(lines.join('\n'));
|
|
711
857
|
return EXIT.OK;
|
|
712
858
|
}
|