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.
Files changed (235) hide show
  1. package/CHANGELOG.md +191 -870
  2. package/README.md +188 -213
  3. package/bin/simt.mjs +19 -55
  4. package/docs/decisions/ADR-014-workspace-is-configuration.md +2 -2
  5. package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
  6. package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
  7. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
  8. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
  9. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
  10. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
  11. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
  12. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
  13. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
  14. package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
  15. 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
  16. package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
  17. package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
  18. package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
  19. package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
  20. package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
  21. package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
  22. package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
  23. package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
  24. package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
  25. package/ecosystems/bun.json +6 -0
  26. package/ecosystems/cargo.json +25 -0
  27. package/ecosystems/composer.json +6 -0
  28. package/ecosystems/dart.json +24 -0
  29. package/ecosystems/deno.json +21 -0
  30. package/ecosystems/devcontainers.json +6 -0
  31. package/ecosystems/docker-compose.json +6 -0
  32. package/ecosystems/docker.json +6 -0
  33. package/ecosystems/dotnet.json +25 -0
  34. package/ecosystems/elixir.json +23 -0
  35. package/ecosystems/gitsubmodule.json +6 -0
  36. package/ecosystems/go.json +25 -0
  37. package/ecosystems/gradle.json +13 -0
  38. package/ecosystems/maven.json +23 -0
  39. package/ecosystems/node.json +25 -0
  40. package/ecosystems/python.json +20 -0
  41. package/ecosystems/ruby.json +23 -0
  42. package/ecosystems/swift.json +25 -0
  43. package/ecosystems/terraform.json +6 -0
  44. package/ecosystems/uv.json +6 -0
  45. package/harnesses/antigravity.json +162 -0
  46. package/harnesses/claude.json +206 -0
  47. package/harnesses/codex.json +136 -0
  48. package/harnesses/copilot.json +88 -0
  49. package/harnesses/cursor.json +140 -0
  50. package/harnesses/deepseek.json +99 -0
  51. package/harnesses/gemini.json +122 -0
  52. package/harnesses/opencode.json +173 -0
  53. package/package.json +12 -2
  54. package/rules/core.md +7 -0
  55. package/rules/mdc/intake-questions.mdc +8 -0
  56. package/rules/mdc/slice-commit.mdc +57 -4
  57. package/src/advice/advice.mjs +3 -2
  58. package/src/agenthooks/prompt.mjs +5 -2
  59. package/src/agenthooks/render.mjs +86 -180
  60. package/src/agenthooks/shim.mjs +1 -1
  61. package/src/agents/render.mjs +15 -18
  62. package/src/api.mjs +31 -0
  63. package/src/argv.mjs +48 -3
  64. package/src/bench/ablate.mjs +3 -4
  65. package/src/bench/adapters.json +163 -105
  66. package/src/bench/adapters.mjs +466 -0
  67. package/src/bench/arms.mjs +333 -0
  68. package/src/bench/grade.mjs +33 -73
  69. package/src/bench/graders.mjs +42 -0
  70. package/src/bench/mine.mjs +121 -475
  71. package/src/bench/probe-ledger.mjs +149 -0
  72. package/src/bench/probes.catalog.mjs +918 -0
  73. package/src/bench/probes.mjs +79 -1064
  74. package/src/bench/render.mjs +111 -0
  75. package/src/bench/report.mjs +120 -962
  76. package/src/bench/run.mjs +185 -802
  77. package/src/bench/variants.mjs +4 -5
  78. package/src/bench/workspace.mjs +160 -0
  79. package/src/brief/assemble.mjs +48 -15
  80. package/src/ci/dependabot.mjs +25 -55
  81. package/src/ci/workflow.mjs +30 -7
  82. package/src/cli.mjs +45 -0
  83. package/src/commands/agent.mjs +4 -13
  84. package/src/commands/agenthook.mjs +70 -40
  85. package/src/commands/bench.mjs +191 -18
  86. package/src/commands/brief.mjs +5 -4
  87. package/src/commands/ci.mjs +13 -14
  88. package/src/commands/compare.mjs +42 -42
  89. package/src/commands/completions.mjs +2 -6
  90. package/src/commands/decide.mjs +4 -4
  91. package/src/commands/distill.mjs +4 -3
  92. package/src/commands/docs.mjs +88 -0
  93. package/src/commands/doctor.mjs +11 -7
  94. package/src/commands/evidence.mjs +6 -9
  95. package/src/commands/explain.mjs +0 -1
  96. package/src/commands/gate.mjs +26 -25
  97. package/src/commands/graph.mjs +2 -3
  98. package/src/commands/harness.mjs +6 -5
  99. package/src/commands/help.mjs +168 -19
  100. package/src/commands/hooks.mjs +119 -49
  101. package/src/commands/ignore.mjs +13 -8
  102. package/src/commands/impact.mjs +8 -15
  103. package/src/commands/index.mjs +318 -193
  104. package/src/commands/init-scan.mjs +90 -0
  105. package/src/commands/init.mjs +124 -105
  106. package/src/commands/lease.mjs +73 -25
  107. package/src/commands/lessons.mjs +10 -9
  108. package/src/commands/local.mjs +3 -3
  109. package/src/commands/map.mjs +24 -14
  110. package/src/commands/mcp.mjs +2 -18
  111. package/src/commands/pack.mjs +9 -6
  112. package/src/commands/plan.mjs +6 -5
  113. package/src/commands/plugin.mjs +2 -2
  114. package/src/commands/prbody.mjs +3 -3
  115. package/src/commands/repoindex.mjs +12 -11
  116. package/src/commands/review.mjs +2 -2
  117. package/src/commands/rules.mjs +37 -16
  118. package/src/commands/shared.mjs +14 -19
  119. package/src/commands/skills.mjs +33 -32
  120. package/src/commands/skillscompile.mjs +7 -8
  121. package/src/commands/skillsinstall.mjs +7 -6
  122. package/src/commands/slice.mjs +220 -74
  123. package/src/commands/spec.mjs +167 -0
  124. package/src/commands/status.mjs +27 -18
  125. package/src/commands/sync.mjs +27 -25
  126. package/src/commands/tests.mjs +18 -21
  127. package/src/commands/tokens.mjs +21 -26
  128. package/src/commands/undo.mjs +5 -15
  129. package/src/commands/upgrade.mjs +15 -7
  130. package/src/commands/why.mjs +15 -21
  131. package/src/compare/hooksstate.mjs +2 -6
  132. package/src/compare/learn.mjs +3 -1
  133. package/src/compare/row.mjs +0 -2
  134. package/src/context.mjs +64 -0
  135. package/src/decisions/index-doc.mjs +46 -0
  136. package/src/decisions/index.mjs +3 -2
  137. package/src/diff.mjs +1 -1
  138. package/src/distill/distill.mjs +14 -1
  139. package/src/distill/engine.mjs +5 -1
  140. package/src/docs/commands.mjs +65 -0
  141. package/src/ecosystem/records.mjs +219 -0
  142. package/src/explain.mjs +31 -0
  143. package/src/gates/index.mjs +385 -0
  144. package/src/graph/compile.mjs +8 -4
  145. package/src/graph/load.mjs +6 -2
  146. package/src/graph/spec.mjs +10 -8
  147. package/src/harness/records.mjs +143 -0
  148. package/src/hooks/capability.mjs +7 -5
  149. package/src/hooks/dispatcher.mjs +209 -60
  150. package/src/hooks/gates.mjs +10 -43
  151. package/src/hooks/resolve.mjs +4 -14
  152. package/src/hooks/shim.mjs +5 -2
  153. package/src/hooks/where.mjs +2 -7
  154. package/src/identity.mjs +31 -12
  155. package/src/index/coverage.mjs +58 -0
  156. package/src/index/extract.mjs +6 -16
  157. package/src/index/gate.mjs +1 -1
  158. package/src/index/govern.mjs +26 -9
  159. package/src/index/impact.mjs +4 -8
  160. package/src/index/map.mjs +14 -15
  161. package/src/index/open.mjs +173 -0
  162. package/src/index/persist.mjs +224 -57
  163. package/src/index/store.mjs +82 -39
  164. package/src/index/tests.mjs +2 -6
  165. package/src/journal.mjs +94 -20
  166. package/src/kernel/card.mjs +37 -63
  167. package/src/kernel/checks.mjs +4 -10
  168. package/src/lease.mjs +102 -7
  169. package/src/ledger/index.mjs +150 -14
  170. package/src/local/client.mjs +4 -6
  171. package/src/locate.mjs +33 -9
  172. package/src/manifest/schema.mjs +213 -0
  173. package/src/mcp/paginate.mjs +33 -0
  174. package/src/mcp/resources.mjs +7 -10
  175. package/src/mcp/schemas.mjs +512 -0
  176. package/src/mcp/server.mjs +119 -25
  177. package/src/mcp/tools-facts.mjs +326 -0
  178. package/src/mcp/tools-read.mjs +221 -0
  179. package/src/mcp/tools.mjs +4 -967
  180. package/src/measure/corpus.mjs +150 -0
  181. package/src/measure/metrics.mjs +236 -0
  182. package/src/measure/records.mjs +211 -0
  183. package/src/measure/session.mjs +3 -2
  184. package/src/measure/stats.mjs +538 -0
  185. package/src/measure/task-schema.mjs +125 -0
  186. package/src/measure/tasks.mjs +69 -0
  187. package/src/memory/decide.mjs +6 -15
  188. package/src/memory/lesson-text.mjs +61 -0
  189. package/src/memory/lessons.mjs +29 -82
  190. package/src/name.mjs +7 -0
  191. package/src/node-version.mjs +37 -0
  192. package/src/output/index.mjs +209 -10
  193. package/src/packs/manage.mjs +32 -12
  194. package/src/packs.mjs +20 -2
  195. package/src/plan/index.mjs +1 -1
  196. package/src/plugins.mjs +22 -16
  197. package/src/prbody.mjs +7 -11
  198. package/src/projection/loaders.mjs +7 -692
  199. package/src/projection/lock.mjs +11 -1
  200. package/src/projection/manifest.mjs +29 -468
  201. package/src/projection/mdc.mjs +7 -8
  202. package/src/projection/project.mjs +126 -221
  203. package/src/projection/region.mjs +561 -0
  204. package/src/projection/rules.mjs +20 -550
  205. package/src/projection/surfaces.mjs +43 -210
  206. package/src/projection/targets.mjs +5 -5
  207. package/src/repo-root.mjs +31 -0
  208. package/src/resolve/eval.mjs +2 -1
  209. package/src/resolve/fingerprint.mjs +19 -22
  210. package/src/resolve/resolve.mjs +1 -41
  211. package/src/review/critic.mjs +10 -13
  212. package/src/run.mjs +98 -0
  213. package/src/secrets.mjs +14 -22
  214. package/src/state/events.mjs +129 -0
  215. package/src/status-snapshot.mjs +233 -0
  216. package/src/status.mjs +2 -6
  217. package/src/sys/git.mjs +82 -0
  218. package/src/sys/proc.mjs +151 -0
  219. package/src/testing/parsers.mjs +265 -0
  220. package/src/testing/patches.mjs +23 -0
  221. package/src/testing/paths.mjs +49 -0
  222. package/src/testing/runners.mjs +186 -0
  223. package/src/testing/vault.mjs +68 -0
  224. package/src/text/frontmatter.mjs +49 -0
  225. package/src/text/tokens.mjs +52 -0
  226. package/src/toolwords.mjs +3 -0
  227. package/src/util/fs.mjs +20 -0
  228. package/src/util/sort.mjs +12 -0
  229. package/src/workspace.mjs +58 -11
  230. package/src/write/plan.mjs +82 -0
  231. package/templates/plan/tracker.json +1 -0
  232. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
  233. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
  234. package/docs/decisions/README.md +0 -600
  235. package/src/bench/tasks.mjs +0 -36
