@p4code/cli 0.1.19 → 0.1.20

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 (85) hide show
  1. package/dist/bin.mjs +703 -20
  2. package/dist/client/assets/{DiffPanel-CseUmWvE.js → DiffPanel-Di8WylWU.js} +2 -2
  3. package/dist/client/assets/{FilePreviewPanel-Dm7l5FFh.js → FilePreviewPanel-CSqqsbYX.js} +2 -2
  4. package/dist/client/assets/{PreviewPanel-DOyykNL9.js → PreviewPanel-BcRUQSMy.js} +2 -2
  5. package/dist/client/assets/{angular-html-YobKPLun.js → angular-html-CPSNyy71.js} +2 -2
  6. package/dist/client/assets/{angular-ts-Ac3I6TFV.js → angular-ts-BgndqOdx.js} +2 -2
  7. package/dist/client/assets/{apl-Dwl73GTS.js → apl-CFWK0bta.js} +2 -2
  8. package/dist/client/assets/arrow-right-wbRntSmq.js +2 -0
  9. package/dist/client/assets/{astro-KBgldnxZ.js → astro-D2uzatlp.js} +2 -2
  10. package/dist/client/assets/{blade-DTU2NGhh.js → blade-DfrK_8z3.js} +2 -2
  11. package/dist/client/assets/{c-COF4kWCp.js → c-DyORVYNJ.js} +2 -2
  12. package/dist/client/assets/{cobol-BA7JsdNr.js → cobol-BUWePdBc.js} +2 -2
  13. package/dist/client/assets/{coffee-PFN_w4qL.js → coffee-Ba8BYHli.js} +2 -2
  14. package/dist/client/assets/{cpp-CpT6w-g5.js → cpp-BX0iAGNA.js} +2 -2
  15. package/dist/client/assets/{crystal-BrlaRT0B.js → crystal-BDSdceoQ.js} +2 -2
  16. package/dist/client/assets/{css-C2LMO3MX.js → css-TKd0Sp86.js} +2 -2
  17. package/dist/client/assets/{edge-DBkQ-Ajd.js → edge-Bus7Aab8.js} +2 -2
  18. package/dist/client/assets/{elixir-EbSNDEx0.js → elixir-BlT2g4EX.js} +2 -2
  19. package/dist/client/assets/{elm-CSs0Wzy2.js → elm-DTGJZ36e.js} +2 -2
  20. package/dist/client/assets/{erb-BBmMQG9b.js → erb-BF4yZGxQ.js} +2 -2
  21. package/dist/client/assets/{fileCommentAnnotations-CifvTzWP.js → fileCommentAnnotations-C5BlQ5s_.js} +2 -2
  22. package/dist/client/assets/{git-rebase-C09BpZDw.js → git-rebase-GAe0_ZI9.js} +2 -2
  23. package/dist/client/assets/{glimmer-js-D4Bqn6Yg.js → glimmer-js-GWmPSTNt.js} +2 -2
  24. package/dist/client/assets/{glimmer-ts-fLCHL8o6.js → glimmer-ts-CDLqKTZ9.js} +2 -2
  25. package/dist/client/assets/{glsl-17sV9oGm.js → glsl-Bv3E3r0O.js} +2 -2
  26. package/dist/client/assets/{graphql-BqFquulN.js → graphql-B5-LupiK.js} +2 -2
  27. package/dist/client/assets/{hack-D6Jpv-j9.js → hack-KTQ3DPKm.js} +2 -2
  28. package/dist/client/assets/{haml-D0eo8PEJ.js → haml-BvC-52XY.js} +2 -2
  29. package/dist/client/assets/{handlebars-HG0XNkEg.js → handlebars-CK9WCq20.js} +2 -2
  30. package/dist/client/assets/{html-Bh3padJ5.js → html-BGjxg0hO.js} +2 -2
  31. package/dist/client/assets/{html-derivative-DVHw4MNX.js → html-derivative-C0EaHUuc.js} +2 -2
  32. package/dist/client/assets/{http-CYCXaOA5.js → http-hvxfwUee.js} +2 -2
  33. package/dist/client/assets/{hurl-D4oK46oh.js → hurl-D5cuiBPB.js} +2 -2
  34. package/dist/client/assets/index-QC2FhlVn.js +836 -0
  35. package/dist/client/assets/{java-nvFYZOCN.js → java-DOkofowB.js} +2 -2
  36. package/dist/client/assets/{javascript-CMBxjf-M.js → javascript-dCesbfJ7.js} +2 -2
  37. package/dist/client/assets/{jinja-vsPdkFFj.js → jinja-BDrVqT_2.js} +2 -2
  38. package/dist/client/assets/{jison-uky2pPNE.js → jison-Dvzh0XRq.js} +2 -2
  39. package/dist/client/assets/{json-DOSqa2F1.js → json-CpV_dVb2.js} +2 -2
  40. package/dist/client/assets/{jsx-Ci7CVNQe.js → jsx-CbaKC498.js} +2 -2
  41. package/dist/client/assets/{julia-J2m7N3_R.js → julia-BKYdI_5W.js} +2 -2
  42. package/dist/client/assets/{just-BDy3oM_N.js → just-FrVMEGvg.js} +2 -2
  43. package/dist/client/assets/{latex-CMD49F7D.js → latex-BSmwt_7j.js} +2 -2
  44. package/dist/client/assets/{liquid-Dq6k2YW6.js → liquid-Bb2nFp-U.js} +2 -2
  45. package/dist/client/assets/{lua-CohtP6J8.js → lua-CEANnQ2c.js} +2 -2
  46. package/dist/client/assets/{marko-C4V2zw1t.js → marko-CjJZ1eHg.js} +2 -2
  47. package/dist/client/assets/{mdc-q1ZKjwYt.js → mdc-B3VSAYVT.js} +2 -2
  48. package/dist/client/assets/{nginx-D2TdEIZ3.js → nginx-BkYJUm4E.js} +2 -2
  49. package/dist/client/assets/{nim-BtWKpYQs.js → nim-D8xT6_xt.js} +2 -2
  50. package/dist/client/assets/{perl-Ci4Pso9h.js → perl-cazo7YHC.js} +2 -2
  51. package/dist/client/assets/{php-CMzpcAju.js → php-C0LwuOo3.js} +2 -2
  52. package/dist/client/assets/{pug-D0sbFhP6.js → pug-OP6SwNFw.js} +2 -2
  53. package/dist/client/assets/{qml-BfWXHzNc.js → qml-C1-WQIQ-.js} +2 -2
  54. package/dist/client/assets/{r-BN4rQDnz.js → r-PkhwsIgx.js} +2 -2
  55. package/dist/client/assets/{razor-D5iw68yr.js → razor-WONkd3aR.js} +2 -2
  56. package/dist/client/assets/{regexp-Ba0WieCb.js → regexp-SDnpa4md.js} +2 -2
  57. package/dist/client/assets/{rst-DQqlRfnq.js → rst-DulobrTh.js} +2 -2
  58. package/dist/client/assets/{ruby-CReh9Gta.js → ruby-RYHm8H3x.js} +2 -2
  59. package/dist/client/assets/{sas-CJ-Cpd8L.js → sas-BBwb67VZ.js} +2 -2
  60. package/dist/client/assets/{scss-CZp-cH6d.js → scss-y2IZtjKJ.js} +2 -2
  61. package/dist/client/assets/{shellscript-hCSuhhJZ.js → shellscript-B3vxtpJZ.js} +2 -2
  62. package/dist/client/assets/{shellsession-BnDNcCDQ.js → shellsession-CvHfy0TS.js} +2 -2
  63. package/dist/client/assets/{soy-BXarcWJY.js → soy-DWpn3a9x.js} +2 -2
  64. package/dist/client/assets/{sql-ADQCXhYd.js → sql-CCfTsIvj.js} +2 -2
  65. package/dist/client/assets/{stata-DSS4IpqF.js → stata-Dr5CN-Tp.js} +2 -2
  66. package/dist/client/assets/{surrealql-DuYcftNm.js → surrealql-DgzXJo35.js} +2 -2
  67. package/dist/client/assets/{svelte-CAXOFInl.js → svelte-JQb5PCjP.js} +2 -2
  68. package/dist/client/assets/{templ-BvTIEIah.js → templ-DQt5AsQ7.js} +2 -2
  69. package/dist/client/assets/{tex-DpnVRTMM.js → tex-B8Qokwwv.js} +2 -2
  70. package/dist/client/assets/textarea-BlxAoGJe.js +46 -0
  71. package/dist/client/assets/{ts-tags-4wlHlovT.js → ts-tags-B-RrSBzw.js} +2 -2
  72. package/dist/client/assets/{tsx-CHDgELdy.js → tsx-C7Q0nn_B.js} +2 -2
  73. package/dist/client/assets/{twig-DPmx2UHF.js → twig-CzVGXsz7.js} +2 -2
  74. package/dist/client/assets/{typescript-CHO8bW-H.js → typescript-irwEvPgw.js} +2 -2
  75. package/dist/client/assets/{vue-CM19I03n.js → vue-BE-nMa-3.js} +2 -2
  76. package/dist/client/assets/{vue-html-ginfkxGQ.js → vue-html-SXHQOmAo.js} +2 -2
  77. package/dist/client/assets/{vue-vine-CGdZxUwf.js → vue-vine-D62pnm9I.js} +2 -2
  78. package/dist/client/assets/{xml-B8CY16OL.js → xml-BnssPo1i.js} +2 -2
  79. package/dist/client/assets/{xsl-COHh7n8v.js → xsl-Dg7cN1LO.js} +2 -2
  80. package/dist/client/assets/{yaml-DXx-djat.js → yaml-BAeQKrXU.js} +2 -2
  81. package/dist/client/index.html +2 -2
  82. package/package.json +1 -1
  83. package/dist/client/assets/arrow-right-BqDszij1.js +0 -2
  84. package/dist/client/assets/index-CV_dK79d.js +0 -783
  85. 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.20";
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),
@@ -7799,6 +7905,11 @@ var TicketResolveError = class extends Schema$1.TaggedErrorClass()("TicketResolv
7799
7905
  * Quick-append to memory lives here too: it is the other thing an agent wants
7800
7906
  * to do *from* a thread, and it shares the toolkit's capability and scope.
7801
7907
  *
7908
+ * Compressing an asset joins them for a different reason. It is a write the
7909
+ * agent could perform with its ordinary file tools, and the point of routing it
7910
+ * through a tool instead is that the server gets to check the rewrite before it
7911
+ * lands - and to keep the original somewhere the rewrite cannot reach.
7912
+ *
7802
7913
  * @module contracts/threadControl
7803
7914
  */
7804
7915
  const ThreadRenameInput = Schema$1.Struct({ title: TrimmedNonEmptyString.annotate({ description: "The new thread title. Replaces the current one." }) });
@@ -7825,6 +7936,23 @@ const MemoryAppendResult = Schema$1.Struct({
7825
7936
  /** Absolute path the append landed in, so the agent can cite or inspect it. */
7826
7937
  path: Schema$1.String
7827
7938
  });
7939
+ const AssetCompressInput = Schema$1.Struct({
7940
+ 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." }),
7941
+ 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." })
7942
+ });
7943
+ const AssetCompressResult = Schema$1.Struct({
7944
+ /** Absolute path that was rewritten. */
7945
+ path: Schema$1.String,
7946
+ /** Where the pre-compression original was kept, outside every synced directory. */
7947
+ backupPath: Schema$1.String,
7948
+ charsBefore: Schema$1.Number,
7949
+ charsAfter: Schema$1.Number,
7950
+ /** Estimated at four characters per token, not measured by a provider tokenizer. */
7951
+ estimatedTokensBefore: Schema$1.Number,
7952
+ estimatedTokensAfter: Schema$1.Number,
7953
+ /** Differences worth reporting that were not worth refusing the write over. */
7954
+ warnings: Schema$1.Array(Schema$1.String)
7955
+ });
7828
7956
  var ThreadToolUnavailableError = class extends Schema$1.TaggedErrorClass()("ThreadToolUnavailableError", {
7829
7957
  capability: Schema$1.Literal("threads"),
7830
7958
  environmentId: EnvironmentId,
@@ -7853,10 +7981,43 @@ var MemoryAppendFailedError = class extends Schema$1.TaggedErrorClass()("MemoryA
7853
7981
  return `Appending to ${this.file} failed: ${this.detail}`;
7854
7982
  }
7855
7983
  };
7984
+ /**
7985
+ * The compression will not be attempted, or the file could not be read.
7986
+ *
7987
+ * Separate from the validation failure below because they want opposite
7988
+ * responses. A refusal is final - the path is not an asset, the name looks like
7989
+ * a credential, the file is not there - and retrying it produces the same
7990
+ * answer. A validation failure is a rewrite that can be corrected.
7991
+ */
7992
+ var AssetCompressRefusedError = class extends Schema$1.TaggedErrorClass()("AssetCompressRefusedError", {
7993
+ path: Schema$1.String,
7994
+ detail: Schema$1.String
7995
+ }) {
7996
+ get message() {
7997
+ return `Compressing ${this.path} was refused: ${this.detail}`;
7998
+ }
7999
+ };
8000
+ /**
8001
+ * The rewrite lost something, and nothing was written.
8002
+ *
8003
+ * Carries the checks that failed rather than a summary, because the agent's
8004
+ * next move is to restore exactly those things from the original and call
8005
+ * again. The file on disk is untouched, so a second failure costs nothing more.
8006
+ */
8007
+ var AssetCompressValidationFailedError = class extends Schema$1.TaggedErrorClass()("AssetCompressValidationFailedError", {
8008
+ path: Schema$1.String,
8009
+ errors: Schema$1.Array(Schema$1.String)
8010
+ }) {
8011
+ get message() {
8012
+ return `The compressed ${this.path} did not validate, so nothing was written: ${this.errors.join(" ")}`;
8013
+ }
8014
+ };
7856
8015
  const ThreadControlToolError = Schema$1.Union([
7857
8016
  ThreadToolUnavailableError,
7858
8017
  ThreadRenameRejectedError,
7859
- MemoryAppendFailedError
8018
+ MemoryAppendFailedError,
8019
+ AssetCompressRefusedError,
8020
+ AssetCompressValidationFailedError
7860
8021
  ]);
7861
8022
  const ThreadWatchEventsInput = Schema$1.Struct({
7862
8023
  threadId: ThreadId.annotate({ description: "The watched thread to read events from. Must be explicitly granted." }),
@@ -12660,6 +12821,53 @@ var _040_TaskExternalRef_default = Effect.gen(function* () {
12660
12821
  `;
12661
12822
  });
12662
12823
  //#endregion
12824
+ //#region src/persistence/Migrations/041_TaskParentTaskId.ts
12825
+ /**
12826
+ * The task a task was split out of, for the plan phase's subtasks.
12827
+ *
12828
+ * A plain column rather than a join table: a task has at most one parent, and
12829
+ * the question asked of it is always "children of this row", which one indexed
12830
+ * column answers.
12831
+ *
12832
+ * No foreign key. Tasks are synced between machines and a child can arrive
12833
+ * before its parent does, or outlive it - a constraint would reject the write
12834
+ * rather than leave a link that resolves later. The board treats a parent it
12835
+ * cannot find as no parent at all.
12836
+ *
12837
+ * The index is non-unique and partial: many children per parent, and the many
12838
+ * rows that are nobody's child stay out of it.
12839
+ */
12840
+ var _041_TaskParentTaskId_default = Effect.gen(function* () {
12841
+ const sql = yield* SqlClient.SqlClient;
12842
+ yield* sql`ALTER TABLE tasks ADD COLUMN parent_task_id TEXT`;
12843
+ yield* sql`
12844
+ CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id
12845
+ ON tasks(parent_task_id)
12846
+ WHERE parent_task_id IS NOT NULL
12847
+ `;
12848
+ });
12849
+ //#endregion
12850
+ //#region src/persistence/Migrations/042_ProjectionThreadsUnpromptedSubagents.ts
12851
+ /**
12852
+ * Whether a thread lets the model spawn subagents without being asked, moved
12853
+ * from a machine-wide setting onto the thread itself.
12854
+ *
12855
+ * Stored as an integer beside `has_actionable_proposed_plan` rather than as a
12856
+ * SQLite boolean, which does not exist; the projection reads it back as
12857
+ * `> 0`.
12858
+ *
12859
+ * The default is `1` — subagents allowed — because that is what every existing
12860
+ * thread has been doing under the old server setting. Anything else would
12861
+ * change the behaviour of live threads on upgrade, which is not a migration's
12862
+ * business.
12863
+ */
12864
+ var _042_ProjectionThreadsUnpromptedSubagents_default = Effect.gen(function* () {
12865
+ yield* (yield* SqlClient.SqlClient)`
12866
+ ALTER TABLE projection_threads
12867
+ ADD COLUMN unprompted_subagents INTEGER NOT NULL DEFAULT 1
12868
+ `;
12869
+ });
12870
+ //#endregion
12663
12871
  //#region src/persistence/Migrations.ts
12664
12872
  /**
12665
12873
  * MigrationsLive - Migration runner with inline loader
@@ -12880,6 +13088,16 @@ const migrationEntries = [
12880
13088
  40,
12881
13089
  "TaskExternalRef",
12882
13090
  _040_TaskExternalRef_default
13091
+ ],
13092
+ [
13093
+ 41,
13094
+ "TaskParentTaskId",
13095
+ _041_TaskParentTaskId_default
13096
+ ],
13097
+ [
13098
+ 42,
13099
+ "ProjectionThreadsUnpromptedSubagents",
13100
+ _042_ProjectionThreadsUnpromptedSubagents_default
12883
13101
  ]
12884
13102
  ];
12885
13103
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -14511,6 +14729,7 @@ const TASK_COLUMNS = `
14511
14729
  repository_key AS "repositoryKey",
14512
14730
  thread_id AS "threadId",
14513
14731
  external_ref_json AS "externalRef",
14732
+ parent_task_id AS "parentTaskId",
14514
14733
  title,
14515
14734
  body,
14516
14735
  status,
@@ -14535,6 +14754,7 @@ const makeTaskRepository = Effect.gen(function* () {
14535
14754
  repository_key,
14536
14755
  thread_id,
14537
14756
  external_ref_json,
14757
+ parent_task_id,
14538
14758
  title,
14539
14759
  body,
14540
14760
  status,
@@ -14552,6 +14772,7 @@ const makeTaskRepository = Effect.gen(function* () {
14552
14772
  ${row.repositoryKey},
14553
14773
  ${row.threadId},
14554
14774
  ${row.externalRef === null ? null : JSON.stringify(row.externalRef)},
14775
+ ${row.parentTaskId},
14555
14776
  ${row.title},
14556
14777
  ${row.body},
14557
14778
  ${row.status},
@@ -14569,6 +14790,7 @@ const makeTaskRepository = Effect.gen(function* () {
14569
14790
  repository_key = excluded.repository_key,
14570
14791
  thread_id = excluded.thread_id,
14571
14792
  external_ref_json = excluded.external_ref_json,
14793
+ parent_task_id = excluded.parent_task_id,
14572
14794
  title = excluded.title,
14573
14795
  body = excluded.body,
14574
14796
  status = excluded.status,
@@ -14668,6 +14890,7 @@ const makeTaskRepository = Effect.gen(function* () {
14668
14890
  ...input.repositoryKey !== void 0 ? { repositoryKey: input.repositoryKey } : {},
14669
14891
  ...input.threadId !== void 0 ? { threadId: input.threadId } : {},
14670
14892
  ...input.externalRef !== void 0 ? { externalRef: input.externalRef } : {},
14893
+ ...input.parentTaskId !== void 0 ? { parentTaskId: input.parentTaskId } : {},
14671
14894
  ...input.title !== void 0 ? { title: input.title } : {},
14672
14895
  ...input.body !== void 0 ? { body: input.body } : {},
14673
14896
  ...input.status !== void 0 ? { status: input.status } : {},
@@ -14706,6 +14929,7 @@ const makeTaskRepository = Effect.gen(function* () {
14706
14929
  ...input.patch.projectId !== void 0 ? { projectId: input.patch.projectId } : {},
14707
14930
  ...input.patch.repositoryKey !== void 0 ? { repositoryKey: input.patch.repositoryKey } : {},
14708
14931
  ...input.patch.threadId !== void 0 ? { threadId: input.patch.threadId } : {},
14932
+ ...input.patch.parentTaskId !== void 0 ? { parentTaskId: input.patch.parentTaskId } : {},
14709
14933
  ...input.patch.title !== void 0 ? { title: input.patch.title } : {},
14710
14934
  ...input.patch.body !== void 0 ? { body: input.patch.body } : {},
14711
14935
  ...input.patch.status !== void 0 ? { status: input.patch.status } : {},
@@ -14840,6 +15064,7 @@ const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Ef
14840
15064
  repositoryKey: input.repositoryKey ?? null,
14841
15065
  threadId: input.threadId ?? null,
14842
15066
  externalRef: input.externalRef ?? null,
15067
+ parentTaskId: input.parentTaskId ?? null,
14843
15068
  title: input.title,
14844
15069
  body: input.body ?? "",
14845
15070
  status: input.status ?? "backlog",
@@ -15159,6 +15384,22 @@ var _006_TaskExternalRef_default = Effect.gen(function* () {
15159
15384
  `;
15160
15385
  });
15161
15386
  //#endregion
15387
+ //#region src/hub/Migrations/007_TaskParentTaskId.ts
15388
+ /**
15389
+ * The task a task was split out of, mirroring the environment server's
15390
+ * migration 041. The hub carries the column so a plan filed on one machine
15391
+ * still reads as a plan on another, rather than as a handful of loose tasks.
15392
+ */
15393
+ var _007_TaskParentTaskId_default = Effect.gen(function* () {
15394
+ const sql = yield* SqlClient.SqlClient;
15395
+ yield* sql`ALTER TABLE tasks ADD COLUMN parent_task_id TEXT`;
15396
+ yield* sql`
15397
+ CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id
15398
+ ON tasks(parent_task_id)
15399
+ WHERE parent_task_id IS NOT NULL
15400
+ `;
15401
+ });
15402
+ //#endregion
15162
15403
  //#region src/hub/Migrations.ts
15163
15404
  /**
15164
15405
  * Hub migrations.
@@ -15199,6 +15440,11 @@ const hubMigrationEntries = [
15199
15440
  6,
15200
15441
  "TaskExternalRef",
15201
15442
  _006_TaskExternalRef_default
15443
+ ],
15444
+ [
15445
+ 7,
15446
+ "TaskParentTaskId",
15447
+ _007_TaskParentTaskId_default
15202
15448
  ]
15203
15449
  ];
15204
15450
  const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -16714,7 +16960,7 @@ const isIgnoredEntry = (entry) => IGNORED_ENTRIES.has(entry) || entry.startsWith
16714
16960
  * Exported so the single-file kinds ask it the same way this one does, rather
16715
16961
  * than each keeping a copy that could drift into a different answer.
16716
16962
  */
16717
- const looksBinary$1 = (content) => content.includes("\0");
16963
+ const looksBinary$2 = (content) => content.includes("\0");
16718
16964
  /**
16719
16965
  * Read one directory as an asset's files, or say why it cannot be one.
16720
16966
  *
@@ -16746,7 +16992,7 @@ const readSkillFiles = Effect.fn("skillDirectory.readSkillFiles")(function* (roo
16746
16992
  if (files.length >= 512) return "too-many-files";
16747
16993
  const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
16748
16994
  if (content === void 0) return "unreadable";
16749
- if (looksBinary$1(content)) return "binary-file";
16995
+ if (looksBinary$2(content)) return "binary-file";
16750
16996
  if (content.length > 524288) return "file-too-large";
16751
16997
  files.push({
16752
16998
  path: relative,
@@ -16870,7 +17116,7 @@ const readAgentDefinitions = Effect.fn("agentDefinitionFiles.read")(function* (a
16870
17116
  });
16871
17117
  continue;
16872
17118
  }
16873
- if (looksBinary$1(content)) {
17119
+ if (looksBinary$2(content)) {
16874
17120
  refused.push({
16875
17121
  name: entry,
16876
17122
  reason: "binary-file"
@@ -17392,7 +17638,7 @@ const isMemoryDirectoryName = (name) => MEMORY_DIRECTORY_NAMES.includes(name);
17392
17638
  */
17393
17639
  const isMemoryAssetName = (name) => isMemoryFileName(name) || isMemoryDirectoryName(name);
17394
17640
  /** The same test the skill reader uses: a NUL is what breaks a JSON text column. */
17395
- const looksBinary = (content) => content.includes("\0");
17641
+ const looksBinary$1 = (content) => content.includes("\0");
17396
17642
  /**
17397
17643
  * Read whichever memory files exist in this machine's config directory.
17398
17644
  *
@@ -17416,7 +17662,7 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
17416
17662
  });
17417
17663
  continue;
17418
17664
  }
17419
- if (looksBinary(content)) {
17665
+ if (looksBinary$1(content)) {
17420
17666
  refused.push({
17421
17667
  name,
17422
17668
  reason: "binary-file"
@@ -18529,6 +18775,8 @@ Never drop not/never/no/only/except: flipping the meaning costs more than any to
18529
18775
 
18530
18776
  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
18777
 
18778
+ 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.
18779
+
18532
18780
  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
18781
 
18534
18782
  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.
@@ -19788,6 +20036,7 @@ const ThreadMetaUpdatedPayload = ThreadMetaUpdatedPayload$1;
19788
20036
  const ThreadRuntimeModeSetPayload = ThreadRuntimeModeSetPayload$1;
19789
20037
  const ThreadInteractionModeSetPayload = ThreadInteractionModeSetPayload$1;
19790
20038
  const ThreadCompressModeSetPayload = ThreadCompressModeSetPayload$1;
20039
+ const ThreadUnpromptedSubagentsSetPayload = ThreadUnpromptedSubagentsSetPayload$1;
19791
20040
  const ThreadDeletedPayload = ThreadDeletedPayload$1;
19792
20041
  const ThreadUnarchivedPayload = ThreadUnarchivedPayload$1;
19793
20042
  const ThreadUnsettledPayload = ThreadUnsettledPayload$1;
@@ -19928,6 +20177,7 @@ function projectEvent(model, event) {
19928
20177
  runtimeMode: payload.runtimeMode,
19929
20178
  interactionMode: payload.interactionMode,
19930
20179
  compressMode: payload.compressMode,
20180
+ unpromptedSubagents: payload.unpromptedSubagents,
19931
20181
  branch: payload.branch,
19932
20182
  worktreePath: payload.worktreePath,
19933
20183
  latestTurn: null,
@@ -20034,6 +20284,13 @@ function projectEvent(model, event) {
20034
20284
  updatedAt: payload.updatedAt
20035
20285
  })
20036
20286
  })));
20287
+ case "thread.unprompted-subagents-set": return decodeForEvent(ThreadUnpromptedSubagentsSetPayload, event.payload, event.type, "payload").pipe(Effect.map((payload) => ({
20288
+ ...nextBase,
20289
+ threads: updateThread(nextBase.threads, payload.threadId, {
20290
+ unpromptedSubagents: payload.unpromptedSubagents,
20291
+ updatedAt: payload.updatedAt
20292
+ })
20293
+ })));
20037
20294
  case "thread.message-sent": return Effect.gen(function* () {
20038
20295
  const payload = yield* decodeForEvent(MessageSentPayloadSchema, event.payload, event.type, "payload");
20039
20296
  const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
@@ -20400,6 +20657,7 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
20400
20657
  runtimeMode: command.runtimeMode,
20401
20658
  interactionMode: command.interactionMode,
20402
20659
  compressMode: command.compressMode,
20660
+ unpromptedSubagents: command.unpromptedSubagents,
20403
20661
  branch: command.branch,
20404
20662
  worktreePath: command.worktreePath,
20405
20663
  createdAt: command.createdAt,
@@ -20679,6 +20937,28 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
20679
20937
  }
20680
20938
  };
20681
20939
  }
20940
+ case "thread.unprompted-subagents.set": {
20941
+ yield* requireThread({
20942
+ readModel,
20943
+ command,
20944
+ threadId: command.threadId
20945
+ });
20946
+ const occurredAt = yield* nowIso$8;
20947
+ return {
20948
+ ...yield* withEventBase({
20949
+ aggregateKind: "thread",
20950
+ aggregateId: command.threadId,
20951
+ occurredAt,
20952
+ commandId: command.commandId
20953
+ }),
20954
+ type: "thread.unprompted-subagents-set",
20955
+ payload: {
20956
+ threadId: command.threadId,
20957
+ unpromptedSubagents: command.unpromptedSubagents,
20958
+ updatedAt: occurredAt
20959
+ }
20960
+ };
20961
+ }
20682
20962
  case "thread.turn.start": {
20683
20963
  const targetThread = yield* requireThread({
20684
20964
  readModel,
@@ -20737,6 +21017,7 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
20737
21017
  runtimeMode: targetThread.runtimeMode,
20738
21018
  interactionMode: targetThread.interactionMode,
20739
21019
  compressMode: targetThread.compressMode,
21020
+ unpromptedSubagents: targetThread.unpromptedSubagents,
20740
21021
  ...sourceProposedPlan !== void 0 ? { sourceProposedPlan } : {},
20741
21022
  createdAt: command.createdAt
20742
21023
  }
@@ -21503,6 +21784,8 @@ const ProjectionThread = Schema$1.Struct({
21503
21784
  runtimeMode: RuntimeMode,
21504
21785
  interactionMode: ProviderInteractionMode,
21505
21786
  compressMode: CompressMode,
21787
+ /** 0 or 1: SQLite has no boolean, matching `hasActionableProposedPlan`. */
21788
+ unpromptedSubagents: NonNegativeInt,
21506
21789
  branch: Schema$1.NullOr(Schema$1.String),
21507
21790
  worktreePath: Schema$1.NullOr(Schema$1.String),
21508
21791
  latestTurnId: Schema$1.NullOr(TurnId),
@@ -22391,6 +22674,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
22391
22674
  runtime_mode,
22392
22675
  interaction_mode,
22393
22676
  compress_mode,
22677
+ unprompted_subagents,
22394
22678
  branch,
22395
22679
  worktree_path,
22396
22680
  latest_turn_id,
@@ -22415,6 +22699,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
22415
22699
  ${row.runtimeMode},
22416
22700
  ${row.interactionMode},
22417
22701
  ${row.compressMode},
22702
+ ${row.unpromptedSubagents},
22418
22703
  ${row.branch},
22419
22704
  ${row.worktreePath},
22420
22705
  ${row.latestTurnId},
@@ -22439,6 +22724,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
22439
22724
  runtime_mode = excluded.runtime_mode,
22440
22725
  interaction_mode = excluded.interaction_mode,
22441
22726
  compress_mode = excluded.compress_mode,
22727
+ unprompted_subagents = excluded.unprompted_subagents,
22442
22728
  branch = excluded.branch,
22443
22729
  worktree_path = excluded.worktree_path,
22444
22730
  latest_turn_id = excluded.latest_turn_id,
@@ -22468,6 +22754,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
22468
22754
  runtime_mode AS "runtimeMode",
22469
22755
  interaction_mode AS "interactionMode",
22470
22756
  compress_mode AS "compressMode",
22757
+ unprompted_subagents AS "unpromptedSubagents",
22471
22758
  branch,
22472
22759
  worktree_path AS "worktreePath",
22473
22760
  latest_turn_id AS "latestTurnId",
@@ -22499,6 +22786,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
22499
22786
  runtime_mode AS "runtimeMode",
22500
22787
  interaction_mode AS "interactionMode",
22501
22788
  compress_mode AS "compressMode",
22789
+ unprompted_subagents AS "unpromptedSubagents",
22502
22790
  branch,
22503
22791
  worktree_path AS "worktreePath",
22504
22792
  latest_turn_id AS "latestTurnId",
@@ -22966,6 +23254,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
22966
23254
  runtimeMode: event.payload.runtimeMode,
22967
23255
  interactionMode: event.payload.interactionMode,
22968
23256
  compressMode: event.payload.compressMode,
23257
+ unpromptedSubagents: event.payload.unpromptedSubagents ? 1 : 0,
22969
23258
  branch: event.payload.branch,
22970
23259
  worktreePath: event.payload.worktreePath,
22971
23260
  latestTurnId: null,
@@ -23090,6 +23379,16 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
23090
23379
  });
23091
23380
  return;
23092
23381
  }
23382
+ case "thread.unprompted-subagents-set": {
23383
+ const existingRow = yield* projectionThreadRepository.getById({ threadId: event.payload.threadId });
23384
+ if (Option.isNone(existingRow)) return;
23385
+ yield* projectionThreadRepository.upsert({
23386
+ ...existingRow.value,
23387
+ unpromptedSubagents: event.payload.unpromptedSubagents ? 1 : 0,
23388
+ updatedAt: event.payload.updatedAt
23389
+ });
23390
+ return;
23391
+ }
23093
23392
  case "thread.deleted": {
23094
23393
  attachmentSideEffects.deletedThreadIds.add(event.payload.threadId);
23095
23394
  const existingRow = yield* projectionThreadRepository.getById({ threadId: event.payload.threadId });
@@ -24412,6 +24711,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
24412
24711
  runtime_mode AS "runtimeMode",
24413
24712
  interaction_mode AS "interactionMode",
24414
24713
  compress_mode AS "compressMode",
24714
+ unprompted_subagents AS "unpromptedSubagents",
24415
24715
  branch,
24416
24716
  worktree_path AS "worktreePath",
24417
24717
  latest_turn_id AS "latestTurnId",
@@ -24443,6 +24743,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
24443
24743
  runtime_mode AS "runtimeMode",
24444
24744
  interaction_mode AS "interactionMode",
24445
24745
  compress_mode AS "compressMode",
24746
+ unprompted_subagents AS "unpromptedSubagents",
24446
24747
  branch,
24447
24748
  worktree_path AS "worktreePath",
24448
24749
  latest_turn_id AS "latestTurnId",
@@ -24476,6 +24777,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
24476
24777
  runtime_mode AS "runtimeMode",
24477
24778
  interaction_mode AS "interactionMode",
24478
24779
  compress_mode AS "compressMode",
24780
+ unprompted_subagents AS "unpromptedSubagents",
24479
24781
  branch,
24480
24782
  worktree_path AS "worktreePath",
24481
24783
  latest_turn_id AS "latestTurnId",
@@ -24809,6 +25111,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
24809
25111
  runtime_mode AS "runtimeMode",
24810
25112
  interaction_mode AS "interactionMode",
24811
25113
  compress_mode AS "compressMode",
25114
+ unprompted_subagents AS "unpromptedSubagents",
24812
25115
  branch,
24813
25116
  worktree_path AS "worktreePath",
24814
25117
  latest_turn_id AS "latestTurnId",
@@ -25111,6 +25414,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25111
25414
  runtimeMode: row.runtimeMode,
25112
25415
  interactionMode: row.interactionMode,
25113
25416
  compressMode: row.compressMode,
25417
+ unpromptedSubagents: row.unpromptedSubagents > 0,
25114
25418
  branch: row.branch,
25115
25419
  worktreePath: row.worktreePath,
25116
25420
  latestTurn: latestTurnByThread.get(row.threadId) ?? null,
@@ -25223,6 +25527,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25223
25527
  runtimeMode: row.runtimeMode,
25224
25528
  interactionMode: row.interactionMode,
25225
25529
  compressMode: row.compressMode,
25530
+ unpromptedSubagents: row.unpromptedSubagents > 0,
25226
25531
  branch: row.branch,
25227
25532
  worktreePath: row.worktreePath,
25228
25533
  latestTurn: latestTurnByThread.get(row.threadId) ?? null,
@@ -25282,6 +25587,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25282
25587
  runtimeMode: row.runtimeMode,
25283
25588
  interactionMode: row.interactionMode,
25284
25589
  compressMode: row.compressMode,
25590
+ unpromptedSubagents: row.unpromptedSubagents > 0,
25285
25591
  branch: row.branch,
25286
25592
  worktreePath: row.worktreePath,
25287
25593
  latestTurn: latestTurnByThread.get(row.threadId) ?? null,
@@ -25337,6 +25643,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25337
25643
  runtimeMode: row.runtimeMode,
25338
25644
  interactionMode: row.interactionMode,
25339
25645
  compressMode: row.compressMode,
25646
+ unpromptedSubagents: row.unpromptedSubagents > 0,
25340
25647
  branch: row.branch,
25341
25648
  worktreePath: row.worktreePath,
25342
25649
  latestTurn: latestTurnByThread.get(row.threadId) ?? null,
@@ -25428,6 +25735,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25428
25735
  runtimeMode: threadRow.value.runtimeMode,
25429
25736
  interactionMode: threadRow.value.interactionMode,
25430
25737
  compressMode: threadRow.value.compressMode,
25738
+ unpromptedSubagents: threadRow.value.unpromptedSubagents > 0,
25431
25739
  branch: threadRow.value.branch,
25432
25740
  worktreePath: threadRow.value.worktreePath,
25433
25741
  latestTurn: Option.isSome(latestTurnRow) ? mapLatestTurn(latestTurnRow.value) : null,
@@ -25464,6 +25772,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
25464
25772
  runtimeMode: threadRow.value.runtimeMode,
25465
25773
  interactionMode: threadRow.value.interactionMode,
25466
25774
  compressMode: threadRow.value.compressMode,
25775
+ unpromptedSubagents: threadRow.value.unpromptedSubagents > 0,
25467
25776
  branch: threadRow.value.branch,
25468
25777
  worktreePath: threadRow.value.worktreePath,
25469
25778
  latestTurn: Option.isSome(latestTurnRow) ? mapLatestTurn(latestTurnRow.value) : null,
@@ -28873,6 +29182,7 @@ const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
28873
29182
  modelSelection: nextProjectDefaultModelSelection,
28874
29183
  interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
28875
29184
  compressMode: DEFAULT_COMPRESS_MODE,
29185
+ unpromptedSubagents: true,
28876
29186
  runtimeMode: "full-access",
28877
29187
  branch: null,
28878
29188
  worktreePath: null,
@@ -34856,6 +35166,7 @@ const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
34856
35166
  repositoryKey: repositoryKey ?? input.repositoryKey ?? null,
34857
35167
  threadId: input.threadId ?? null,
34858
35168
  externalRef: input.externalRef ?? null,
35169
+ parentTaskId: input.parentTaskId ?? null,
34859
35170
  title: input.title,
34860
35171
  body: input.body ?? "",
34861
35172
  status: input.status ?? "backlog",
@@ -45693,6 +46004,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
45693
46004
  runtimeMode: bootstrap.createThread.runtimeMode,
45694
46005
  interactionMode: bootstrap.createThread.interactionMode,
45695
46006
  compressMode: bootstrap.createThread.compressMode,
46007
+ unpromptedSubagents: bootstrap.createThread.unpromptedSubagents,
45696
46008
  branch: bootstrap.createThread.branch,
45697
46009
  worktreePath: bootstrap.createThread.worktreePath,
45698
46010
  createdAt: bootstrap.createThread.createdAt
@@ -45957,6 +46269,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
45957
46269
  runtimeMode: DEFAULT_RUNTIME_MODE$2,
45958
46270
  interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
45959
46271
  compressMode: DEFAULT_COMPRESS_MODE,
46272
+ unpromptedSubagents: true,
45960
46273
  bootstrap: { createThread: {
45961
46274
  projectId: plan.projectId,
45962
46275
  title: threadTitle,
@@ -45964,6 +46277,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
45964
46277
  runtimeMode: DEFAULT_RUNTIME_MODE$2,
45965
46278
  interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
45966
46279
  compressMode: DEFAULT_COMPRESS_MODE,
46280
+ unpromptedSubagents: true,
45967
46281
  branch: null,
45968
46282
  worktreePath: null,
45969
46283
  createdAt
@@ -46374,6 +46688,7 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
46374
46688
  ...row.projectId === null ? {} : { projectId: row.projectId },
46375
46689
  ...row.repositoryKey === null ? {} : { repositoryKey: row.repositoryKey },
46376
46690
  ...row.threadId === null ? {} : { threadId: row.threadId },
46691
+ ...row.parentTaskId === null ? {} : { parentTaskId: row.parentTaskId },
46377
46692
  title: row.title,
46378
46693
  body: row.body,
46379
46694
  status: row.status,
@@ -52502,7 +52817,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
52502
52817
  const mcpSession = readMcpProviderSession(input.threadId);
52503
52818
  const externalMcpServers = options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers;
52504
52819
  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;
52820
+ const unpromptedSubagents = input.unpromptedSubagents !== void 0 ? input.unpromptedSubagents : options?.resolveUnpromptedSubagents === void 0 ? DEFAULT_SERVER_SETTINGS.enableUnpromptedSubagents : yield* options.resolveUnpromptedSubagents;
52506
52821
  const compressRuleset = compressRulesetFor(input.compressMode ?? "off");
52507
52822
  const systemPromptAppend = [
52508
52823
  ...narrateBeforeTools ? [NARRATE_BEFORE_TOOLS_PROMPT] : [],
@@ -52601,6 +52916,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
52601
52916
  startedAt,
52602
52917
  basePermissionMode: permissionMode,
52603
52918
  currentApiModelId: apiModelId,
52919
+ currentUnpromptedSubagents: unpromptedSubagents,
52604
52920
  resumeSessionId: sessionId,
52605
52921
  pendingApprovals,
52606
52922
  pendingUserInputs,
@@ -52672,7 +52988,13 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
52672
52988
  const context = yield* requireSession(input.threadId);
52673
52989
  const modelSelection = input.modelSelection !== void 0 && input.modelSelection.instanceId === boundInstanceId ? input.modelSelection : void 0;
52674
52990
  if (context.turnState?.synthetic === true) yield* completeTurn(context, "completed");
52675
- const message = yield* buildUserMessageEffect(input, {
52991
+ const turnSubagentsPrompt = input.unpromptedSubagents !== void 0 && input.unpromptedSubagents !== context.currentUnpromptedSubagents ? input.unpromptedSubagents ? SUBAGENTS_ALLOWED_PROMPT : SUBAGENTS_ON_REQUEST_PROMPT : void 0;
52992
+ if (input.unpromptedSubagents !== void 0) context.currentUnpromptedSubagents = input.unpromptedSubagents;
52993
+ const turnInput = turnSubagentsPrompt === void 0 ? input : {
52994
+ ...input,
52995
+ input: input.input === void 0 ? turnSubagentsPrompt : `${turnSubagentsPrompt}\n\n${input.input}`
52996
+ };
52997
+ const message = yield* buildUserMessageEffect(turnInput, {
52676
52998
  fileSystem,
52677
52999
  attachmentsDir: serverConfig.attachmentsDir,
52678
53000
  boundInstanceId
@@ -85945,6 +86267,28 @@ const findTask = (taskId) => Effect.gen(function* () {
85945
86267
  const tasks = yield* TaskRepository;
85946
86268
  return isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
85947
86269
  });
86270
+ /**
86271
+ * Resolve a parent reference to the UUID the column stores.
86272
+ *
86273
+ * A planning agent works in readable ids - it has just read `P4-12` off the
86274
+ * board - but the link is stored as the row key, because readable ids are
86275
+ * allocated per store and the one it quotes may have been minted on another
86276
+ * machine. Absent stays absent and an explicit null stays null: those mean
86277
+ * "leave it" and "clear it", neither of which is a reference to resolve.
86278
+ *
86279
+ * A readable id that names no task fails rather than being stored raw. The
86280
+ * opposite would file a subtask whose parent link can never resolve, which
86281
+ * reads on the board as a task that is part of nothing. A UUID is taken on
86282
+ * trust, matching the column's lack of a foreign key: a parent may legitimately
86283
+ * arrive after its child on a board synced from elsewhere.
86284
+ */
86285
+ const resolveParentTaskId = (parentTaskId, operation) => Effect.gen(function* () {
86286
+ if (parentTaskId === void 0 || parentTaskId === null) return parentTaskId;
86287
+ if (!isReadableTaskId(parentTaskId)) return parentTaskId;
86288
+ const parent = yield* findTask(parentTaskId).pipe(Effect.catch(toTaskStoreError(operation)));
86289
+ if (Option.isNone(parent)) return yield* new TaskNotFoundError({ taskId: parentTaskId });
86290
+ return parent.value.taskId;
86291
+ });
85948
86292
  const TaskToolkitHandlersLive = TaskToolkit.toLayer({
85949
86293
  task_list: (input) => Effect.gen(function* () {
85950
86294
  yield* requireTaskCapability();
@@ -85972,7 +86316,11 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
85972
86316
  const timestamp = yield* nowIso$1;
85973
86317
  const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
85974
86318
  const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
85975
- const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
86319
+ const parentTaskId = yield* resolveParentTaskId(input.parentTaskId, "task_create");
86320
+ const row = buildTaskRow(parentTaskId === void 0 || parentTaskId === null ? input : {
86321
+ ...input,
86322
+ parentTaskId
86323
+ }, taskId, timestamp, repositoryKey);
85976
86324
  return yield* tasks.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("task_create")));
85977
86325
  }),
85978
86326
  task_update: (input) => Effect.gen(function* () {
@@ -85981,13 +86329,12 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
85981
86329
  const timestamp = yield* nowIso$1;
85982
86330
  const taskId = isReadableTaskId(input.taskId) ? Option.getOrNull(yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_update"))))?.taskId : input.taskId;
85983
86331
  if (taskId === void 0) return yield* new TaskNotFoundError({ taskId: input.taskId });
85984
- const patch = input.projectId === void 0 ? {
85985
- ...input,
85986
- taskId
85987
- } : {
86332
+ const parentTaskId = yield* resolveParentTaskId(input.parentTaskId, "task_update");
86333
+ const patch = {
85988
86334
  ...input,
85989
86335
  taskId,
85990
- repositoryKey: yield* resolveTaskRepositoryKey(input.projectId)
86336
+ ...parentTaskId === void 0 ? {} : { parentTaskId },
86337
+ ...input.projectId === void 0 ? {} : { repositoryKey: yield* resolveTaskRepositoryKey(input.projectId) }
85991
86338
  };
85992
86339
  const patched = yield* tasks.patch(patch, timestamp).pipe(Effect.catch(toTaskStoreError("task_update")));
85993
86340
  if (Option.isNone(patched)) return yield* new TaskNotFoundError({ taskId: input.taskId });
@@ -85999,6 +86346,242 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
85999
86346
  })
86000
86347
  });
86001
86348
  //#endregion
86349
+ //#region src/sync/assetCompression.ts
86350
+ /**
86351
+ * Checking that a compressed asset still says what the original said.
86352
+ *
86353
+ * Compression here is a rewrite of prose an agent reads before it does anything
86354
+ * else: a skill, a memory file, a subagent definition. Rewriting that prose is
86355
+ * cheap to get almost right and expensive to get slightly wrong, because the
86356
+ * damage is silent. A dropped URL, a mangled command, a heading that no longer
86357
+ * matches the sentence pointing at it - none of that fails loudly. It just makes
86358
+ * the agent behave a little differently on every machine that syncs the file.
86359
+ *
86360
+ * So the write is gated on a comparison rather than on the model's word for it.
86361
+ * The rules are ported from caveman's `validate.py`, with the ones this task
86362
+ * calls out - headings, code blocks, URLs, paths, bullet counts - promoted to
86363
+ * refusals rather than warnings. A refusal costs one retry; a bad write costs
86364
+ * whatever the agent does next, on every machine, until someone notices.
86365
+ *
86366
+ * Pure on purpose. Everything here is a string comparison, so every rule can be
86367
+ * tested without a filesystem, a provider or a thread, and the tool that calls
86368
+ * it is left with only the parts that genuinely touch disk.
86369
+ *
86370
+ * @module sync/assetCompression
86371
+ */
86372
+ /**
86373
+ * How far the bullet count may move before the rewrite stops being compression.
86374
+ *
86375
+ * Not zero. Compression legitimately folds a two-line bullet into one, and a
86376
+ * rule that forbade that would forbid the feature. Caveman's own threshold,
86377
+ * kept because it is the one their receipts were measured against.
86378
+ */
86379
+ const BULLET_COUNT_TOLERANCE = .15;
86380
+ /**
86381
+ * Characters per token, for the before/after figures.
86382
+ *
86383
+ * An estimate, and labelled as one everywhere it surfaces. The exact count
86384
+ * belongs to whichever provider tokenizer will read the file, and asking one
86385
+ * would mean a network call inside a validation path that is otherwise pure.
86386
+ * Four is the usual English-prose approximation and is good enough to answer
86387
+ * the only question anyone asks here: did this get meaningfully smaller.
86388
+ */
86389
+ const CHARS_PER_TOKEN_ESTIMATE = 4;
86390
+ const URL_REGEX = /https?:\/\/[^\s)]+/gu;
86391
+ /**
86392
+ * Sentence punctuation swept up by the URL match.
86393
+ *
86394
+ * `See https://svgl.app for the catalogue.` and `Catalogue: https://svgl.app.`
86395
+ * name the same URL, and a comparison that disagreed would refuse every rewrite
86396
+ * that moved a link to the end of a sentence - which is most of them.
86397
+ */
86398
+ const URL_TRAILING_PUNCTUATION_REGEX = /[.,;:!?'"\])]+$/u;
86399
+ const FENCE_LINE_REGEX = /^(\s{0,3})(`{3,}|~{3,})(.*)$/u;
86400
+ const HEADING_REGEX = /^(#{1,6})\s+(.*)$/gmu;
86401
+ const BULLET_REGEX = /^\s*[-*+]\s+/gmu;
86402
+ const INLINE_CODE_REGEX = /`([^`]+)`/gu;
86403
+ /**
86404
+ * Crude, and deliberately so.
86405
+ *
86406
+ * Either a path prefix (`./`, `../`, `/`, a drive letter) or a separator inside
86407
+ * the match. It over-matches - `and/or` is a "path" by this rule - and the
86408
+ * over-match is the safe direction: a false positive refuses a rewrite that
86409
+ * dropped the phrase, and a false negative lets a real path go missing.
86410
+ */
86411
+ const PATH_REGEX = /(?:\.\/|\.\.\/|\/|[A-Za-z]:\\)[\w\-/\\.]+|[\w\-.]+[/\\][\w\-/\\.]+/gu;
86412
+ /** YAML frontmatter: `---` on the first line, `---` on a line of its own after. */
86413
+ const FRONTMATTER_REGEX = /^---\r?\n[\s\S]*?\r?\n---\r?\n/u;
86414
+ /**
86415
+ * Filenames that almost certainly hold secrets, and the directories that do.
86416
+ *
86417
+ * Compressing a file means handing its bytes to a model, so this is checked
86418
+ * before the read rather than after it. The asset roots should never contain
86419
+ * any of these, which is exactly why the check is cheap to keep: it costs
86420
+ * nothing in the ordinary case and is the only thing standing between a
86421
+ * misplaced `credentials.md` and a provider's API.
86422
+ */
86423
+ 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;
86424
+ const SENSITIVE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
86425
+ ".ssh",
86426
+ ".aws",
86427
+ ".gnupg",
86428
+ ".kube",
86429
+ ".docker"
86430
+ ]);
86431
+ const SENSITIVE_NAME_TOKENS = [
86432
+ "secret",
86433
+ "credential",
86434
+ "password",
86435
+ "passwd",
86436
+ "apikey",
86437
+ "accesskey",
86438
+ "token",
86439
+ "privatekey"
86440
+ ];
86441
+ const segmentsOf = (filePath) => filePath.split(/[/\\]/u).filter((segment) => segment.length > 0);
86442
+ /**
86443
+ * Whether this path looks like something that must not be sent to a model.
86444
+ *
86445
+ * Name-based, because that is all that is knowable before the read. Separators
86446
+ * inside the name are normalized away first so `api-key.md` and `api_key.md`
86447
+ * are the same answer.
86448
+ */
86449
+ const isSensitiveAssetPath = (filePath) => {
86450
+ const segments = segmentsOf(filePath);
86451
+ const name = segments.at(-1) ?? "";
86452
+ if (SENSITIVE_BASENAME_REGEX.test(name)) return true;
86453
+ if (segments.some((segment) => SENSITIVE_PATH_SEGMENTS.has(segment.toLowerCase()))) return true;
86454
+ const flattened = name.toLowerCase().replace(/[_\-\s.]/gu, "");
86455
+ return SENSITIVE_NAME_TOKENS.some((token) => flattened.includes(token));
86456
+ };
86457
+ /** The frontmatter block including its delimiters, or the empty string. */
86458
+ const frontmatterOf = (text) => FRONTMATTER_REGEX.exec(text)?.[0] ?? "";
86459
+ const headingsOf = (text) => [...text.matchAll(HEADING_REGEX)].map((match) => `${match[1] ?? ""} ${(match[2] ?? "").trim()}`);
86460
+ /**
86461
+ * Fenced code blocks, as whole strings, fences included.
86462
+ *
86463
+ * Line-based rather than one regex, so a closing fence is only a closing fence
86464
+ * when CommonMark says it is: same character, at least as long as the opener,
86465
+ * nothing after it. That is what lets a four-backtick block wrap three-backtick
86466
+ * content without the extractor stopping in the middle of it.
86467
+ *
86468
+ * An unclosed fence is skipped rather than reported. It means the markdown was
86469
+ * already malformed, and including it would fail every compression of a file
86470
+ * that was broken before anyone touched it.
86471
+ */
86472
+ const codeBlocksOf = (text) => {
86473
+ const lines = text.split("\n");
86474
+ const blocks = [];
86475
+ let index = 0;
86476
+ while (index < lines.length) {
86477
+ const opening = FENCE_LINE_REGEX.exec(lines[index] ?? "");
86478
+ if (opening === null) {
86479
+ index += 1;
86480
+ continue;
86481
+ }
86482
+ const fence = opening[2] ?? "";
86483
+ const collected = [lines[index] ?? ""];
86484
+ index += 1;
86485
+ let closed = false;
86486
+ while (index < lines.length) {
86487
+ const line = lines[index] ?? "";
86488
+ const closing = FENCE_LINE_REGEX.exec(line);
86489
+ const closingFence = closing?.[2] ?? "";
86490
+ if (closing !== null && closingFence[0] === fence[0] && closingFence.length >= fence.length && (closing[3] ?? "").trim().length === 0) {
86491
+ collected.push(line);
86492
+ closed = true;
86493
+ index += 1;
86494
+ break;
86495
+ }
86496
+ collected.push(line);
86497
+ index += 1;
86498
+ }
86499
+ if (closed) blocks.push(collected.join("\n"));
86500
+ }
86501
+ return blocks;
86502
+ };
86503
+ const withoutCodeBlocks = (text) => codeBlocksOf(text).reduce((rest, block) => rest.replace(block, ""), text);
86504
+ const inlineCodeCounts = (text) => {
86505
+ const counts = /* @__PURE__ */ new Map();
86506
+ for (const match of withoutCodeBlocks(text).matchAll(INLINE_CODE_REGEX)) {
86507
+ const span = match[1] ?? "";
86508
+ counts.set(span, (counts.get(span) ?? 0) + 1);
86509
+ }
86510
+ return counts;
86511
+ };
86512
+ const urlsOf = (text) => new Set([...text.matchAll(URL_REGEX)].map((match) => match[0].replace(URL_TRAILING_PUNCTUATION_REGEX, "")));
86513
+ /**
86514
+ * Paths, with URLs taken out of the text first.
86515
+ *
86516
+ * `//svgl.app` inside `https://svgl.app` is a path by the pattern's reckoning,
86517
+ * so leaving the URLs in would have every link contribute a phantom path whose
86518
+ * spelling changes with the punctuation after it.
86519
+ */
86520
+ const pathsOf = (text) => new Set([...text.replace(URL_REGEX, " ").matchAll(PATH_REGEX)].map((match) => match[0]));
86521
+ const countOf = (text, pattern) => [...text.matchAll(pattern)].length;
86522
+ const missingFrom = (original, compressed) => [...original].filter((value) => !compressed.has(value));
86523
+ const addedTo = (original, compressed) => [...compressed].filter((value) => !original.has(value));
86524
+ const quoteList = (values) => values.map((value) => `'${value}'`).join(", ");
86525
+ /**
86526
+ * Compare a compressed rewrite against the original it came from.
86527
+ *
86528
+ * Errors are the things whose loss changes what the file instructs: a code
86529
+ * block, a URL, a path, an inline span, a heading, the frontmatter. Additions
86530
+ * are warnings rather than errors throughout, because a rewrite that names one
86531
+ * more path than the original is doing something odd but is not losing
86532
+ * anything, and a refusal there would be a refusal nobody can act on.
86533
+ */
86534
+ const validateCompression = (original, compressed) => {
86535
+ const errors = [];
86536
+ const warnings = [];
86537
+ if (frontmatterOf(original) !== frontmatterOf(compressed)) errors.push("The YAML frontmatter changed. It is metadata the provider parses, not prose; return it byte-identical.");
86538
+ const originalHeadings = headingsOf(original);
86539
+ const compressedHeadings = headingsOf(compressed);
86540
+ 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.`);
86541
+ const originalBlocks = codeBlocksOf(original);
86542
+ const compressedBlocks = codeBlocksOf(compressed);
86543
+ 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.`);
86544
+ const originalUrls = urlsOf(original);
86545
+ const compressedUrls = urlsOf(compressed);
86546
+ const lostUrls = missingFrom(originalUrls, compressedUrls);
86547
+ if (lostUrls.length > 0) errors.push(`URLs missing from the rewrite: ${quoteList(lostUrls)}.`);
86548
+ const addedUrls = addedTo(originalUrls, compressedUrls);
86549
+ if (addedUrls.length > 0) warnings.push(`URLs the original did not have: ${quoteList(addedUrls)}.`);
86550
+ const originalPaths = pathsOf(original);
86551
+ const compressedPaths = pathsOf(compressed);
86552
+ const lostPaths = missingFrom(originalPaths, compressedPaths);
86553
+ if (lostPaths.length > 0) errors.push(`Paths missing from the rewrite: ${quoteList(lostPaths)}.`);
86554
+ const addedPaths = addedTo(originalPaths, compressedPaths);
86555
+ if (addedPaths.length > 0) warnings.push(`Paths the original did not have: ${quoteList(addedPaths)}.`);
86556
+ const originalInline = inlineCodeCounts(original);
86557
+ const compressedInline = inlineCodeCounts(compressed);
86558
+ const lostInline = [...originalInline].filter(([span, count]) => (compressedInline.get(span) ?? 0) < count);
86559
+ if (lostInline.length > 0) errors.push(`Inline code spans missing from the rewrite: ${quoteList(lostInline.map(([span]) => span))}.`);
86560
+ const originalBullets = countOf(original, BULLET_REGEX);
86561
+ const compressedBullets = countOf(compressed, BULLET_REGEX);
86562
+ if (originalBullets > 0) {
86563
+ 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.`);
86564
+ }
86565
+ return {
86566
+ errors,
86567
+ warnings
86568
+ };
86569
+ };
86570
+ /** Roughly how many tokens this text costs to read. Estimated, never measured. */
86571
+ const estimateTokens = (text) => Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
86572
+ /**
86573
+ * The name a backup of this file is kept under.
86574
+ *
86575
+ * Timestamped rather than fixed. Caveman refuses to compress when a backup
86576
+ * already exists, which protects the first original at the cost of blocking
86577
+ * every later compression; keeping one backup per write protects every version
86578
+ * instead, including the edits someone made between two compressions.
86579
+ *
86580
+ * The parent directory's name is carried in the path rather than the file name
86581
+ * so that `global-memory/feedback_svgl.md` and `svgl/SKILL.md` cannot collide.
86582
+ */
86583
+ const backupFileName = (input) => `${input.fileName}.${input.atIso.replace(/[:.]/gu, "-")}.original`;
86584
+ //#endregion
86002
86585
  //#region src/mcp/toolkits/threads/tools.ts
86003
86586
  /**
86004
86587
  * Self-scoped on purpose: neither tool takes a thread id. The thread acted on
@@ -86030,10 +86613,46 @@ const MemoryAppendTool = Tool.make("memory_append", {
86030
86613
  Path.Path
86031
86614
  ]
86032
86615
  }).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);
86616
+ /**
86617
+ * Overwrite an asset with a compressed version of itself, if it survives the
86618
+ * comparison.
86619
+ *
86620
+ * A tool rather than an instruction to use `Edit` because the guarantee is the
86621
+ * point: the original is copied out of every synced directory first, the
86622
+ * rewrite is compared against it, and a rewrite that lost a URL or a code block
86623
+ * is refused with the list of what it lost. An agent editing the file directly
86624
+ * can be asked to preserve those things but cannot be prevented from not.
86625
+ */
86626
+ const AssetCompressTool = Tool.make("asset_compress", {
86627
+ 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.",
86628
+ parameters: AssetCompressInput,
86629
+ success: AssetCompressResult,
86630
+ failure: ThreadControlToolError,
86631
+ dependencies: [
86632
+ McpInvocationContext,
86633
+ ServerConfig$1,
86634
+ ServerSettingsService,
86635
+ FileSystem.FileSystem,
86636
+ Path.Path
86637
+ ]
86638
+ }).annotate(Tool.Title, "Compress an asset").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false);
86639
+ const ThreadToolkit = Toolkit.make(ThreadRenameTool, MemoryAppendTool, AssetCompressTool);
86034
86640
  //#endregion
86035
86641
  //#region src/mcp/toolkits/threads/handlers.ts
86036
86642
  const DEFAULT_MEMORY_APPEND_TARGET = "CLAUDE.md";
86643
+ /**
86644
+ * Where originals are kept.
86645
+ *
86646
+ * Under the server's state directory, which is the one place on this machine
86647
+ * that is neither an asset root nor inside one. A backup next to the file would
86648
+ * be published by the next sync and re-read by the next agent turn, which is
86649
+ * the opposite of what compressing the file was for.
86650
+ */
86651
+ const COMPRESS_BACKUP_DIRNAME = "asset-compress-backups";
86652
+ /** Prose only. A skill may ship scripts, and a rewritten script is a broken one. */
86653
+ const COMPRESSIBLE_EXTENSION = ".md";
86654
+ /** The same test the asset readers use: a NUL is what breaks a JSON text column. */
86655
+ const looksBinary = (content) => content.includes("\0");
86037
86656
  const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
86038
86657
  thread_rename: (input) => Effect.gen(function* () {
86039
86658
  const invocation = yield* requireThreadCapability();
@@ -86083,6 +86702,68 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
86083
86702
  file,
86084
86703
  path: filePath
86085
86704
  };
86705
+ }),
86706
+ asset_compress: (input) => Effect.gen(function* () {
86707
+ yield* requireThreadCapability();
86708
+ const path = yield* Path.Path;
86709
+ const fileSystem = yield* FileSystem.FileSystem;
86710
+ const config = yield* ServerConfig$1;
86711
+ const claudeHome = { homePath: (yield* (yield* ServerSettingsService).getSettings.pipe(Effect.orElseSucceed(() => void 0)))?.providers.claudeAgent.homePath ?? "" };
86712
+ const refuse = (target, detail) => new AssetCompressRefusedError({
86713
+ path: target,
86714
+ detail
86715
+ });
86716
+ if (!path.isAbsolute(input.path)) return yield* refuse(input.path, "The path must be absolute.");
86717
+ const filePath = path.resolve(input.path);
86718
+ const fileName = path.basename(filePath);
86719
+ 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.`);
86720
+ 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.");
86721
+ const configDir = yield* resolveClaudeConfigDirPath(claudeHome, process.env);
86722
+ const skillsDir = yield* resolveClaudeUserSkillsDir(claudeHome);
86723
+ const agentsDir = yield* resolveClaudeUserAgentsDir(claudeHome);
86724
+ const within = (root) => {
86725
+ const relative = path.relative(root, filePath);
86726
+ return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
86727
+ };
86728
+ const memoryDirectoryName = path.relative(configDir, filePath).split(path.sep)[0] ?? "";
86729
+ 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 ")}.`);
86730
+ const original = yield* fileSystem.readFileString(filePath).pipe(Effect.orElseSucceed(() => void 0));
86731
+ if (original === void 0) return yield* refuse(filePath, "The file could not be read.");
86732
+ if (looksBinary(original)) return yield* refuse(filePath, "The file is not text.");
86733
+ if (original.trim().length === 0) return yield* refuse(filePath, "The file is empty.");
86734
+ const compressed = input.content;
86735
+ if (compressed.trim().length === 0) return yield* refuse(filePath, "The rewrite is blank.");
86736
+ if (compressed.trim() === original.trim()) return yield* refuse(filePath, "The rewrite is the file that is already there. Nothing was written.");
86737
+ 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.`);
86738
+ const validation = validateCompression(original, compressed);
86739
+ if (validation.errors.length > 0) return yield* new AssetCompressValidationFailedError({
86740
+ path: filePath,
86741
+ errors: validation.errors
86742
+ });
86743
+ const backupPath = path.join(config.stateDir, COMPRESS_BACKUP_DIRNAME, path.basename(path.dirname(filePath)), backupFileName({
86744
+ fileName,
86745
+ atIso: DateTime.formatIso(yield* DateTime.now)
86746
+ }));
86747
+ if (((yield* writeFileStringAtomically({
86748
+ filePath: backupPath,
86749
+ contents: original
86750
+ }).pipe(Effect.as(true), Effect.orElseSucceed(() => false))) ? yield* fileSystem.readFileString(backupPath).pipe(Effect.orElseSucceed(() => void 0)) : void 0) !== original) {
86751
+ yield* fileSystem.remove(backupPath, { force: true }).pipe(Effect.ignore);
86752
+ return yield* refuse(filePath, `The original could not be backed up to '${backupPath}', so it was left as it is.`);
86753
+ }
86754
+ yield* writeFileStringAtomically({
86755
+ filePath,
86756
+ contents: compressed
86757
+ }).pipe(Effect.mapError((cause) => refuse(filePath, `The write failed: ${cause.message}. The original is at '${backupPath}'.`)));
86758
+ return {
86759
+ path: filePath,
86760
+ backupPath,
86761
+ charsBefore: original.length,
86762
+ charsAfter: compressed.length,
86763
+ estimatedTokensBefore: estimateTokens(original),
86764
+ estimatedTokensAfter: estimateTokens(compressed),
86765
+ warnings: validation.warnings
86766
+ };
86086
86767
  })
86087
86768
  });
86088
86769
  //#endregion
@@ -87666,7 +88347,8 @@ const make$2 = Effect.gen(function* () {
87666
88347
  modelSelection: desiredModelSelection,
87667
88348
  ...input?.resumeCursor !== void 0 ? { resumeCursor: input.resumeCursor } : {},
87668
88349
  runtimeMode: desiredRuntimeMode,
87669
- compressMode: thread.compressMode
88350
+ compressMode: thread.compressMode,
88351
+ unpromptedSubagents: thread.unpromptedSubagents
87670
88352
  });
87671
88353
  const bindSessionToThread = (session) => Effect.gen(function* () {
87672
88354
  if (session.providerInstanceId === void 0) return yield* new ProviderAdapterRequestError({
@@ -87768,7 +88450,8 @@ const make$2 = Effect.gen(function* () {
87768
88450
  ...normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {},
87769
88451
  ...modelForTurn !== void 0 ? { modelSelection: modelForTurn } : {},
87770
88452
  ...input.interactionMode !== void 0 ? { interactionMode: input.interactionMode } : {},
87771
- compressMode
88453
+ compressMode,
88454
+ unpromptedSubagents: thread.unpromptedSubagents
87772
88455
  };
87773
88456
  });
87774
88457
  const maybeGenerateAndRenameWorktreeBranchForFirstTurn = Effect.fn("maybeGenerateAndRenameWorktreeBranchForFirstTurn")(function* (input) {