@p4code/cli 0.1.19 → 0.1.21

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 (88) hide show
  1. package/dist/bin.mjs +1262 -55
  2. package/dist/client/assets/{DiffPanel-CseUmWvE.js → DiffPanel-2Gqo7b0N.js} +3 -3
  3. package/dist/client/assets/{FilePreviewPanel-Dm7l5FFh.js → FilePreviewPanel-Po3D7bHa.js} +5 -5
  4. package/dist/client/assets/PreviewPanel-ebSBxbsf.js +2 -0
  5. package/dist/client/assets/{angular-html-YobKPLun.js → angular-html-BhHAxMde.js} +2 -2
  6. package/dist/client/assets/{angular-ts-Ac3I6TFV.js → angular-ts-CxZFyQM4.js} +2 -2
  7. package/dist/client/assets/{apl-Dwl73GTS.js → apl-D8rOr4fH.js} +2 -2
  8. package/dist/client/assets/arrow-right-Dseaz2v4.js +2 -0
  9. package/dist/client/assets/{astro-KBgldnxZ.js → astro-DYSYdOU9.js} +2 -2
  10. package/dist/client/assets/{blade-DTU2NGhh.js → blade-DaHnYPa_.js} +2 -2
  11. package/dist/client/assets/{c-COF4kWCp.js → c-oRWOBveW.js} +2 -2
  12. package/dist/client/assets/{cobol-BA7JsdNr.js → cobol-Djebq6RA.js} +2 -2
  13. package/dist/client/assets/{coffee-PFN_w4qL.js → coffee-CKhxS88r.js} +2 -2
  14. package/dist/client/assets/{cpp-CpT6w-g5.js → cpp-BjVgWq1h.js} +2 -2
  15. package/dist/client/assets/{crystal-BrlaRT0B.js → crystal-DXzIaEKx.js} +2 -2
  16. package/dist/client/assets/{css-C2LMO3MX.js → css-7AulbBAq.js} +2 -2
  17. package/dist/client/assets/{edge-DBkQ-Ajd.js → edge-B3VgcX6t.js} +2 -2
  18. package/dist/client/assets/{elixir-EbSNDEx0.js → elixir-DGUU7Wwf.js} +2 -2
  19. package/dist/client/assets/{elm-CSs0Wzy2.js → elm-B9tr8qBJ.js} +2 -2
  20. package/dist/client/assets/{erb-BBmMQG9b.js → erb-BgSl2c_f.js} +2 -2
  21. package/dist/client/assets/{fileCommentAnnotations-CifvTzWP.js → fileCommentAnnotations-C06ZwrTh.js} +2 -2
  22. package/dist/client/assets/{git-rebase-C09BpZDw.js → git-rebase-CJMhrhmS.js} +2 -2
  23. package/dist/client/assets/{glimmer-js-D4Bqn6Yg.js → glimmer-js-Dzf5VMSx.js} +2 -2
  24. package/dist/client/assets/{glimmer-ts-fLCHL8o6.js → glimmer-ts-CHD8gl5-.js} +2 -2
  25. package/dist/client/assets/{glsl-17sV9oGm.js → glsl-CDpKClnF.js} +2 -2
  26. package/dist/client/assets/{graphql-BqFquulN.js → graphql-DX6Ul3G5.js} +2 -2
  27. package/dist/client/assets/{hack-D6Jpv-j9.js → hack-NU-7p0gC.js} +2 -2
  28. package/dist/client/assets/{haml-D0eo8PEJ.js → haml-Cf7Y-ZgP.js} +2 -2
  29. package/dist/client/assets/{handlebars-HG0XNkEg.js → handlebars-BOvzNDfr.js} +2 -2
  30. package/dist/client/assets/{html-Bh3padJ5.js → html-CokKN6Uo.js} +2 -2
  31. package/dist/client/assets/{html-derivative-DVHw4MNX.js → html-derivative-CcTOncd6.js} +2 -2
  32. package/dist/client/assets/{http-CYCXaOA5.js → http-CGr88I6S.js} +2 -2
  33. package/dist/client/assets/{hurl-D4oK46oh.js → hurl-BfdIF20D.js} +2 -2
  34. package/dist/client/assets/index-BwGqPoA8.js +847 -0
  35. package/dist/client/assets/index-eSuLivhH.css +1 -0
  36. package/dist/client/assets/{java-nvFYZOCN.js → java-DjcrYPKU.js} +2 -2
  37. package/dist/client/assets/{javascript-CMBxjf-M.js → javascript-C1msiELJ.js} +2 -2
  38. package/dist/client/assets/{jinja-vsPdkFFj.js → jinja-BP-z74Sl.js} +2 -2
  39. package/dist/client/assets/{jison-uky2pPNE.js → jison-DTU_ib6_.js} +2 -2
  40. package/dist/client/assets/{json-DOSqa2F1.js → json-n2oR8huX.js} +2 -2
  41. package/dist/client/assets/{jsx-Ci7CVNQe.js → jsx-DLc8ZGPF.js} +2 -2
  42. package/dist/client/assets/{julia-J2m7N3_R.js → julia-BFtTfktg.js} +2 -2
  43. package/dist/client/assets/{just-BDy3oM_N.js → just-CBZfmiGm.js} +2 -2
  44. package/dist/client/assets/{latex-CMD49F7D.js → latex-B9jcgQKp.js} +2 -2
  45. package/dist/client/assets/{liquid-Dq6k2YW6.js → liquid-D-PCb97a.js} +2 -2
  46. package/dist/client/assets/{lua-CohtP6J8.js → lua-CeAbwjz7.js} +2 -2
  47. package/dist/client/assets/{marko-C4V2zw1t.js → marko-D2oCpzm8.js} +2 -2
  48. package/dist/client/assets/{mdc-q1ZKjwYt.js → mdc-BN4ff7dr.js} +2 -2
  49. package/dist/client/assets/{nginx-D2TdEIZ3.js → nginx-TigpgX3q.js} +2 -2
  50. package/dist/client/assets/{nim-BtWKpYQs.js → nim-BbkIxZP5.js} +2 -2
  51. package/dist/client/assets/{perl-Ci4Pso9h.js → perl-Dq3UNlmA.js} +2 -2
  52. package/dist/client/assets/{php-CMzpcAju.js → php-CDBbIDUr.js} +2 -2
  53. package/dist/client/assets/{pug-D0sbFhP6.js → pug-X1om8_0C.js} +2 -2
  54. package/dist/client/assets/{qml-BfWXHzNc.js → qml-C_aweCjl.js} +2 -2
  55. package/dist/client/assets/{r-BN4rQDnz.js → r-BH02pZo7.js} +2 -2
  56. package/dist/client/assets/{razor-D5iw68yr.js → razor-DFd9duVt.js} +2 -2
  57. package/dist/client/assets/{regexp-Ba0WieCb.js → regexp-BYgWQcZZ.js} +2 -2
  58. package/dist/client/assets/{rst-DQqlRfnq.js → rst-DS4ajG1_.js} +2 -2
  59. package/dist/client/assets/{ruby-CReh9Gta.js → ruby-rrP3E-yX.js} +2 -2
  60. package/dist/client/assets/{sas-CJ-Cpd8L.js → sas-qOp3AbMp.js} +2 -2
  61. package/dist/client/assets/{scss-CZp-cH6d.js → scss-BePdknOf.js} +2 -2
  62. package/dist/client/assets/{shellscript-hCSuhhJZ.js → shellscript-Di7DOIUv.js} +2 -2
  63. package/dist/client/assets/{shellsession-BnDNcCDQ.js → shellsession-DS-qEJoT.js} +2 -2
  64. package/dist/client/assets/{soy-BXarcWJY.js → soy-CT_pCK5o.js} +2 -2
  65. package/dist/client/assets/{sql-ADQCXhYd.js → sql-CVynNG6j.js} +2 -2
  66. package/dist/client/assets/{stata-DSS4IpqF.js → stata-B2fKxQHN.js} +2 -2
  67. package/dist/client/assets/{surrealql-DuYcftNm.js → surrealql-BOcosx9e.js} +2 -2
  68. package/dist/client/assets/{svelte-CAXOFInl.js → svelte-BQBQ3ysY.js} +2 -2
  69. package/dist/client/assets/{templ-BvTIEIah.js → templ-cinsfbYE.js} +2 -2
  70. package/dist/client/assets/{tex-DpnVRTMM.js → tex-PiJ0CI8J.js} +2 -2
  71. package/dist/client/assets/textarea-DV6gvhvo.js +46 -0
  72. package/dist/client/assets/{ts-tags-4wlHlovT.js → ts-tags-jRjqE2jk.js} +2 -2
  73. package/dist/client/assets/{tsx-CHDgELdy.js → tsx-DqzTbomV.js} +2 -2
  74. package/dist/client/assets/{twig-DPmx2UHF.js → twig-5U1Vd9zl.js} +2 -2
  75. package/dist/client/assets/{typescript-CHO8bW-H.js → typescript-DmYNCX8k.js} +2 -2
  76. package/dist/client/assets/{vue-CM19I03n.js → vue-BOE7yHne.js} +2 -2
  77. package/dist/client/assets/{vue-html-ginfkxGQ.js → vue-html-roHe5KnN.js} +2 -2
  78. package/dist/client/assets/{vue-vine-CGdZxUwf.js → vue-vine-YC-R0SEx.js} +2 -2
  79. package/dist/client/assets/{xml-B8CY16OL.js → xml-B-QQaugn.js} +2 -2
  80. package/dist/client/assets/{xsl-COHh7n8v.js → xsl-CyPKxPW7.js} +2 -2
  81. package/dist/client/assets/{yaml-DXx-djat.js → yaml-Bq2pe1E4.js} +2 -2
  82. package/dist/client/index.html +3 -3
  83. package/package.json +1 -1
  84. package/dist/client/assets/PreviewPanel-DOyykNL9.js +0 -2
  85. package/dist/client/assets/arrow-right-BqDszij1.js +0 -2
  86. package/dist/client/assets/index-CV_dK79d.js +0 -783
  87. package/dist/client/assets/index-Dol3ssFp.css +0 -1
  88. package/dist/client/assets/textarea-etKSOtDN.js +0 -46
package/dist/bin.mjs CHANGED
@@ -233,7 +233,7 @@ const make$76 = () => {
233
233
  const layer$72 = Layer.sync(NetService, make$76);
234
234
  //#endregion
235
235
  //#region package.json
236
- var version = "0.1.19";
236
+ var version = "0.1.21";
237
237
  //#endregion
238
238
  //#region src/config.ts
239
239
  /**
@@ -1838,6 +1838,7 @@ const OrchestrationThread = Schema$1.Struct({
1838
1838
  runtimeMode: RuntimeMode,
1839
1839
  interactionMode: ProviderInteractionMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE))),
1840
1840
  compressMode: CompressMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_COMPRESS_MODE))),
1841
+ unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
1841
1842
  branch: Schema$1.NullOr(TrimmedNonEmptyString),
1842
1843
  worktreePath: Schema$1.NullOr(TrimmedNonEmptyString),
1843
1844
  latestTurn: Schema$1.NullOr(OrchestrationLatestTurn),
@@ -1879,6 +1880,7 @@ const OrchestrationThreadShell = Schema$1.Struct({
1879
1880
  runtimeMode: RuntimeMode,
1880
1881
  interactionMode: ProviderInteractionMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE))),
1881
1882
  compressMode: CompressMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_COMPRESS_MODE))),
1883
+ unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
1882
1884
  branch: Schema$1.NullOr(TrimmedNonEmptyString),
1883
1885
  worktreePath: Schema$1.NullOr(TrimmedNonEmptyString),
1884
1886
  latestTurn: Schema$1.NullOr(OrchestrationLatestTurn),
@@ -2002,6 +2004,7 @@ const ThreadCreateCommand = Schema$1.Struct({
2002
2004
  runtimeMode: RuntimeMode,
2003
2005
  interactionMode: ProviderInteractionMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE))),
2004
2006
  compressMode: CompressMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_COMPRESS_MODE))),
2007
+ unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
2005
2008
  branch: Schema$1.NullOr(TrimmedNonEmptyString),
2006
2009
  worktreePath: Schema$1.NullOr(TrimmedNonEmptyString),
2007
2010
  createdAt: IsoDateTime
@@ -2075,6 +2078,18 @@ const ThreadCompressModeSetCommand = Schema$1.Struct({
2075
2078
  compressMode: CompressMode,
2076
2079
  createdAt: IsoDateTime
2077
2080
  });
2081
+ /**
2082
+ * No decoding default on the value, matching the compress command beside it: a
2083
+ * command is an instruction someone issued, and defaulting the very field it
2084
+ * exists to carry would turn a malformed dispatch into a silent write.
2085
+ */
2086
+ const ThreadUnpromptedSubagentsSetCommand = Schema$1.Struct({
2087
+ type: Schema$1.Literal("thread.unprompted-subagents.set"),
2088
+ commandId: CommandId,
2089
+ threadId: ThreadId,
2090
+ unpromptedSubagents: Schema$1.Boolean,
2091
+ createdAt: IsoDateTime
2092
+ });
2078
2093
  const ThreadTurnStartBootstrapCreateThread = Schema$1.Struct({
2079
2094
  projectId: ProjectId,
2080
2095
  title: TrimmedNonEmptyString,
@@ -2082,6 +2097,7 @@ const ThreadTurnStartBootstrapCreateThread = Schema$1.Struct({
2082
2097
  runtimeMode: RuntimeMode,
2083
2098
  interactionMode: ProviderInteractionMode,
2084
2099
  compressMode: CompressMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_COMPRESS_MODE))),
2100
+ unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
2085
2101
  branch: Schema$1.NullOr(TrimmedNonEmptyString),
2086
2102
  worktreePath: Schema$1.NullOr(TrimmedNonEmptyString),
2087
2103
  createdAt: IsoDateTime
@@ -2112,6 +2128,7 @@ const ThreadTurnStartCommand = Schema$1.Struct({
2112
2128
  runtimeMode: RuntimeMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE$2))),
2113
2129
  interactionMode: ProviderInteractionMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE))),
2114
2130
  compressMode: CompressMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_COMPRESS_MODE))),
2131
+ unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
2115
2132
  bootstrap: Schema$1.optional(ThreadTurnStartBootstrap),
2116
2133
  sourceProposedPlan: Schema$1.optional(SourceProposedPlanReference),
2117
2134
  createdAt: IsoDateTime
@@ -2131,6 +2148,7 @@ const ClientThreadTurnStartCommand = Schema$1.Struct({
2131
2148
  runtimeMode: RuntimeMode,
2132
2149
  interactionMode: ProviderInteractionMode,
2133
2150
  compressMode: CompressMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_COMPRESS_MODE))),
2151
+ unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
2134
2152
  bootstrap: Schema$1.optional(ThreadTurnStartBootstrap),
2135
2153
  sourceProposedPlan: Schema$1.optional(SourceProposedPlanReference),
2136
2154
  createdAt: IsoDateTime
@@ -2187,6 +2205,7 @@ const DispatchableClientOrchestrationCommand = Schema$1.Union([
2187
2205
  ThreadRuntimeModeSetCommand,
2188
2206
  ThreadInteractionModeSetCommand,
2189
2207
  ThreadCompressModeSetCommand,
2208
+ ThreadUnpromptedSubagentsSetCommand,
2190
2209
  ThreadTurnStartCommand,
2191
2210
  ThreadTurnInterruptCommand,
2192
2211
  ThreadApprovalRespondCommand,
@@ -2210,6 +2229,7 @@ const ClientOrchestrationCommand = Schema$1.Union([
2210
2229
  ThreadRuntimeModeSetCommand,
2211
2230
  ThreadInteractionModeSetCommand,
2212
2231
  ThreadCompressModeSetCommand,
2232
+ ThreadUnpromptedSubagentsSetCommand,
2213
2233
  ClientThreadTurnStartCommand,
2214
2234
  ThreadTurnInterruptCommand,
2215
2235
  ThreadApprovalRespondCommand,
@@ -2301,6 +2321,7 @@ const OrchestrationEventType = Schema$1.Literals([
2301
2321
  "thread.runtime-mode-set",
2302
2322
  "thread.interaction-mode-set",
2303
2323
  "thread.compress-mode-set",
2324
+ "thread.unprompted-subagents-set",
2304
2325
  "thread.message-sent",
2305
2326
  "thread.turn-start-requested",
2306
2327
  "thread.turn-interrupt-requested",
@@ -2351,6 +2372,7 @@ const ThreadCreatedPayload$1 = Schema$1.Struct({
2351
2372
  runtimeMode: RuntimeMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE$2))),
2352
2373
  interactionMode: ProviderInteractionMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE))),
2353
2374
  compressMode: CompressMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_COMPRESS_MODE))),