@@ -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 { execFile } from 'node:child_process';
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
- const exec = promisify(execFile);
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 git(root, ['rev-parse', '--git-common-dir']);
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.root ?? process.cwd();
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 resolved = await resolveSlice(main, id);
239
- if (resolved.error) return fail(resolved.error, EXIT.USAGE, resolved.remedy);
240
- const { title, doc } = resolved.slice;
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 = `run: ${CLI} slice start ${id} --yes`;
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
- const preview = await planDone(main, id, { state: started.state, dryRun: true });
257
- if (preview.error) return fail(preview.error, EXIT.USAGE, preview.remedy ?? `run: ${CLI} plan status`);
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: { from: preview.from, to: preview.to, changed: preview.wouldChange },
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
- `state ${preview.from} -> ${preview.to}`,
362
+ stateLine,
287
363
  `apply ${apply}`,
288
364
  ];
289
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
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
- const row = await planDone(main, id, { state: started.state, dryRun: false });
336
- if (row.error) {
337
- return fail(`${branch} exists with its brief, but the tracker row was not updated: ${row.error}`, EXIT.FAIL, row.remedy ?? `run: ${CLI} plan status`);
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: { from: row.from, to: row.to, changed: row.changed },
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
- `state ${row.from} -> ${row.to}`,
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'), { maxBytes: flags.maxBytes ?? 0 });
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
- if (resolved.error) return fail(resolved.error, EXIT.USAGE, resolved.remedy);
417
- const { title, doc } = resolved.slice;
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
- const row = await planDone(main, id, { sha: null, test, dryRun: true });
442
- if (row.error) return fail(row.error, EXIT.USAGE, row.remedy ?? `run: ${CLI} plan status`);
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: { from: row.from, to: row.to, changed: row.wouldChange },
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
- `state ${row.from} -> ${row.to}`,
569
+ stateLine,
469
570
  `apply ${apply}`,
470
571
  ];
