simt-dev 0.6.3 → 0.7.1
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 +162 -841
- package/README.md +21 -135
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.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-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- 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/package.json +13 -3
- package/rules/core.md +9 -2
- package/rules/mdc/slice-commit.mdc +57 -4
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +3 -3
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +3 -2
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +141 -9
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/prompt.mjs +10 -3
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/api.mjs +29 -0
- package/src/argv.mjs +37 -3
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/adapters.json +16 -0
- package/src/bench/corpus.mjs +150 -0
- package/src/bench/grade.mjs +23 -49
- package/src/bench/mine.mjs +247 -36
- package/src/bench/probes.mjs +104 -68
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +260 -84
- package/src/bench/run.mjs +251 -150
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +44 -3
- package/src/ci/workflow.mjs +46 -14
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +205 -0
- package/src/commands/agenthook.mjs +113 -20
- package/src/commands/bench.mjs +192 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +50 -49
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +10 -9
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +19 -12
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +98 -28
- package/src/commands/graph.mjs +20 -8
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +160 -31
- package/src/commands/hooks.mjs +163 -50
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +8 -9
- package/src/commands/index.mjs +278 -180
- package/src/commands/init.mjs +171 -38
- package/src/commands/lease.mjs +206 -39
- package/src/commands/lessons.mjs +72 -15
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +47 -27
- package/src/commands/mcp.mjs +0 -1
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +3 -3
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +5 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +124 -33
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +82 -45
- package/src/commands/skillscompile.mjs +12 -12
- package/src/commands/skillsinstall.mjs +14 -2
- package/src/commands/slice.mjs +396 -26
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +50 -33
- package/src/commands/tests.mjs +11 -11
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +29 -31
- package/src/commands/upgrade.mjs +40 -4
- package/src/commands/why.mjs +19 -12
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -3
- package/src/completions.mjs +5 -0
- package/src/context.mjs +64 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +17 -2
- package/src/distill/engine.mjs +5 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +18 -6
- package/src/graph/load.mjs +42 -20
- package/src/hooks/capability.mjs +17 -4
- package/src/hooks/dispatcher.mjs +513 -36
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +11 -10
- package/src/hooks/shim.mjs +17 -9
- package/src/identity.mjs +96 -15
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +307 -47
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +372 -84
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +97 -15
- package/src/ledger/index.mjs +120 -25
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/resources.mjs +12 -13
- package/src/mcp/server.mjs +151 -38
- package/src/mcp/tools.mjs +90 -16
- package/src/measure/session.mjs +3 -2
- package/src/memory/decide.mjs +98 -43
- package/src/memory/lessons.mjs +168 -33
- package/src/name.mjs +9 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +111 -3
- package/src/packs/manage.mjs +17 -3
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +31 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +6 -2
- package/src/prbody.mjs +7 -5
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +31 -14
- package/src/projection/manifest.mjs +89 -53
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +58 -6
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +88 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +24 -15
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +6 -3
- package/src/secrets.mjs +165 -22
- package/src/state.mjs +76 -0
- package/src/status.mjs +9 -4
- package/src/toolwords.mjs +8 -0
- package/src/tsconfig.mjs +39 -99
- package/src/workspace.mjs +55 -10
- package/templates/plan/tracker.json +1 -1
- package/docs/decisions/README.md +0 -591
package/src/commands/slice.mjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A row in a tracker is a promise, and these are the two moments it is acted on.
|
|
4
4
|
// `start` isolates the work: a linked worktree on a branch named for the row, so the
|
|
5
|
-
// main checkout stays free for whatever else is running, and
|
|
6
|
-
//
|
|
7
|
-
// agent picks it up. `finish` closes the loop the way the repository's own hook would:
|
|
5
|
+
// main checkout stays free for whatever else is running, and a task brief assembled in
|
|
6
|
+
// the main checkout at `.simt/local/briefs/<id>.md`, beside the worktree and outside
|
|
7
|
+
// its tree, where an agent picks it up. `finish` closes the loop the way the repository's own hook would:
|
|
8
8
|
// the declared gates run through the shared dispatcher first, because a command that
|
|
9
9
|
// commits for you may not assume a hook is installed to stop it, then the commit
|
|
10
10
|
// carries a Conventional subject built from the row's own title, and only then does
|
|
@@ -33,15 +33,41 @@ import { promisify } from 'node:util';
|
|
|
33
33
|
|
|
34
34
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
35
35
|
import { planDone, planNext } from '../plan/index.mjs';
|
|
36
|
+
import { loadFormat } from '../plan/template.mjs';
|
|
36
37
|
import { assembleBrief } from '../brief/assemble.mjs';
|
|
37
38
|
import { renderGateReport, runGates, stagedFiles } from '../hooks/dispatcher.mjs';
|
|
39
|
+
import { GATES } from '../hooks/gates.mjs';
|
|
40
|
+
import { indexTreeId, passEntryForGate, readLedger } from '../ledger/index.mjs';
|
|
41
|
+
import {
|
|
42
|
+
DEFAULT_TTL_SECONDS,
|
|
43
|
+
activeLeases,
|
|
44
|
+
canonicalLeasePath,
|
|
45
|
+
readLeases,
|
|
46
|
+
withLeaseLock,
|
|
47
|
+
writeLeases,
|
|
48
|
+
} from '../lease.mjs';
|
|
49
|
+
import { SESSION_ENV } from '../agenthooks/policy.mjs';
|
|
50
|
+
import { assertVersionPin } from '../package-version.mjs';
|
|
51
|
+
import { coreIgnoreCase } from '../repo-root.mjs';
|
|
38
52
|
import { readManifest } from '../projection/manifest.mjs';
|
|
39
|
-
import { CLI, MANIFEST_FILE
|
|
53
|
+
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
54
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
55
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
40
56
|
|
|
41
57
|
const exec = promisify(execFile);
|
|
42
58
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The state a started slice moves to: the template's `started`-role state, not
|
|
61
|
+
* a hard-coded word. A repository that renamed its states still starts slices,
|
|
62
|
+
* and one that deleted the in-progress state fails closed in `plan done` on a
|
|
63
|
+
* word its own template never defined (review 0003 Q23).
|
|
64
|
+
*/
|
|
65
|
+
async function startedState(main) {
|
|
66
|
+
const loaded = await loadFormat(main);
|
|
67
|
+
if (loaded.error) return { error: loaded.error, remedy: loaded.remedy };
|
|
68
|
+
const { format } = loaded;
|
|
69
|
+
return { state: format.startedWrite ?? format.canonical('in-progress') ?? 'In progress' };
|
|
70
|
+
}
|
|
45
71
|
|
|
46
72
|
/**
|
|
47
73
|
* A Conventional Commits type: one lower-case word, optionally scoped, optionally
|
|
@@ -74,12 +100,16 @@ async function mainCheckout(root) {
|
|
|
74
100
|
}
|
|
75
101
|
}
|
|
76
102
|
|
|
77
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* The branch and worktree a slice owns. Both derive from the id: one id, one spelling.
|
|
105
|
+
* The worktree lives under machine-local state (review 0003 Q19): a worktree inside
|
|
106
|
+
* the checkout would be recorded as an embedded gitlink and would change the tree id.
|
|
107
|
+
*/
|
|
78
108
|
function places(main, id) {
|
|
79
109
|
const slug = id.toLowerCase();
|
|
80
110
|
return {
|
|
81
111
|
branch: `slice/${slug}`,
|
|
82
|
-
worktree: join(main,
|
|
112
|
+
worktree: join(main, LOCAL_DIR, 'worktrees', slug),
|
|
83
113
|
};
|
|
84
114
|
}
|
|
85
115
|
|
|
@@ -96,6 +126,15 @@ function subjectFor(type, id, title) {
|
|
|
96
126
|
return `${type}(${id.toLowerCase()}): ${description}`;
|
|
97
127
|
}
|
|
98
128
|
|
|
129
|
+
/**
|
|
130
|
+
* The trailer of every commit this tool writes: attribution to the tool, in the
|
|
131
|
+
* form git renders as a co-author. The slice-commit rule ends every message it
|
|
132
|
+
* defines with the same line, so a commit the tool writes and one an agent
|
|
133
|
+
* writes from the rule agree on attribution. Module-local: one caller needs no
|
|
134
|
+
* export, and an export is a promise to a caller (deadcode).
|
|
135
|
+
*/
|
|
136
|
+
const CO_AUTHOR = 'Co-Authored-By: Simt Dev <noreply@simt.space>';
|
|
137
|
+
|
|
99
138
|
/** Resolve one named slice through `plan next`, which already knows missing and ambiguous. */
|
|
100
139
|
async function resolveSlice(main, id) {
|
|
101
140
|
const found = await planNext(main, id);
|
|
@@ -104,8 +143,83 @@ async function resolveSlice(main, id) {
|
|
|
104
143
|
return { slice: found.next };
|
|
105
144
|
}
|
|
106
145
|
|
|
146
|
+
/** The session holding the slice, or null when the caller named none: a person
|
|
147
|
+
* at a terminal has no session, and starting without one proceeds unleased
|
|
148
|
+
* rather than refusing the work. */
|
|
149
|
+
function sliceSession(flags) {
|
|
150
|
+
const session = flags.session ?? process.env[SESSION_ENV] ?? null;
|
|
151
|
+
return typeof session === 'string' && session !== '' ? session : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The slice's lease path, in the spelling the pre-edit hook compares: the
|
|
156
|
+
* worktree directory, repository-relative. Entries carry `kind: 'slice'` so a
|
|
157
|
+
* later schema (review 0003 Q74) can tell slice claims from path claims; the
|
|
158
|
+
* v1 reader keeps the extra fields without understanding them.
|
|
159
|
+
*/
|
|
160
|
+
function sliceLeaseRel(main, worktree) {
|
|
161
|
+
return canonicalLeasePath(main, worktree, { ignoreCase: coreIgnoreCase(main) });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Claim the slice for one session: a second session starting the same slice
|
|
166
|
+
* is refused with the holder's name instead of racing it to the worktree.
|
|
167
|
+
* A claim held by this session is refreshed; anything else on the file is
|
|
168
|
+
* left alone.
|
|
169
|
+
*/
|
|
170
|
+
async function claimSlice(main, id, leaseRel, session) {
|
|
171
|
+
try {
|
|
172
|
+
return await withLeaseLock(main, async () => {
|
|
173
|
+
const read = await readLeases(main);
|
|
174
|
+
if (read.error !== null) return { error: read.error, path: read.path };
|
|
175
|
+
const held = activeLeases(read);
|
|
176
|
+
const other = held.find(
|
|
177
|
+
(l) => l.session !== session && ((l.kind === 'slice' && l.slice === id) || (leaseRel !== null && l.path === leaseRel)),
|
|
178
|
+
);
|
|
179
|
+
if (other) return { conflict: other };
|
|
180
|
+
const mine = {
|
|
181
|
+
path: leaseRel,
|
|
182
|
+
session,
|
|
183
|
+
kind: 'slice',
|
|
184
|
+
slice: id,
|
|
185
|
+
expiresAt: Date.now() + DEFAULT_TTL_SECONDS * 1000,
|
|
186
|
+
};
|
|
187
|
+
const kept = held.filter((l) => !(l.kind === 'slice' && l.slice === id && l.session === session));
|
|
188
|
+
const path = await writeLeases(main, [...kept, mine]);
|
|
189
|
+
return { mine, path };
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return { error: error.message };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Release this session's claim on the slice. Best-effort by design: `finish`
|
|
198
|
+
* calls it after the commit exists, so a lease failure is a note about a TTL
|
|
199
|
+
* that will clean itself up, never a second failure stacked on a success.
|
|
200
|
+
*/
|
|
201
|
+
async function releaseSlice(main, id, session) {
|
|
202
|
+
if (session === null) return { released: false };
|
|
203
|
+
try {
|
|
204
|
+
return await withLeaseLock(main, async () => {
|
|
205
|
+
const read = await readLeases(main);
|
|
206
|
+
if (read.error !== null) return { error: read.error };
|
|
207
|
+
const held = activeLeases(read);
|
|
208
|
+
const mine = held.filter((l) => l.kind === 'slice' && l.slice === id && l.session === session);
|
|
209
|
+
if (mine.length === 0) return { released: false };
|
|
210
|
+
await writeLeases(
|
|
211
|
+
main,
|
|
212
|
+
held.filter((l) => !mine.includes(l)),
|
|
213
|
+
);
|
|
214
|
+
return { released: true };
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return { error: error.message };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
107
221
|
export async function sliceCommand(sub, args, flags) {
|
|
108
|
-
const root = flags
|
|
222
|
+
const root = commandRoot(flags);
|
|
109
223
|
if (sub === 'start') {
|
|
110
224
|
const id = args[0];
|
|
111
225
|
if (!id) return fail(`${CLI} slice start needs a slice id`, EXIT.USAGE, `run: ${CLI} plan next`);
|
|
@@ -116,7 +230,16 @@ export async function sliceCommand(sub, args, flags) {
|
|
|
116
230
|
if (!id) return fail(`${CLI} slice finish needs a slice id`, EXIT.USAGE, `run: ${CLI} plan next`);
|
|
117
231
|
return finishSlice(root, id, flags);
|
|
118
232
|
}
|
|
119
|
-
|
|
233
|
+
if (sub === 'abandon') {
|
|
234
|
+
const id = args[0];
|
|
235
|
+
if (!id) return fail(`${CLI} slice abandon needs a slice id`, EXIT.USAGE, `run: ${CLI} plan next`);
|
|
236
|
+
return abandonSlice(root, id, flags);
|
|
237
|
+
}
|
|
238
|
+
return fail(
|
|
239
|
+
`unknown slice subcommand ${sub}`,
|
|
240
|
+
EXIT.USAGE,
|
|
241
|
+
`run: ${CLI} slice start <id> | ${CLI} slice finish <id> | ${CLI} slice abandon <id>`,
|
|
242
|
+
);
|
|
120
243
|
}
|
|
121
244
|
|
|
122
245
|
async function startSlice(root, id, flags) {
|
|
@@ -126,16 +249,25 @@ async function startSlice(root, id, flags) {
|
|
|
126
249
|
if (resolved.error) return fail(resolved.error, EXIT.USAGE, resolved.remedy);
|
|
127
250
|
const { title, doc } = resolved.slice;
|
|
128
251
|
const { branch, worktree } = places(main, id);
|
|
129
|
-
|
|
252
|
+
// The brief lives beside the worktree, not in it: a file inside the worktree
|
|
253
|
+
// is untracked content in the slice's own tree, and the tree id must name
|
|
254
|
+
// the code, not the briefing (review 0003 Q19, Q23).
|
|
255
|
+
const briefPath = join(main, LOCAL_DIR, 'briefs', `${id.toLowerCase()}.md`);
|
|
256
|
+
const leaseRel = sliceLeaseRel(main, worktree);
|
|
257
|
+
const session = sliceSession(flags);
|
|
130
258
|
const task = `${id}: ${String(title ?? '').trim()}`;
|
|
131
259
|
const apply = `run: ${CLI} slice start ${id} --yes`;
|
|
132
260
|
|
|
261
|
+
const started = await startedState(main);
|
|
262
|
+
if (started.error) return fail(started.error, EXIT.USAGE, started.remedy);
|
|
133
263
|
// The row first, dry, against the main checkout: it is the only part of `start`
|
|
134
264
|
// that can refuse for a reason about the tracker rather than about git, and a
|
|
135
265
|
// preview that discovers it late is a preview that half-described the work.
|
|
136
|
-
const preview = await planDone(main, id, { state:
|
|
266
|
+
const preview = await planDone(main, id, { state: started.state, dryRun: true });
|
|
137
267
|
if (preview.error) return fail(preview.error, EXIT.USAGE, preview.remedy ?? `run: ${CLI} plan status`);
|
|
138
268
|
|
|
269
|
+
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)`;
|
|
270
|
+
|
|
139
271
|
if (!mode.apply) {
|
|
140
272
|
if (isJson(flags)) {
|
|
141
273
|
emitJson({
|
|
@@ -148,6 +280,7 @@ async function startSlice(root, id, flags) {
|
|
|
148
280
|
branch,
|
|
149
281
|
worktree: relative(main, worktree),
|
|
150
282
|
brief: { path: relative(main, briefPath), bytes: null },
|
|
283
|
+
lease: session === null ? null : { slice: id, path: leaseRel, session },
|
|
151
284
|
state: { from: preview.from, to: preview.to, changed: preview.wouldChange },
|
|
152
285
|
remedy: apply,
|
|
153
286
|
});
|
|
@@ -159,10 +292,11 @@ async function startSlice(root, id, flags) {
|
|
|
159
292
|
`branch ${branch}`,
|
|
160
293
|
`worktree ${relative(main, worktree)}`,
|
|
161
294
|
`brief ${relative(main, briefPath)} (assembled on apply)`,
|
|
295
|
+
leaseLine,
|
|
162
296
|
`state ${preview.from} -> ${preview.to}`,
|
|
163
297
|
`apply ${apply}`,
|
|
164
298
|
];
|
|
165
|
-
emit(lines.join('\n')
|
|
299
|
+
emit(lines.join('\n'));
|
|
166
300
|
return EXIT.OK;
|
|
167
301
|
}
|
|
168
302
|
|
|
@@ -178,6 +312,26 @@ async function startSlice(root, id, flags) {
|
|
|
178
312
|
return fail(`the index at ${main} is empty and will not build here`, EXIT.UNSUPPORTED, `run: ${CLI} index`);
|
|
179
313
|
}
|
|
180
314
|
|
|
315
|
+
// Claimed before the worktree exists: a second session starting the same slice
|
|
316
|
+
// is refused here, not after a worktree that then has to be taken back down.
|
|
317
|
+
let lease = null;
|
|
318
|
+
if (session !== null) {
|
|
319
|
+
const claimed = await claimSlice(main, id, leaseRel, session);
|
|
320
|
+
if (claimed.error) {
|
|
321
|
+
return fail(`the slice could not be leased: ${claimed.error}`, EXIT.FAIL, `run: ${CLI} lease to see every claim`);
|
|
322
|
+
}
|
|
323
|
+
if (claimed.conflict) {
|
|
324
|
+
const holder = claimed.conflict.session;
|
|
325
|
+
const until = new Date(claimed.conflict.expiresAt).toISOString();
|
|
326
|
+
return fail(
|
|
327
|
+
`${id} is already started by session ${holder} (lease until ${until})`,
|
|
328
|
+
EXIT.FAIL,
|
|
329
|
+
`wait for the TTL or ask ${holder} to finish or abandon it; run: ${CLI} lease to see every claim`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
lease = claimed.mine;
|
|
333
|
+
}
|
|
334
|
+
|
|
181
335
|
try {
|
|
182
336
|
await git(main, ['worktree', 'add', '-b', branch, worktree]);
|
|
183
337
|
} catch (error) {
|
|
@@ -185,15 +339,16 @@ async function startSlice(root, id, flags) {
|
|
|
185
339
|
return fail(`the worktree could not be created: ${detail}`, EXIT.FAIL, `run: git -C ${main} worktree list`);
|
|
186
340
|
}
|
|
187
341
|
|
|
188
|
-
await mkdir(
|
|
342
|
+
await mkdir(dirname(briefPath), { recursive: true });
|
|
189
343
|
await writeFile(briefPath, packet.packed.text, 'utf8');
|
|
190
344
|
|
|
191
|
-
const row = await planDone(main, id, { state:
|
|
345
|
+
const row = await planDone(main, id, { state: started.state, dryRun: false });
|
|
192
346
|
if (row.error) {
|
|
193
347
|
return fail(`${branch} exists with its brief, but the tracker row was not updated: ${row.error}`, EXIT.FAIL, row.remedy ?? `run: ${CLI} plan status`);
|
|
194
348
|
}
|
|
195
349
|
|
|
196
350
|
const bytes = Buffer.byteLength(packet.packed.text);
|
|
351
|
+
if (session === null) note('note no session: the slice is not leased; a second start will not see this one');
|
|
197
352
|
if (isJson(flags)) {
|
|
198
353
|
emitJson({
|
|
199
354
|
ok: true,
|
|
@@ -204,6 +359,7 @@ async function startSlice(root, id, flags) {
|
|
|
204
359
|
branch,
|
|
205
360
|
worktree: relative(main, worktree),
|
|
206
361
|
brief: { path: relative(main, briefPath), bytes },
|
|
362
|
+
lease,
|
|
207
363
|
state: { from: row.from, to: row.to, changed: row.changed },
|
|
208
364
|
});
|
|
209
365
|
return EXIT.OK;
|
|
@@ -214,18 +370,62 @@ async function startSlice(root, id, flags) {
|
|
|
214
370
|
`branch ${branch}`,
|
|
215
371
|
`worktree ${relative(main, worktree)}`,
|
|
216
372
|
`brief ${relative(main, briefPath)} ${bytes} bytes`,
|
|
373
|
+
lease === null ? 'lease (none)' : `lease slice ${id} as ${session}`,
|
|
217
374
|
`state ${row.from} -> ${row.to}`,
|
|
218
375
|
];
|
|
219
|
-
emit(lines.join('\n')
|
|
376
|
+
emit(lines.join('\n'));
|
|
220
377
|
return EXIT.OK;
|
|
221
378
|
}
|
|
222
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Split the declared gates into the ones that must run and the ones the ledger
|
|
382
|
+
* already proves: a gate with a passing entry on exactly this staged tree, and
|
|
383
|
+
* no later failure for the same gate on the same tree, does not run again.
|
|
384
|
+
* That is the double gate the review verified — the dispatcher, then the
|
|
385
|
+
* commit hook — and the seed of the gate-run cache (review 0003 Q23).
|
|
386
|
+
*
|
|
387
|
+
* Fail-closed throughout: no tree, no ledger, a version mismatch, or an
|
|
388
|
+
* unknown gate all mean the gate runs. An entry recorded under another binary
|
|
389
|
+
* is not evidence for this one, so a pin mismatch disables the cache and
|
|
390
|
+
* routes through `runGates`, which fails on the mismatch itself.
|
|
391
|
+
*/
|
|
392
|
+
async function splitGates(root, declared, manifestDoc) {
|
|
393
|
+
if (!Array.isArray(declared) || declared.length === 0) return { fresh: [], skipped: [], tree: null };
|
|
394
|
+
if (assertVersionPin(manifestDoc)) return { fresh: [...declared], skipped: [], tree: null };
|
|
395
|
+
const stagedTree = await indexTreeId(root).catch(() => null);
|
|
396
|
+
if (stagedTree === null) return { fresh: [...declared], skipped: [], tree: null };
|
|
397
|
+
const { entries } = await readLedger(root).catch(() => ({ entries: [] }));
|
|
398
|
+
const fresh = [];
|
|
399
|
+
const skipped = [];
|
|
400
|
+
for (const name of declared) {
|
|
401
|
+
if (GATES[name] === undefined) {
|
|
402
|
+
fresh.push(name);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const pass = passEntryForGate(entries, name, stagedTree);
|
|
406
|
+
if (pass === null) fresh.push(name);
|
|
407
|
+
else skipped.push({ name, at: pass.ts });
|
|
408
|
+
}
|
|
409
|
+
return { fresh, skipped, tree: stagedTree };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** A gate result that did not run: the ledger line is the evidence. */
|
|
413
|
+
function cachedResult({ name, at }) {
|
|
414
|
+
return {
|
|
415
|
+
name,
|
|
416
|
+
status: 'passed',
|
|
417
|
+
detail: `already passed on this tree at ${at} (ledger); not re-run`,
|
|
418
|
+
cached: true,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
223
422
|
async function finishSlice(root, id, flags) {
|
|
224
423
|
const mode = writeMode(flags);
|
|
225
424
|
const main = await mainCheckout(root);
|
|
226
425
|
const resolved = await resolveSlice(main, id);
|
|
227
426
|
if (resolved.error) return fail(resolved.error, EXIT.USAGE, resolved.remedy);
|
|
228
427
|
const { title, doc } = resolved.slice;
|
|
428
|
+
const { branch, worktree } = places(main, id);
|
|
229
429
|
|
|
230
430
|
const type = String(flags.type ?? 'feat');
|
|
231
431
|
if (!TYPE.test(type)) {
|
|
@@ -245,10 +445,15 @@ async function finishSlice(root, id, flags) {
|
|
|
245
445
|
const apply = `run: ${CLI} slice finish ${id}${test ? ` --test ${JSON.stringify(test)}` : ''} --yes`;
|
|
246
446
|
|
|
247
447
|
const staged = await stagedFiles(root);
|
|
448
|
+
const split = await splitGates(root, declared, manifest.manifest);
|
|
248
449
|
|
|
249
450
|
if (!mode.apply) {
|
|
250
451
|
const row = await planDone(main, id, { sha: null, test, dryRun: true });
|
|
251
452
|
if (row.error) return fail(row.error, EXIT.USAGE, row.remedy ?? `run: ${CLI} plan status`);
|
|
453
|
+
const gateLine =
|
|
454
|
+
declared.length === 0
|
|
455
|
+
? '(none declared)'
|
|
456
|
+
: `${declared.join(', ')}${split.skipped.length > 0 ? ` (already passed on this tree: ${split.skipped.map((s) => s.name).join(', ')})` : ''}`;
|
|
252
457
|
if (isJson(flags)) {
|
|
253
458
|
emitJson({
|
|
254
459
|
ok: true,
|
|
@@ -258,7 +463,7 @@ async function finishSlice(root, id, flags) {
|
|
|
258
463
|
dryRun: true,
|
|
259
464
|
subject,
|
|
260
465
|
staged,
|
|
261
|
-
gates: { event: 'pre-commit', declared },
|
|
466
|
+
gates: { event: 'pre-commit', declared, skipped: split.skipped.map((s) => s.name) },
|
|
262
467
|
state: { from: row.from, to: row.to, changed: row.wouldChange },
|
|
263
468
|
remedy: apply,
|
|
264
469
|
});
|
|
@@ -267,25 +472,72 @@ async function finishSlice(root, id, flags) {
|
|
|
267
472
|
const lines = [
|
|
268
473
|
`repo ${root}`,
|
|
269
474
|
`slice ${id} ${doc}`,
|
|
270
|
-
`gates pre-commit ${
|
|
475
|
+
`gates pre-commit ${gateLine} (run on apply)`,
|
|
271
476
|
`staged ${staged.length} file(s)${staged.length === 0 ? ' (finish refuses without one)' : ''}`,
|
|
272
477
|
`subject ${subject}`,
|
|
273
478
|
`state ${row.from} -> ${row.to}`,
|
|
274
479
|
`apply ${apply}`,
|
|
275
480
|
];
|
|
276
|
-
emit(lines.join('\n')
|
|
481
|
+
emit(lines.join('\n'));
|
|
277
482
|
return EXIT.OK;
|
|
278
483
|
}
|
|
279
484
|
|
|
485
|
+
// The commit lands on the slice branch or not at all: finishing from `main`
|
|
486
|
+
// commits main's tree under the slice's subject, which is the defect this
|
|
487
|
+
// guard closes (review 0003 Q23).
|
|
488
|
+
const head = await git(root, ['rev-parse', '--abbrev-ref', 'HEAD']).catch(() => null);
|
|
489
|
+
if (head !== branch) {
|
|
490
|
+
return fail(
|
|
491
|
+
`not on the slice branch: HEAD is ${head ?? 'unknown'}, expected ${branch}`,
|
|
492
|
+
EXIT.USAGE,
|
|
493
|
+
`move to the slice worktree (${relative(main, worktree)}) and rerun: ${apply}`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
280
497
|
if (staged.length === 0) {
|
|
281
498
|
return fail(`${root} has nothing staged to commit`, EXIT.USAGE, `stage what the slice changed, then rerun: ${apply}`);
|
|
282
499
|
}
|
|
283
500
|
|
|
284
501
|
// The gates the hook would run, run here. This command is the one place a commit
|
|
285
502
|
// happens where a hook could not have stopped it, so it runs the same dispatcher
|
|
286
|
-
// over the same event, in the checkout that holds the staged diff
|
|
287
|
-
|
|
288
|
-
|
|
503
|
+
// over the same event, in the checkout that holds the staged diff — except the
|
|
504
|
+
// gates the ledger already proves for this tree, which are reported, not re-run.
|
|
505
|
+
const manifestForGates =
|
|
506
|
+
split.fresh.length === declared.length
|
|
507
|
+
? manifest.manifest
|
|
508
|
+
: { ...manifest.manifest, gates: { ...(manifest.manifest?.gates ?? {}), preCommit: split.fresh } };
|
|
509
|
+
const freshGates =
|
|
510
|
+
split.fresh.length === 0 && declared.length > 0
|
|
511
|
+
? { event: 'pre-commit', gates: [], failed: false, notes: [] }
|
|
512
|
+
: await runGates('pre-commit', { root, manifest: manifestForGates });
|
|
513
|
+
if (freshGates.error) return fail(freshGates.error, EXIT.USAGE);
|
|
514
|
+
|
|
515
|
+
// A gate that rewrites the tree (the formatter restaging) moves the commit
|
|
516
|
+
// off the tree the skipped gates passed on. Their evidence is stale, so they
|
|
517
|
+
// run once on the new tree; those results stand, the way fresh results do.
|
|
518
|
+
let skipped = split.skipped;
|
|
519
|
+
if (skipped.length > 0 && split.tree !== null) {
|
|
520
|
+
const afterTree = await indexTreeId(root).catch(() => null);
|
|
521
|
+
if (afterTree !== null && afterTree !== split.tree) {
|
|
522
|
+
const { entries } = await readLedger(root).catch(() => ({ entries: [] }));
|
|
523
|
+
const stale = skipped.filter((s) => passEntryForGate(entries, s.name, afterTree) === null);
|
|
524
|
+
if (stale.length > 0) {
|
|
525
|
+
const rerun = await runGates('pre-commit', {
|
|
526
|
+
root,
|
|
527
|
+
manifest: { ...manifest.manifest, gates: { ...(manifest.manifest?.gates ?? {}), preCommit: stale.map((s) => s.name) } },
|
|
528
|
+
});
|
|
529
|
+
if (rerun.error) return fail(rerun.error, EXIT.USAGE);
|
|
530
|
+
freshGates.gates.push(...rerun.gates);
|
|
531
|
+
if (rerun.failed) freshGates.failed = true;
|
|
532
|
+
for (const n of rerun.notes ?? []) freshGates.notes.push(n);
|
|
533
|
+
skipped = skipped.filter((s) => !stale.some((t) => t.name === s.name));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const byName = new Map(freshGates.gates.map((g) => [g.name, g]));
|
|
539
|
+
const combined = declared.map((name) => byName.get(name) ?? cachedResult(skipped.find((s) => s.name === name)));
|
|
540
|
+
const gates = { ...freshGates, gates: combined };
|
|
289
541
|
|
|
290
542
|
if (gates.failed) {
|
|
291
543
|
const failed = gates.gates.filter((g) => g.status === 'failed').map((g) => g.name).join(', ');
|
|
@@ -303,17 +555,18 @@ async function finishSlice(root, id, flags) {
|
|
|
303
555
|
return EXIT.FAIL;
|
|
304
556
|
}
|
|
305
557
|
for (const n of gates.notes ?? []) note(`note ${n}`);
|
|
306
|
-
emit(renderGateReport('pre-commit', declared, gates).join('\n')
|
|
558
|
+
emit(renderGateReport('pre-commit', declared, gates).join('\n'));
|
|
307
559
|
return EXIT.FAIL;
|
|
308
560
|
}
|
|
309
561
|
|
|
310
562
|
let sha = null;
|
|
563
|
+
const message = `${subject}\n\n${CO_AUTHOR}`;
|
|
311
564
|
try {
|
|
312
|
-
await git(root, ['commit', '-m',
|
|
565
|
+
await git(root, ['commit', '-m', message]);
|
|
313
566
|
sha = await git(root, ['rev-parse', 'HEAD']);
|
|
314
567
|
} catch (error) {
|
|
315
568
|
const detail = String(error.stderr ?? error.message ?? error).trim().split('\n')[0];
|
|
316
|
-
return fail(`the gates passed but the commit failed: ${detail}`, EXIT.FAIL, `run: git -C ${root} commit -m ${JSON.stringify(
|
|
569
|
+
return fail(`the gates passed but the commit failed: ${detail}`, EXIT.FAIL, `run: git -C ${root} commit -m ${JSON.stringify(message)}`);
|
|
317
570
|
}
|
|
318
571
|
|
|
319
572
|
const row = await planDone(main, id, { sha, test, dryRun: false });
|
|
@@ -321,6 +574,12 @@ async function finishSlice(root, id, flags) {
|
|
|
321
574
|
return fail(`${sha} is committed, but the tracker row could not record it: ${row.error}`, EXIT.FAIL, row.remedy ?? `run: ${CLI} plan status`);
|
|
322
575
|
}
|
|
323
576
|
|
|
577
|
+
// The slice is done: its lease goes with it. A failure here is a note — the
|
|
578
|
+
// commit exists, and a lease the TTL will reap is not a second failure.
|
|
579
|
+
const released = await releaseSlice(main, id, sliceSession(flags));
|
|
580
|
+
if (released.error) note(`note the slice lease could not be released (${released.error}); the TTL reaps it`);
|
|
581
|
+
|
|
582
|
+
for (const s of skipped) note(`note ${s.name}: already passed on this tree; not re-run`);
|
|
324
583
|
if (isJson(flags)) {
|
|
325
584
|
emitJson({
|
|
326
585
|
ok: true,
|
|
@@ -348,6 +607,117 @@ async function finishSlice(root, id, flags) {
|
|
|
348
607
|
];
|
|
349
608
|
if (row.evidenceRecorded) lines.push(`evidence ${sha}${test ? ` test: ${test}` : ''}`);
|
|
350
609
|
else if (row.evidenceNote) lines.push(`note ${row.evidenceNote}`);
|
|
351
|
-
emit(lines.join('\n')
|
|
610
|
+
emit(lines.join('\n'));
|
|
611
|
+
return EXIT.OK;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* `simt slice abandon <id>`: take the slice back down. The worktree is
|
|
616
|
+
* removed, the branch deleted, and the tracker row returns to the ready
|
|
617
|
+
* state — abandonment is a state the tracker can see, not a worktree that
|
|
618
|
+
* silently rots. Preview names everything that would be destroyed; `--yes`
|
|
619
|
+
* is the consent to destroy it (review 0003 Q23).
|
|
620
|
+
*/
|
|
621
|
+
async function abandonSlice(root, id, flags) {
|
|
622
|
+
const mode = writeMode(flags);
|
|
623
|
+
const main = await mainCheckout(root);
|
|
624
|
+
const resolved = await resolveSlice(main, id);
|
|
625
|
+
if (resolved.error) return fail(resolved.error, EXIT.USAGE, resolved.remedy);
|
|
626
|
+
const { doc } = resolved.slice;
|
|
627
|
+
const { branch, worktree } = places(main, id);
|
|
628
|
+
const session = sliceSession(flags);
|
|
629
|
+
const apply = `run: ${CLI} slice abandon ${id} --yes`;
|
|
630
|
+
|
|
631
|
+
const loaded = await loadFormat(main);
|
|
632
|
+
if (loaded.error) return fail(loaded.error, EXIT.USAGE, loaded.remedy);
|
|
633
|
+
const readyDisplay = loaded.format.canonical(loaded.format.ready) ?? 'Queued';
|
|
634
|
+
const preview = await planDone(main, id, { state: readyDisplay, dryRun: true });
|
|
635
|
+
if (preview.error) return fail(preview.error, EXIT.USAGE, preview.remedy ?? `run: ${CLI} plan status`);
|
|
636
|
+
|
|
637
|
+
if (!mode.apply) {
|
|
638
|
+
if (isJson(flags)) {
|
|
639
|
+
emitJson({
|
|
640
|
+
ok: true,
|
|
641
|
+
command: 'slice abandon',
|
|
642
|
+
root,
|
|
643
|
+
id,
|
|
644
|
+
dryRun: true,
|
|
645
|
+
branch,
|
|
646
|
+
worktree: relative(main, worktree),
|
|
647
|
+
state: { from: preview.from, to: preview.to, changed: preview.wouldChange },
|
|
648
|
+
remedy: apply,
|
|
649
|
+
});
|
|
650
|
+
return EXIT.OK;
|
|
651
|
+
}
|
|
652
|
+
const lines = [
|
|
653
|
+
`repo ${root}`,
|
|
654
|
+
`slice ${id} ${doc}`,
|
|
655
|
+
`remove ${relative(main, worktree)} and branch ${branch} (on apply)`,
|
|
656
|
+
`state ${preview.from} -> ${preview.to}`,
|
|
657
|
+
`apply ${apply}`,
|
|
658
|
+
];
|
|
659
|
+
emit(lines.join('\n'));
|
|
660
|
+
return EXIT.OK;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Abandoning from inside the worktree would pull the floor out: the shell's
|
|
664
|
+
// own directory is the thing being removed. Compared through git, not through
|
|
665
|
+
// strings, because the caller's spelling of the path may differ in symlinks.
|
|
666
|
+
const top = await git(root, ['rev-parse', '--show-toplevel']).catch(() => null);
|
|
667
|
+
if (top !== null && (top === worktree || top.startsWith(`${worktree}/`))) {
|
|
668
|
+
return fail(
|
|
669
|
+
`cannot abandon ${id} from inside its worktree`,
|
|
670
|
+
EXIT.USAGE,
|
|
671
|
+
`move to the main checkout (${main}) and rerun: ${apply}`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const removed = [];
|
|
676
|
+
try {
|
|
677
|
+
await git(main, ['worktree', 'remove', '--force', worktree]);
|
|
678
|
+
removed.push(`removed ${relative(main, worktree)}`);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
const detail = String(error.stderr ?? error.message ?? error).trim().split('\n')[0];
|
|
681
|
+
removed.push(`kept ${relative(main, worktree)} (${detail})`);
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
await git(main, ['branch', '-D', branch]);
|
|
685
|
+
removed.push(`deleted ${branch}`);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
const detail = String(error.stderr ?? error.message ?? error).trim().split('\n')[0];
|
|
688
|
+
removed.push(`kept ${branch} (${detail})`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const row = await planDone(main, id, { state: readyDisplay, dryRun: false });
|
|
692
|
+
if (row.error) {
|
|
693
|
+
return fail(
|
|
694
|
+
`the worktree is down, but the tracker row was not updated: ${row.error}`,
|
|
695
|
+
EXIT.FAIL,
|
|
696
|
+
row.remedy ?? `run: ${CLI} plan status`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const released = await releaseSlice(main, id, session);
|
|
701
|
+
const leaseLine =
|
|
702
|
+
released.released === true
|
|
703
|
+
? `released the slice lease`
|
|
704
|
+
: released.error
|
|
705
|
+
? `the slice lease could not be released (${released.error}); the TTL reaps it`
|
|
706
|
+
: 'no slice lease was held';
|
|
707
|
+
if (isJson(flags)) {
|
|
708
|
+
emitJson({
|
|
709
|
+
ok: true,
|
|
710
|
+
command: 'slice abandon',
|
|
711
|
+
root,
|
|
712
|
+
id,
|
|
713
|
+
branch,
|
|
714
|
+
worktree: relative(main, worktree),
|
|
715
|
+
lease: released.released === true ? { slice: id, session } : null,
|
|
716
|
+
state: { from: row.from, to: row.to, changed: row.changed },
|
|
717
|
+
});
|
|
718
|
+
return EXIT.OK;
|
|
719
|
+
}
|
|
720
|
+
const lines = [`repo ${root}`, `slice ${id} ${doc}`, ...removed, `state ${row.from} -> ${row.to}`, `lease ${leaseLine}`];
|
|
721
|
+
emit(lines.join('\n'));
|
|
352
722
|
return EXIT.OK;
|
|
353
723
|
}
|