@sublang/playbook 14.1.0 → 15.1.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 (32) hide show
  1. package/README.md +2 -2
  2. package/docs/configuration.md +15 -9
  3. package/docs/embedding.md +7 -5
  4. package/package.json +3 -1
  5. package/reference/sdlc/code.md +1 -1
  6. package/reference/sdlc/code.playbook/bin/launch-config.js +55 -16
  7. package/reference/sdlc/code.playbook/bin/playbook.js +14 -9
  8. package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
  9. package/reference/sdlc/code.playbook/bin/run.js +12 -8
  10. package/reference/sdlc/code.playbook/code.fsm.js +1 -1
  11. package/reference/sdlc/code.playbook/code.fsm.ts +1 -1
  12. package/reference/sdlc/code.playbook/code.gears.md +2 -2
  13. package/reference/sdlc/decide.md +1 -1
  14. package/reference/sdlc/decide.playbook/decide.fsm.js +1 -1
  15. package/reference/sdlc/decide.playbook/decide.fsm.ts +1 -1
  16. package/reference/sdlc/decide.playbook/decide.gears.md +1 -1
  17. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +1 -9
  18. package/reference/sdlc/decide.playbook/decide.playbook.js +158 -32
  19. package/reference/sdlc/decide.playbook/decide.playbook.ts +229 -46
  20. package/reference/sdlc/review.md +1 -1
  21. package/reference/sdlc/review.playbook/review.fsm.js +1 -1
  22. package/reference/sdlc/review.playbook/review.fsm.ts +1 -1
  23. package/reference/sdlc/review.playbook/review.gears.md +1 -1
  24. package/slc/prefix-prompts.mjs +190 -0
  25. package/slc/prefix.md +200 -0
  26. package/slc/slc.pin-inputs.json +1 -0
  27. package/src/xstate-playbook-runtime.d.ts +35 -1
  28. package/src/xstate-playbook-runtime.js +82 -47
  29. package/src/xstate-playbook-runtime.ts +107 -60
  30. package/src/xstate-runtime.d.ts +3 -1
  31. package/src/xstate-runtime.js +9 -2
  32. package/src/xstate-runtime.ts +12 -2
package/README.md CHANGED
@@ -65,7 +65,7 @@ playbook
65
65
  Type a task, enter `/code <task>` for implementation, or enter
66
66
  `/decide <question>` for an independently proposed and reviewed decision.
67
67
 
68
- On first launch, Playbook writes its config to `${SPEX_HOME:-$HOME/.spex}/playbook/playbook.config.yaml`. A config left at the former `${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook.config.yaml` is moved there on the next launch, unless a relative primary locator would change targets; that case is rejected unchanged with absolute replacements.
68
+ On first launch, Playbook writes its config to `${SPEX_HOME:-$HOME/.spex}/config/playbook.config.yaml`. A config left at either former location — the root's `playbook/playbook.config.yaml` or `${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook.config.yaml` — is moved there once on the next launch, unless a relative primary locator would change targets; that case is rejected unchanged with absolute replacements.
69
69
 
70
70
  The same config, compiled Captain, enabled playbooks, stable players, and
71
71
  nested calls power headless turns. Both front ends create the same durable
@@ -107,7 +107,7 @@ SLC's `playbook` pipeline has three phases:
107
107
  2. **GEARS → FSM** ([slc/gears2fsm.md](slc/gears2fsm.md)) — maps each item to an XState state that invokes the Captain, a player, another playbook, or a local script.
108
108
  3. **FSM → runtime** ([slc/link.md](slc/link.md)) — links the machine to a host-independent interface for user input, agent calls, status, and telemetry.
109
109
 
110
- The default [optimization pass](slc/optimize.md) replaces eligible mechanical steps with local shell scripts; `--no-optimize` skips it.
110
+ The default passes run between the first two phases: the [optimization pass](slc/optimize.md) replaces eligible mechanical steps with local shell scripts, and the [prompt-prefix pass](slc/prefix.md) moves each prompt's relayed runtime values after its instructions so repeated runs share a cacheable prompt prefix; `--no-optimize` skips both.
111
111
  Inspect the complete [Captain](reference/sdlc/captain.md), [CODE](reference/sdlc/code.md), [REVIEW](reference/sdlc/review.md), and [DECIDE](reference/sdlc/decide.md) examples.