471
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
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 git(root, ['rev-parse', '--abbrev-ref', 'HEAD']).catch(() => null);
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'), { maxBytes: flags.maxBytes ?? 0 });
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', subject]);
555
- sha = await git(root, ['rev-parse', 'HEAD']);
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(subject)}`);
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
- const row = await planDone(main, id, { sha, test, dryRun: false });
562
- if (row.error) {
563
- return fail(`${sha} is committed, but the tracker row could not record it: ${row.error}`, EXIT.FAIL, row.remedy ?? `run: ${CLI} plan status`);
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
- evidence: { recorded: row.evidenceRecorded, sha: row.sha, test: row.test, note: row.evidenceNote },
583
- row: { file: relative(main, row.file), line: row.line, from: row.from, to: row.to, changed: row.changed },
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 (row.evidenceRecorded) lines.push(`evidence ${sha}${test ? ` test: ${test}` : ''}`);
598
- else if (row.evidenceNote) lines.push(`note ${row.evidenceNote}`);
599
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
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
- if (resolved.error) return fail(resolved.error, EXIT.USAGE, resolved.remedy);
615
- const { doc } = resolved.slice;
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
- const loaded = await loadFormat(main);
621
- if (loaded.error) return fail(loaded.error, EXIT.USAGE, loaded.remedy);
622
- const readyDisplay = loaded.format.canonical(loaded.format.ready) ?? 'Queued';
623
- const preview = await planDone(main, id, { state: readyDisplay, dryRun: true });
624
- if (preview.error) return fail(preview.error, EXIT.USAGE, preview.remedy ?? `run: ${CLI} plan status`);
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: { from: preview.from, to: preview.to, changed: preview.wouldChange },
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
- `state ${preview.from} -> ${preview.to}`,
770
+ stateLine,
646
771
  `apply ${apply}`,
647
772
  ];
648
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
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 git(root, ['rev-parse', '--show-toplevel']).catch(() => null);
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
- const row = await planDone(main, id, { state: readyDisplay, dryRun: false });
681
- if (row.error) {
682
- return fail(
683
- `the worktree is down, but the tracker row was not updated: ${row.error}`,
684
- EXIT.FAIL,
685
- row.remedy ?? `run: ${CLI} plan status`,
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: { from: row.from, to: row.to, changed: row.changed },
845
+ state: abandonStateJson,
706
846
  });
707
847
  return EXIT.OK;
708
848
  }
709
- const lines = [`repo ${root}`, `slice ${id} ${doc}`, ...removed, `state ${row.from} -> ${row.to}`, `lease ${leaseLine}`];
710
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
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
  }