2375
+ unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
2354
2376
  branch: Schema$1.NullOr(TrimmedNonEmptyString),
2355
2377
  worktreePath: Schema$1.NullOr(TrimmedNonEmptyString),
2356
2378
  createdAt: IsoDateTime,
@@ -2413,6 +2435,16 @@ const ThreadCompressModeSetPayload$1 = Schema$1.Struct({
2413
2435
  compressMode: CompressMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_COMPRESS_MODE))),
2414
2436
  updatedAt: IsoDateTime
2415
2437
  });
2438
+ /**
2439
+ * One thread's subagent policy changed. Its own event rather than a field on
2440
+ * the compress one, for the reason every event here is narrow: replaying a
2441
+ * combined event would rewrite whichever half the user did not touch.
2442
+ */
2443
+ const ThreadUnpromptedSubagentsSetPayload$1 = Schema$1.Struct({
2444
+ threadId: ThreadId,
2445
+ unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
2446
+ updatedAt: IsoDateTime
2447
+ });
2416
2448
  const ThreadMessageSentPayload = Schema$1.Struct({
2417
2449
  threadId: ThreadId,
2418
2450
  messageId: MessageId,
@@ -2432,6 +2464,7 @@ const ThreadTurnStartRequestedPayload = Schema$1.Struct({
2432
2464
  runtimeMode: RuntimeMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE$2))),
2433
2465
  interactionMode: ProviderInteractionMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE))),
2434
2466
  compressMode: CompressMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_COMPRESS_MODE))),
2467
+ unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
2435
2468
  sourceProposedPlan: Schema$1.optional(SourceProposedPlanReference),
2436
2469
  createdAt: IsoDateTime
2437
2470
  });
@@ -2581,6 +2614,11 @@ const OrchestrationEvent = Schema$1.Union([
2581
2614
  type: Schema$1.Literal("thread.compress-mode-set"),
2582
2615
  payload: ThreadCompressModeSetPayload$1
2583
2616
  }),
