dotmd-cli 0.87.1 → 0.88.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/README.md +14 -5
- package/bin/dotmd.mjs +45 -11
- package/package.json +1 -1
- package/src/atomic-mutation.mjs +8 -0
- package/src/baton.mjs +140 -49
- package/src/commands.mjs +1 -1
- package/src/decisions.mjs +1 -1
- package/src/lifecycle.mjs +26 -7
package/README.md
CHANGED
|
@@ -97,20 +97,21 @@ Restart Claude Code, or run `/reload-plugins`, after a plugin update.
|
|
|
97
97
|
```bash
|
|
98
98
|
runlist init # create config, docs/, and the generated index
|
|
99
99
|
runlist new plan auth-refresh # scaffold a typed document
|
|
100
|
-
runlist
|
|
101
|
-
runlist
|
|
100
|
+
runlist plans # compact live plan dashboard
|
|
101
|
+
runlist briefing # comprehensive active-work view
|
|
102
102
|
runlist check # validate schema, references, and lifecycle shape
|
|
103
103
|
runlist doctor # preview repairs; add --apply to write
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
`runlist
|
|
106
|
+
`runlist plans` is the compact orientation view. `runlist briefing` lists live
|
|
107
|
+
plans with next steps and grows with the corpus. `runlist context` is the fuller
|
|
107
108
|
human/LLM briefing, while `runlist agent-context` emits bounded structured JSON for
|
|
108
109
|
agent integrations.
|
|
109
110
|
|
|
110
111
|
## Core Workflow
|
|
111
112
|
|
|
112
113
|
```bash
|
|
113
|
-
runlist
|
|
114
|
+
runlist plans
|
|
114
115
|
runlist use docs/plans/auth-refresh.md
|
|
115
116
|
runlist set awaiting docs/plans/auth-refresh.md --note "Need API owner decision"
|
|
116
117
|
runlist set active docs/plans/auth-refresh.md --note "Decision received"
|
|
@@ -130,7 +131,15 @@ runlist baton @/tmp/resume.md
|
|
|
130
131
|
```
|
|
131
132
|
|
|
132
133
|
Baton refuses when a handoff for the same work is already pending, so one piece
|
|
133
|
-
of work never has two resume prompts.
|
|
134
|
+
of work never has two resume prompts. Inspect the pending handoff with
|
|
135
|
+
`runlist prompts show <slug>`. Keep it if current; if stale, pass `--replace`
|
|
136
|
+
with a new draft. Baton archives the previous text and refreshes the pending
|
|
137
|
+
prompt in the same operation as any plan release.
|
|
138
|
+
|
|
139
|
+
Repositories with a commit wrapper can export `batonCommitCommand(message, paths)`
|
|
140
|
+
from `dotmd.config.mjs` and return an argv array such as
|
|
141
|
+
`['just', 'commit', message, ...paths]`. Baton quotes the printed command and
|
|
142
|
+
excludes session prompts from `paths`.
|
|
134
143
|
|
|
135
144
|
Saved prompts are local session state. Consume them with `runlist use`; inspect
|
|
136
145
|
without consuming via `runlist prompts show`. Consuming a baton prompt also claims
|
package/bin/dotmd.mjs
CHANGED
|
@@ -676,10 +676,11 @@ Options:
|
|
|
676
676
|
--json Output as JSON ({ owned, prompts, errors, previousSelf,
|
|
677
677
|
fleet, recentRejections, misuseRecap, drift })`,
|
|
678
678
|
|
|
679
|
-
briefing: `runlist briefing —
|
|
679
|
+
briefing: `runlist briefing — comprehensive live-work summary
|
|
680
680
|
|
|
681
|
-
Shows plan
|
|
682
|
-
|
|
681
|
+
Shows every live plan with its next step, plus doc/research counts and health.
|
|
682
|
+
Output grows with the corpus; use \`runlist plans\` for compact orientation or
|
|
683
|
+
\`runlist agent-context\` for structured agent context.
|
|
683
684
|
|
|
684
685
|
Options:
|
|
685
686
|
--json Output as JSON`,
|
|
@@ -1270,13 +1271,21 @@ Options:
|
|
|
1270
1271
|
--note "why" Append the reason to ## Version History (plan mode only)
|
|
1271
1272
|
--message / --body Inline body (one-liners; prefer @path or stdin)
|
|
1272
1273
|
--force Recover another session's plan (explicit path required)
|
|
1274
|
+
--replace Replace exactly one pending handoff; archive its prior text
|
|
1273
1275
|
--json Structured repository/session/generated file result
|
|
1274
1276
|
--dry-run, -n Preview without writing
|
|
1275
1277
|
|
|
1278
|
+
Repo-specific commit hint (dotmd.config.mjs):
|
|
1279
|
+
export function batonCommitCommand(message, paths) {
|
|
1280
|
+
return ['just', 'commit', message, ...paths];
|
|
1281
|
+
}
|
|
1282
|
+
The function returns argv; baton shell-quotes each argument before printing.
|
|
1283
|
+
|
|
1276
1284
|
Examples:
|
|
1277
1285
|
runlist baton @/tmp/draft.md
|
|
1278
1286
|
runlist baton checkout-fixes @/tmp/draft.md
|
|
1279
1287
|
runlist baton docs/plans/auth.md @/tmp/draft.md
|
|
1288
|
+
runlist baton docs/plans/auth.md @/tmp/new-draft.md --replace
|
|
1280
1289
|
runlist baton --status paused --note "blocked on review" @/tmp/d.md
|
|
1281
1290
|
cat /tmp/draft.md | runlist baton
|
|
1282
1291
|
|
|
@@ -1295,11 +1304,13 @@ cooperating transaction:
|
|
|
1295
1304
|
grant ownership. A live pickup-hook delivery lease blocks release and force
|
|
1296
1305
|
takeover; hooks are at-least-once and deduplicate the stable operationId.
|
|
1297
1306
|
|
|
1298
|
-
Slug mode (no plan involved) saves resume-<slug>
|
|
1299
|
-
|
|
1307
|
+
Slug mode (no plan involved) saves resume-<slug> without a status change or
|
|
1308
|
+
commit. A bare word that names a plan is treated as that plan.
|
|
1300
1309
|
|
|
1301
1310
|
Baton saves nothing while a handoff for the same work is pending (resume-<name>,
|
|
1302
|
-
or a pending prompt linked to the plan):
|
|
1311
|
+
or a pending prompt linked to the plan): inspect it with \`runlist prompts show\`.
|
|
1312
|
+
Keep it if current; use \`--replace\` with a new draft if stale. Replacement
|
|
1313
|
+
preserves the prior text under archived/ and refuses multiple matches.
|
|
1303
1314
|
With no @file, \`-\` or --message, baton reads stdin only when something is piped
|
|
1304
1315
|
in; an open pipe that sends nothing is given up on after a moment.`,
|
|
1305
1316
|
|
|
@@ -1983,7 +1994,15 @@ async function main() {
|
|
|
1983
1994
|
if (command === 'glossary') { const { runGlossary } = await import('../src/glossary.mjs'); runGlossary(restArgs, config); return; }
|
|
1984
1995
|
if (command === 'model') { const { runModel } = await import('../src/model.mjs'); await runModel(restArgs, config); return; }
|
|
1985
1996
|
if (command === 'show') { const { runShow } = await import('../src/show.mjs'); runShow(restArgs, config); return; }
|
|
1986
|
-
if (command === 'decisions') {
|
|
1997
|
+
if (command === 'decisions') {
|
|
1998
|
+
const { runDecisions } = await import('../src/decisions.mjs');
|
|
1999
|
+
const result = runDecisions(restArgs, config);
|
|
2000
|
+
if (result?.defects?.length) {
|
|
2001
|
+
const first = result.defects[0];
|
|
2002
|
+
_exitFailureMessage = `${result.defects.length} decision defect(s); first: ${first.doc}:${first.line} ${first.message}`;
|
|
2003
|
+
}
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
1987
2006
|
if (command === 'export') { const { runExport } = await import('../src/export.mjs'); runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
|
|
1988
2007
|
|
|
1989
2008
|
// Lifecycle commands
|
|
@@ -2175,7 +2194,10 @@ async function main() {
|
|
|
2175
2194
|
writeCheckPreviewNote();
|
|
2176
2195
|
process.stdout.write('\n' + renderCheck(freshIndex, config, { errorsOnly, noCollapse, verbose }));
|
|
2177
2196
|
}
|
|
2178
|
-
if (freshIndex.errors.length > 0)
|
|
2197
|
+
if (freshIndex.errors.length > 0) {
|
|
2198
|
+
process.exitCode = 1;
|
|
2199
|
+
_exitFailureMessage = checkFailureSummary(freshIndex.errors);
|
|
2200
|
+
}
|
|
2179
2201
|
return;
|
|
2180
2202
|
}
|
|
2181
2203
|
|
|
@@ -2185,13 +2207,19 @@ async function main() {
|
|
|
2185
2207
|
|
|
2186
2208
|
if (args.includes('--json')) {
|
|
2187
2209
|
process.stdout.write(JSON.stringify(checkJson(index), null, 2) + '\n');
|
|
2188
|
-
if (index.errors.length > 0)
|
|
2210
|
+
if (index.errors.length > 0) {
|
|
2211
|
+
process.exitCode = 1;
|
|
2212
|
+
_exitFailureMessage = checkFailureSummary(index.errors);
|
|
2213
|
+
}
|
|
2189
2214
|
return;
|
|
2190
2215
|
}
|
|
2191
2216
|
|
|
2192
2217
|
writeCheckPreviewNote();
|
|
2193
2218
|
process.stdout.write(renderCheck(index, config, { errorsOnly, noCollapse, verbose }));
|
|
2194
|
-
if (index.errors.length > 0)
|
|
2219
|
+
if (index.errors.length > 0) {
|
|
2220
|
+
process.exitCode = 1;
|
|
2221
|
+
_exitFailureMessage = checkFailureSummary(index.errors);
|
|
2222
|
+
}
|
|
2195
2223
|
return;
|
|
2196
2224
|
}
|
|
2197
2225
|
|
|
@@ -2416,9 +2444,15 @@ async function main() {
|
|
|
2416
2444
|
let _resolvedConfig = null;
|
|
2417
2445
|
let _resolvedCommand = null;
|
|
2418
2446
|
let _suppressObservability = false;
|
|
2447
|
+
let _exitFailureMessage = null;
|
|
2419
2448
|
const _startMs = Date.now();
|
|
2420
2449
|
const _invocationArgs = process.argv.slice(2);
|
|
2421
2450
|
|
|
2451
|
+
function checkFailureSummary(errors) {
|
|
2452
|
+
const first = errors[0];
|
|
2453
|
+
return `${errors.length} check error(s); first: ${first.path ? `${first.path}: ` : ''}${first.message}`;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2422
2456
|
function _journalExit(err) {
|
|
2423
2457
|
if (_suppressObservability || _resolvedCommand === 'hud' || _invocationArgs.includes('--dry-run') || _invocationArgs.includes('-n')) return;
|
|
2424
2458
|
try {
|
|
@@ -2433,7 +2467,7 @@ function _journalExit(err) {
|
|
|
2433
2467
|
// A command that reports its own failure through the exit code (check with
|
|
2434
2468
|
// errors, a failed update step) is a failure too.
|
|
2435
2469
|
const code = Number(process.exitCode ?? 0);
|
|
2436
|
-
const failure = err ?? (code !== 0 ? { name: 'ExitStatus', message: `exited with status ${code}` } : null);
|
|
2470
|
+
const failure = err ?? (code !== 0 ? { name: 'ExitStatus', message: _exitFailureMessage ?? `exited with status ${code}` } : null);
|
|
2437
2471
|
if (failure) {
|
|
2438
2472
|
try {
|
|
2439
2473
|
recordGlobalError({
|
package/package.json
CHANGED
package/src/atomic-mutation.mjs
CHANGED
|
@@ -1310,6 +1310,10 @@ export function moveFileAtomic(sourcePath, targetPath, render, options) {
|
|
|
1310
1310
|
// transaction manifest exists, so a guard conflict can never leave a
|
|
1311
1311
|
// transaction to recover from.
|
|
1312
1312
|
for (const guard of guards) {
|
|
1313
|
+
if (guard.absent) {
|
|
1314
|
+
if (existsSync(guard.path)) throw new MutationConflictError(`File appeared while the move mutation set was being prepared: ${path.resolve(guard.path)}`);
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1313
1317
|
const snapshot = snapshotFile(guard.path);
|
|
1314
1318
|
if (guard.expectedContent !== undefined && snapshot.content !== guard.expectedContent) {
|
|
1315
1319
|
throw new MutationConflictError(`File changed while the move mutation set was being prepared: ${snapshot.path}`);
|
|
@@ -1657,6 +1661,10 @@ export function mutateFileSet({ updates = [], creations = [], guards = [] }, opt
|
|
|
1657
1661
|
// is created — a guard conflict must leave the tree byte-identical, not even
|
|
1658
1662
|
// an empty directory behind.
|
|
1659
1663
|
for (const guard of guards) {
|
|
1664
|
+
if (guard.absent) {
|
|
1665
|
+
if (existsSync(guard.path)) throw new MutationConflictError(`File appeared while the mutation set was being prepared: ${path.resolve(guard.path)}`);
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1660
1668
|
const snapshot = snapshotFile(guard.path);
|
|
1661
1669
|
if (guard.expectedContent !== undefined && snapshot.content !== guard.expectedContent) {
|
|
1662
1670
|
throw new MutationConflictError(`File changed while the mutation set was being prepared: ${snapshot.path}`);
|
package/src/baton.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { readFileSync, existsSync,
|
|
1
|
+
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
4
|
-
import { asString, toRepoPath, die, warn, isArchivedPath, resolveRefPath } from './util.mjs';
|
|
4
|
+
import { asString, toRepoPath, die, warn, isArchivedPath, resolveRefPath, nowIso } from './util.mjs';
|
|
5
5
|
import { buildIndex, resolveDocArg } from './index.mjs';
|
|
6
6
|
import { preparePromptDocument, runNew, readBodyInput, readPipedBodyInput } from './new.mjs';
|
|
7
|
-
import { ensurePlanCompletionBeforeRelease, planHasPendingCompletion, runSet } from './lifecycle.mjs';
|
|
7
|
+
import { ensurePlanCompletionBeforeRelease, planHasPendingCompletion, renderLifecycleMutation, runSet } from './lifecycle.mjs';
|
|
8
8
|
import { green, dim } from './color.mjs';
|
|
9
|
-
import { authorizeManagedSource } from './managed-path.mjs';
|
|
9
|
+
import { authorizeManagedDestination, authorizeManagedSource } from './managed-path.mjs';
|
|
10
|
+
import { mutateFileSet } from './atomic-mutation.mjs';
|
|
10
11
|
import { assertPlanMutationAuthorized, authoritativeSessionId, listOwnedPlans, readPlanOwnership } from './pickup.mjs';
|
|
11
12
|
|
|
12
13
|
// `dotmd baton` is the one-command handoff: save the resume prompt AND release
|
|
@@ -56,7 +57,8 @@ function pendingHandoffs(promptPath, planPath, config) {
|
|
|
56
57
|
try { planRef = asString(parseSimpleFrontmatter(extractFrontmatter(readFileSync(abs, 'utf8')).frontmatter).plan); }
|
|
57
58
|
catch { continue; }
|
|
58
59
|
const linked = planRef ? resolveRefPath(planRef, path.dirname(abs), config.repoRoot) : null;
|
|
59
|
-
if (linked && realpathSync(linked) === planReal) found.add(doc.path);
|
|
60
|
+
try { if (linked && realpathSync(linked) === planReal) found.add(doc.path); }
|
|
61
|
+
catch { /* stale plan link is not evidence this prompt belongs to the plan */ }
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
return [...found];
|
|
@@ -65,7 +67,36 @@ function pendingHandoffs(promptPath, planPath, config) {
|
|
|
65
67
|
function refusePendingHandoff(pending) {
|
|
66
68
|
const lines = pending.map(p => ` ${p}`).join('\n');
|
|
67
69
|
const slug = path.basename(pending[0], '.md');
|
|
68
|
-
die(`Nothing saved: a handoff for this is already pending:\n${lines}\
|
|
70
|
+
die(`Nothing saved: a handoff for this is already pending:\n${lines}\nInspect it with \`runlist prompts show ${slug}\`. If it is current, keep it and do not hand off again. If it is stale, re-run baton with \`--replace\` to preserve the old handoff under archived/.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function archivedCopyPath(promptPath, config) {
|
|
74
|
+
const dir = path.join(path.dirname(promptPath), config.archiveDir);
|
|
75
|
+
const ext = path.extname(promptPath);
|
|
76
|
+
const stem = path.basename(promptPath, ext);
|
|
77
|
+
let candidate = path.join(dir, `${stem}${ext}`);
|
|
78
|
+
for (let n = 2; existsSync(candidate); n++) candidate = path.join(dir, `${stem}-${n}${ext}`);
|
|
79
|
+
return authorizeManagedDestination(candidate, config, { kind: 'Baton replacement archive' }).path;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function shellQuote(value) {
|
|
83
|
+
const raw = String(value);
|
|
84
|
+
return /^[A-Za-z0-9_./:@+-]+$/.test(raw) ? raw : `"${raw.replace(/["\\$`]/g, '\\$&')}"`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function commitCommand(config, message, paths) {
|
|
88
|
+
const fallback = ['git', 'commit', '-m', message, '--', ...paths];
|
|
89
|
+
let argv = fallback;
|
|
90
|
+
if (typeof config.hooks.batonCommitCommand === 'function') {
|
|
91
|
+
try {
|
|
92
|
+
const custom = config.hooks.batonCommitCommand(message, [...paths]);
|
|
93
|
+
if (!Array.isArray(custom) || custom.length === 0 || !custom.every(part => typeof part === 'string' && part.length > 0)) {
|
|
94
|
+
throw new Error('return a nonempty string argv array');
|
|
95
|
+
}
|
|
96
|
+
argv = custom;
|
|
97
|
+
} catch (err) { warn(`batonCommitCommand failed (${err.message}); printing the default git command.`); }
|
|
98
|
+
}
|
|
99
|
+
return argv.map(shellQuote).join(' ');
|
|
69
100
|
}
|
|
70
101
|
|
|
71
102
|
// Is this positional a filesystem reference (must resolve, typos die) or a
|
|
@@ -83,6 +114,7 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
83
114
|
let note = null;
|
|
84
115
|
let bodyFlag = null;
|
|
85
116
|
let force = false;
|
|
117
|
+
let replace = false;
|
|
86
118
|
const positionals = [];
|
|
87
119
|
for (let i = 0; i < argv.length; i++) {
|
|
88
120
|
const a = argv[i];
|
|
@@ -90,6 +122,7 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
90
122
|
if (a === '--note' && argv[i + 1]) { note = argv[++i]; continue; }
|
|
91
123
|
if ((a === '--body' || a === '--message') && argv[i + 1]) { bodyFlag = argv[++i]; continue; }
|
|
92
124
|
if (a === '--force') { force = true; continue; }
|
|
125
|
+
if (a === '--replace') { replace = true; continue; }
|
|
93
126
|
if (a === '--json') continue;
|
|
94
127
|
if (!a.startsWith('-') || a === '-' || a.startsWith('@')) { positionals.push(a); continue; }
|
|
95
128
|
die(`Unknown flag for \`runlist baton\`: ${a}`);
|
|
@@ -152,16 +185,45 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
152
185
|
|
|
153
186
|
const nameBase = planPath ? path.basename(planPath, '.md') : promptSlug;
|
|
154
187
|
const slugBase = nameBase.startsWith('resume-') ? nameBase : `resume-${nameBase}`;
|
|
155
|
-
|
|
188
|
+
let replacement = null;
|
|
189
|
+
const resolvePending = () => {
|
|
156
190
|
const target = preparePromptDocument(slugBase, body, config, { dryRun: true });
|
|
157
191
|
const pending = pendingHandoffs(target.filePath, planPath, config);
|
|
158
|
-
if (
|
|
192
|
+
if (!replace) {
|
|
193
|
+
if (pending.length) refusePendingHandoff(pending);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (pending.length !== 1) {
|
|
197
|
+
const detail = pending.length ? `Found ${pending.length}:\n${pending.map(p => ` ${p}`).join('\n')}` : 'No pending handoff matches this work.';
|
|
198
|
+
die(`Nothing replaced: --replace requires exactly one pending handoff. ${detail}`);
|
|
199
|
+
}
|
|
200
|
+
const oldRepoPath = pending[0];
|
|
201
|
+
const oldPath = authorizeManagedSource(path.resolve(config.repoRoot, oldRepoPath), config, { kind: 'Baton replacement source' }).path;
|
|
202
|
+
const oldRaw = readFileSync(oldPath, 'utf8');
|
|
203
|
+
const { frontmatter, body: oldBody } = extractFrontmatter(oldRaw);
|
|
204
|
+
const fm = parseSimpleFrontmatter(frontmatter);
|
|
205
|
+
if (asString(fm.type) !== 'prompt' || asString(fm.status) !== 'pending') {
|
|
206
|
+
die(`Nothing replaced: ${oldRepoPath} is not a pending prompt.`);
|
|
207
|
+
}
|
|
208
|
+
const priorPlanRef = asString(fm.plan);
|
|
209
|
+
const priorPlanPath = priorPlanRef ? resolveRefPath(priorPlanRef, path.dirname(oldPath), config.repoRoot) : null;
|
|
210
|
+
if (!planPath && priorPlanRef) {
|
|
211
|
+
die(`Nothing replaced: ${oldRepoPath} links a plan (${priorPlanRef}). Use \`runlist baton <plan-file> @<draft-file> --replace\` so the plan link and release stay together.`);
|
|
212
|
+
}
|
|
213
|
+
replacement = {
|
|
214
|
+
oldPath, oldRepoPath, oldRaw,
|
|
215
|
+
archivedPath: archivedCopyPath(oldPath, config),
|
|
216
|
+
alreadyCurrent: oldBody.trim() === body.trim()
|
|
217
|
+
&& (!planPath || (priorPlanPath && path.resolve(priorPlanPath) === path.resolve(planPath))),
|
|
218
|
+
canonicalPath: target.filePath,
|
|
219
|
+
};
|
|
159
220
|
};
|
|
160
221
|
|
|
161
222
|
let repoPath = null;
|
|
162
223
|
let oldStatus = null;
|
|
163
224
|
let ownershipPath = null;
|
|
164
225
|
let planGuard = null;
|
|
226
|
+
let wouldReleaseClaim = false;
|
|
165
227
|
if (planPath) {
|
|
166
228
|
planPath = authorizeManagedSource(planPath, config, { kind: 'Baton plan source' }).path;
|
|
167
229
|
repoPath = toRepoPath(planPath, config.repoRoot);
|
|
@@ -187,6 +249,7 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
187
249
|
const sessionId = authoritativeSessionId();
|
|
188
250
|
const ownership = assertPlanMutationAuthorized(repoPath, config, { sessionId, force });
|
|
189
251
|
const ownedHere = ownership?.state === 'owned' && ownership.sessionId === sessionId;
|
|
252
|
+
wouldReleaseClaim = ownership?.state === 'owned';
|
|
190
253
|
// Baton's release only means something when there is a claim to release. A
|
|
191
254
|
// plan that is neither in-session nor owned here carries a status someone
|
|
192
255
|
// chose on purpose (`awaiting`, `blocked`, a repo's own `awaiting-testing`),
|
|
@@ -208,16 +271,36 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
208
271
|
if (note) warn('--note ignored — the plan\'s status is unchanged, so there is no transition to record.');
|
|
209
272
|
}
|
|
210
273
|
// Before the plan-completion step: a refusal must leave nothing changed.
|
|
211
|
-
|
|
274
|
+
resolvePending();
|
|
275
|
+
// A status transition may file the plan at a new path. Refreshing the
|
|
276
|
+
// prompt inside that transaction keeps its plan link valid even when its
|
|
277
|
+
// body text happened to be identical to the previous handoff.
|
|
278
|
+
if (replacement && status !== oldStatus) replacement.alreadyCurrent = false;
|
|
212
279
|
ownershipPath = readPlanOwnership(repoPath, config)?.recordPath ?? null;
|
|
213
280
|
if (!dryRun) ensurePlanCompletionBeforeRelease(repoPath, config, { testHooks: opts.testHooks });
|
|
214
281
|
else if (planHasPendingCompletion(repoPath, config)) process.stderr.write(`${dim('[dry-run]')} Pending claim completion would block this release.\n`);
|
|
215
282
|
} else {
|
|
216
|
-
|
|
283
|
+
resolvePending();
|
|
217
284
|
if (statusFlag) warn(`--status ignored — no plan involved in this handoff (saving the prompt only).`);
|
|
218
285
|
if (note) warn(`--note ignored — no plan involved in this handoff (notes land in a plan's Version History).`);
|
|
219
286
|
}
|
|
220
287
|
|
|
288
|
+
const preparedReplacement = prepared => {
|
|
289
|
+
if (!replacement || replacement.alreadyCurrent) return { updates: [], creations: [], guards: [] };
|
|
290
|
+
let archivedContent = renderLifecycleMutation(replacement.oldRaw,
|
|
291
|
+
{ status: 'archived', updated: nowIso() }, () => 'Archived.');
|
|
292
|
+
if (repoPath) archivedContent = archivedContent.replace(/^plan:[ \t]*\S.*$/m, `plan: ${repoPath}`);
|
|
293
|
+
return {
|
|
294
|
+
updates: [{ path: replacement.oldPath, expectedContent: replacement.oldRaw, content: prepared.content }],
|
|
295
|
+
creations: [{ path: replacement.archivedPath, content: archivedContent }],
|
|
296
|
+
guards: path.resolve(replacement.canonicalPath) === path.resolve(replacement.oldPath)
|
|
297
|
+
? [] : [{ path: replacement.canonicalPath, absent: true }],
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
if (dryRun && !json && replacement && !replacement.alreadyCurrent) {
|
|
301
|
+
process.stdout.write(`${dim('[dry-run]')} Would preserve old handoff: ${replacement.oldRepoPath} → ${toRepoPath(replacement.archivedPath, config.repoRoot)}\n`);
|
|
302
|
+
}
|
|
303
|
+
|
|
221
304
|
// Plan mode publishes the already-stamped prompt, status/history update, and
|
|
222
305
|
// ownership release in one transaction. Slug mode has no plan transaction.
|
|
223
306
|
let createdSlug = null;
|
|
@@ -230,29 +313,39 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
230
313
|
if (json) process.stdout.write = chunk => { muted.push(String(chunk)); return true; };
|
|
231
314
|
try {
|
|
232
315
|
if (!planPath) {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
316
|
+
const promptName = replacement ? path.basename(replacement.oldPath, '.md') : slugBase;
|
|
317
|
+
const prepared = preparePromptDocument(promptName, body, config, { dryRun });
|
|
318
|
+
if (replacement) {
|
|
319
|
+
const mutation = preparedReplacement(prepared);
|
|
320
|
+
if (!dryRun && !replacement.alreadyCurrent) mutateFileSet(mutation, { repoRoot: config.repoRoot, testHooks: opts.testHooks });
|
|
321
|
+
newResult = { sessionFiles: [replacement.oldRepoPath, ...(replacement.alreadyCurrent ? [] : [toRepoPath(replacement.archivedPath, config.repoRoot)])] };
|
|
322
|
+
} else {
|
|
323
|
+
try {
|
|
324
|
+
newResult = await runNew(['prompt', slugBase, '--body', body], config, { dryRun, deferIndex: true });
|
|
325
|
+
} catch (err) {
|
|
326
|
+
// Lost a race with another baton between the pending check and the write.
|
|
327
|
+
if (/File already exists/.test(String(err?.message))) refusePendingHandoff([prepared.repoPath]);
|
|
328
|
+
throw err;
|
|
329
|
+
}
|
|
240
330
|
}
|
|
241
|
-
createdSlug =
|
|
242
|
-
promptRepoPath = prepared.repoPath;
|
|
331
|
+
createdSlug = promptName;
|
|
332
|
+
promptRepoPath = replacement?.oldRepoPath ?? prepared.repoPath;
|
|
243
333
|
} else {
|
|
244
|
-
const
|
|
334
|
+
const promptName = replacement ? path.basename(replacement.oldPath, '.md') : slugBase;
|
|
335
|
+
const prepared = preparePromptDocument(promptName, body, config, { plan: repoPath, dryRun });
|
|
336
|
+
const mutation = preparedReplacement(prepared);
|
|
245
337
|
const setArgs = [status, planPath];
|
|
246
338
|
if (force) setArgs.push('--force');
|
|
247
339
|
if (note && !planGuard) setArgs.push('--note', note);
|
|
248
340
|
try {
|
|
249
|
-
if (dryRun) process.stdout.write(`${dim('[dry-run]')} Would create: ${prepared.repoPath}\n`);
|
|
341
|
+
if (dryRun) process.stdout.write(`${dim('[dry-run]')} Would ${replacement ? (replacement.alreadyCurrent ? 'keep' : 'replace') : 'create'}: ${replacement?.oldRepoPath ?? prepared.repoPath}\n`);
|
|
250
342
|
archiveResult = await runSet(setArgs, config, {
|
|
251
343
|
dryRun,
|
|
252
344
|
viaBaton: true,
|
|
253
345
|
testHooks: opts.testHooks,
|
|
254
|
-
|
|
255
|
-
|
|
346
|
+
additionalUpdates: dryRun ? [] : mutation.updates,
|
|
347
|
+
creations: dryRun ? [] : replacement ? mutation.creations : [{ path: prepared.filePath, content: prepared.content }],
|
|
348
|
+
guards: dryRun ? [] : [...mutation.guards, ...(planGuard ? [planGuard] : [])],
|
|
256
349
|
deferIndex: true,
|
|
257
350
|
});
|
|
258
351
|
} catch (err) {
|
|
@@ -260,9 +353,9 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
260
353
|
throw err;
|
|
261
354
|
}
|
|
262
355
|
createdSlug = prepared.slug;
|
|
263
|
-
promptRepoPath = prepared.repoPath;
|
|
356
|
+
promptRepoPath = replacement?.oldRepoPath ?? prepared.repoPath;
|
|
264
357
|
statusChanged = oldStatus !== status;
|
|
265
|
-
if (!dryRun) {
|
|
358
|
+
if (!dryRun && !replacement) {
|
|
266
359
|
try { config.hooks.onNew?.({ path: prepared.repoPath, status: 'pending', title: prepared.slug, type: 'prompt' }); }
|
|
267
360
|
catch (err) { warn(`Hook 'onNew' threw: ${err.message}`); }
|
|
268
361
|
}
|
|
@@ -271,24 +364,12 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
271
364
|
if (json) process.stdout.write = originalStdoutWrite;
|
|
272
365
|
}
|
|
273
366
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (!dryRun && promptRepoPath && archiveResult?.newRepoPath && archiveResult.newRepoPath !== repoPath) {
|
|
281
|
-
const promptPath = path.join(config.repoRoot, promptRepoPath);
|
|
282
|
-
try {
|
|
283
|
-
const { frontmatter, body } = extractFrontmatter(readFileSync(promptPath, 'utf8'));
|
|
284
|
-
// Rewritten in place rather than through replaceFrontmatterField, which
|
|
285
|
-
// always emits a folded block scalar — right for prose fields, wrong for
|
|
286
|
-
// a path every other prompt carries on one line. Baton wrote this line
|
|
287
|
-
// itself moments ago, so the single-line form is guaranteed.
|
|
288
|
-
const rewritten = frontmatter.replace(/^plan:[ \t]*\S.*$/m, `plan: ${archiveResult.newRepoPath}`);
|
|
289
|
-
if (rewritten !== frontmatter) writeFileSync(promptPath, `---\n${rewritten}\n---\n${body}`, 'utf8');
|
|
290
|
-
}
|
|
291
|
-
catch (err) { warn(`Saved the prompt, but could not repoint its plan link to ${archiveResult.newRepoPath}: ${err.message}`); }
|
|
367
|
+
if (!dryRun && replacement && !replacement.alreadyCurrent) {
|
|
368
|
+
const archivedRepoPath = toRepoPath(replacement.archivedPath, config.repoRoot);
|
|
369
|
+
try { config.hooks.onArchive?.({ path: archivedRepoPath, oldStatus: 'pending' }, { oldPath: replacement.oldRepoPath, newPath: archivedRepoPath }); }
|
|
370
|
+
catch (err) { warn(`Hook 'onArchive' threw: ${err.message}`); }
|
|
371
|
+
try { config.hooks.onNew?.({ path: replacement.oldRepoPath, status: 'pending', title: createdSlug, type: 'prompt' }); }
|
|
372
|
+
catch (err) { warn(`Hook 'onNew' threw: ${err.message}`); }
|
|
292
373
|
}
|
|
293
374
|
|
|
294
375
|
const normalizeRepoPath = candidate => {
|
|
@@ -297,13 +378,14 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
297
378
|
};
|
|
298
379
|
const touched = (archiveResult?.touched ?? (planPath && statusChanged ? [repoPath] : [])).map(normalizeRepoPath);
|
|
299
380
|
const ownershipRepoPath = normalizeRepoPath(ownershipPath);
|
|
300
|
-
const
|
|
301
|
-
const
|
|
381
|
+
const archivedPromptRepoPath = replacement && !replacement.alreadyCurrent ? toRepoPath(replacement.archivedPath, config.repoRoot) : null;
|
|
382
|
+
const repositoryFiles = [...new Set(touched.filter(candidate => candidate && candidate !== ownershipRepoPath && candidate !== promptRepoPath && candidate !== archivedPromptRepoPath && candidate !== normalizeRepoPath(config.indexPath)))];
|
|
383
|
+
const sessionFiles = [...new Set([...(newResult?.sessionFiles ?? []), promptRepoPath, archivedPromptRepoPath, ownershipRepoPath].filter(Boolean))];
|
|
302
384
|
const deferredGeneratedFiles = [...new Set(newResult?.deferredGeneratedFiles ?? (config.indexPath ? [normalizeRepoPath(config.indexPath)] : []))];
|
|
303
385
|
const operationResult = {
|
|
304
386
|
operation: 'baton',
|
|
305
387
|
dryRun: Boolean(dryRun),
|
|
306
|
-
disposition: dryRun ? 'would-change' : 'applied',
|
|
388
|
+
disposition: replacement?.alreadyCurrent && !statusChanged && !wouldReleaseClaim ? 'already-current' : dryRun ? 'would-change' : 'applied',
|
|
307
389
|
wouldChange: Boolean(dryRun),
|
|
308
390
|
mode: planPath ? 'plan' : 'slug',
|
|
309
391
|
status: planPath ? { from: oldStatus, to: status, changed: statusChanged } : null,
|
|
@@ -313,6 +395,14 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
313
395
|
deferredGeneratedFiles,
|
|
314
396
|
prompt: promptRepoPath,
|
|
315
397
|
plan: archiveResult?.newRepoPath ?? repoPath,
|
|
398
|
+
planMovement: planPath ? { from: repoPath, to: archiveResult?.newRepoPath ?? repoPath } : null,
|
|
399
|
+
replacement: replacement ? {
|
|
400
|
+
previousPrompt: replacement.oldRepoPath,
|
|
401
|
+
archivedPrompt: archivedPromptRepoPath,
|
|
402
|
+
pendingPrompt: promptRepoPath,
|
|
403
|
+
alreadyCurrent: replacement.alreadyCurrent,
|
|
404
|
+
} : null,
|
|
405
|
+
claimRelease: planPath ? { wouldRelease: wouldReleaseClaim, released: !dryRun && wouldReleaseClaim } : null,
|
|
316
406
|
};
|
|
317
407
|
|
|
318
408
|
const prefix = dryRun ? dim('[dry-run] ') : '';
|
|
@@ -320,13 +410,14 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
320
410
|
process.stdout.write(JSON.stringify(operationResult, null, 2) + '\n');
|
|
321
411
|
return operationResult;
|
|
322
412
|
}
|
|
323
|
-
|
|
413
|
+
const verdict = replacement?.alreadyCurrent ? '✓ Handoff kept' : replacement ? '✓ Baton replaced' : '✓ Baton passed';
|
|
414
|
+
process.stderr.write(`\n${prefix}${green(verdict)}: ${createdSlug} (the next session's hud surfaces it — nothing to paste into chat)\n`);
|
|
415
|
+
if (archivedPromptRepoPath) process.stderr.write(dim(`${prefix}Previous handoff preserved: ${archivedPromptRepoPath}\n`));
|
|
324
416
|
if (planGuard) {
|
|
325
417
|
process.stderr.write(dim(`${repoPath} left at \`${oldStatus}\` — it was not in-session, so there was no claim to release.\n`));
|
|
326
418
|
process.stderr.write(dim(`Meant to change it? runlist set <status> ${repoPath}\n`));
|
|
327
419
|
}
|
|
328
420
|
if (statusChanged) {
|
|
329
|
-
const pathspec = operationResult.repositoryFiles.join(' ');
|
|
330
421
|
let gitignored = false;
|
|
331
422
|
try {
|
|
332
423
|
const { isGitIgnored } = await import('./git.mjs');
|
|
@@ -336,7 +427,7 @@ export async function runBaton(argv, config, opts = {}) {
|
|
|
336
427
|
process.stderr.write(dim(`${repoPath} is gitignored — no commit needed.\n`));
|
|
337
428
|
} else {
|
|
338
429
|
process.stderr.write(`${prefix}Commit the repository files (session files stay OUT of the pathspec):\n`);
|
|
339
|
-
process.stderr.write(`${prefix}
|
|
430
|
+
process.stderr.write(`${prefix} ${commitCommand(config, `baton: ${path.basename(planPath, '.md')} ${oldStatus} → ${status}`, operationResult.repositoryFiles)}\n`);
|
|
340
431
|
}
|
|
341
432
|
}
|
|
342
433
|
if (operationResult.deferredGeneratedFiles.length) process.stderr.write(dim(`Generated index deferred: ${operationResult.deferredGeneratedFiles.join(', ')}\n`));
|
package/src/commands.mjs
CHANGED
|
@@ -123,7 +123,7 @@ const definitions = [
|
|
|
123
123
|
], { aliases: ['prompt'] }),
|
|
124
124
|
command('use', mutates('managed source when starting/consuming; docs remain read-only'), 'workflow', [form('[file]', { args: positionals(0, 1), options: [flag('--json'), flag('--full'), flag('--no-index'), flag('--show-files'), flag('--force'), flag('--no-claim')] })]),
|
|
125
125
|
command('next', mutates('managed prompt source and same-root archive destination'), 'workflow', [form('', { options: [flag('--json'), flag('--no-index'), flag('--show-files'), flag('--force'), flag('--no-claim')] })]),
|
|
126
|
-
command('baton', mutates('managed plan/prompt sources and managed prompt destination'), 'workflow', [form('[plan|slug] <@<file>|->', { args: positionals(0, 2), options: [value('--status'), value('--note'), value('--body', '--message'), flag('--force'), flag('--json')] })]),
|
|
126
|
+
command('baton', mutates('managed plan/prompt sources and managed prompt destination'), 'workflow', [form('[plan|slug] <@<file>|->', { args: positionals(0, 2), options: [value('--status'), value('--note'), value('--body', '--message'), flag('--force'), flag('--replace'), flag('--json')] })]),
|
|
127
127
|
command('runlist', mutates('managed hubs/children and managed scaffold destinations'), 'workflow', [
|
|
128
128
|
form('<hub>', { args: positionals(1, 1), options: [flag('--json')] }),
|
|
129
129
|
form('next <hub>', { subcommands: ['next'], args: positionals(1, 1), options: [flag('--json'), flag('--full'), flag('--no-index'), flag('--show-files')] }),
|
package/src/decisions.mjs
CHANGED
|
@@ -689,7 +689,7 @@ export function runDecisions(args, config) {
|
|
|
689
689
|
process.stdout.write(`${items.length} decision items in ${docs.length} documents; ${defects.length} defects.\n`);
|
|
690
690
|
}
|
|
691
691
|
if (defects.length) process.exitCode = 1;
|
|
692
|
-
return;
|
|
692
|
+
return { defects };
|
|
693
693
|
}
|
|
694
694
|
|
|
695
695
|
let docFilter = null;
|
package/src/lifecycle.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { extractFrontmatter, parseSimpleFrontmatter, normalizeEol } from './frontmatter.mjs';
|
|
4
|
-
import { asString, toRepoPath, die, warn, resolveDocPath, escapeRegex, nowIso, suggestCandidates, emitFilesFooter, isArchivedPath, currentSessionId } from './util.mjs';
|
|
4
|
+
import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso, suggestCandidates, emitFilesFooter, isArchivedPath, currentSessionId } from './util.mjs';
|
|
5
5
|
import { readJournalEntries } from './journal.mjs';
|
|
6
6
|
import { captureGitIndexGeneration, getGitLastModifiedBatch, getGitLastSubstantiveModifiedBatch, isTracked } from './git.mjs';
|
|
7
7
|
import { buildIndex, collectDocFiles, resolveDocArg } from './index.mjs';
|
|
@@ -78,6 +78,22 @@ function commitLifecycleMutation(filePath, targetPath, config, updates, historyF
|
|
|
78
78
|
};
|
|
79
79
|
};
|
|
80
80
|
if (targetPath) {
|
|
81
|
+
// A companion prompt created or refreshed by baton links this plan before
|
|
82
|
+
// its status transition decides whether to file it elsewhere. Stamp the
|
|
83
|
+
// destination into that companion before publishing any transaction path.
|
|
84
|
+
const newRepoPath = toRepoPath(targetPath, config.repoRoot);
|
|
85
|
+
const retargetPrompt = item => {
|
|
86
|
+
if (typeof item.content !== 'string') return item;
|
|
87
|
+
const { frontmatter, body } = extractFrontmatter(item.content);
|
|
88
|
+
if (!frontmatter) return item;
|
|
89
|
+
const fm = parseSimpleFrontmatter(frontmatter);
|
|
90
|
+
const linked = asString(fm.plan);
|
|
91
|
+
const resolved = linked ? resolveRefPath(linked, path.dirname(item.path), config.repoRoot) : null;
|
|
92
|
+
if (asString(fm.type) !== 'prompt' || !resolved || path.resolve(resolved) !== path.resolve(filePath)) return item;
|
|
93
|
+
const rewritten = frontmatter.replace(/^plan:[ \t]*\S.*$/m, `plan: ${newRepoPath}`);
|
|
94
|
+
return { ...item, content: `---\n${rewritten}\n---\n${body}` };
|
|
95
|
+
};
|
|
96
|
+
const creations = (options.creations ?? []).map(retargetPrompt);
|
|
81
97
|
let result;
|
|
82
98
|
const tracked = isTracked(filePath, config.repoRoot);
|
|
83
99
|
const gitIndex = tracked ? captureGitIndexGeneration(config.repoRoot) : null;
|
|
@@ -89,8 +105,8 @@ function commitLifecycleMutation(filePath, targetPath, config, updates, historyF
|
|
|
89
105
|
const identities = createReferenceIdentitySet([filePath, ...allFiles]);
|
|
90
106
|
const referenceFields = configuredReferenceFields(config);
|
|
91
107
|
const additionalUpdates = options.skipInboundRefs
|
|
92
|
-
? (options.additionalUpdates ?? [])
|
|
93
|
-
: (options.additionalUpdates ?? []).map(item => ({
|
|
108
|
+
? (options.additionalUpdates ?? []).map(retargetPrompt)
|
|
109
|
+
: (options.additionalUpdates ?? []).map(item => retargetPrompt({
|
|
94
110
|
...item,
|
|
95
111
|
content: item.content === undefined ? undefined : rewriteDocumentReferences(item.content, {
|
|
96
112
|
sourcePath: item.path, repoRoot: config.repoRoot, identities, oldPath: filePath, newPath: targetPath, referenceFields,
|
|
@@ -108,7 +124,7 @@ function commitLifecycleMutation(filePath, targetPath, config, updates, historyF
|
|
|
108
124
|
sourcePath: docFile, repoRoot: config.repoRoot, identities, oldPath: filePath, newPath: targetPath, referenceFields,
|
|
109
125
|
}),
|
|
110
126
|
})), ...additionalUpdates],
|
|
111
|
-
creations
|
|
127
|
+
creations,
|
|
112
128
|
guards: options.guards ?? [],
|
|
113
129
|
gitMove: tracked,
|
|
114
130
|
gitIndex,
|
|
@@ -506,7 +522,7 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
506
522
|
process.stdout.write(`${prefix} Would append Version History: - **${today}** Status: ${oldStatus ?? 'unknown'} → ${newStatus} — ${note}\n`);
|
|
507
523
|
}
|
|
508
524
|
process.stdout.write(`${prefix} ${toRepoPath(finalPath, config.repoRoot)}: ${oldStatus ?? 'unknown'} → ${newStatus}\n`);
|
|
509
|
-
return;
|
|
525
|
+
return { dryRun: true, oldRepoPath: toRepoPath(filePath, config.repoRoot), newRepoPath: toRepoPath(finalPath, config.repoRoot), touched: [] };
|
|
510
526
|
}
|
|
511
527
|
|
|
512
528
|
const mutationResult = commitLifecycleMutation(filePath, targetPath, config, { status: newStatus, updated: today }, currentOld => {
|
|
@@ -516,6 +532,7 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
516
532
|
createSection: Boolean(note),
|
|
517
533
|
additionalUpdates: opts.additionalUpdates,
|
|
518
534
|
creations: opts.creations,
|
|
535
|
+
guards: opts.guards,
|
|
519
536
|
testHooks: opts.testHooks,
|
|
520
537
|
});
|
|
521
538
|
|
|
@@ -845,7 +862,7 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
845
862
|
if (config.hooks?.onArchive) {
|
|
846
863
|
out.write(`${prefix} Would fire hook: onArchive\n`);
|
|
847
864
|
}
|
|
848
|
-
return;
|
|
865
|
+
return { dryRun: true, oldRepoPath, newRepoPath, touched: [] };
|
|
849
866
|
}
|
|
850
867
|
|
|
851
868
|
const mutationResult = commitLifecycleMutation(filePath, targetPath, config, { status: targetStatus, updated: today },
|
|
@@ -1008,6 +1025,7 @@ export async function runSet(argv, config, opts = {}) {
|
|
|
1008
1025
|
dryRun, note, archiveStatus: newStatus, force, testHooks: opts.testHooks, deferIndex: opts.deferIndex,
|
|
1009
1026
|
additionalUpdates: opts.additionalUpdates,
|
|
1010
1027
|
creations: opts.creations,
|
|
1028
|
+
guards: opts.guards,
|
|
1011
1029
|
});
|
|
1012
1030
|
}
|
|
1013
1031
|
|
|
@@ -1059,8 +1077,9 @@ export async function runSet(argv, config, opts = {}) {
|
|
|
1059
1077
|
dryRun,
|
|
1060
1078
|
suppressDeprecation: true,
|
|
1061
1079
|
note,
|
|
1062
|
-
additionalUpdates: releaseUpdate ? [releaseUpdate] : [],
|
|
1080
|
+
additionalUpdates: [...(opts.additionalUpdates ?? []), ...(releaseUpdate ? [releaseUpdate] : [])],
|
|
1063
1081
|
creations: opts.creations,
|
|
1082
|
+
guards: opts.guards,
|
|
1064
1083
|
testHooks: opts.testHooks,
|
|
1065
1084
|
deferIndex: opts.deferIndex,
|
|
1066
1085
|
});
|