112
112
 
113
113
  ## Contributing
@@ -4,24 +4,30 @@
4
4
  # Configuring agents
5
5
 
6
6
  Fresh launches and ordinary reopens read one config at
7
- `${SPEX_HOME:-$HOME/.spex}/playbook/playbook.config.yaml`. The
7
+ `${SPEX_HOME:-$HOME/.spex}/config/playbook.config.yaml`. The
8
8
  first launch seeds it from the bundled starter and prints the path;
9
9
  later launches reuse it untouched.
10
10
 
11
- On the first launching command after upgrading Playbook, it moves a config from the
12
- former `${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook.config.yaml` path
13
- when the canonical path is absent. The one-time move preserves bytes and
14
- permissions and leaves no compatibility alias, so running an older Spex host
15
- afterward could seed a second file at the former path.
11
+ On the first launching command after upgrading Playbook, it moves a config from
12
+ either former location when the canonical path is absent — the root's
13
+ `playbook/playbook.config.yaml` first, then
14
+ `${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook.config.yaml`. Exactly one
15
+ file moves; the one-time move preserves bytes and permissions, takes the former
16
+ file's directory with it when that leaves it empty, and leaves no compatibility
17
+ alias, so running an older Spex host afterward could seed a second file at the
18
+ path it still resolves.
16
19
 
17
- The current guard rejects relocation when a legacy relative `sessions` value
20
+ The current guard rejects relocation when a former relative `sessions` value
18
21
  or relative filesystem `playbooks.<id>.from` would resolve differently below
19
22
  the new directory. It leaves the former file unchanged and names every
20
23
  target-preserving absolute replacement. Apply those replacements and retry;
21
- Playbook does not rewrite the user-authored file.
24
+ Playbook does not rewrite the user-authored file. `config/` sits at the depth
25
+ the root's `playbook/` did, so a relative locator reaching outside the directory
26
+ keeps its target across that move; only one pointing into the directory is
27
+ refused as above.
22
28
 
23
29
  ```sh
24
- $EDITOR "${SPEX_HOME:-$HOME/.spex}/playbook/playbook.config.yaml"
30
+ $EDITOR "${SPEX_HOME:-$HOME/.spex}/config/playbook.config.yaml"
25
31
  ```
26
32
 
27
33
  ## Anatomy
package/docs/embedding.md CHANGED
@@ -445,21 +445,23 @@ const url = import.meta.resolve('@sublang/playbook/slc/link.md');
445
445
  const link = await readFile(fileURLToPath(url), 'utf8');