2617
+ Schema$1.Struct({
2618
+ ...EventBaseFields,
2619
+ type: Schema$1.Literal("thread.unprompted-subagents-set"),
2620
+ payload: ThreadUnpromptedSubagentsSetPayload$1
2621
+ }),
2584
2622
  Schema$1.Struct({
2585
2623
  ...EventBaseFields,
2586
2624
  type: Schema$1.Literal("thread.message-sent"),
@@ -4587,7 +4625,8 @@ const ProviderSessionStartInput = Schema$1.Struct({
4587
4625
  approvalPolicy: Schema$1.optional(ProviderApprovalPolicy),
4588
4626
  sandboxMode: Schema$1.optional(ProviderSandboxMode),
4589
4627
  runtimeMode: RuntimeMode,
4590
- compressMode: Schema$1.optional(CompressMode)
4628
+ compressMode: Schema$1.optional(CompressMode),
4629
+ unpromptedSubagents: Schema$1.optional(Schema$1.Boolean)
4591
4630
  });
4592
4631
  const ProviderSendTurnInput = Schema$1.Struct({
4593
4632
  threadId: ThreadId,
@@ -4595,7 +4634,8 @@ const ProviderSendTurnInput = Schema$1.Struct({
4595
4634
  attachments: Schema$1.optional(Schema$1.Array(ChatAttachment).check(Schema$1.isMaxLength(8))),
4596
4635
  modelSelection: Schema$1.optional(ModelSelection),
4597
4636
  interactionMode: Schema$1.optional(ProviderInteractionMode),
4598
- compressMode: Schema$1.optional(CompressMode)
4637
+ compressMode: Schema$1.optional(CompressMode),
4638
+ unpromptedSubagents: Schema$1.optional(Schema$1.Boolean)
4599
4639
  });
4600
4640
  Schema$1.Struct({
4601
4641
  threadId: ThreadId,
@@ -5853,6 +5893,45 @@ const ServerSettings = Schema$1.Struct({
5853
5893
  * instruction can override an earlier general one.
5854
5894
  */
5855
5895
  enableUnpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
5896
+ /**
5897
+ * Whether the board offers the scoping agent: the Scope action on a task and
5898
+ * the panel that drafts a new one by interview.
5899
+ *
5900
+ * A switch rather than a permanent fixture because the discipline it imposes
5901
+ * — one question at a time, no file edits, a task body with verifiable
5902
+ * criteria — is a way of working, not a fact about the product. Someone who
5903
+ * writes their own tasks wants the button gone, not ignored.
5904
+ *
5905
+ * Off hides the entry points. It does not delete the skill file already
5906
+ * written into a repository, and it does not stop anyone invoking the same
5907
+ * discipline by name from an ordinary chat: that is what {@link disabledSkills}
5908
+ * is for.
5909
+ */
5910
+ enableScopingAgent: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
5911
+ /**
5912
+ * Whether the board offers the plan phase: splitting one scoped task into
5913
+ * ordered subtasks. Separate from {@link enableScopingAgent} because the two
5914
+ * are useful apart — scoping a task by interview says nothing about wanting
5915
+ * it decomposed, and a board that plans everything into five rows is a board
5916
+ * nobody reads.
5917
+ */
5918
+ enablePlanPhase: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
5919
+ /**
5920
+ * Whether the scoping and planning flows write their generated `SKILL.md`
5921
+ * into the repository they run in.
5922
+ *
5923
+ * Its own switch because it is the only one of the three with a side effect
5924
+ * outside p4code: the file lands in the working tree and shows up in
5925
+ * `git status`, which is unwelcome in a repo whose `.claude/` is checked in
5926
+ * and shared. Off does not cost the discipline — the thread seed carries the
5927
+ * rules inline instead, exactly as the create flow already does for providers
5928
+ * with no notion of a skill — it costs only invoking it by name later from an
5929
+ * ordinary chat.
5930
+ *
5931
+ * Files already written are left alone. Nothing here deletes anything from a
5932
+ * user's repository.
5933
+ */
5934
+ writeGeneratedSkillsToRepo: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
5856
5935
  automaticGitFetchInterval: Schema$1.DurationFromMillis.pipe(Schema$1.withDecodingDefault(Effect.succeed(Duration.toMillis(DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL)))),
5857
5936
  defaultThreadEnvMode: ThreadEnvMode.pipe(Schema$1.withDecodingDefault(Effect.succeed("local"))),
5858
5937
  newWorktreesStartFromOrigin: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
@@ -5988,6 +6067,9 @@ const ServerSettingsPatch = Schema$1.Struct({
5988
6067
  enableProviderUpdateChecks: Schema$1.optionalKey(Schema$1.Boolean),
5989
6068
  enableToolCallNarration: Schema$1.optionalKey(Schema$1.Boolean),
5990
6069
  enableUnpromptedSubagents: Schema$1.optionalKey(Schema$1.Boolean),
6070
+ enableScopingAgent: Schema$1.optionalKey(Schema$1.Boolean),
6071
+ enablePlanPhase: Schema$1.optionalKey(Schema$1.Boolean),
6072
+ writeGeneratedSkillsToRepo: Schema$1.optionalKey(Schema$1.Boolean),
5991
6073
  automaticGitFetchInterval: Schema$1.optionalKey(Schema$1.DurationFromMillis),
5992
6074
  defaultThreadEnvMode: Schema$1.optionalKey(ThreadEnvMode),
5993
6075
  newWorktreesStartFromOrigin: Schema$1.optionalKey(Schema$1.Boolean),
@@ -7263,6 +7345,21 @@ const TaskExternalRef = Schema$1.Struct({
7263
7345
  url: TrimmedNonEmptyString
7264
7346
  });
7265
7347
  const ExternalRefField = TaskExternalRef.pipe(Schema$1.annotateEncoded({ description: "The tracker issue this task mirrors, e.g. a Linear ticket. Null for a task that originated on this board." }));
7348
+ /**
7349
+ * The task this one was split out of.
7350
+ *
7351
+ * Carried as a field rather than as a sentence in the body because the plan
7352
+ * phase writes it and the board reads it: a parent chip, a subtask list and
7353
+ * "does this still have unfinished children" are all questions about the graph,
7354
+ * and prose cannot be asked them. Stored as the parent's UUID, but accepted as
7355
+ * either a UUID or a readable id on the way in - a planning agent works in
7356
+ * `P4-12`, and making it look up a UUID first is a round trip for nothing.
7357
+ *
7358
+ * One level is all this promises. Nothing forbids a subtask of a subtask, and
7359
+ * nothing walks the chain either; the board renders the direct children of a
7360
+ * task and stops.
7361
+ */
7362
+ const ParentTaskIdField = TaskId.pipe(Schema$1.annotateEncoded({ description: "The task this one is a subtask of - the parent's UUID or readable id (e.g. P4-12). Null for a task that is not part of a larger one." }));
7266
7363
  const LabelsField = Schema$1.Array(TaskLabel).check(Schema$1.isMaxLength(TASK_LABELS_MAX_COUNT)).pipe(Schema$1.annotateEncoded({ description: "Free-form tags used to group and filter tasks." }));
7267
7364
  const TaskIdField = TaskId.pipe(Schema$1.annotateEncoded({ description: "Id of the task to act on - the UUID or the readable id (e.g. P4-12)." }));
7268
7365
  const Task = Schema$1.Struct({
@@ -7311,6 +7408,13 @@ const Task = Schema$1.Struct({
7311
7408
  * `null` rather than failing the whole board.
7312
7409
  */
7313
7410
  externalRef: Schema$1.NullOr(TaskExternalRef).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7411
+ /**
7412
+ * The task this one was split out of, always as a UUID once stored - a
7413
+ * readable id given on the way in is resolved before the row is written, so
7414
+ * the link survives a board where readable ids were allocated elsewhere.
7415
+ * Nullable-with-default for the same version-skew reason as `readableId`.
7416
+ */
7417
+ parentTaskId: Schema$1.NullOr(TaskId).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7314
7418
  title: TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_TITLE_MAX_LENGTH)),
7315
7419
  body: Schema$1.String,
7316
7420
  status: StoredStatusField,
@@ -7325,6 +7429,7 @@ const TaskCreateInput = Schema$1.Struct({
7325
7429
  repositoryKey: Schema$1.optional(RepositoryKeyField),
7326
7430
  threadId: Schema$1.optional(ThreadIdField),
7327
7431
  externalRef: Schema$1.optional(ExternalRefField),
7432
+ parentTaskId: Schema$1.optional(ParentTaskIdField),
7328
7433
  title: TitleField,
7329
7434
  body: Schema$1.optional(BodyField),
7330
7435
  status: Schema$1.optional(StatusField),
@@ -7364,6 +7469,7 @@ const TaskUpdateInput = Schema$1.Struct({
7364
7469
  repositoryKey: Schema$1.optional(Schema$1.NullOr(RepositoryKeyField)),
7365
7470
  threadId: Schema$1.optional(Schema$1.NullOr(ThreadIdField)),
7366
7471
  externalRef: Schema$1.optional(Schema$1.NullOr(ExternalRefField)),
7472
+ parentTaskId: Schema$1.optional(Schema$1.NullOr(ParentTaskIdField)),
7367
7473
  title: Schema$1.optional(TitleField),
7368
7474
  body: Schema$1.optional(BodyField),
7369
7475
  status: Schema$1.optional(StatusField),
@@ -7783,22 +7889,44 @@ var TicketResolveError = class extends Schema$1.TaggedErrorClass()("TicketResolv
7783
7889
  //#endregion
7784
7890
  //#region ../../packages/contracts/src/threadControl.ts
7785
7891
  /**
7786
- * Thread control over MCP - the self-scoped toolkit an agent uses to act on
7787
- * the thread it is running in.
7788
- *
7789
- * Deliberately narrow. The reported need was an agent renaming its own thread;
7790
- * that needs no cross-thread reach, and the per-session bearer token is the
7791
- * only thing guarding `/mcp` on a remote-reachable server, so a capability
7792
- * that could rename arbitrary threads would be a capability to vandalise the
7793
- * workspace from one leaked token. Delete and create are withheld outright,
7794
- * the same answer the task toolkit gave. Settle is absent for a structural
7795
- * reason rather than a policy one: the decider refuses `thread.settle` while
7796
- * the thread's session is starting or running, and a self-scoped MCP call
7797
- * always arrives from its own live session, so the call could never succeed.
7892
+ * Thread control over MCP - the toolkit an agent uses to act on its own thread
7893
+ * and on the threads it started.
7894
+ *
7895
+ * **The scope rule.** A per-session bearer token is the only thing guarding
7896
+ * `/mcp` on a remote-reachable server, so reach is granted, never assumed. A
7897
+ * session may address its own thread and the threads it created through
7898
+ * `thread_spawn`, and nothing else. A leaked token therefore still reaches only
7899
+ * work that token itself began. The grant is held on the credential rather than
7900
+ * derived from the board, so it cannot widen because someone edited a task.
7901
+ *
7902
+ * **Delete is not exposed, and that is the decision rather than an omission.**
7903
+ * It is the one irreversible action here and a thread carries its whole
7904
+ * transcript. The task toolkit gave the same answer for the same reason, and a
7905
+ * thread is the more expensive thing to lose. An agent that wants a thread gone
7906
+ * settles it and says so; a person deletes it.
7907
+ *
7908
+ * **Spawning is one level deep.** `thread_spawn` seeds a first message and
7909
+ * starts the turn, because a create that cannot say anything is not the feature
7910
+ * anyone asked for. A thread that was itself spawned may not spawn: the depth
7911
+ * cap is what a runaway loop runs into, and one level is enough for "split this
7912
+ * task and start the pieces" while still being a structural bound rather than a
7913
+ * prompt asking nicely.
7914
+ *
7915
+ * **Settle is queued rather than refused.** The decider refuses `thread.settle`
7916
+ * while the target's session is starting or running, and an MCP call from a
7917
+ * thread's own session always arrives while that session is alive - so the
7918
+ * self-addressed case could never succeed as an immediate command. It is
7919
+ * recorded instead and applied when the session stops, which is what an agent
7920
+ * settling its own thread means anyway: finish, then close.
7798
7921
  *
7799
7922
  * Quick-append to memory lives here too: it is the other thing an agent wants
7800
7923
  * to do *from* a thread, and it shares the toolkit's capability and scope.
7801
7924
  *
7925
+ * Compressing an asset joins them for a different reason. It is a write the
7926
+ * agent could perform with its ordinary file tools, and the point of routing it
7927
+ * through a tool instead is that the server gets to check the rewrite before it
7928
+ * lands - and to keep the original somewhere the rewrite cannot reach.
7929
+ *
7802
7930
  * @module contracts/threadControl
7803
7931
  */
7804
7932
  const ThreadRenameInput = Schema$1.Struct({ title: TrimmedNonEmptyString.annotate({ description: "The new thread title. Replaces the current one." }) });
@@ -7807,6 +7935,64 @@ const ThreadRenameResult = Schema$1.Struct({
7807
7935
  title: TrimmedNonEmptyString
7808
7936
  });
7809
7937
  /**
7938
+ * Which thread a control tool acts on.
7939
+ *
7940
+ * Optional, and absent means the caller's own thread. That default is what
7941
+ * keeps the common case honest: an agent adjusting itself never has to name an
7942
+ * id, so it cannot name the wrong one, and every tool below reads the same way
7943
+ * whether or not it is addressed elsewhere.
7944
+ */
7945
+ const ControlTargetThreadId = Schema$1.optional(ThreadId.annotate({ description: "The thread to act on. Omit for this session's own thread. Only threads this session started with thread_spawn can be named here; any other id is refused." }));
7946
+ const ThreadSpawnInput = Schema$1.Struct({
7947
+ title: TrimmedNonEmptyString.annotate({ description: "Title for the new thread. Shown on the board and in the sidebar." }),
7948
+ prompt: TrimmedNonEmptyString.annotate({ description: "The first message to send. The thread starts its turn immediately, so this is the whole brief the new agent gets." }),
7949
+ projectId: Schema$1.optional(ProjectId.annotate({ description: "Project to start the thread in. Defaults to this session's own project." })),
7950
+ runtimeMode: Schema$1.optional(RuntimeMode.annotate({ description: "Permission mode for the new thread. Defaults to this session's own." })),
7951
+ interactionMode: Schema$1.optional(ProviderInteractionMode),
7952
+ compressMode: Schema$1.optional(CompressMode),
7953
+ unpromptedSubagents: Schema$1.optional(Schema$1.Boolean)
7954
+ });
7955
+ const ThreadSpawnResult = Schema$1.Struct({
7956
+ /** Watchable with `thread_watch_events`, and addressable by every tool here. */
7957
+ threadId: ThreadId,
7958
+ projectId: ProjectId,
7959
+ title: TrimmedNonEmptyString
7960
+ });
7961
+ const ThreadConfigureInput = Schema$1.Struct({
7962
+ threadId: ControlTargetThreadId,
7963
+ title: Schema$1.optional(TrimmedNonEmptyString),
7964
+ modelSelection: Schema$1.optional(ModelSelection),
7965
+ runtimeMode: Schema$1.optional(RuntimeMode),
7966
+ interactionMode: Schema$1.optional(ProviderInteractionMode),
7967
+ compressMode: Schema$1.optional(CompressMode),
7968
+ unpromptedSubagents: Schema$1.optional(Schema$1.Boolean)
7969
+ });
7970
+ const ThreadConfigureResult = Schema$1.Struct({
7971
+ threadId: ThreadId,
7972
+ /** The settings that were actually changed, in the order they were applied. */
7973
+ applied: Schema$1.Array(Schema$1.String)
7974
+ });
7975
+ const ThreadSettleInput = Schema$1.Struct({ threadId: ControlTargetThreadId });
7976
+ /**
7977
+ * Settled now, or recorded to settle when the session stops.
7978
+ *
7979
+ * Two outcomes rather than one because the difference is the agent's to know: a
7980
+ * queued settle has not happened yet, and a thread that goes on to do more work
7981
+ * still settles at the end of it.
7982
+ */
7983
+ const ThreadSettleResult = Schema$1.Struct({
7984
+ threadId: ThreadId,
7985
+ outcome: Schema$1.Literals(["settled", "queued"])
7986
+ });
7987
+ const ThreadSnoozeInput = Schema$1.Struct({
7988
+ threadId: ControlTargetThreadId,
7989
+ snoozedUntil: IsoDateTime.annotate({ description: "When the thread should come back, as an ISO 8601 timestamp." })
7990
+ });
7991
+ const ThreadSnoozeResult = Schema$1.Struct({
7992
+ threadId: ThreadId,
7993
+ snoozedUntil: IsoDateTime
7994
+ });
7995
+ /**
7810
7996
  * Which memory file an append lands in.
7811
7997
  *
7812
7998
  * A closed union rather than a filename, because the target directory also
@@ -7825,6 +8011,23 @@ const MemoryAppendResult = Schema$1.Struct({
7825
8011
  /** Absolute path the append landed in, so the agent can cite or inspect it. */
7826
8012
  path: Schema$1.String
7827
8013
  });
8014
+ const AssetCompressInput = Schema$1.Struct({
8015
+ path: TrimmedNonEmptyString.annotate({ description: "Absolute path of the asset file to overwrite. Must sit inside one of this machine's agent asset directories: user skills, the provider config directory that holds the memory files, or the subagent definitions directory." }),
8016
+ content: Schema$1.String.check(Schema$1.isNonEmpty()).annotate({ description: "The compressed file, whole. Headings, fenced code blocks, inline code, URLs, paths and any YAML frontmatter must come back unchanged; only the prose around them may be rewritten." })
8017
+ });
8018
+ const AssetCompressResult = Schema$1.Struct({
8019
+ /** Absolute path that was rewritten. */
8020
+ path: Schema$1.String,
8021
+ /** Where the pre-compression original was kept, outside every synced directory. */
8022
+ backupPath: Schema$1.String,
8023
+ charsBefore: Schema$1.Number,
8024
+ charsAfter: Schema$1.Number,
8025
+ /** Estimated at four characters per token, not measured by a provider tokenizer. */
8026
+ estimatedTokensBefore: Schema$1.Number,
8027
+ estimatedTokensAfter: Schema$1.Number,
8028
+ /** Differences worth reporting that were not worth refusing the write over. */
8029
+ warnings: Schema$1.Array(Schema$1.String)
8030
+ });
7828
8031
  var ThreadToolUnavailableError = class extends Schema$1.TaggedErrorClass()("ThreadToolUnavailableError", {
7829
8032
  capability: Schema$1.Literal("threads"),
7830
8033
  environmentId: EnvironmentId,
@@ -7853,10 +8056,77 @@ var MemoryAppendFailedError = class extends Schema$1.TaggedErrorClass()("MemoryA
7853
8056
  return `Appending to ${this.file} failed: ${this.detail}`;
7854
8057
  }
7855
8058
  };
8059
+ /**
8060
+ * The compression will not be attempted, or the file could not be read.
8061
+ *
8062
+ * Separate from the validation failure below because they want opposite
8063
+ * responses. A refusal is final - the path is not an asset, the name looks like
8064
+ * a credential, the file is not there - and retrying it produces the same
8065
+ * answer. A validation failure is a rewrite that can be corrected.
8066
+ */
8067
+ var AssetCompressRefusedError = class extends Schema$1.TaggedErrorClass()("AssetCompressRefusedError", {
8068
+ path: Schema$1.String,
8069
+ detail: Schema$1.String
8070
+ }) {
8071
+ get message() {
8072
+ return `Compressing ${this.path} was refused: ${this.detail}`;
8073
+ }
8074
+ };
8075
+ /**
8076
+ * The rewrite lost something, and nothing was written.
8077
+ *
8078
+ * Carries the checks that failed rather than a summary, because the agent's
8079
+ * next move is to restore exactly those things from the original and call
8080
+ * again. The file on disk is untouched, so a second failure costs nothing more.
8081
+ */
8082
+ var AssetCompressValidationFailedError = class extends Schema$1.TaggedErrorClass()("AssetCompressValidationFailedError", {
8083
+ path: Schema$1.String,
8084
+ errors: Schema$1.Array(Schema$1.String)
8085
+ }) {
8086
+ get message() {
8087
+ return `The compressed ${this.path} did not validate, so nothing was written: ${this.errors.join(" ")}`;
8088
+ }
8089
+ };
8090
+ /**
8091
+ * The named thread is outside this session's grant.
8092
+ *
8093
+ * Deliberately the same answer whether the thread does not exist or exists and
8094
+ * belongs to someone else: a caller that could tell those apart could map the
8095
+ * whole board one id at a time.
8096
+ */
8097
+ var ThreadControlNotPermittedError = class extends Schema$1.TaggedErrorClass()("ThreadControlNotPermittedError", { threadId: ThreadId }) {
8098
+ get message() {
8099
+ return `This session cannot act on thread ${this.threadId}. A session may act on its own thread and on threads it started with thread_spawn.`;
8100
+ }
8101
+ };
8102
+ /** A spawned thread asked to spawn. One level is the whole allowance. */
8103
+ var ThreadSpawnNotPermittedError = class extends Schema$1.TaggedErrorClass()("ThreadSpawnNotPermittedError", {
8104
+ threadId: ThreadId,
8105
+ detail: Schema$1.String
8106
+ }) {
8107
+ get message() {
8108
+ return `Thread ${this.threadId} cannot start another thread: ${this.detail}`;
8109
+ }
8110
+ };
8111
+ /** The orchestration engine declined or failed a thread control command. */
8112
+ var ThreadControlRejectedError = class extends Schema$1.TaggedErrorClass()("ThreadControlRejectedError", {
8113
+ threadId: ThreadId,
8114
+ commandType: Schema$1.String,
8115
+ detail: Schema$1.String
8116
+ }) {
8117
+ get message() {
8118
+ return `${this.commandType} on thread ${this.threadId} failed: ${this.detail}`;
8119
+ }
8120
+ };
7856
8121
  const ThreadControlToolError = Schema$1.Union([
7857
8122
  ThreadToolUnavailableError,
8123
+ ThreadControlNotPermittedError,
8124
+ ThreadSpawnNotPermittedError,
8125
+ ThreadControlRejectedError,
7858
8126
  ThreadRenameRejectedError,
7859
- MemoryAppendFailedError
8127
+ MemoryAppendFailedError,
8128
+ AssetCompressRefusedError,
8129
+ AssetCompressValidationFailedError
7860
8130
  ]);
7861
8131
  const ThreadWatchEventsInput = Schema$1.Struct({
7862
8132
  threadId: ThreadId.annotate({ description: "The watched thread to read events from. Must be explicitly granted." }),
@@ -12660,6 +12930,53 @@ var _040_TaskExternalRef_default = Effect.gen(function* () {
12660
12930
  `;
12661
12931
  });
12662
12932
  //#endregion
12933
+ //#region src/persistence/Migrations/041_TaskParentTaskId.ts
12934
+ /**
12935
+ * The task a task was split out of, for the plan phase's subtasks.
12936
+ *
12937
+ * A plain column rather than a join table: a task has at most one parent, and
12938
+ * the question asked of it is always "children of this row", which one indexed
12939
+ * column answers.
12940
+ *
12941
+ * No foreign key. Tasks are synced between machines and a child can arrive
12942
+ * before its parent does, or outlive it - a constraint would reject the write
12943
+ * rather than leave a link that resolves later. The board treats a parent it
12944
+ * cannot find as no parent at all.
12945
+ *
12946
+ * The index is non-unique and partial: many children per parent, and the many
12947
+ * rows that are nobody's child stay out of it.
12948
+ */
12949
+ var _041_TaskParentTaskId_default = Effect.gen(function* () {
12950
+ const sql = yield* SqlClient.SqlClient;
12951
+ yield* sql`ALTER TABLE tasks ADD COLUMN parent_task_id TEXT`;
12952
+ yield* sql`
12953
+ CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id
12954
+ ON tasks(parent_task_id)
12955
+ WHERE parent_task_id IS NOT NULL
12956
+ `;
12957
+ });
12958
+ //#endregion
12959
+ //#region src/persistence/Migrations/042_ProjectionThreadsUnpromptedSubagents.ts
12960
+ /**
12961
+ * Whether a thread lets the model spawn subagents without being asked, moved
12962
+ * from a machine-wide setting onto the thread itself.
12963
+ *
12964
+ * Stored as an integer beside `has_actionable_proposed_plan` rather than as a
12965
+ * SQLite boolean, which does not exist; the projection reads it back as
12966
+ * `> 0`.
12967
+ *
12968
+ * The default is `1` — subagents allowed — because that is what every existing
12969
+ * thread has been doing under the old server setting. Anything else would
12970
+ * change the behaviour of live threads on upgrade, which is not a migration's
12971
+ * business.
12972
+ */
12973
+ var _042_ProjectionThreadsUnpromptedSubagents_default = Effect.gen(function* () {
12974
+ yield* (yield* SqlClient.SqlClient)`
12975
+ ALTER TABLE projection_threads
12976
+ ADD COLUMN unprompted_subagents INTEGER NOT NULL DEFAULT 1
12977
+ `;
12978
+ });
12979
+ //#endregion
12663
12980
  //#region src/persistence/Migrations.ts
12664
12981
  /**
12665
12982
  * MigrationsLive - Migration runner with inline loader
@@ -12880,6 +13197,16 @@ const migrationEntries = [
12880
13197
  40,
12881
13198
  "TaskExternalRef",
12882
13199
  _040_TaskExternalRef_default
13200
+ ],
13201
+ [
13202
+ 41,
13203
+ "TaskParentTaskId",
13204
+ _041_TaskParentTaskId_default
13205
+ ],
13206
+ [
13207
+ 42,
13208
+ "ProjectionThreadsUnpromptedSubagents",
13209
+ _042_ProjectionThreadsUnpromptedSubagents_default
12883
13210
  ]
12884
13211
  ];
12885
13212
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -14511,6 +14838,7 @@ const TASK_COLUMNS = `
14511
14838
  repository_key AS "repositoryKey",
14512
14839
  thread_id AS "threadId",
14513
14840
  external_ref_json AS "externalRef",
14841
+ parent_task_id AS "parentTaskId",
14514
14842
  title,
14515
14843
  body,
14516
14844
  status,
@@ -14535,6 +14863,7 @@ const makeTaskRepository = Effect.gen(function* () {
14535
14863
  repository_key,
14536
14864
  thread_id,
14537
14865
  external_ref_json,
14866
+ parent_task_id,
14538
14867
  title,
14539
14868
  body,
14540
14869
  status,
@@ -14552,6 +14881,7 @@ const makeTaskRepository = Effect.gen(function* () {
14552
14881
  ${row.repositoryKey},
14553
14882
  ${row.threadId},
14554
14883
  ${row.externalRef === null ? null : JSON.stringify(row.externalRef)},
14884
+ ${row.parentTaskId},
14555
14885
  ${row.title},
14556
14886
  ${row.body},
14557
14887
  ${row.status},
@@ -14569,6 +14899,7 @@ const makeTaskRepository = Effect.gen(function* () {
14569
14899
  repository_key = excluded.repository_key,
14570
14900
  thread_id = excluded.thread_id,
14571
14901
  external_ref_json = excluded.external_ref_json,
14902
+ parent_task_id = excluded.parent_task_id,
14572
14903
  title = excluded.title,
14573
14904
  body = excluded.body,
14574
14905
  status = excluded.status,
@@ -14668,6 +14999,7 @@ const makeTaskRepository = Effect.gen(function* () {
14668
14999
  ...input.repositoryKey !== void 0 ? { repositoryKey: input.repositoryKey } : {},
14669
15000
  ...input.threadId !== void 0 ? { threadId: input.threadId } : {},
14670
15001
  ...input.externalRef !== void 0 ? { externalRef: input.externalRef } : {},
15002
+ ...input.parentTaskId !== void 0 ? { parentTaskId: input.parentTaskId } : {},
14671
15003
  ...input.title !== void 0 ? { title: input.title } : {},
14672
15004
  ...input.body !== void 0 ? { body: input.body } : {},
14673
15005
  ...input.status !== void 0 ? { status: input.status } : {},
@@ -14706,6 +15038,8 @@ const makeTaskRepository = Effect.gen(function* () {
14706
15038
  ...input.patch.projectId !== void 0 ? { projectId: input.patch.projectId } : {},
14707
15039
  ...input.patch.repositoryKey !== void 0 ? { repositoryKey: input.patch.repositoryKey } : {},
14708
15040
  ...input.patch.threadId !== void 0 ? { threadId: input.patch.threadId } : {},
15041
+ ...input.patch.externalRef !== void 0 ? { externalRef: input.patch.externalRef } : {},
15042
+ ...input.patch.parentTaskId !== void 0 ? { parentTaskId: input.patch.parentTaskId } : {},
14709
15043
  ...input.patch.title !== void 0 ? { title: input.patch.title } : {},
14710
15044
  ...input.patch.body !== void 0 ? { body: input.patch.body } : {},
14711
15045
  ...input.patch.status !== void 0 ? { status: input.patch.status } : {},
@@ -14840,6 +15174,7 @@ const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Ef
14840
15174
  repositoryKey: input.repositoryKey ?? null,
14841
15175
  threadId: input.threadId ?? null,
14842
15176
  externalRef: input.externalRef ?? null,
15177
+ parentTaskId: input.parentTaskId ?? null,
14843
15178
  title: input.title,
14844
15179
  body: input.body ?? "",
14845
15180
  status: input.status ?? "backlog",
@@ -15159,6 +15494,22 @@ var _006_TaskExternalRef_default = Effect.gen(function* () {
15159
15494
  `;
15160
15495
  });
15161
15496
  //#endregion
15497
+ //#region src/hub/Migrations/007_TaskParentTaskId.ts
15498
+ /**
15499
+ * The task a task was split out of, mirroring the environment server's
15500
+ * migration 041. The hub carries the column so a plan filed on one machine
15501
+ * still reads as a plan on another, rather than as a handful of loose tasks.
15502
+ */
15503
+ var _007_TaskParentTaskId_default = Effect.gen(function* () {
15504
+ const sql = yield* SqlClient.SqlClient;
15505
+ yield* sql`ALTER TABLE tasks ADD COLUMN parent_task_id TEXT`;
15506
+ yield* sql`
15507
+ CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id
15508
+ ON tasks(parent_task_id)
15509
+ WHERE parent_task_id IS NOT NULL
15510
+ `;
15511
+ });
15512
+ //#endregion
15162
15513
  //#region src/hub/Migrations.ts
15163
15514
  /**
15164
15515
  * Hub migrations.
@@ -15199,6 +15550,11 @@ const hubMigrationEntries = [
15199
15550
  6,
15200
15551
  "TaskExternalRef",
15201
15552
  _006_TaskExternalRef_default
15553
+ ],
15554
+ [
15555
+ 7,
15556
+ "TaskParentTaskId",
15557
+ _007_TaskParentTaskId_default
15202
15558
  ]
15203
15559
  ];
15204
15560
  const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -16714,7 +17070,7 @@ const isIgnoredEntry = (entry) => IGNORED_ENTRIES.has(entry) || entry.startsWith
16714
17070
  * Exported so the single-file kinds ask it the same way this one does, rather
16715
17071
  * than each keeping a copy that could drift into a different answer.
16716
17072
  */
16717
- const looksBinary$1 = (content) => content.includes("\0");
17073
+ const looksBinary$2 = (content) => content.includes("\0");
16718
17074
  /**
16719
17075
  * Read one directory as an asset's files, or say why it cannot be one.
16720
17076
  *
@@ -16746,7 +17102,7 @@ const readSkillFiles = Effect.fn("skillDirectory.readSkillFiles")(function* (roo
16746
17102
  if (files.length >= 512) return "too-many-files";
16747
17103
  const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
16748
17104
  if (content === void 0) return "unreadable";
16749
- if (looksBinary$1(content)) return "binary-file";
17105
+ if (looksBinary$2(content)) return "binary-file";
16750
17106
  if (content.length > 524288) return "file-too-large";
16751
17107
  files.push({
16752
17108
  path: relative,
@@ -16870,7 +17226,7 @@ const readAgentDefinitions = Effect.fn("agentDefinitionFiles.read")(function* (a
16870
17226
  });
16871
17227
  continue;
16872
17228
  }
16873
- if (looksBinary$1(content)) {
17229
+ if (looksBinary$2(content)) {
16874
17230
  refused.push({
16875
17231
  name: entry,
16876
17232
  reason: "binary-file"
@@ -17392,7 +17748,7 @@ const isMemoryDirectoryName = (name) => MEMORY_DIRECTORY_NAMES.includes(name);
17392
17748
  */
17393
17749
  const isMemoryAssetName = (name) => isMemoryFileName(name) || isMemoryDirectoryName(name);
17394
17750
  /** The same test the skill reader uses: a NUL is what breaks a JSON text column. */
17395
- const looksBinary = (content) => content.includes("\0");
17751
+ const looksBinary$1 = (content) => content.includes("\0");
17396
17752
  /**
17397
17753
  * Read whichever memory files exist in this machine's config directory.
17398
17754
  *
@@ -17416,7 +17772,7 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
17416
17772
  });
17417
17773
  continue;
17418
17774
  }
17419
- if (looksBinary(content)) {
17775
+ if (looksBinary$1(content)) {
17420
17776
  refused.push({
17421
17777
  name,
17422
17778
  reason: "binary-file"
@@ -18529,6 +18885,8 @@ Never drop not/never/no/only/except: flipping the meaning costs more than any to
18529
18885
 
18530
18886
  Tool calls: fire direct. No plan, no progress note between calls, never announce the next call. If the harness asks for a short lead-in before tool calls, write that one line and nothing more. Otherwise text before a call only to clarify, warn about a security or irreversible action, or resolve ambiguity.
18531
18887
 
18888
+ Subagent and tool output is raw material, not the answer. Never relay a subagent report, a file listing, or command output verbatim - state the conclusion it supports and cite the file and line that back it. Its length is its own and is never a licence to answer at that length. Quote from it only where the exact wording is the point.
18889
+
18532
18890
  Preserve the user's dominant language: reply in the language the user writes, whatever language the examples or the surrounding context use. Compress the style, not the language. Every emitted line in that language, lead-in lines included, not just the final reply. Always keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim unless the user explicitly asks for translation.
18533
18891
 
18534
18892
  Dropping articles applies to article languages only. Where small markers carry case or role (particles, postpositions), keep them: grammar, not filler. Compress politeness and filler instead.
@@ -18622,6 +18980,16 @@ function compressTurnReminderFor(mode) {
18622
18980
  */
18623
18981
  const PRESET_MAX_RESULT_ROWS = 20;
18624
18982
  /**
18983
+ * The tool calls a preset run is allowed.
18984
+ *
18985
+ * Fifteen because a located answer typically takes six to ten, and the runs
18986
+ * that overshoot do not overshoot slightly: measured against real transcripts,
18987
+ * the long tail sits between sixty and a hundred and thirty calls and carries
18988
+ * nearly all of the cost. A cap that a good run never reaches is a cap that only
18989
+ * ever bites the runs that had stopped making progress.
18990
+ */
18991
+ const PRESET_MAX_TOOL_CALLS = 15;
18992
+ /**
18625
18993
  * Named `p4-*` on purpose.
18626
18994
  *
18627
18995
  * The directory is shared with whatever the user has written by hand, and
@@ -18651,6 +19019,34 @@ names a path that is not there, answer with the BLOCKED line below. Guessing at
18651
19019
  the missing half of a request produces an answer that reads as confident and is
18652
19020
  not checkable, which is worse than no answer.`;
18653
19021
  /**
19022
+ * What every preset says about the run in between.
19023
+ *
19024
+ * Three rules, each aimed at a measured failure. The cap bounds the quadratic.
19025
+ * The ban on nesting stops a bounded run from spawning an unbounded one, which
19026
+ * is the same bill again with nothing watching it. The stop-at-the-cap clause is
19027
+ * what makes the number safe to enforce: without somewhere to put a partial
19028
+ * answer, a capped subagent either grinds past the cap or throws away what it
19029
+ * had, and both are worse than a short table with a named gap.
19030
+ */
19031
+ const WORK_BUDGET = `## What the run costs
19032
+
19033
+ At most ${PRESET_MAX_TOOL_CALLS} tool calls. Most answers of this kind take six
19034
+ to ten, so treat the cap as the point where something has gone wrong rather than
19035
+ as a target to spend.
19036
+
19037
+ Spend them on calls that can fail usefully: before each one, know what a hit and
19038
+ a miss would each tell you. Batch searches into one call separated by echoed
19039
+ markers rather than issuing them one at a time, bound every output with a head
19040
+ or a line count, and read a file only after a search has told you which lines
19041
+ matter, and read those lines. Never repeat a call you have already made.
19042
+
19043
+ Do not spawn a subagent of your own. You are the bounded run; one that starts
19044
+ another has spent the budget twice with nothing watching the second half.
19045
+
19046
+ Reaching the cap ends the run. Return the rows you have verified, and add a
19047
+ final line naming the search you would have run next. With no verified rows at
19048
+ all, that is the BLOCKED line.`;
19049
+ /**
18654
19050
  * What every preset says about the answer.
18655
19051
  *
18656
19052
  * The two sentinels matter as much as the table. Without `NONE` a clean run
@@ -18669,7 +19065,8 @@ const outputContract = (row, extra) => [
18669
19065
  ` ${row}`,
18670
19066
  "",
18671
19067
  "Nothing to report: the single word NONE.",
18672
- "Request insufficient: BLOCKED - <what is missing>, on one line.",
19068
+ "Request insufficient, or the budget spent before anything was verified:",
19069
+ "BLOCKED - <what is missing>, on one line.",
18673
19070
  "",
18674
19071
  "Paths are repository-relative and carry a line number. A row without one is",
18675
19072
  "a row you have not verified: open the file and get it.",
@@ -18681,6 +19078,11 @@ const outputContract = (row, extra) => [
18681
19078
  * The compression ruleset goes last so the contract above it is read first.
18682
19079
  * Both are instructions to the same model and neither overrides the other:
18683
19080
  * the contract fixes the shape, the ruleset fixes the prose inside it.
19081
+ *
19082
+ * `model` is optional, and omitting it inherits the session's. Naming a cheaper
19083
+ * tier is worth it only where the job is mechanical enough that the tier is not
19084
+ * what makes the answer right, which is true of locating a symbol and is not
19085
+ * true of deciding whether a line is a bug.
18684
19086
  */
18685
19087
  const buildPreset = (input) => {
18686
19088
  const name = `${PRESET_NAME_PREFIX}${input.name}`;
@@ -18689,6 +19091,7 @@ const buildPreset = (input) => {
18689
19091
  `name: ${name}`,
18690
19092
  `description: ${JSON.stringify(input.description)}`,
18691
19093
  `tools: ${input.tools}`,
19094
+ ...input.model === void 0 ? [] : [`model: ${input.model}`],
18692
19095
  "---",
18693
19096
  "",
18694
19097
  input.body.trim(),
@@ -18711,12 +19114,15 @@ const AGENT_PRESETS = [buildPreset({
18711
19114
  name: "investigator",
18712
19115
  description: "Find where something lives in the codebase. Use when the answer is a set of locations rather than a change. Returns a fixed table of path:line rows and nothing else.",
18713
19116
  tools: "Read, Grep, Glob, Bash",
19117
+ model: "sonnet",
18714
19118
  summary: "Locates code and answers with a path:line table.",
18715
19119
  body: `You locate code. You do not change it, and you say no more about any location
18716
19120
  than the one clause that explains why it is in the list.
18717
19121
 
18718
19122
  ${INPUT_CONTRACT}
18719
19123
 
19124
+ ${WORK_BUDGET}
19125
+
18720
19126
  ${outputContract("path:line - symbol - one clause on why it matters", [
18721
19127
  "",
18722
19128
  "Symbol is the function, type or constant the line belongs to, not a paraphrase",
@@ -18734,6 +19140,8 @@ approval is a list that has to be re-read to find the three rows that matter.
18734
19140
 
18735
19141
  ${INPUT_CONTRACT}
18736
19142
 
19143
+ ${WORK_BUDGET}
19144
+
18737
19145
  ${outputContract("path:line - severity - what is wrong, and what it breaks", [
18738
19146
  "",
18739
19147
  "Severity is one of three words:",
@@ -19788,6 +20196,7 @@ const ThreadMetaUpdatedPayload = ThreadMetaUpdatedPayload$1;
19788
20196
  const ThreadRuntimeModeSetPayload = ThreadRuntimeModeSetPayload$1;
19789
20197
  const ThreadInteractionModeSetPayload = ThreadInteractionModeSetPayload$1;
19790
20198
  const ThreadCompressModeSetPayload = ThreadCompressModeSetPayload$1;
20199
+ const ThreadUnpromptedSubagentsSetPayload = ThreadUnpromptedSubagentsSetPayload$1;
19791
20200
  const ThreadDeletedPayload = ThreadDeletedPayload$1;
19792
20201
  const ThreadUnarchivedPayload = ThreadUnarchivedPayload$1;
19793
20202
  const ThreadUnsettledPayload = ThreadUnsettledPayload$1;
@@ -19928,6 +20337,7 @@ function projectEvent(model, event) {
19928
20337
  runtimeMode: payload.runtimeMode,
19929
20338
  interactionMode: payload.interactionMode,
19930
20339
  compressMode: payload.compressMode,
20340
+ unpromptedSubagents: payload.unpromptedSubagents,
19931
20341
  branch: payload.branch,
19932
20342
  worktreePath: payload.worktreePath,
19933
20343
  latestTurn: null,
@@ -20034,6 +20444,13 @@ function projectEvent(model, event) {
20034
20444
  updatedAt: payload.updatedAt
20035
20445
  })
20036
20446
  })));
20447
+ case "thread.unprompted-subagents-set": return decodeForEvent(ThreadUnpromptedSubagentsSetPayload, event.payload, event.type, "payload").pipe(Effect.map((payload) => ({
20448
+ ...nextBase,
20449
+ threads: updateThread(nextBase.threads, payload.threadId, {
20450
+ unpromptedSubagents: payload.unpromptedSubagents,
20451
+ updatedAt: payload.updatedAt
20452
+ })
20453
+ })));
20037
20454
  case "thread.message-sent": return Effect.gen(function* () {
20038
20455
  const payload = yield* decodeForEvent(MessageSentPayloadSchema, event.payload, event.type, "payload");
20039
20456
  const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
@@ -20400,6 +20817,7 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
20400
20817
  runtimeMode: command.runtimeMode,
20401
20818
  interactionMode: command.interactionMode,
20402
20819
  compressMode: command.compressMode,
20820
+ unpromptedSubagents: command.unpromptedSubagents,
20403
20821
  branch: command.branch,
20404
20822
  worktreePath: command.worktreePath,
20405
20823
  createdAt: command.createdAt,
@@ -20679,6 +21097,28 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
20679
21097
  }
20680
21098
  };
20681
21099
  }
21100
+ case "thread.unprompted-subagents.set": {
21101
+ yield* requireThread({
21102
+ readModel,
21103
+ command,
21104
+ threadId: command.threadId
21105
+ });
21106
+ const occurredAt = yield* nowIso$8;
21107
+ return {
21108
+ ...yield* withEventBase({
21109
+ aggregateKind: "thread",
21110
+ aggregateId: command.threadId,
21111
+ occurredAt,
21112
+ commandId: command.commandId
21113
+ }),
21114
+ type: "thread.unprompted-subagents-set",
21115
+ payload: {
21116
+ threadId: command.threadId,
21117
+ unpromptedSubagents: command.unpromptedSubagents,
21118
+ updatedAt: occurredAt
21119
+ }
21120
+ };
21121
+ }
20682
21122
  case "thread.turn.start": {
20683
21123
  const targetThread = yield* requireThread({
20684
21124
  readModel,
@@ -20737,6 +21177,7 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
20737
21177
  runtimeMode: targetThread.runtimeMode,
20738
21178
  interactionMode: targetThread.interactionMode,
20739
21179
  compressMode: targetThread.compressMode,
21180
+ unpromptedSubagents: targetThread.unpromptedSubagents,
20740
21181
  ...sourceProposedPlan !== void 0 ? { sourceProposedPlan } : {},
20741
21182
  createdAt: command.createdAt
20742
21183
  }
@@ -21503,6 +21944,8 @@ const ProjectionThread = Schema$1.Struct({
21503
21944
  runtimeMode: RuntimeMode,
21504
21945
  interactionMode: ProviderInteractionMode,
21505
21946
  compressMode: CompressMode,
21947
+ /** 0 or 1: SQLite has no boolean, matching `hasActionableProposedPlan`. */
21948
+ unpromptedSubagents: NonNegativeInt,
21506
21949
  branch: Schema$1.NullOr(Schema$1.String),
21507
21950
  worktreePath: Schema$1.NullOr(Schema$1.String),
21508
21951
  latestTurnId: Schema$1.NullOr(TurnId),
@@ -22391,6 +22834,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
22391
22834
  runtime_mode,
22392
22835
  interaction_mode,
22393
22836
  compress_mode,
22837
+ unprompted_subagents,
22394
22838
  branch,
22395
22839
  worktree_path,
22396
22840
  latest_turn_id,
@@ -22415,6 +22859,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
22415
22859
  ${row.runtimeMode},
22416
22860
  ${row.interactionMode},
22417
22861
  ${row.compressMode},
22862
+ ${row.unpromptedSubagents},
22418
22863
  ${row.branch},
22419
22864
  ${row.worktreePath},
22420
22865
  ${row.latestTurnId},
@@ -22439,6 +22884,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
22439
22884
  runtime_mode = excluded.runtime_mode,
22440
22885
  interaction_mode = excluded.interaction_mode,
22441
22886
  compress_mode = excluded.compress_mode,
22887
+ unprompted_subagents = excluded.unprompted_subagents,
22442
22888
  branch = excluded.branch,
22443
22889
  worktree_path = excluded.worktree_path,
22444
22890
  latest_turn_id = excluded.latest_turn_id,
@@ -22468,6 +22914,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
22468
22914
  runtime_mode AS "runtimeMode",
22469
22915
  interaction_mode AS "interactionMode",
22470
22916
  compress_mode AS "compressMode",
22917
+ unprompted_subagents AS "unpromptedSubagents",
22471
22918
  branch,
22472
22919
  worktree_path AS "worktreePath",
22473
22920
  latest_turn_id AS "latestTurnId",
@@ -22499,6 +22946,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
22499
22946
  runtime_mode AS "runtimeMode",
22500
22947
  interaction_mode AS "interactionMode",
22501
22948
  compress_mode AS "compressMode",
22949
+ unprompted_subagents AS "unpromptedSubagents",
22502
22950
  branch,
22503
22951
  worktree_path AS "worktreePath",
22504
22952
  latest_turn_id AS "latestTurnId",
@@ -22966,6 +23414,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
22966
23414
  runtimeMode: event.payload.runtimeMode,
22967
23415
  interactionMode: event.payload.interactionMode,
22968
23416
  compressMode: event.payload.compressMode,
23417
+ unpromptedSubagents: event.payload.unpromptedSubagents ? 1 : 0,
22969
23418
  branch: event.payload.branch,
22970
23419
  worktreePath: event.payload.worktreePath,
22971
23420
  latestTurnId: null,
@@ -23090,6 +23539,16 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
23090
23539
  });
23091
23540
  return;
23092
23541
  }
23542
+ case "thread.unprompted-subagents-set": {
23543
+ const existingRow = yield* projectionThreadRepository.getById({ threadId: event.payload.threadId });
23544
+ if (Option.isNone(existingRow)) return;
23545
+ yield* projectionThreadRepository.upsert({
23546
+ ...existingRow.value,
23547
+ unpromptedSubagents: event.payload.unpromptedSubagents ? 1 : 0,
23548
+ updatedAt: event.payload.updatedAt
23549
+ });
23550
+ return;
23551
+ }
23093
23552
  case "thread.deleted": {
23094
23553
  attachmentSideEffects.deletedThreadIds.add(event.payload.threadId);
23095
23554
  const existingRow = yield* projectionThreadRepository.getById({ threadId: event.payload.threadId });
@@ -24412,6 +24871,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
24412
24871
  runtime_mode AS "runtimeMode",
24413
24872
  interaction_mode AS "interactionMode",
24414
24873
  compress_mode AS "compressMode",
24874
+ unprompted_subagents AS "unpromptedSubagents",
24415
24875
  branch,
24416
24876
  worktree_path AS "worktreePath",
24417
24877
  latest_turn_id AS "latestTurnId",
@@ -24443,6 +24903,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
24443
24903
  runtime_mode AS "runtimeMode",
24444
24904
  interaction_mode AS "interactionMode",
24445
24905
  compress_mode AS "compressMode",
24906
+ unprompted_subagents AS "unpromptedSubagents",
24446
24907
  branch,
24447
24908
  worktree_path AS "worktreePath",
24448
24909
  latest_turn_id AS "latestTurnId",
@@ -24476,6 +24937,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
24476
24937
  runtime_mode AS "runtimeMode",
24477
24938
  interaction_mode AS "interactionMode",
24478
24939
  compress_mode AS "compressMode",
24940
+ unprompted_subagents AS "unpromptedSubagents",
24479
24941
  branch,
24480
24942
  worktree_path AS "worktreePath",
24481
24943
  latest_turn_id AS "latestTurnId",
@@ -24809,6 +25271,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
24809
25271
  runtime_mode AS "runtimeMode",
24810
25272
  interaction_mode AS "interactionMode",
24811
25273
  compress_mode AS "compressMode",
25274
+ unprompted_subagents AS "unpromptedSubagents",
24812
25275
  branch,
24813
25276
  worktree_path AS "worktreePath",
24814
25277
  latest_turn_id AS "latestTurnId",
@@ -25111,6 +25574,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25111
25574
  runtimeMode: row.runtimeMode,
25112
25575
  interactionMode: row.interactionMode,
25113
25576
  compressMode: row.compressMode,
25577
+ unpromptedSubagents: row.unpromptedSubagents > 0,
25114
25578
  branch: row.branch,
25115
25579
  worktreePath: row.worktreePath,
25116
25580
  latestTurn: latestTurnByThread.get(row.threadId) ?? null,
@@ -25223,6 +25687,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25223
25687
  runtimeMode: row.runtimeMode,
25224
25688
  interactionMode: row.interactionMode,
25225
25689
  compressMode: row.compressMode,
25690
+ unpromptedSubagents: row.unpromptedSubagents > 0,
25226
25691
  branch: row.branch,
25227
25692
  worktreePath: row.worktreePath,
25228
25693
  latestTurn: latestTurnByThread.get(row.threadId) ?? null,
@@ -25282,6 +25747,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25282
25747
  runtimeMode: row.runtimeMode,
25283
25748
  interactionMode: row.interactionMode,
25284
25749
  compressMode: row.compressMode,
25750
+ unpromptedSubagents: row.unpromptedSubagents > 0,
25285
25751
  branch: row.branch,
25286
25752
  worktreePath: row.worktreePath,
25287
25753
  latestTurn: latestTurnByThread.get(row.threadId) ?? null,
@@ -25337,6 +25803,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25337
25803
  runtimeMode: row.runtimeMode,
25338
25804
  interactionMode: row.interactionMode,
25339
25805
  compressMode: row.compressMode,
25806
+ unpromptedSubagents: row.unpromptedSubagents > 0,
25340
25807
  branch: row.branch,
25341
25808
  worktreePath: row.worktreePath,
25342
25809
  latestTurn: latestTurnByThread.get(row.threadId) ?? null,
@@ -25428,6 +25895,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25428
25895
  runtimeMode: threadRow.value.runtimeMode,
25429
25896
  interactionMode: threadRow.value.interactionMode,
25430
25897
  compressMode: threadRow.value.compressMode,
25898
+ unpromptedSubagents: threadRow.value.unpromptedSubagents > 0,
25431
25899
  branch: threadRow.value.branch,
25432
25900
  worktreePath: threadRow.value.worktreePath,
25433
25901
  latestTurn: Option.isSome(latestTurnRow) ? mapLatestTurn(latestTurnRow.value) : null,
@@ -25464,6 +25932,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25464
25932
  runtimeMode: threadRow.value.runtimeMode,
25465
25933
  interactionMode: threadRow.value.interactionMode,
25466
25934
  compressMode: threadRow.value.compressMode,
25935
+ unpromptedSubagents: threadRow.value.unpromptedSubagents > 0,
25467
25936
  branch: threadRow.value.branch,
25468
25937
  worktreePath: threadRow.value.worktreePath,
25469
25938
  latestTurn: Option.isSome(latestTurnRow) ? mapLatestTurn(latestTurnRow.value) : null,
@@ -28873,6 +29342,7 @@ const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
28873
29342
  modelSelection: nextProjectDefaultModelSelection,
28874
29343
  interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
28875
29344
  compressMode: DEFAULT_COMPRESS_MODE,
29345
+ unpromptedSubagents: true,
28876
29346
  runtimeMode: "full-access",
28877
29347
  branch: null,
28878
29348
  worktreePath: null,
@@ -34856,6 +35326,7 @@ const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
34856
35326
  repositoryKey: repositoryKey ?? input.repositoryKey ?? null,
34857
35327
  threadId: input.threadId ?? null,
34858
35328
  externalRef: input.externalRef ?? null,
35329
+ parentTaskId: input.parentTaskId ?? null,
34859
35330
  title: input.title,
34860
35331
  body: input.body ?? "",
34861
35332
  status: input.status ?? "backlog",
@@ -38261,6 +38732,38 @@ const requireThreadCapability = Effect.fn("mcp.requireThreadCapability")(functio
38261
38732
  });
38262
38733
  return invocation;
38263
38734
  });
38735
+ /**
38736
+ * The thread a control tool may act on, given what it was asked to act on.
38737
+ *
38738
+ * Two checks like `requireWatchCapability`, and for the same reason: the
38739
+ * capability says this session controls threads at all, the grant says which.
38740
+ * An omitted id resolves to the session's own thread, which is always in the
38741
+ * grant and is why the common call needs no id and so cannot name a wrong one.
38742
+ */
38743
+ const requireThreadControlTarget = Effect.fn("mcp.requireThreadControlTarget")(function* (requested) {
38744
+ const invocation = yield* requireThreadCapability();
38745
+ if (requested === void 0 || requested === invocation.threadId) return {
38746
+ invocation,
38747
+ threadId: invocation.threadId
38748
+ };
38749
+ if (!invocation.controlThreadIds?.has(requested)) return yield* new ThreadControlNotPermittedError({ threadId: requested });
38750
+ return {
38751
+ invocation,
38752
+ threadId: requested
38753
+ };
38754
+ });
38755
+ /**
38756
+ * The guard on starting a thread, which is a different question from acting on
38757
+ * one: it is not "which thread" but "may this session make more of them".
38758
+ */
38759
+ const requireThreadSpawn = Effect.fn("mcp.requireThreadSpawn")(function* () {
38760
+ const invocation = yield* requireThreadCapability();
38761
+ if (invocation.mayCreateThreads !== true) return yield* new ThreadSpawnNotPermittedError({
38762
+ threadId: invocation.threadId,
38763
+ detail: "this thread was itself started by an agent, and spawning goes one level deep so a runaway loop has a bound"
38764
+ });
38765
+ return invocation;
38766
+ });
38264
38767
  //#endregion
38265
38768
  //#region src/mcp/PreviewAutomationBroker.ts
38266
38769
  var PreviewAutomationBroker = class extends Context.Service()("@p4code/cli/mcp/PreviewAutomationBroker") {};
@@ -45693,6 +46196,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
45693
46196
  runtimeMode: bootstrap.createThread.runtimeMode,
45694
46197
  interactionMode: bootstrap.createThread.interactionMode,
45695
46198
  compressMode: bootstrap.createThread.compressMode,
46199
+ unpromptedSubagents: bootstrap.createThread.unpromptedSubagents,
45696
46200
  branch: bootstrap.createThread.branch,
45697
46201
  worktreePath: bootstrap.createThread.worktreePath,
45698
46202
  createdAt: bootstrap.createThread.createdAt
@@ -45957,6 +46461,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
45957
46461
  runtimeMode: DEFAULT_RUNTIME_MODE$2,
45958
46462
  interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
45959
46463
  compressMode: DEFAULT_COMPRESS_MODE,
46464
+ unpromptedSubagents: true,
45960
46465
  bootstrap: { createThread: {
45961
46466
  projectId: plan.projectId,
45962
46467
  title: threadTitle,
@@ -45964,6 +46469,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
45964
46469
  runtimeMode: DEFAULT_RUNTIME_MODE$2,
45965
46470
  interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
45966
46471
  compressMode: DEFAULT_COMPRESS_MODE,
46472
+ unpromptedSubagents: true,
45967
46473
  branch: null,
45968
46474
  worktreePath: null,
45969
46475
  createdAt
@@ -46374,6 +46880,8 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
46374
46880
  ...row.projectId === null ? {} : { projectId: row.projectId },
46375
46881
  ...row.repositoryKey === null ? {} : { repositoryKey: row.repositoryKey },
46376
46882
  ...row.threadId === null ? {} : { threadId: row.threadId },
46883
+ ...row.externalRef === null ? {} : { externalRef: row.externalRef },
46884
+ ...row.parentTaskId === null ? {} : { parentTaskId: row.parentTaskId },
46377
46885
  title: row.title,
46378
46886
  body: row.body,
46379
46887
  status: row.status,
@@ -47261,7 +47769,10 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
47261
47769
  const crypto = yield* Crypto.Crypto;
47262
47770
  const environmentId = yield* (yield* ServerEnvironment).getEnvironmentId;
47263
47771
  const httpServer = yield* HttpServer.HttpServer;
47264
- const state = yield* SynchronizedRef.make({ records: /* @__PURE__ */ new Map() });
47772
+ const state = yield* SynchronizedRef.make({
47773
+ records: /* @__PURE__ */ new Map(),
47774
+ spawnedThreadIds: /* @__PURE__ */ new Set()
47775
+ });
47265
47776
  const currentTimeMillis = options.now ? Effect.sync(options.now) : Clock.currentTimeMillis;
47266
47777
  const livenessWindowMs = options.livenessWindowMs ?? DEFAULT_LIVENESS_WINDOW_MS;
47267
47778
  const endpoint = httpServer.address._tag === "TcpAddress" ? `http://${getHttpMcpEndpointHost(httpServer.address.hostname)}:${httpServer.address.port}/mcp` : "http://127.0.0.1/mcp";
@@ -47282,29 +47793,35 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
47282
47793
  "threads"
47283
47794
  ]);
47284
47795
  if (watchThreadIds.size > 0) capabilities.add("watch");
47285
- const scope = {
47796
+ const threadId = ThreadId.make(request.threadId);
47797
+ const scopeWith = (mayCreateThreads) => ({
47286
47798
  environmentId,
47287
- threadId: ThreadId.make(request.threadId),
47799
+ threadId,
47288
47800
  providerSessionId,
47289
47801
  providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),
47290
47802
  capabilities,
47291
47803
  ...watchThreadIds.size > 0 ? { watchThreadIds } : {},
47804
+ mayCreateThreads,
47292
47805
  issuedAt
47293
- };
47294
- yield* SynchronizedRef.update(state, ({ records }) => {
47806
+ });
47807
+ yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
47295
47808
  const next = new Map(pruneDead(records, issuedAt));
47296
47809
  next.set(tokenHash, {
47297
47810
  tokenHash,
47298
- scope,
47811
+ scope: scopeWith(!spawnedThreadIds.has(threadId)),
47812
+ controlThreadIds: /* @__PURE__ */ new Set(),
47299
47813
  lastAliveAt: issuedAt
47300
47814
  });
47301
- return { records: next };
47815
+ return {
47816
+ records: next,
47817
+ spawnedThreadIds
47818
+ };
47302
47819
  });
47303
47820
  return { config: {
47304
47821
  environmentId,
47305
- threadId: scope.threadId,
47822
+ threadId,
47306
47823
  providerSessionId,
47307
- providerInstanceId: scope.providerInstanceId,
47824
+ providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),
47308
47825
  endpoint,
47309
47826
  authorizationHeader: `Bearer ${rawToken}`
47310
47827
  } };
@@ -47313,42 +47830,76 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
47313
47830
  if (rawToken.length === 0) return void 0;
47314
47831
  const tokenHash = yield* hashToken(rawToken);
47315
47832
  const timestamp = yield* currentTimeMillis;
47316
- return yield* SynchronizedRef.modify(state, ({ records }) => {
47833
+ return yield* SynchronizedRef.modify(state, ({ records, spawnedThreadIds }) => {
47317
47834
  const current = pruneDead(records, timestamp);
47318
47835
  const record = current.get(tokenHash);
47319
- if (!record) return [void 0, { records: current }];
47836
+ if (!record) return [void 0, {
47837
+ records: current,
47838
+ spawnedThreadIds
47839
+ }];
47320
47840
  const next = new Map(current);
47321
47841
  next.set(tokenHash, {
47322
47842
  ...record,
47323
47843
  lastAliveAt: timestamp
47324
47844
  });
47325
- return [record.scope, { records: next }];
47845
+ return [record.controlThreadIds.size === 0 ? record.scope : {
47846
+ ...record.scope,
47847
+ capabilities: /* @__PURE__ */ new Set([...record.scope.capabilities, "watch"]),
47848
+ controlThreadIds: record.controlThreadIds,
47849
+ watchThreadIds: /* @__PURE__ */ new Set([...record.scope.watchThreadIds ?? [], ...record.controlThreadIds])
47850
+ }, {
47851
+ records: next,
47852
+ spawnedThreadIds
47853
+ }];
47326
47854
  });
47327
47855
  });
47328
47856
  const touch = Effect.fn("McpSessionRegistry.touch")(function* (threadId) {
47329
47857
  const timestamp = yield* currentTimeMillis;
47330
- yield* SynchronizedRef.update(state, ({ records }) => {
47858
+ yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
47331
47859
  const current = pruneDead(records, timestamp);
47332
47860
  const next = new Map(current);
47333
47861
  for (const [tokenHash, record] of current) if (record.scope.threadId === threadId) next.set(tokenHash, {
47334
47862
  ...record,
47335
47863
  lastAliveAt: timestamp
47336
47864
  });
47337
- return { records: next };
47865
+ return {
47866
+ records: next,
47867
+ spawnedThreadIds
47868
+ };
47869
+ });
47870
+ });
47871
+ const recordSpawnedThread = Effect.fn("McpSessionRegistry.recordSpawnedThread")(function* (input) {
47872
+ yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
47873
+ const next = new Map(records);
47874
+ for (const [tokenHash, record] of records) if (record.scope.providerSessionId === input.providerSessionId) next.set(tokenHash, {
47875
+ ...record,
47876
+ controlThreadIds: /* @__PURE__ */ new Set([...record.controlThreadIds, input.threadId])
47877
+ });
47878
+ return {
47879
+ records: next,
47880
+ spawnedThreadIds: /* @__PURE__ */ new Set([...spawnedThreadIds, input.threadId])
47881
+ };
47338
47882
  });
47339
47883
  });
47340
- const revokeWhere = (predicate) => SynchronizedRef.update(state, ({ records }) => ({ records: new Map(Array.from(records).filter(([, record]) => !predicate(record))) }));
47884
+ const revokeWhere = (predicate) => SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => ({
47885
+ records: new Map(Array.from(records).filter(([, record]) => !predicate(record))),
47886
+ spawnedThreadIds
47887
+ }));
47341
47888
  return McpSessionRegistry.of({
47342
47889
  issue,
47343
47890
  resolve,
47344
47891
  touch,
47892
+ recordSpawnedThread,
47345
47893
  revokeProviderSession: Effect.fn("McpSessionRegistry.revokeProviderSession")(function* (providerSessionId) {
47346
47894
  yield* revokeWhere((record) => record.scope.providerSessionId === providerSessionId);
47347
47895
  }),
47348
47896
  revokeThread: Effect.fn("McpSessionRegistry.revokeThread")(function* (threadId) {
47349
47897
  yield* revokeWhere((record) => record.scope.threadId === threadId);
47350
47898
  }),
47351
- revokeAll: SynchronizedRef.set(state, { records: /* @__PURE__ */ new Map() })
47899
+ revokeAll: SynchronizedRef.update(state, ({ spawnedThreadIds }) => ({
47900
+ records: /* @__PURE__ */ new Map(),
47901
+ spawnedThreadIds
47902
+ }))
47352
47903
  });
47353
47904
  });
47354
47905
  let activeMcpSessionRegistry;
@@ -52502,7 +53053,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
52502
53053
  const mcpSession = readMcpProviderSession(input.threadId);
52503
53054
  const externalMcpServers = options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers;
52504
53055
  const narrateBeforeTools = options?.resolveToolCallNarration === void 0 ? DEFAULT_SERVER_SETTINGS.enableToolCallNarration : yield* options.resolveToolCallNarration;
52505
- const unpromptedSubagents = options?.resolveUnpromptedSubagents === void 0 ? DEFAULT_SERVER_SETTINGS.enableUnpromptedSubagents : yield* options.resolveUnpromptedSubagents;
53056
+ const unpromptedSubagents = input.unpromptedSubagents !== void 0 ? input.unpromptedSubagents : options?.resolveUnpromptedSubagents === void 0 ? DEFAULT_SERVER_SETTINGS.enableUnpromptedSubagents : yield* options.resolveUnpromptedSubagents;
52506
53057
  const compressRuleset = compressRulesetFor(input.compressMode ?? "off");
52507
53058
  const systemPromptAppend = [
52508
53059
  ...narrateBeforeTools ? [NARRATE_BEFORE_TOOLS_PROMPT] : [],
@@ -52601,6 +53152,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
52601
53152
  startedAt,
52602
53153
  basePermissionMode: permissionMode,
52603
53154
  currentApiModelId: apiModelId,
53155
+ currentUnpromptedSubagents: unpromptedSubagents,
52604
53156
  resumeSessionId: sessionId,
52605
53157
  pendingApprovals,
52606
53158
  pendingUserInputs,
@@ -52672,7 +53224,13 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
52672
53224
  const context = yield* requireSession(input.threadId);
52673
53225
  const modelSelection = input.modelSelection !== void 0 && input.modelSelection.instanceId === boundInstanceId ? input.modelSelection : void 0;
52674
53226
  if (context.turnState?.synthetic === true) yield* completeTurn(context, "completed");
52675
- const message = yield* buildUserMessageEffect(input, {
53227
+ const turnSubagentsPrompt = input.unpromptedSubagents !== void 0 && input.unpromptedSubagents !== context.currentUnpromptedSubagents ? input.unpromptedSubagents ? SUBAGENTS_ALLOWED_PROMPT : SUBAGENTS_ON_REQUEST_PROMPT : void 0;
53228
+ if (input.unpromptedSubagents !== void 0) context.currentUnpromptedSubagents = input.unpromptedSubagents;
53229
+ const turnInput = turnSubagentsPrompt === void 0 ? input : {
53230
+ ...input,
53231
+ input: input.input === void 0 ? turnSubagentsPrompt : `${turnSubagentsPrompt}\n\n${input.input}`
53232
+ };
53233
+ const message = yield* buildUserMessageEffect(turnInput, {
52676
53234
  fileSystem,
52677
53235
  attachmentsDir: serverConfig.attachmentsDir,
52678
53236
  boundInstanceId
@@ -85945,6 +86503,28 @@ const findTask = (taskId) => Effect.gen(function* () {
85945
86503
  const tasks = yield* TaskRepository;
85946
86504
  return isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
85947
86505
  });
86506
+ /**
86507
+ * Resolve a parent reference to the UUID the column stores.
86508
+ *
86509
+ * A planning agent works in readable ids - it has just read `P4-12` off the
86510
+ * board - but the link is stored as the row key, because readable ids are
86511
+ * allocated per store and the one it quotes may have been minted on another
86512
+ * machine. Absent stays absent and an explicit null stays null: those mean
86513
+ * "leave it" and "clear it", neither of which is a reference to resolve.
86514
+ *
86515
+ * A readable id that names no task fails rather than being stored raw. The
86516
+ * opposite would file a subtask whose parent link can never resolve, which
86517
+ * reads on the board as a task that is part of nothing. A UUID is taken on
86518
+ * trust, matching the column's lack of a foreign key: a parent may legitimately
86519
+ * arrive after its child on a board synced from elsewhere.
86520
+ */
86521
+ const resolveParentTaskId = (parentTaskId, operation) => Effect.gen(function* () {
86522
+ if (parentTaskId === void 0 || parentTaskId === null) return parentTaskId;
86523
+ if (!isReadableTaskId(parentTaskId)) return parentTaskId;
86524
+ const parent = yield* findTask(parentTaskId).pipe(Effect.catch(toTaskStoreError(operation)));
86525
+ if (Option.isNone(parent)) return yield* new TaskNotFoundError({ taskId: parentTaskId });
86526
+ return parent.value.taskId;
86527
+ });
85948
86528
  const TaskToolkitHandlersLive = TaskToolkit.toLayer({
85949
86529
  task_list: (input) => Effect.gen(function* () {
85950
86530
  yield* requireTaskCapability();
@@ -85972,7 +86552,11 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
85972
86552
  const timestamp = yield* nowIso$1;
85973
86553
  const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
85974
86554
  const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
85975
- const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
86555
+ const parentTaskId = yield* resolveParentTaskId(input.parentTaskId, "task_create");
86556
+ const row = buildTaskRow(parentTaskId === void 0 || parentTaskId === null ? input : {
86557
+ ...input,
86558
+ parentTaskId
86559
+ }, taskId, timestamp, repositoryKey);
85976
86560
  return yield* tasks.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("task_create")));
85977
86561
  }),
85978
86562
  task_update: (input) => Effect.gen(function* () {
@@ -85981,13 +86565,12 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
85981
86565
  const timestamp = yield* nowIso$1;
85982
86566
  const taskId = isReadableTaskId(input.taskId) ? Option.getOrNull(yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_update"))))?.taskId : input.taskId;
85983
86567
  if (taskId === void 0) return yield* new TaskNotFoundError({ taskId: input.taskId });
85984
- const patch = input.projectId === void 0 ? {
85985
- ...input,
85986
- taskId
85987
- } : {
86568
+ const parentTaskId = yield* resolveParentTaskId(input.parentTaskId, "task_update");
86569
+ const patch = {
85988
86570
  ...input,
85989
86571
  taskId,
85990
- repositoryKey: yield* resolveTaskRepositoryKey(input.projectId)
86572
+ ...parentTaskId === void 0 ? {} : { parentTaskId },
86573
+ ...input.projectId === void 0 ? {} : { repositoryKey: yield* resolveTaskRepositoryKey(input.projectId) }
85991
86574
  };
85992
86575
  const patched = yield* tasks.patch(patch, timestamp).pipe(Effect.catch(toTaskStoreError("task_update")));
85993
86576
  if (Option.isNone(patched)) return yield* new TaskNotFoundError({ taskId: input.taskId });
@@ -85999,14 +86582,295 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
85999
86582
  })
86000
86583
  });
86001
86584
  //#endregion
86585
+ //#region src/orchestration/pendingThreadSettles.ts
86586
+ /**
86587
+ * Threads that asked to settle while their session was still running.
86588
+ *
86589
+ * The decider refuses `thread.settle` for a thread whose session is starting or
86590
+ * running, and an agent settling its own thread always calls from inside that
86591
+ * session - so the command it wants can never be the command it sends. The
86592
+ * intent is recorded here instead and replayed when the session stops, which is
86593
+ * what "settle this thread" meant from the agent's side anyway: finish, then
86594
+ * close.
86595
+ *
86596
+ * A module-level set rather than a service because it has to be reachable from
86597
+ * two layer trees that do not otherwise meet - an MCP tool handler on one side,
86598
+ * the provider command reactor on the other - and threading a new service
86599
+ * through both to hold a set of ids is more wiring than the thing is worth.
86600
+ * `McpProviderSession` and the active-registry handle are the same shape for
86601
+ * the same reason. It is intentionally not durable: a queued settle belongs to
86602
+ * a session, and a server restart has already ended every session there was.
86603
+ */
86604
+ const pending = /* @__PURE__ */ new Set();
86605
+ const queueThreadSettle = (threadId) => {
86606
+ pending.add(threadId);
86607
+ };
86608
+ /**
86609
+ * Removes the intent and reports whether there was one, so a caller cannot
86610
+ * settle the same thread twice by reading and then forgetting to clear.
86611
+ */
86612
+ const takeQueuedThreadSettle = (threadId) => pending.delete(threadId);
86613
+ //#endregion
86614
+ //#region src/sync/assetCompression.ts
86615
+ /**
86616
+ * Checking that a compressed asset still says what the original said.
86617
+ *
86618
+ * Compression here is a rewrite of prose an agent reads before it does anything
86619
+ * else: a skill, a memory file, a subagent definition. Rewriting that prose is
86620
+ * cheap to get almost right and expensive to get slightly wrong, because the
86621
+ * damage is silent. A dropped URL, a mangled command, a heading that no longer
86622
+ * matches the sentence pointing at it - none of that fails loudly. It just makes
86623
+ * the agent behave a little differently on every machine that syncs the file.
86624
+ *
86625
+ * So the write is gated on a comparison rather than on the model's word for it.
86626
+ * The rules are ported from caveman's `validate.py`, with the ones this task
86627
+ * calls out - headings, code blocks, URLs, paths, bullet counts - promoted to
86628
+ * refusals rather than warnings. A refusal costs one retry; a bad write costs
86629
+ * whatever the agent does next, on every machine, until someone notices.
86630
+ *
86631
+ * Pure on purpose. Everything here is a string comparison, so every rule can be
86632
+ * tested without a filesystem, a provider or a thread, and the tool that calls
86633
+ * it is left with only the parts that genuinely touch disk.
86634
+ *
86635
+ * @module sync/assetCompression
86636
+ */
86637
+ /**
86638
+ * How far the bullet count may move before the rewrite stops being compression.
86639
+ *
86640
+ * Not zero. Compression legitimately folds a two-line bullet into one, and a
86641
+ * rule that forbade that would forbid the feature. Caveman's own threshold,
86642
+ * kept because it is the one their receipts were measured against.
86643
+ */
86644
+ const BULLET_COUNT_TOLERANCE = .15;
86645
+ /**
86646
+ * Characters per token, for the before/after figures.
86647
+ *
86648
+ * An estimate, and labelled as one everywhere it surfaces. The exact count
86649
+ * belongs to whichever provider tokenizer will read the file, and asking one
86650
+ * would mean a network call inside a validation path that is otherwise pure.
86651
+ * Four is the usual English-prose approximation and is good enough to answer
86652
+ * the only question anyone asks here: did this get meaningfully smaller.
86653
+ */
86654
+ const CHARS_PER_TOKEN_ESTIMATE = 4;
86655
+ const URL_REGEX = /https?:\/\/[^\s)]+/gu;
86656
+ /**
86657
+ * Sentence punctuation swept up by the URL match.
86658
+ *
86659
+ * `See https://svgl.app for the catalogue.` and `Catalogue: https://svgl.app.`
86660
+ * name the same URL, and a comparison that disagreed would refuse every rewrite
86661
+ * that moved a link to the end of a sentence - which is most of them.
86662
+ */
86663
+ const URL_TRAILING_PUNCTUATION_REGEX = /[.,;:!?'"\])]+$/u;
86664
+ const FENCE_LINE_REGEX = /^(\s{0,3})(`{3,}|~{3,})(.*)$/u;
86665
+ const HEADING_REGEX = /^(#{1,6})\s+(.*)$/gmu;
86666
+ const BULLET_REGEX = /^\s*[-*+]\s+/gmu;
86667
+ const INLINE_CODE_REGEX = /`([^`]+)`/gu;
86668
+ /**
86669
+ * Crude, and deliberately so.
86670
+ *
86671
+ * Either a path prefix (`./`, `../`, `/`, a drive letter) or a separator inside
86672
+ * the match. It over-matches - `and/or` is a "path" by this rule - and the
86673
+ * over-match is the safe direction: a false positive refuses a rewrite that
86674
+ * dropped the phrase, and a false negative lets a real path go missing.
86675
+ */
86676
+ const PATH_REGEX = /(?:\.\/|\.\.\/|\/|[A-Za-z]:\\)[\w\-/\\.]+|[\w\-.]+[/\\][\w\-/\\.]+/gu;
86677
+ /** YAML frontmatter: `---` on the first line, `---` on a line of its own after. */
86678
+ const FRONTMATTER_REGEX = /^---\r?\n[\s\S]*?\r?\n---\r?\n/u;
86679
+ /**
86680
+ * Filenames that almost certainly hold secrets, and the directories that do.
86681
+ *
86682
+ * Compressing a file means handing its bytes to a model, so this is checked
86683
+ * before the read rather than after it. The asset roots should never contain
86684
+ * any of these, which is exactly why the check is cheap to keep: it costs
86685
+ * nothing in the ordinary case and is the only thing standing between a
86686
+ * misplaced `credentials.md` and a provider's API.
86687
+ */
86688
+ const SENSITIVE_BASENAME_REGEX = /^(\.env(\..+)?|\.netrc|credentials(\..+)?|secrets?(\..+)?|passwords?(\..+)?|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?|authorized_keys|known_hosts|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg))$/iu;
86689
+ const SENSITIVE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
86690
+ ".ssh",
86691
+ ".aws",
86692
+ ".gnupg",
86693
+ ".kube",
86694
+ ".docker"
86695
+ ]);
86696
+ const SENSITIVE_NAME_TOKENS = [
86697
+ "secret",
86698
+ "credential",
86699
+ "password",
86700
+ "passwd",
86701
+ "apikey",
86702
+ "accesskey",
86703
+ "token",
86704
+ "privatekey"
86705
+ ];
86706
+ const segmentsOf = (filePath) => filePath.split(/[/\\]/u).filter((segment) => segment.length > 0);
86707
+ /**
86708
+ * Whether this path looks like something that must not be sent to a model.
86709
+ *
86710
+ * Name-based, because that is all that is knowable before the read. Separators
86711
+ * inside the name are normalized away first so `api-key.md` and `api_key.md`
86712
+ * are the same answer.
86713
+ */
86714
+ const isSensitiveAssetPath = (filePath) => {
86715
+ const segments = segmentsOf(filePath);
86716
+ const name = segments.at(-1) ?? "";
86717
+ if (SENSITIVE_BASENAME_REGEX.test(name)) return true;
86718
+ if (segments.some((segment) => SENSITIVE_PATH_SEGMENTS.has(segment.toLowerCase()))) return true;
86719
+ const flattened = name.toLowerCase().replace(/[_\-\s.]/gu, "");
86720
+ return SENSITIVE_NAME_TOKENS.some((token) => flattened.includes(token));
86721
+ };
86722
+ /** The frontmatter block including its delimiters, or the empty string. */
86723
+ const frontmatterOf = (text) => FRONTMATTER_REGEX.exec(text)?.[0] ?? "";
86724
+ const headingsOf = (text) => [...text.matchAll(HEADING_REGEX)].map((match) => `${match[1] ?? ""} ${(match[2] ?? "").trim()}`);
86725
+ /**
86726
+ * Fenced code blocks, as whole strings, fences included.
86727
+ *
86728
+ * Line-based rather than one regex, so a closing fence is only a closing fence
86729
+ * when CommonMark says it is: same character, at least as long as the opener,
86730
+ * nothing after it. That is what lets a four-backtick block wrap three-backtick
86731
+ * content without the extractor stopping in the middle of it.
86732
+ *
86733
+ * An unclosed fence is skipped rather than reported. It means the markdown was
86734
+ * already malformed, and including it would fail every compression of a file
86735
+ * that was broken before anyone touched it.
86736
+ */
86737
+ const codeBlocksOf = (text) => {
86738
+ const lines = text.split("\n");
86739
+ const blocks = [];
86740
+ let index = 0;
86741
+ while (index < lines.length) {
86742
+ const opening = FENCE_LINE_REGEX.exec(lines[index] ?? "");
86743
+ if (opening === null) {
86744
+ index += 1;
86745
+ continue;
86746
+ }
86747
+ const fence = opening[2] ?? "";
86748
+ const collected = [lines[index] ?? ""];
86749
+ index += 1;
86750
+ let closed = false;
86751
+ while (index < lines.length) {
86752
+ const line = lines[index] ?? "";
86753
+ const closing = FENCE_LINE_REGEX.exec(line);
86754
+ const closingFence = closing?.[2] ?? "";
86755
+ if (closing !== null && closingFence[0] === fence[0] && closingFence.length >= fence.length && (closing[3] ?? "").trim().length === 0) {
86756
+ collected.push(line);
86757
+ closed = true;
86758
+ index += 1;
86759
+ break;
86760
+ }
86761
+ collected.push(line);
86762
+ index += 1;
86763
+ }
86764
+ if (closed) blocks.push(collected.join("\n"));
86765
+ }
86766
+ return blocks;
86767
+ };
86768
+ const withoutCodeBlocks = (text) => codeBlocksOf(text).reduce((rest, block) => rest.replace(block, ""), text);
86769
+ const inlineCodeCounts = (text) => {
86770
+ const counts = /* @__PURE__ */ new Map();
86771
+ for (const match of withoutCodeBlocks(text).matchAll(INLINE_CODE_REGEX)) {
86772
+ const span = match[1] ?? "";
86773
+ counts.set(span, (counts.get(span) ?? 0) + 1);
86774
+ }
86775
+ return counts;
86776
+ };
86777
+ const urlsOf = (text) => new Set([...text.matchAll(URL_REGEX)].map((match) => match[0].replace(URL_TRAILING_PUNCTUATION_REGEX, "")));
86778
+ /**
86779
+ * Paths, with URLs taken out of the text first.
86780
+ *
86781
+ * `//svgl.app` inside `https://svgl.app` is a path by the pattern's reckoning,
86782
+ * so leaving the URLs in would have every link contribute a phantom path whose
86783
+ * spelling changes with the punctuation after it.
86784
+ */
86785
+ const pathsOf = (text) => new Set([...text.replace(URL_REGEX, " ").matchAll(PATH_REGEX)].map((match) => match[0]));
86786
+ const countOf = (text, pattern) => [...text.matchAll(pattern)].length;
86787
+ const missingFrom = (original, compressed) => [...original].filter((value) => !compressed.has(value));
86788
+ const addedTo = (original, compressed) => [...compressed].filter((value) => !original.has(value));
86789
+ const quoteList = (values) => values.map((value) => `'${value}'`).join(", ");
86790
+ /**
86791
+ * Compare a compressed rewrite against the original it came from.
86792
+ *
86793
+ * Errors are the things whose loss changes what the file instructs: a code
86794
+ * block, a URL, a path, an inline span, a heading, the frontmatter. Additions
86795
+ * are warnings rather than errors throughout, because a rewrite that names one
86796
+ * more path than the original is doing something odd but is not losing
86797
+ * anything, and a refusal there would be a refusal nobody can act on.
86798
+ */
86799
+ const validateCompression = (original, compressed) => {
86800
+ const errors = [];
86801
+ const warnings = [];
86802
+ if (frontmatterOf(original) !== frontmatterOf(compressed)) errors.push("The YAML frontmatter changed. It is metadata the provider parses, not prose; return it byte-identical.");
86803
+ const originalHeadings = headingsOf(original);
86804
+ const compressedHeadings = headingsOf(compressed);
86805
+ if (originalHeadings.length !== compressedHeadings.length || originalHeadings.some((heading, at) => heading !== compressedHeadings[at])) errors.push(`Headings changed: ${originalHeadings.length} in the original, ${compressedHeadings.length} in the rewrite. Keep every heading, in order, with its exact text.`);
86806
+ const originalBlocks = codeBlocksOf(original);
86807
+ const compressedBlocks = codeBlocksOf(compressed);
86808
+ if (originalBlocks.length !== compressedBlocks.length || originalBlocks.some((block, at) => block !== compressedBlocks[at])) errors.push(`Code blocks are not byte-identical: ${originalBlocks.length} in the original, ${compressedBlocks.length} in the rewrite. Copy them through unchanged.`);
86809
+ const originalUrls = urlsOf(original);
86810
+ const compressedUrls = urlsOf(compressed);
86811
+ const lostUrls = missingFrom(originalUrls, compressedUrls);
86812
+ if (lostUrls.length > 0) errors.push(`URLs missing from the rewrite: ${quoteList(lostUrls)}.`);
86813
+ const addedUrls = addedTo(originalUrls, compressedUrls);
86814
+ if (addedUrls.length > 0) warnings.push(`URLs the original did not have: ${quoteList(addedUrls)}.`);
86815
+ const originalPaths = pathsOf(original);
86816
+ const compressedPaths = pathsOf(compressed);
86817
+ const lostPaths = missingFrom(originalPaths, compressedPaths);
86818
+ if (lostPaths.length > 0) errors.push(`Paths missing from the rewrite: ${quoteList(lostPaths)}.`);
86819
+ const addedPaths = addedTo(originalPaths, compressedPaths);
86820
+ if (addedPaths.length > 0) warnings.push(`Paths the original did not have: ${quoteList(addedPaths)}.`);
86821
+ const originalInline = inlineCodeCounts(original);
86822
+ const compressedInline = inlineCodeCounts(compressed);
86823
+ const lostInline = [...originalInline].filter(([span, count]) => (compressedInline.get(span) ?? 0) < count);
86824
+ if (lostInline.length > 0) errors.push(`Inline code spans missing from the rewrite: ${quoteList(lostInline.map(([span]) => span))}.`);
86825
+ const originalBullets = countOf(original, BULLET_REGEX);
86826
+ const compressedBullets = countOf(compressed, BULLET_REGEX);
86827
+ if (originalBullets > 0) {
86828
+ if (Math.abs(originalBullets - compressedBullets) / originalBullets > BULLET_COUNT_TOLERANCE) errors.push(`Bullet count moved from ${originalBullets} to ${compressedBullets}, past the ${Math.round(BULLET_COUNT_TOLERANCE * 100)}% tolerance. Compress the wording of each bullet rather than merging or dropping bullets.`);
86829
+ }
86830
+ return {
86831
+ errors,
86832
+ warnings
86833
+ };
86834
+ };
86835
+ /** Roughly how many tokens this text costs to read. Estimated, never measured. */
86836
+ const estimateTokens = (text) => Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
86837
+ /**
86838
+ * The name a backup of this file is kept under.
86839
+ *
86840
+ * Timestamped rather than fixed. Caveman refuses to compress when a backup
86841
+ * already exists, which protects the first original at the cost of blocking
86842
+ * every later compression; keeping one backup per write protects every version
86843
+ * instead, including the edits someone made between two compressions.
86844
+ *
86845
+ * The parent directory's name is carried in the path rather than the file name
86846
+ * so that `global-memory/feedback_svgl.md` and `svgl/SKILL.md` cannot collide.
86847
+ */
86848
+ const backupFileName = (input) => `${input.fileName}.${input.atIso.replace(/[:.]/gu, "-")}.original`;
86849
+ //#endregion
86002
86850
  //#region src/mcp/toolkits/threads/tools.ts
86003
86851
  /**
86004
- * Self-scoped on purpose: neither tool takes a thread id. The thread acted on
86005
- * is always the one in `McpInvocationScope`, exactly as `task_current` resolves
86006
- * it, so a leaked per-session token cannot reach any other thread. Delete,
86007
- * create and archive are deliberately not exposed; settle could never succeed
86008
- * from here because the decider refuses it while the calling session is alive.
86852
+ * Scoped to what this session started, on purpose.
86853
+ *
86854
+ * A control tool may name a thread, but only one this session created with
86855
+ * `thread_spawn`; omitting the id means the session's own thread, which is the
86856
+ * common case and the one that cannot be got wrong. The grant lives on the
86857
+ * credential in `McpSessionRegistry`, so a leaked per-session token still
86858
+ * reaches only the work that token itself began, and its reach cannot widen
86859
+ * because something changed on the board.
86860
+ *
86861
+ * Delete and archive stay unexposed. Deletion is the one irreversible action in
86862
+ * this file and a thread carries its whole transcript, which is a worse thing
86863
+ * to lose than the task row the task toolkit withheld delete for.
86864
+ *
86865
+ * `thread_spawn` goes one level: a thread that was itself spawned is issued a
86866
+ * credential that cannot spawn, so a loop meets a bound rather than a request
86867
+ * not to loop. `thread_settle` on a live session is queued and applied when the
86868
+ * session stops, because the decider refuses an immediate settle there and a
86869
+ * call from a thread's own session always arrives while it is alive.
86870
+ *
86871
+ * @module mcp/toolkits/threads/tools
86009
86872
  */
86873
+ /** Self-only, and staying that way: `thread_configure` is where a title on another thread is set. */
86010
86874
  const ThreadRenameTool = Tool.make("thread_rename", {
86011
86875
  description: "Rename this agent session's own thread. Use it when the seeded title no longer matches what the thread turned out to be about. Only the current thread can be renamed; no other thread is reachable.",
86012
86876
  parameters: ThreadRenameInput,
@@ -86018,6 +86882,54 @@ const ThreadRenameTool = Tool.make("thread_rename", {
86018
86882
  Crypto.Crypto
86019
86883
  ]
86020
86884
  }).annotate(Tool.Title, "Rename this thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86885
+ const ThreadSpawnTool = Tool.make("thread_spawn", {
86886
+ description: "Start a new agent thread and send it a first message, then return its id. Use it to hand a piece of work to a fresh thread - a subtask you just planned, a job that wants its own transcript. The new thread inherits this one's project, model and permission mode unless you say otherwise, and can then be watched with thread_watch_events and adjusted with thread_configure. A thread that was itself started this way cannot start another.",
86887
+ parameters: ThreadSpawnInput,
86888
+ success: ThreadSpawnResult,
86889
+ failure: ThreadControlToolError,
86890
+ dependencies: [
86891
+ McpInvocationContext,
86892
+ McpSessionRegistry,
86893
+ OrchestrationEngineService,
86894
+ ProjectionThreadRepository,
86895
+ Crypto.Crypto
86896
+ ]
86897
+ }).annotate(Tool.Title, "Start a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
86898
+ const ThreadConfigureTool = Tool.make("thread_configure", {
86899
+ description: "Change a thread's settings: title, model, permission mode, interaction mode, compression, and whether it may use subagents without asking. Omit threadId to configure this session's own thread; name one only if this session started it. Every field is optional and an omitted field is left alone; the settings actually changed come back in the result.",
86900
+ parameters: ThreadConfigureInput,
86901
+ success: ThreadConfigureResult,
86902
+ failure: ThreadControlToolError,
86903
+ dependencies: [
86904
+ McpInvocationContext,
86905
+ OrchestrationEngineService,
86906
+ Crypto.Crypto
86907
+ ]
86908
+ }).annotate(Tool.Title, "Configure a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86909
+ const ThreadSettleTool = Tool.make("thread_settle", {
86910
+ description: "Settle a thread, marking its work finished so it drops out of the active list. Omit threadId for this session's own thread; name one only if this session started it. A thread whose session is still running cannot be settled on the spot, so the request is recorded and applied when that session stops - the result says which of the two happened.",
86911
+ parameters: ThreadSettleInput,
86912
+ success: ThreadSettleResult,
86913
+ failure: ThreadControlToolError,
86914
+ dependencies: [
86915
+ McpInvocationContext,
86916
+ OrchestrationEngineService,
86917
+ ProjectionThreadRepository,
86918
+ ProjectionThreadSessionRepository,
86919
+ Crypto.Crypto
86920
+ ]
86921
+ }).annotate(Tool.Title, "Settle a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86922
+ const ThreadSnoozeTool = Tool.make("thread_snooze", {
86923
+ description: "Snooze a thread until a given time, hiding it from the active list until then. Omit threadId for this session's own thread; name one only if this session started it.",
86924
+ parameters: ThreadSnoozeInput,
86925
+ success: ThreadSnoozeResult,
86926
+ failure: ThreadControlToolError,
86927
+ dependencies: [
86928
+ McpInvocationContext,
86929
+ OrchestrationEngineService,
86930
+ Crypto.Crypto
86931
+ ]
86932
+ }).annotate(Tool.Title, "Snooze a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86021
86933
  const MemoryAppendTool = Tool.make("memory_append", {
86022
86934
  description: "Append a block of markdown to the user-scope memory file (CLAUDE.md by default, AGENTS.md on request). Use it to record a durable preference or fact the user asked to remember. Appends only - it never rewrites or removes existing memory.",
86023
86935
  parameters: MemoryAppendInput,
@@ -86030,11 +86942,221 @@ const MemoryAppendTool = Tool.make("memory_append", {
86030
86942
  Path.Path
86031
86943
  ]
86032
86944
  }).annotate(Tool.Title, "Append to memory").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
86033
- const ThreadToolkit = Toolkit.make(ThreadRenameTool, MemoryAppendTool);
86945
+ /**
86946
+ * Overwrite an asset with a compressed version of itself, if it survives the
86947
+ * comparison.
86948
+ *
86949
+ * A tool rather than an instruction to use `Edit` because the guarantee is the
86950
+ * point: the original is copied out of every synced directory first, the
86951
+ * rewrite is compared against it, and a rewrite that lost a URL or a code block
86952
+ * is refused with the list of what it lost. An agent editing the file directly
86953
+ * can be asked to preserve those things but cannot be prevented from not.
86954
+ */
86955
+ const AssetCompressTool = Tool.make("asset_compress", {
86956
+ description: "Overwrite one agent asset file - a skill, a memory file, a subagent definition - with a compressed rewrite of it, after checking the rewrite kept everything that carries meaning. Headings, fenced code blocks, inline code, URLs, paths and YAML frontmatter must come back unchanged; only the prose may be rewritten. The original is backed up outside every synced directory first. If the check fails nothing is written and the failed checks are returned, so a corrected rewrite can be sent as a second call.",
86957
+ parameters: AssetCompressInput,
86958
+ success: AssetCompressResult,
86959
+ failure: ThreadControlToolError,
86960
+ dependencies: [
86961
+ McpInvocationContext,
86962
+ ServerConfig$1,
86963
+ ServerSettingsService,
86964
+ FileSystem.FileSystem,
86965
+ Path.Path
86966
+ ]
86967
+ }).annotate(Tool.Title, "Compress an asset").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false);
86968
+ const ThreadToolkit = Toolkit.make(ThreadSpawnTool, ThreadConfigureTool, ThreadSettleTool, ThreadSnoozeTool, ThreadRenameTool, MemoryAppendTool, AssetCompressTool);
86034
86969
  //#endregion
86035
86970
  //#region src/mcp/toolkits/threads/handlers.ts
86036
86971
  const DEFAULT_MEMORY_APPEND_TARGET = "CLAUDE.md";
86972
+ /**
86973
+ * Where originals are kept.
86974
+ *
86975
+ * Under the server's state directory, which is the one place on this machine
86976
+ * that is neither an asset root nor inside one. A backup next to the file would
86977
+ * be published by the next sync and re-read by the next agent turn, which is
86978
+ * the opposite of what compressing the file was for.
86979
+ */
86980
+ const COMPRESS_BACKUP_DIRNAME = "asset-compress-backups";
86981
+ /** Prose only. A skill may ship scripts, and a rewritten script is a broken one. */
86982
+ const COMPRESSIBLE_EXTENSION = ".md";
86983
+ /** The same test the asset readers use: a NUL is what breaks a JSON text column. */
86984
+ const looksBinary = (content) => content.includes("\0");
86985
+ /**
86986
+ * Every control command shares this: mint an id, dispatch, and turn a decider
86987
+ * refusal into one error the agent can read.
86988
+ */
86989
+ const dispatchControl = Effect.fn("mcp.threads.dispatchControl")(function* (command, threadId) {
86990
+ yield* (yield* OrchestrationEngineService).dispatch(command).pipe(Effect.mapError((cause) => new ThreadControlRejectedError({
86991
+ threadId,
86992
+ commandType: command.type,
86993
+ detail: cause.message
86994
+ })));
86995
+ });
86996
+ const newCommandId = Effect.gen(function* () {
86997
+ const crypto = yield* Crypto.Crypto;
86998
+ return CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
86999
+ });
86037
87000
  const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
87001
+ thread_spawn: (input) => Effect.gen(function* () {
87002
+ const invocation = yield* requireThreadSpawn();
87003
+ const registry = yield* McpSessionRegistry;
87004
+ const threads = yield* ProjectionThreadRepository;
87005
+ const crypto = yield* Crypto.Crypto;
87006
+ const parent = yield* threads.getById({ threadId: invocation.threadId }).pipe(Effect.mapError((cause) => new ThreadSpawnNotPermittedError({
87007
+ threadId: invocation.threadId,
87008
+ detail: `this thread could not be read to take its defaults from: ${cause.message}`
87009
+ })));
87010
+ if (Option.isNone(parent)) return yield* new ThreadSpawnNotPermittedError({
87011
+ threadId: invocation.threadId,
87012
+ detail: "this thread is not on the board, so there is nothing to start a thread beside"
87013
+ });
87014
+ const template = parent.value;
87015
+ const projectId = input.projectId ?? template.projectId;
87016
+ const runtimeMode = input.runtimeMode ?? template.runtimeMode;
87017
+ const interactionMode = input.interactionMode ?? template.interactionMode;
87018
+ const compressMode = input.compressMode ?? template.compressMode;
87019
+ const unpromptedSubagents = input.unpromptedSubagents ?? template.unpromptedSubagents === 1;
87020
+ const threadId = ThreadId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
87021
+ const createdAt = DateTime.formatIso(yield* DateTime.now);
87022
+ yield* dispatchControl({
87023
+ type: "thread.create",
87024
+ commandId: yield* newCommandId,
87025
+ threadId,
87026
+ projectId,
87027
+ title: input.title,
87028
+ modelSelection: template.modelSelection,
87029
+ runtimeMode,
87030
+ interactionMode,
87031
+ compressMode,
87032
+ unpromptedSubagents,
87033
+ branch: null,
87034
+ worktreePath: null,
87035
+ createdAt
87036
+ }, threadId);
87037
+ yield* registry.recordSpawnedThread({
87038
+ providerSessionId: invocation.providerSessionId,
87039
+ threadId
87040
+ });
87041
+ yield* dispatchControl({
87042
+ type: "thread.turn.start",
87043
+ commandId: yield* newCommandId,
87044
+ threadId,
87045
+ message: {
87046
+ messageId: MessageId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)),
87047
+ role: "user",
87048
+ text: input.prompt,
87049
+ attachments: []
87050
+ },
87051
+ modelSelection: template.modelSelection,
87052
+ titleSeed: input.title,
87053
+ runtimeMode,
87054
+ interactionMode,
87055
+ compressMode,
87056
+ unpromptedSubagents,
87057
+ createdAt
87058
+ }, threadId);
87059
+ return {
87060
+ threadId,
87061
+ projectId,
87062
+ title: input.title
87063
+ };
87064
+ }),
87065
+ thread_configure: (input) => Effect.gen(function* () {
87066
+ const { threadId } = yield* requireThreadControlTarget(input.threadId);
87067
+ const createdAt = DateTime.formatIso(yield* DateTime.now);
87068
+ const applied = [];
87069
+ if (input.title !== void 0 || input.modelSelection !== void 0) {
87070
+ yield* dispatchControl({
87071
+ type: "thread.meta.update",
87072
+ commandId: yield* newCommandId,
87073
+ threadId,
87074
+ ...input.title !== void 0 ? { title: input.title } : {},
87075
+ ...input.modelSelection !== void 0 ? { modelSelection: input.modelSelection } : {}
87076
+ }, threadId);
87077
+ if (input.title !== void 0) applied.push("title");
87078
+ if (input.modelSelection !== void 0) applied.push("modelSelection");
87079
+ }
87080
+ if (input.runtimeMode !== void 0) {
87081
+ yield* dispatchControl({
87082
+ type: "thread.runtime-mode.set",
87083
+ commandId: yield* newCommandId,
87084
+ threadId,
87085
+ runtimeMode: input.runtimeMode,
87086
+ createdAt
87087
+ }, threadId);
87088
+ applied.push("runtimeMode");
87089
+ }
87090
+ if (input.interactionMode !== void 0) {
87091
+ yield* dispatchControl({
87092
+ type: "thread.interaction-mode.set",
87093
+ commandId: yield* newCommandId,
87094
+ threadId,
87095
+ interactionMode: input.interactionMode,
87096
+ createdAt
87097
+ }, threadId);
87098
+ applied.push("interactionMode");
87099
+ }
87100
+ if (input.compressMode !== void 0) {
87101
+ yield* dispatchControl({
87102
+ type: "thread.compress-mode.set",
87103
+ commandId: yield* newCommandId,
87104
+ threadId,
87105
+ compressMode: input.compressMode,
87106
+ createdAt
87107
+ }, threadId);
87108
+ applied.push("compressMode");
87109
+ }
87110
+ if (input.unpromptedSubagents !== void 0) {
87111
+ yield* dispatchControl({
87112
+ type: "thread.unprompted-subagents.set",
87113
+ commandId: yield* newCommandId,
87114
+ threadId,
87115
+ unpromptedSubagents: input.unpromptedSubagents,
87116
+ createdAt
87117
+ }, threadId);
87118
+ applied.push("unpromptedSubagents");
87119
+ }
87120
+ return {
87121
+ threadId,
87122
+ applied
87123
+ };
87124
+ }),
87125
+ thread_settle: (input) => Effect.gen(function* () {
87126
+ const { threadId } = yield* requireThreadControlTarget(input.threadId);
87127
+ const threads = yield* ProjectionThreadRepository;
87128
+ const session = yield* (yield* ProjectionThreadSessionRepository).getByThreadId({ threadId }).pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
87129
+ if (!(Option.isSome(session) && (session.value.status === "starting" || session.value.status === "running"))) {
87130
+ yield* dispatchControl({
87131
+ type: "thread.settle",
87132
+ commandId: yield* newCommandId,
87133
+ threadId
87134
+ }, threadId);
87135
+ return {
87136
+ threadId,
87137
+ outcome: "settled"
87138
+ };
87139
+ }
87140
+ queueThreadSettle(threadId);
87141
+ yield* threads.getById({ threadId }).pipe(Effect.orDie);
87142
+ return {
87143
+ threadId,
87144
+ outcome: "queued"
87145
+ };
87146
+ }),
87147
+ thread_snooze: (input) => Effect.gen(function* () {
87148
+ const { threadId } = yield* requireThreadControlTarget(input.threadId);
87149
+ yield* dispatchControl({
87150
+ type: "thread.snooze",
87151
+ commandId: yield* newCommandId,
87152
+ threadId,
87153
+ snoozedUntil: input.snoozedUntil
87154
+ }, threadId);
87155
+ return {
87156
+ threadId,
87157
+ snoozedUntil: input.snoozedUntil
87158
+ };
87159
+ }),
86038
87160
  thread_rename: (input) => Effect.gen(function* () {
86039
87161
  const invocation = yield* requireThreadCapability();
86040
87162
  const engine = yield* OrchestrationEngineService;
@@ -86083,6 +87205,68 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
86083
87205
  file,
86084
87206
  path: filePath
86085
87207
  };
87208
+ }),
87209
+ asset_compress: (input) => Effect.gen(function* () {
87210
+ yield* requireThreadCapability();
87211
+ const path = yield* Path.Path;
87212
+ const fileSystem = yield* FileSystem.FileSystem;
87213
+ const config = yield* ServerConfig$1;
87214
+ const claudeHome = { homePath: (yield* (yield* ServerSettingsService).getSettings.pipe(Effect.orElseSucceed(() => void 0)))?.providers.claudeAgent.homePath ?? "" };
87215
+ const refuse = (target, detail) => new AssetCompressRefusedError({
87216
+ path: target,
87217
+ detail
87218
+ });
87219
+ if (!path.isAbsolute(input.path)) return yield* refuse(input.path, "The path must be absolute.");
87220
+ const filePath = path.resolve(input.path);
87221
+ const fileName = path.basename(filePath);
87222
+ if (path.extname(fileName).toLowerCase() !== COMPRESSIBLE_EXTENSION) return yield* refuse(filePath, `Only ${COMPRESSIBLE_EXTENSION} files are compressed. Scripts and data files an asset ships alongside its prose are left alone.`);
87223
+ if (isSensitiveAssetPath(filePath)) return yield* refuse(filePath, "The name looks like credentials, keys or secrets, and compressing sends the file's contents to a model.");
87224
+ const configDir = yield* resolveClaudeConfigDirPath(claudeHome, process.env);
87225
+ const skillsDir = yield* resolveClaudeUserSkillsDir(claudeHome);
87226
+ const agentsDir = yield* resolveClaudeUserAgentsDir(claudeHome);
87227
+ const within = (root) => {
87228
+ const relative = path.relative(root, filePath);
87229
+ return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
87230
+ };
87231
+ const memoryDirectoryName = path.relative(configDir, filePath).split(path.sep)[0] ?? "";
87232
+ if (!(within(skillsDir) || within(agentsDir) || path.dirname(filePath) === configDir && isMemoryFileName(fileName) || within(configDir) && isMemoryDirectoryName(memoryDirectoryName))) return yield* refuse(filePath, `Not an agent asset. Compressible files live under '${skillsDir}', under '${agentsDir}', or in '${configDir}' as a memory file or inside ${MEMORY_DIRECTORY_NAMES.join(" or ")}.`);
87233
+ const original = yield* fileSystem.readFileString(filePath).pipe(Effect.orElseSucceed(() => void 0));
87234
+ if (original === void 0) return yield* refuse(filePath, "The file could not be read.");
87235
+ if (looksBinary(original)) return yield* refuse(filePath, "The file is not text.");
87236
+ if (original.trim().length === 0) return yield* refuse(filePath, "The file is empty.");
87237
+ const compressed = input.content;
87238
+ if (compressed.trim().length === 0) return yield* refuse(filePath, "The rewrite is blank.");
87239
+ if (compressed.trim() === original.trim()) return yield* refuse(filePath, "The rewrite is the file that is already there. Nothing was written.");
87240
+ if (compressed.length >= original.length) return yield* refuse(filePath, `The rewrite is not smaller: ${original.length} characters became ${compressed.length}. Send the file's contents alone, with no commentary or wrapping fence.`);
87241
+ const validation = validateCompression(original, compressed);
87242
+ if (validation.errors.length > 0) return yield* new AssetCompressValidationFailedError({
87243
+ path: filePath,
87244
+ errors: validation.errors
87245
+ });
87246
+ const backupPath = path.join(config.stateDir, COMPRESS_BACKUP_DIRNAME, path.basename(path.dirname(filePath)), backupFileName({
87247
+ fileName,
87248
+ atIso: DateTime.formatIso(yield* DateTime.now)
87249
+ }));
87250
+ if (((yield* writeFileStringAtomically({
87251
+ filePath: backupPath,
87252
+ contents: original
87253
+ }).pipe(Effect.as(true), Effect.orElseSucceed(() => false))) ? yield* fileSystem.readFileString(backupPath).pipe(Effect.orElseSucceed(() => void 0)) : void 0) !== original) {
87254
+ yield* fileSystem.remove(backupPath, { force: true }).pipe(Effect.ignore);
87255
+ return yield* refuse(filePath, `The original could not be backed up to '${backupPath}', so it was left as it is.`);
87256
+ }
87257
+ yield* writeFileStringAtomically({
87258
+ filePath,
87259
+ contents: compressed
87260
+ }).pipe(Effect.mapError((cause) => refuse(filePath, `The write failed: ${cause.message}. The original is at '${backupPath}'.`)));
87261
+ return {
87262
+ path: filePath,
87263
+ backupPath,
87264
+ charsBefore: original.length,
87265
+ charsAfter: compressed.length,
87266
+ estimatedTokensBefore: estimateTokens(original),
87267
+ estimatedTokensAfter: estimateTokens(compressed),
87268
+ warnings: validation.warnings
87269
+ };
86086
87270
  })
86087
87271
  });
86088
87272
  //#endregion
@@ -87533,13 +88717,34 @@ const make$2 = Effect.gen(function* () {
87533
88717
  if (providerError) return providerError.detail;
87534
88718
  return Cause.pretty(cause);
87535
88719
  };
88720
+ /**
88721
+ * Applies a settle an agent asked for while its session was still running.
88722
+ *
88723
+ * The decider refuses `thread.settle` for a live session, so `thread_settle`
88724
+ * over MCP records the intent instead of failing. This is the moment it
88725
+ * becomes legal: the session has just been written as stopped, so the same
88726
+ * command the agent could not send now succeeds. Taken from the queue rather
88727
+ * than read, so a second stop cannot settle the thread twice.
88728
+ */
88729
+ const flushQueuedSettle = Effect.fnUntraced(function* (threadId) {
88730
+ if (!takeQueuedThreadSettle(threadId)) return;
88731
+ const commandId = yield* serverCommandId("queued-thread-settle");
88732
+ yield* orchestrationEngine.dispatch({
88733
+ type: "thread.settle",
88734
+ commandId,
88735
+ threadId
88736
+ }).pipe(Effect.catchCause((cause) => Effect.logDebug("queued thread settle did not apply", {
88737
+ threadId,
88738
+ cause
88739
+ })));
88740
+ });
87536
88741
  const setThreadSession = (input) => serverCommandId("provider-session-set").pipe(Effect.flatMap((commandId) => orchestrationEngine.dispatch({
87537
88742
  type: "thread.session.set",
87538
88743
  commandId,
87539
88744
  threadId: input.threadId,
87540
88745
  session: input.session,
87541
88746
  createdAt: input.createdAt
87542
- })));
88747
+ })), Effect.tap(() => input.session.status === "stopped" || input.session.status === "error" ? flushQueuedSettle(input.threadId) : Effect.void));
87543
88748
  const setThreadSessionErrorOnTurnStartFailure = Effect.fnUntraced(function* (input) {
87544
88749
  const thread = yield* resolveThread(input.threadId);
87545
88750
  if (!thread) return;
@@ -87666,7 +88871,8 @@ const make$2 = Effect.gen(function* () {
87666
88871
  modelSelection: desiredModelSelection,
87667
88872
  ...input?.resumeCursor !== void 0 ? { resumeCursor: input.resumeCursor } : {},
87668
88873
  runtimeMode: desiredRuntimeMode,
87669
- compressMode: thread.compressMode
88874
+ compressMode: thread.compressMode,
88875
+ unpromptedSubagents: thread.unpromptedSubagents
87670
88876
  });
87671
88877
  const bindSessionToThread = (session) => Effect.gen(function* () {
87672
88878
  if (session.providerInstanceId === void 0) return yield* new ProviderAdapterRequestError({
@@ -87768,7 +88974,8 @@ const make$2 = Effect.gen(function* () {
87768
88974
  ...normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {},
87769
88975
  ...modelForTurn !== void 0 ? { modelSelection: modelForTurn } : {},
87770
88976
  ...input.interactionMode !== void 0 ? { interactionMode: input.interactionMode } : {},
87771
- compressMode
88977
+ compressMode,
88978
+ unpromptedSubagents: thread.unpromptedSubagents
87772
88979
  };
87773
88980
  });
87774
88981
  const maybeGenerateAndRenameWorktreeBranchForFirstTurn = Effect.fn("maybeGenerateAndRenameWorktreeBranchForFirstTurn")(function* (input) {