446
446
  ```
447
447
 
448
- The four specs are [`slc/text2gears.md`](../slc/text2gears.md),
448
+ The five specs are [`slc/text2gears.md`](../slc/text2gears.md),
449
449
  [`slc/gears2fsm.md`](../slc/gears2fsm.md),
450
450
  [`slc/link.md`](../slc/link.md) — the FSM-to-runtime contract that
451
- `@sublang/playbook/runtime` projects into TypeScript — and
452
- [`slc/optimize.md`](../slc/optimize.md).
451
+ `@sublang/playbook/runtime` projects into TypeScript —
452
+ [`slc/optimize.md`](../slc/optimize.md), and
453
+ [`slc/prefix.md`](../slc/prefix.md).
453
454
 
454
- Five supporting assets use the same resolution mechanism:
455
+ Six supporting assets use the same resolution mechanism:
455
456
 
456
457
  | Asset | Purpose |
457
458
  | --- | --- |
458
459
  | [`slc/workflow-contracts.json`](../slc/workflow-contracts.json) | Public output schemas and default literal target bindings for the builtin REVIEW, DECIDE, CODE, BRANCH, and PR workflows; compilers and embedders can read them without importing workflow implementations. |
459
460
  | [`slc/materialize-link.mjs`](../slc/materialize-link.mjs) | Optional CLI that emits a thin linked module from a supported FSM and JSON descriptor; see the profiles and invocation in `slc/link.md`. |
460
461
  | [`slc/scaffold-fsm.mjs`](../slc/scaffold-fsm.mjs) | Optional CLI that initializes an incomplete typed FSM from supported GEARS source. |
462
+ | [`slc/prefix-prompts.mjs`](../slc/prefix-prompts.mjs) | CLI that performs the prompt-prefix pass's rewrite of a GEARS package exactly; see the invocation in `slc/prefix.md`. |
461
463
  | [`slc/experiments/fsm-scaffold-guidance.md`](../slc/experiments/fsm-scaffold-guidance.md) | Opt-in compiler guidance for the initializer, outside ordinary phase discovery. |
462
- | [`slc/slc.pin-inputs.json`](../slc/slc.pin-inputs.json) | SLC semantic-input closures for `gears2fsm` and `link`, including the workflow catalog and, for `link`, the materializer. Other phases use SLC's inline-input discovery. |
464
+ | [`slc/slc.pin-inputs.json`](../slc/slc.pin-inputs.json) | SLC semantic-input closures for `gears2fsm`, `prefix`, and `link`, including the workflow catalog, the prefix tool, and, for `link`, the materializer. Other phases use SLC's inline-input discovery. |
463
465
 
464
466
  For example, resolve `@sublang/playbook/slc/workflow-contracts.json`, read it
465
467
  with `readFile`, and parse the result as JSON to inspect the published workflow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "14.1.0",
3
+ "version": "15.1.0",
4
4
  "type": "module",
5
5
  "description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, DEV, BRANCH, and PR workflows driven by GEARS specs.",
6
6
  "license": "Apache-2.0",
@@ -46,6 +46,8 @@
46
46
  "slc/gears2fsm.md",
47
47
  "slc/text2gears.md",
48
48
  "slc/optimize.md",
49
+ "slc/prefix.md",
50
+ "slc/prefix-prompts.mjs",
49
51
  "reference/sdlc/captain.md",
50
52
  "reference/sdlc/code.md",
51
53
  "reference/sdlc/review.md",
@@ -66,7 +66,7 @@ Do not re-run tests or builds whose inputs have not changed since any previous r
66
66
  Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
67
67
  Make the commit message explain concisely what changed and why, including relevant verification.
68
68
  Identify every new commit you make.
69
- Coder is <coder-llm>.
69
+ Credit every AI that contributed to this commit: Coder <coder-llm>.
70
70
  ```
71
71
 
72
72
  Every new-intent phase has two semantic outcomes: direct implementation and new IR; the new-IR outcome identifies the created IR.
@@ -15,6 +15,7 @@ import {
15
15
  mkdirSync,
16
16
  mkdtempSync,
17
17
  readFileSync,
18
+ rmdirSync,
18
19
  rmSync,
19
20
  writeFileSync,
20
21
  } from "node:fs";
@@ -133,9 +134,11 @@ export function mergeSelectedConfigs(base, overlay, selectedMembers) {
133
134
  );
134
135
  }
135
136
 
136
- // DR-043: the config lives under the shared Spex root, resolved exactly as
137
- // Spex's own shells resolve it so both hosts open one file. The singular
138
- // `playbook/` namespace is ours; Spex owns the plural `playbooks/` library.
137
+ // DR-043/DR-064: the config lives under the shared Spex root, resolved exactly
138
+ // as Spex's own shells resolve it so both hosts open one file. `config/` is the
139
+ // root's directory of human-authored configuration files, each named for the
140
+ // format it follows; the rest of the root is laid out the same way, by what a
141
+ // directory holds.
139
142
  export function resolveSpexHome(env = process.env, home = homedir()) {
140
143
  const explicit = env.SPEX_HOME;
141
144
  if (typeof explicit === "string" && explicit.trim().length > 0) {
@@ -148,7 +151,7 @@ export function resolveSpexHome(env = process.env, home = homedir()) {
148
151
  }
149
152
 
150
153
  export function resolveUserConfigPath(env = process.env, home = homedir()) {
151
- return join(resolveSpexHome(env, home), "playbook", "playbook.config.yaml");
154
+ return join(resolveSpexHome(env, home), "config", "playbook.config.yaml");
152
155
  }
153
156
 
154
157
  // The pre-DR-043 location, kept only to relocate a config written there.
@@ -160,6 +163,18 @@ export function resolveLegacyUserConfigPath(
160
163
  return join(configHome, "playbook", "playbook.config.yaml");
161
164
  }
162
165
 
166
+ // DR-064: the locations a launch relocates from, the nearer first — the root's
167
+ // pre-DR-064 `playbook/` namespace, then the pre-DR-043 XDG path.
168
+ export function resolveFormerUserConfigPaths(
169
+ env = process.env,
170
+ home = homedir(),
171
+ ) {
172
+ return [
173
+ join(resolveSpexHome(env, home), "playbook", "playbook.config.yaml"),
174
+ resolveLegacyUserConfigPath(env, home),
175
+ ];
176
+ }
177
+
163
178
  // PBCLI-46/78: session selection needs this one root locator before the
164
179
  // complete launch plan can be selected and normalized. Read only that scalar
165
180
  // from each layer so an ordinary reopen does not inspect unrelated current
@@ -1203,22 +1218,24 @@ export function deriveLaunchReadiness(
1203
1218
  return { adapters, ...checkReadiness(adapters, env, home) };
1204
1219
  }
1205
1220
 
1206
- // DR-043: a user-authored config cannot be regenerated, so the one-time move
1207
- // to the canonical path is the deliberate exception to this project's
1221
+ // DR-043/DR-064: a user-authored config cannot be regenerated, so the one-time
1222
+ // move to the canonical path is the deliberate exception to this project's
1208
1223
  // reject-don't-migrate posture. It runs before seeding, never clobbers a
1209
- // canonical file, and is a no-op once the legacy file is gone.
1224
+ // canonical file, and is a no-op once the former file is gone. It answers
1225
+ // whether it published, so a caller walking the ordered former locations stops
1226
+ // at the one that moved.
1210
1227
  export function relocateLegacyUserConfig(
1211
1228
  userConfigPath,
1212
1229
  legacyUserConfigPath,
1213
1230
  onNotice,
1214
1231
  ) {
1215
- if (legacyUserConfigPath === undefined) return;
1232
+ if (legacyUserConfigPath === undefined) return false;
1216
1233
  // Any canonical filesystem entry wins, including a dangling symlink. The
1217
1234
  // relocation is considered only when that exact pathname is absent; this
1218
1235
  // keeps an obsolete or malformed legacy entry from blocking a valid config.
1219
1236
  try {
1220
1237
  lstatSync(userConfigPath);
1221
- return;
1238
+ return false;
1222
1239
  } catch (error) {
1223
1240
  if (error?.code !== "ENOENT") throw error;
1224
1241
  }
@@ -1226,7 +1243,7 @@ export function relocateLegacyUserConfig(
1226
1243
  try {
1227
1244
  source = lstatSync(legacyUserConfigPath);
1228
1245
  } catch (error) {
1229
- if (error?.code === "ENOENT") return;
1246
+ if (error?.code === "ENOENT") return false;
1230
1247
  throw error;
1231
1248
  }
1232
1249
  if (!source.isFile()) {
@@ -1246,11 +1263,18 @@ export function relocateLegacyUserConfig(
1246
1263
  const stagedPath = join(stagingDir, "playbook.config.yaml");
1247
1264
  let published = false;
1248
1265
  try {
1249
- copyFileSync(
1250
- legacyUserConfigPath,
1251
- stagedPath,
1252
- constants.COPYFILE_EXCL,
1253
- );
1266
+ try {
1267
+ copyFileSync(
1268
+ legacyUserConfigPath,
1269
+ stagedPath,
1270
+ constants.COPYFILE_EXCL,
1271
+ );
1272
+ } catch (error) {
1273
+ // The former file went between inspection and staging: there is
1274
+ // nothing left to move, and nothing to report.
1275
+ if (error?.code === "ENOENT") return false;
1276
+ throw error;
1277
+ }
1254
1278
  chmodSync(stagedPath, source.mode & 0o7777);
1255
1279
  assertLegacyRelocationLocatorsSafe(
1256
1280
  readFileSync(stagedPath, "utf8"),
@@ -1268,12 +1292,27 @@ export function relocateLegacyUserConfig(
1268
1292
  } finally {
1269
1293
  rmSync(stagingDir, { recursive: true, force: true });
1270
1294
  }
1271
- if (!published) return;
1295
+ if (!published) return false;
1272
1296
 
1273
1297
  rmSync(legacyUserConfigPath, { force: true });
1298
+ // The former file's directory went with it when it held nothing else. A
1299
+ // plain rmdir is the whole removal: it refuses a directory with any other
1300
+ // entry, so nothing the user put beside the config is ever swept away.
1301
+ try {
1302
+ rmdirSync(dirname(legacyUserConfigPath));
1303
+ } catch (error) {
1304
+ if (
1305
+ error?.code !== "ENOTEMPTY" &&
1306
+ error?.code !== "EEXIST" &&
1307
+ error?.code !== "ENOENT"
1308
+ ) {
1309
+ throw error;
1310
+ }
1311
+ }
1274
1312
  onNotice(
1275
1313
  `playbook: moved config from ${legacyUserConfigPath} to ${userConfigPath}\n`,
1276
1314
  );
1315
+ return true;
1277
1316
  }
1278
1317
 
1279
1318
  function assertLegacyRelocationLocatorsSafe(
@@ -24,7 +24,7 @@ import {
24
24
  projectTmuxConfig,
25
25
  resolveLaunchSessionsDir,
26
26
  relocateLegacyUserConfig,
27
- resolveLegacyUserConfigPath,
27
+ resolveFormerUserConfigPaths,
28
28
  resolveUserConfigPath,
29
29
  checkReadiness,
30
30
  } from "./launch-config.js";
@@ -233,16 +233,21 @@ export async function runPlaybookCli(options = {}) {
233
233
  return { code: COMPOSITION_FAILURE_EXIT_CODE };
234
234
  }
235
235
 
236
- // DR-043: only a validated managed launch may relocate the legacy config.
237
- // Help and raw --config return above without changing either config path;
238
- // the delegated `run` front end applies the same boundary independently.
236
+ // DR-043/DR-064: only a validated managed launch may relocate a config left
237
+ // at a former location. Help and raw --config return above without changing
238
+ // any config path; the delegated `run` front end applies the same boundary
239
+ // independently. The nearer former location goes first, and the one that
240
+ // moves ends the walk.
239
241
  if (options.userConfigPath === undefined) {
240
242
  try {
241
- relocateLegacyUserConfig(
242
- userConfigPath,
243
- resolveLegacyUserConfigPath(env, home),
244
- (line) => stderr.write(line),
245
- );
243
+ for (const formerPath of resolveFormerUserConfigPaths(env, home)) {
244
+ const moved = relocateLegacyUserConfig(
245
+ userConfigPath,
246
+ formerPath,
247
+ (line) => stderr.write(line),
248
+ );
249
+ if (moved) break;
250
+ }
246
251
  } catch (error) {
247
252
  stderr.write(`playbook: ${errorMessage(error)}\n`);
248
253
  return { code: COMPOSITION_FAILURE_EXIT_CODE };
@@ -520,8 +520,6 @@ function projectionText(projection) {
520
520
  return JSON.stringify(projection);
521
521
  }
522
522
 
523
- const MISSING_CONTENT_IDENTITY = Object.freeze({ kind: 'missing' });
524
-
525
523
  function hasPath(projection, path) {
526
524
  return Object.prototype.hasOwnProperty.call(projection, path);
527
525
  }
@@ -555,9 +553,9 @@ function sameTreeEntry(left, right) {
555
553
  return left.mode === right.mode && left.oid === right.oid;
556
554
  }
557
555
 
558
- // The baseline recorded nothing at the path: a deleted worktree entry, or a
556
+ // The entry records nothing at the path: a deleted worktree entry, or a
559
557
  // staged deletion whose index holds no object.
560
- function baselineEntryIsDeletion(entry) {
558
+ function entryIsDeletion(entry) {
561
559
  return entry.worktree === undefined
562
560
  ? entry.indexMode === '000000'
563
561
  : entry.worktree.kind === 'missing';
@@ -615,14 +613,57 @@ function sameContentIdentity(left, right) {
615
613
  );
616
614
  }
617
615
 
616
+ // A worktree deletion loses the Boss's content only when neither the after
617
+ // index nor the after HEAD tree holds it; content the after HEAD cannot
618
+ // produce fails closed as lost.
619
+ async function deletionRetainsContent(baseline, after, path, entry, afterEntry) {
620
+ try {
621
+ const baselineIdentity = await baselineContentIdentity(after.worktree, entry);
622
+ if (
623
+ afterEntry.kind === 'ordinary' &&
624
+ afterEntry.indexMode !== undefined &&
625
+ afterEntry.indexMode !== '000000'
626
+ ) {
627
+ const indexIdentity = await blobContentIdentity(
628
+ after.worktree,
629
+ afterEntry.indexMode,
630
+ afterEntry.indexOid,
631
+ );
632
+ if (sameContentIdentity(baselineIdentity, indexIdentity)) return true;
633
+ }
634
+ if (baseline.head === after.head) return false;
635
+ const treeEntry = await afterTreeEntry(after.worktree, after.head, path);
636
+ if (treeEntry === undefined || treeEntry.mode === '160000') return false;
637
+ const treeIdentity = await blobContentIdentity(
638
+ after.worktree,
639
+ treeEntry.mode,
640
+ treeEntry.oid,
641
+ );
642
+ return sameContentIdentity(baselineIdentity, treeIdentity);
643
+ } catch {
644
+ return false;
645
+ }
646
+ }
647
+
618
648
  // DR-062 §1: the fate of one baseline projection entry at the end of a
619
649
  // governed call — `preserved`, `altered-uncommitted`, `altered-committed`,
620
- // `absorbed`, or `lost`.
650
+ // `absorbed`, or `lost`. Loss is decided before alteration: a kind that
651
+ // admits no content matching, a worktree deletion whose content survives in
652
+ // neither the index nor the new HEAD, and a commit that dropped the path all
653
+ // leave the Boss's content nowhere.
621
654
  async function baselineEntryFate(baseline, after, path, entry) {
622
655
  if (hasPath(after.projection, path)) {
623
- return JSON.stringify(after.projection[path]) === JSON.stringify(entry)
624
- ? 'preserved'
625
- : 'altered-uncommitted';
656
+ const afterEntry = after.projection[path];
657
+ if (JSON.stringify(afterEntry) === JSON.stringify(entry)) {
658
+ return 'preserved';
659
+ }
660
+ if (nonComparableBaselineEntry(entry)) return 'lost';
661
+ if (entryIsDeletion(afterEntry) && !entryIsDeletion(entry)) {
662
+ return (await deletionRetainsContent(baseline, after, path, entry, afterEntry))
663
+ ? 'altered-uncommitted'
664
+ : 'lost';
665
+ }
666
+ return 'altered-uncommitted';
626
667
  }
627
668
  // The entry left the working tree, so only a commit this call made can hold
628
669
  // the Boss's content; a call that moved no HEAD made none.
@@ -635,15 +676,15 @@ async function baselineEntryFate(baseline, after, path, entry) {
635
676
  // carried no change of the Boss's and his content is nowhere.
636
677
  return 'lost';
637
678
  }
638
- if (baselineEntryIsDeletion(entry)) {
679
+ if (entryIsDeletion(entry)) {
639
680
  return treeEntry === undefined ? 'absorbed' : 'lost';
640
681
  }
641
- if (treeEntry?.mode === '160000') return 'lost';
682
+ // A commit that dropped the path, or replaced it with a gitlink, carries
683
+ // no content of the Boss's to compare.
684
+ if (treeEntry === undefined || treeEntry.mode === '160000') return 'lost';
642
685
  const [baselineIdentity, treeIdentity] = await Promise.all([
643
686
  baselineContentIdentity(after.worktree, entry),
644
- treeEntry === undefined
645
- ? MISSING_CONTENT_IDENTITY
646
- : blobContentIdentity(after.worktree, treeEntry.mode, treeEntry.oid),
687
+ blobContentIdentity(after.worktree, treeEntry.mode, treeEntry.oid),
647
688
  ]);
648
689
  return sameContentIdentity(baselineIdentity, treeIdentity)
649
690
  ? 'absorbed'
@@ -35,7 +35,7 @@ import {
35
35
  projectHostAgent,
36
36
  resolveLaunchSessionsDir,
37
37
  relocateLegacyUserConfig,
38
- resolveLegacyUserConfigPath,
38
+ resolveFormerUserConfigPaths,
39
39
  resolveUserConfigPath,
40
40
  snapshotRegistryEntry,
41
41
  } from "./launch-config.js";
@@ -127,15 +127,19 @@ export async function runPlaybookRun(options = {}) {
127
127
 
128
128
  const userConfigPath =
129
129
  options.userConfigPath ?? resolveUserConfigPath(env, home);
130
- // DR-043: move a pre-relocation config to the canonical path before any
131
- // read, seed, or plan work observes its absence.
130
+ // DR-043/DR-064: move a config left at a former location to the canonical
131
+ // path before any read, seed, or plan work observes its absence. The nearer
132
+ // former location goes first, and the one that moves ends the walk.
132
133
  if (options.userConfigPath === undefined) {
133
134
  try {
134
- relocateLegacyUserConfig(
135
- userConfigPath,
136
- resolveLegacyUserConfigPath(env, home),
137
- (line) => stderr.write(line),
138
- );
135
+ for (const formerPath of resolveFormerUserConfigPaths(env, home)) {
136
+ const moved = relocateLegacyUserConfig(
137
+ userConfigPath,
138
+ formerPath,
139
+ (line) => stderr.write(line),
140
+ );
141
+ if (moved) break;
142
+ }
139
143
  } catch (error) {
140
144
  await writeStream(stderr, `playbook run: ${message(error)}\n`);
141
145
  return { code: EXIT.argument };
@@ -9,7 +9,7 @@ const APPENDED_PHASE_PROMPT = [
9
9
  "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
10
10
  'Make the commit message explain concisely what changed and why, including relevant verification.',
11
11
  'Identify every new commit you make.',
12
- 'Coder is <coder-llm>.',
12
+ 'Credit every AI that contributed to this commit: Coder <coder-llm>.',
13
13
  ];
14
14
  const FIRST_PHASE_PROMPT = [
15
15
  '> Original request: <caller-input>',
@@ -152,7 +152,7 @@ const APPENDED_PHASE_PROMPT = [
152
152
  "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
153
153
  'Make the commit message explain concisely what changed and why, including relevant verification.',
154
154
  'Identify every new commit you make.',
155
- 'Coder is <coder-llm>.',
155
+ 'Credit every AI that contributed to this commit: Coder <coder-llm>.',
156
156
  ];
157
157
 
158
158
  const FIRST_PHASE_PROMPT = [
@@ -36,7 +36,7 @@ When the first coding phase begins, Captain shall prompt Coder:
36
36
  > Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
37
37
  > Make the commit message explain concisely what changed and why, including relevant verification.
38
38
  > Identify every new commit you make.
39
- > Coder is <coder-llm>.
39
+ > Credit every AI that contributed to this commit: Coder <coder-llm>.
40
40
 
41
41
  Results:
42
42
  - `directCommit`: Coder completed and committed the direct implementation phase. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.
@@ -77,7 +77,7 @@ When a later IR-task phase begins, Captain shall prompt Coder:
77
77
  > Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
78
78
  > Make the commit message explain concisely what changed and why, including relevant verification.
79
79
  > Identify every new commit you make.
80
- > Coder is <coder-llm>.
80
+ > Credit every AI that contributed to this commit: Coder <coder-llm>.
81
81
 
82
82
  Results:
83
83
  - `moreTasks`: Coder completed and committed exactly the IR's next unfinished task and at least one task remains. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the continued IR, and `irTask` naming the implemented task.
@@ -43,7 +43,7 @@ Do not change code or implement the design.
43
43
  Commit the result as one new commit, following @specs/packages/git.md.
44
44
  Make the commit message explain concisely what changed and why.
45
45
  Identify every new commit you make.
46
- Coder is <coder-llm> and Reviewer is <reviewer-llm>.
46
+ Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose proposal it carries.
47
47
 
48
48
  > Original topic: \<caller-topic\>
49
49
  > Reviewer's independent proposal: \<reviewer-proposal\>
@@ -26,7 +26,7 @@ const SYNTHESIZE_COMMIT_PROMPT = [
26
26
  'Commit the result as one new commit, following @specs/packages/git.md.',
27
27
  'Make the commit message explain concisely what changed and why.',
28
28
  'Identify every new commit you make.',
29
- 'Coder is <coder-llm> and Reviewer is <reviewer-llm>.',
29
+ 'Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose proposal it carries.',
30
30
  '',
31
31
  '> Original topic: <caller-topic>',
32
32
  "> Reviewer's independent proposal: <reviewer-proposal>",
@@ -168,7 +168,7 @@ const SYNTHESIZE_COMMIT_PROMPT = [
168
168
  'Commit the result as one new commit, following @specs/packages/git.md.',
169
169
  'Make the commit message explain concisely what changed and why.',
170
170
  'Identify every new commit you make.',
171
- 'Coder is <coder-llm> and Reviewer is <reviewer-llm>.',
171
+ 'Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose proposal it carries.',
172
172
  '',
173
173
  '> Original topic: <caller-topic>',
174
174
  "> Reviewer's independent proposal: <reviewer-proposal>",
@@ -66,7 +66,7 @@ When both independent proposals are complete, Captain shall relay the complete t
66
66
  > Commit the result as one new commit, following @specs/packages/git.md.
67
67
  > Make the commit message explain concisely what changed and why.
68
68
  > Identify every new commit you make.
69
- > Coder is <coder-llm> and Reviewer is <reviewer-llm>.
69
+ > Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose proposal it carries.
70
70
  >
71
71
  > > Original topic: <caller-topic>
72
72
  > > Reviewer's independent proposal: <reviewer-proposal>
@@ -1,3 +1,4 @@
1
+ import { normalizeErrorCompact, normalizeErrorFull } from '../../../src/xstate-runtime.js';
1
2
  import { type PlayerInput, type DecideEvent, type DecideInput, type PendingBossQuestion } from './decide.fsm.js';
2
3
  import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
3
4
  import type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlayerResult, PlayerSessionStore, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookControlAction, PlaybookControlReceipt, PlaybookControlView, PlaybookPendingCall, PlaybookPorts, PlaybookRunResult, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType } from '@sublang/playbook/runtime';
@@ -25,15 +26,6 @@ declare function buildAdjudicatorPrompt(input: PlayerInput, playerOutput: string
25
26
  readonly error: string;
26
27
  }): string;
27
28
  declare function combineSignals(a: AbortSignal | undefined, b: AbortSignal | undefined): AbortSignal;
28
- declare function normalizeErrorCompact(err: unknown): {
29
- name: string;
30
- message: string;
31
- } | undefined;
32
- declare function normalizeErrorFull(err: unknown): {
33
- name: string;
34
- message: string;
35
- stack?: string;
36
- } | undefined;
37
29
  declare function pendingQuestionsFromContext(context: Record<string, unknown>): PendingBossQuestion[];
38
30
  declare function pendingQuestionsForState(state: PlaybookState, context: Record<string, unknown>): PendingBossQuestion[];
39
31
  export declare const createPlaybookRuntime: PlaybookRuntimeFactory<DecidePlaybookRuntimeConstruction>;