@p4code/cli 0.1.6 → 0.1.8

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 (86) hide show
  1. package/dist/bin.mjs +911 -518
  2. package/dist/client/assets/{DiffPanel-CcuhH24b.js → DiffPanel-D4tNpljU.js} +2 -2
  3. package/dist/client/assets/{FilePreviewPanel-IupIzTGt.js → FilePreviewPanel-xICJSzZ6.js} +2 -2
  4. package/dist/client/assets/{PreviewPanel-CMRKi8Jr.js → PreviewPanel-CK9d1fc5.js} +2 -2
  5. package/dist/client/assets/{angular-html-D4VEGoU_.js → angular-html-BgzZxO9w.js} +2 -2
  6. package/dist/client/assets/{angular-ts-DDbN_Pcg.js → angular-ts-YFEXVhe6.js} +2 -2
  7. package/dist/client/assets/{apl-BH7XGSiX.js → apl-BoBwF9HI.js} +2 -2
  8. package/dist/client/assets/arrow-right-CXxWzaql.js +2 -0
  9. package/dist/client/assets/{astro-DKjLEfvq.js → astro-Ds5TXooD.js} +2 -2
  10. package/dist/client/assets/{blade-mO6atsi4.js → blade-CDtJf5YR.js} +2 -2
  11. package/dist/client/assets/{c-drphy09y.js → c-JH0umSmP.js} +2 -2
  12. package/dist/client/assets/{cobol-Do365H-h.js → cobol-gMss6BA3.js} +2 -2
  13. package/dist/client/assets/{coffee-CJ9y15YY.js → coffee-DESPXYwo.js} +2 -2
  14. package/dist/client/assets/{cpp-C1FVZ39w.js → cpp-CZztA1XN.js} +2 -2
  15. package/dist/client/assets/{crystal-D1Y9mCOy.js → crystal-CDWlJpLE.js} +2 -2
  16. package/dist/client/assets/{css-C1chQpfJ.js → css-CNPTTtX1.js} +2 -2
  17. package/dist/client/assets/{edge-Cir90YUi.js → edge-DSlrkPAr.js} +2 -2
  18. package/dist/client/assets/{elixir-D8U_xdUO.js → elixir-DrLl_wv7.js} +2 -2
  19. package/dist/client/assets/{elm-DFyeiMH7.js → elm-CwB92FlT.js} +2 -2
  20. package/dist/client/assets/{erb-C6TvSrSi.js → erb-DXHrZ2ye.js} +2 -2
  21. package/dist/client/assets/{fileCommentAnnotations-BotL9n2H.js → fileCommentAnnotations-CuJ4hnrK.js} +2 -2
  22. package/dist/client/assets/{git-rebase-C7PDtezn.js → git-rebase-uRnCoWg0.js} +2 -2
  23. package/dist/client/assets/{glimmer-js-CXugsloP.js → glimmer-js-DU9fMoZx.js} +2 -2
  24. package/dist/client/assets/{glimmer-ts-IiHVp7il.js → glimmer-ts-Bftqha5Z.js} +2 -2
  25. package/dist/client/assets/{glsl-eBdXjOgx.js → glsl-CVVphTeN.js} +2 -2
  26. package/dist/client/assets/{graphql-CBwWowq8.js → graphql-CwED2DHh.js} +2 -2
  27. package/dist/client/assets/{hack-DoKoRNA-.js → hack-CwiQSrzl.js} +2 -2
  28. package/dist/client/assets/{haml-B2cA0ELm.js → haml-C2iiqAZE.js} +2 -2
  29. package/dist/client/assets/{handlebars-OQNujFnM.js → handlebars-BtvKqvl0.js} +2 -2
  30. package/dist/client/assets/{html-B9l55IY7.js → html-BRvZZmrv.js} +2 -2
  31. package/dist/client/assets/{html-derivative-C24d_8BY.js → html-derivative-rVjUDzCo.js} +2 -2
  32. package/dist/client/assets/{http-Ca2KxOLT.js → http-Cvb4plLT.js} +2 -2
  33. package/dist/client/assets/{hurl-BuLZo6wH.js → hurl-TX6OEub-.js} +2 -2
  34. package/dist/client/assets/index-CTDey6k4.css +1 -0
  35. package/dist/client/assets/{index-B-nUm8w7.js → index-DJPm8QPz.js} +203 -203
  36. package/dist/client/assets/{java-BEVFbCvA.js → java-CudutWch.js} +2 -2
  37. package/dist/client/assets/{javascript-AWDSrR1H.js → javascript-D7gB3c_x.js} +2 -2
  38. package/dist/client/assets/{jinja-CFK-VDc5.js → jinja-Ce0WqJfV.js} +2 -2
  39. package/dist/client/assets/{jison-CSmyyQ0V.js → jison-2U6TlYgq.js} +2 -2
  40. package/dist/client/assets/{json-D0qpEJUc.js → json-oe5h_2FV.js} +2 -2
  41. package/dist/client/assets/{jsx-smdyqdvA.js → jsx-B0f4Z6sx.js} +2 -2
  42. package/dist/client/assets/{julia-DWo_5QH9.js → julia-By6UDEAG.js} +2 -2
  43. package/dist/client/assets/{just-74GGYpCk.js → just-YTIO1wok.js} +2 -2
  44. package/dist/client/assets/{latex-d8vfKbSv.js → latex-B9HLUatB.js} +2 -2
  45. package/dist/client/assets/{liquid-CsWNZJPs.js → liquid-DajcsI9e.js} +2 -2
  46. package/dist/client/assets/{lua-x3z7TbKl.js → lua-CINm0eWo.js} +2 -2
  47. package/dist/client/assets/{marko-CJyhwQKw.js → marko-DXvL73SK.js} +2 -2
  48. package/dist/client/assets/{mdc-CtbrrOYL.js → mdc-CYsP-lJY.js} +2 -2
  49. package/dist/client/assets/{nginx-CY-uw8dq.js → nginx-CPKmhI0k.js} +2 -2
  50. package/dist/client/assets/{nim-DqT4w7rK.js → nim-DodS9Uy_.js} +2 -2
  51. package/dist/client/assets/{perl-CE3_DH1L.js → perl-COac5jCW.js} +2 -2
  52. package/dist/client/assets/{php-Zxsq1XMP.js → php-DLugOmp_.js} +2 -2
  53. package/dist/client/assets/{pug-DnCImvlN.js → pug-ukoxsRP-.js} +2 -2
  54. package/dist/client/assets/{qml-DLfoubjW.js → qml-DEoQyDDD.js} +2 -2
  55. package/dist/client/assets/{r-BtfLsgk8.js → r-CePyxHEB.js} +2 -2
  56. package/dist/client/assets/{razor-CzAGh9fe.js → razor-uhGlTCzi.js} +2 -2
  57. package/dist/client/assets/{regexp-Bw17HBbP.js → regexp-BYNm_E6v.js} +2 -2
  58. package/dist/client/assets/{rst-DhRr0i5h.js → rst-BMMyVK7q.js} +2 -2
  59. package/dist/client/assets/{ruby-BnQcHU-H.js → ruby-BBl_q7lV.js} +2 -2
  60. package/dist/client/assets/{sas-mMUu1htl.js → sas-q_pdbMhY.js} +2 -2
  61. package/dist/client/assets/{scss-0_lOQ1qp.js → scss-Bf5qLSyj.js} +2 -2
  62. package/dist/client/assets/{shellscript-BAdG8zJQ.js → shellscript-CnkO-FF6.js} +2 -2
  63. package/dist/client/assets/{shellsession-upfFHc_2.js → shellsession-CGTIDec0.js} +2 -2
  64. package/dist/client/assets/{soy-Dzncl5z6.js → soy-D_X4Ckow.js} +2 -2
  65. package/dist/client/assets/{sql-Ca2FgbAh.js → sql-wOawmdfM.js} +2 -2
  66. package/dist/client/assets/{stata-D8IymSlL.js → stata-CKQN4lyV.js} +2 -2
  67. package/dist/client/assets/{surrealql-CZsXvHAi.js → surrealql-tjpKGjMP.js} +2 -2
  68. package/dist/client/assets/{svelte-nM61Il-m.js → svelte-mCzdghlf.js} +2 -2
  69. package/dist/client/assets/{templ-DzjlBYVo.js → templ-COV4wNUr.js} +2 -2
  70. package/dist/client/assets/{tex-BPwIdn9G.js → tex-CC1f6JM7.js} +2 -2
  71. package/dist/client/assets/textarea-BDADSCjn.js +46 -0
  72. package/dist/client/assets/{ts-tags-BL9D6Ojm.js → ts-tags-CrIJ77ir.js} +2 -2
  73. package/dist/client/assets/{tsx-AKDVGVPx.js → tsx-IX8SGkp3.js} +2 -2
  74. package/dist/client/assets/{twig-B79f_Jz8.js → twig-DXR_a9UJ.js} +2 -2
  75. package/dist/client/assets/{typescript-BsyojNKj.js → typescript-BgLtzzgs.js} +2 -2
  76. package/dist/client/assets/{vue-BTqtDJT-.js → vue-Dzfv___3.js} +2 -2
  77. package/dist/client/assets/{vue-html-B_UfyILk.js → vue-html-BHyzHcFt.js} +2 -2
  78. package/dist/client/assets/{vue-vine-B8EK-Iln.js → vue-vine-DOSBq7Kw.js} +2 -2
  79. package/dist/client/assets/{xml-Do-3lCfL.js → xml-BI5bRQfx.js} +2 -2
  80. package/dist/client/assets/{xsl-CwgwYGXC.js → xsl-B4FzWFML.js} +2 -2
  81. package/dist/client/assets/{yaml-C4XlY-p5.js → yaml-CHgKneKM.js} +2 -2
  82. package/dist/client/index.html +3 -3
  83. package/package.json +1 -1
  84. package/dist/client/assets/arrow-right-B-_6vDIF.js +0 -2
  85. package/dist/client/assets/index-DfKhU-QO.css +0 -1
  86. package/dist/client/assets/textarea-DhTMBBxg.js +0 -46
package/dist/bin.mjs CHANGED
@@ -233,7 +233,7 @@ const make$73 = () => {
233
233
  const layer$69 = Layer.sync(NetService, make$73);
234
234
  //#endregion
235
235
  //#region package.json
236
- var version = "0.1.6";
236
+ var version = "0.1.8";
237
237
  //#endregion
238
238
  //#region src/config.ts
239
239
  /**
@@ -5634,22 +5634,11 @@ const ClaudeSettings = makeProviderSettingsSchema({
5634
5634
  placeholder: "e.g. --chrome",
5635
5635
  clearWhenEmpty: "omit"
5636
5636
  }
5637
- })),
5638
- /**
5639
- * The SDK preset system prompt omits the interactive CLI's terminal-tone
5640
- * sections, so the model tends to open a turn with a tool call instead of
5641
- * saying what it is about to do. This appends that instruction back.
5642
- */
5643
- narrateBeforeTools: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true)), Schema$1.annotateKey({
5644
- title: "Explain before tool calls",
5645
- description: "Ask Claude to write one short sentence before its first tool call in a turn.",
5646
- providerSettingsForm: { control: "switch" }
5647
5637
  }))
5648
5638
  }, { order: [
5649
5639
  "binaryPath",
5650
5640
  "homePath",
5651
- "launchArgs",
5652
- "narrateBeforeTools"
5641
+ "launchArgs"
5653
5642
  ] });
5654
5643
  const CursorSettings = makeProviderSettingsSchema({
5655
5644
  enabled: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(false)), Schema$1.annotateKey({ providerSettingsForm: { hidden: true } })),
@@ -5735,6 +5724,13 @@ const TaskBoardSource = Schema$1.Literals(["local", "hub"]);
5735
5724
  const ServerSettings = Schema$1.Struct({
5736
5725
  enableAssistantStreaming: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(false))),
5737
5726
  enableProviderUpdateChecks: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
5727
+ /**
5728
+ * The Claude SDK preset system prompt omits the interactive CLI's
5729
+ * terminal-tone sections, so the model tends to open a turn with a tool call
5730
+ * instead of saying what it is about to do. This appends that instruction
5731
+ * back for sessions started by the Claude provider.
5732
+ */
5733
+ enableToolCallNarration: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
5738
5734
  automaticGitFetchInterval: Schema$1.DurationFromMillis.pipe(Schema$1.withDecodingDefault(Effect.succeed(Duration.toMillis(DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL)))),
5739
5735
  defaultThreadEnvMode: ThreadEnvMode.pipe(Schema$1.withDecodingDefault(Effect.succeed("local"))),
5740
5736
  newWorktreesStartFromOrigin: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
@@ -5838,8 +5834,7 @@ const ClaudeSettingsPatch = Schema$1.Struct({
5838
5834
  binaryPath: Schema$1.optionalKey(TrimmedString),
5839
5835
  homePath: Schema$1.optionalKey(TrimmedString),
5840
5836
  customModels: Schema$1.optionalKey(Schema$1.Array(Schema$1.String)),
5841
- launchArgs: Schema$1.optionalKey(TrimmedString),
5842
- narrateBeforeTools: Schema$1.optionalKey(Schema$1.Boolean)
5837
+ launchArgs: Schema$1.optionalKey(TrimmedString)
5843
5838
  });
5844
5839
  const CursorSettingsPatch = Schema$1.Struct({
5845
5840
  enabled: Schema$1.optionalKey(Schema$1.Boolean),
@@ -5862,6 +5857,7 @@ const OpenCodeSettingsPatch = Schema$1.Struct({
5862
5857
  const ServerSettingsPatch = Schema$1.Struct({
5863
5858
  enableAssistantStreaming: Schema$1.optionalKey(Schema$1.Boolean),
5864
5859
  enableProviderUpdateChecks: Schema$1.optionalKey(Schema$1.Boolean),
5860
+ enableToolCallNarration: Schema$1.optionalKey(Schema$1.Boolean),
5865
5861
  automaticGitFetchInterval: Schema$1.optionalKey(Schema$1.DurationFromMillis),
5866
5862
  defaultThreadEnvMode: Schema$1.optionalKey(ThreadEnvMode),
5867
5863
  newWorktreesStartFromOrigin: Schema$1.optionalKey(Schema$1.Boolean),
@@ -7016,6 +7012,341 @@ const GitActionProgressEvent = Schema$1.Union([
7016
7012
  GitActionFailedEvent
7017
7013
  ]);
7018
7014
  //#endregion
7015
+ //#region ../../packages/contracts/src/task.ts
7016
+ const TASK_TITLE_MAX_LENGTH = 512;
7017
+ const TASK_LABEL_MAX_LENGTH = 64;
7018
+ const TASK_LABELS_MAX_COUNT = 32;
7019
+ const TASK_ASSIGNEE_MAX_LENGTH = 128;
7020
+ /**
7021
+ * Board columns, in board order. `backlog` is the default for newly created
7022
+ * tasks — an agent filing a task mid-session is recording work that exists, not
7023
+ * committing to doing it next.
7024
+ */
7025
+ const TaskStatus = Schema$1.Literals([
7026
+ "backlog",
7027
+ "todo",
7028
+ "in_progress",
7029
+ "in_review",
7030
+ "done"
7031
+ ]);
7032
+ const TaskPriority = Schema$1.Literals([
7033
+ "none",
7034
+ "low",
7035
+ "medium",
7036
+ "high",
7037
+ "urgent"
7038
+ ]);
7039
+ /**
7040
+ * A per-prefix sequence prefix, e.g. `T` or `P4`. Declared by a project in its
7041
+ * `p4.json` (`taskPrefix`); always stored and compared uppercase.
7042
+ */
7043
+ const TASK_READABLE_ID_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,9}$/;
7044
+ /**
7045
+ * The shape of a readable task id: prefix, dash, 1-based sequence number.
7046
+ * Deliberately disjoint from the UUID `taskId`, which is what lets a lookup
7047
+ * accept either and tell them apart by shape alone.
7048
+ */
7049
+ const TASK_READABLE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,9}-[1-9][0-9]*$/;
7050
+ /** Whether a task reference is a readable id (`P4-12`) rather than a UUID. */
7051
+ const isReadableTaskId = (reference) => TASK_READABLE_ID_PATTERN.test(reference);
7052
+ const TaskLabel = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_LABEL_MAX_LENGTH));
7053
+ /**
7054
+ * Agent-facing field descriptions. These ride on the *encoded* side because
7055
+ * every one of these schemas is a brand or a check over a transformation, and
7056
+ * the JSON Schema an agent actually reads is generated from the encoded schema
7057
+ * — a plain `.annotate` on the decoded side is silently dropped.
7058
+ */
7059
+ const ProjectIdField = ProjectId.pipe(Schema$1.annotateEncoded({ description: "Id of the project this task belongs to." }));
7060
+ const ThreadIdField = ThreadId.pipe(Schema$1.annotateEncoded({ description: "Id of the agent thread working this task." }));
7061
+ const TitleField = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_TITLE_MAX_LENGTH)).pipe(Schema$1.annotateEncoded({ description: "One-line summary of the work. Required." }));
7062
+ const BodyField = Schema$1.String.pipe(Schema$1.annotateEncoded({ description: "Markdown detail: context, acceptance criteria, links." }));
7063
+ const StatusField = TaskStatus.pipe(Schema$1.annotateEncoded({ description: "Board column: backlog (recorded, not queued), todo (queued next), in_progress, in_review, done." }));
7064
+ const PriorityField = TaskPriority.pipe(Schema$1.annotateEncoded({ description: "Relative urgency: none, low, medium, high or urgent." }));
7065
+ const AssigneeField = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSIGNEE_MAX_LENGTH)).pipe(Schema$1.annotateEncoded({ description: "Who owns the task — a person's handle or an agent name." }));
7066
+ /**
7067
+ * The portable half of "which repository is this task about".
7068
+ *
7069
+ * `projectId` cannot answer that across machines: it is a UUID minted when a
7070
+ * project is added, and the same checkout added on two servers gets two
7071
+ * different ids. A canonical repository key — the normalized primary git remote
7072
+ * — is the same string wherever the repo is cloned, so it survives the trip
7073
+ * through a shared board.
7074
+ *
7075
+ * Null for a task that is not about a repository, and also for a checkout with
7076
+ * no remote, where there is nothing stable to key on.
7077
+ */
7078
+ const RepositoryKeyField = TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "Canonical repository key (normalized git remote, e.g. github.com/owner/repo). Portable across machines, unlike projectId." }));
7079
+ 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." }));
7080
+ 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)." }));
7081
+ const Task = Schema$1.Struct({
7082
+ taskId: TaskId,
7083
+ /**
7084
+ * The human-readable id, e.g. `T-1` or `P4-12`: a per-prefix sequence
7085
+ * allocated by the store that owns the row, never reused, with gaps where
7086
+ * tasks were deleted. Nullable only for version skew - a row read from a
7087
+ * store that has not run the backfill migration decodes to `null` rather
7088
+ * than failing the whole board.
7089
+ */
7090
+ readableId: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7091
+ /**
7092
+ * The project this task belongs to, or `null` for a task that is not scoped
7093
+ * to one. This references the existing workspace project registry rather than
7094
+ * a task-board-private notion of a project.
7095
+ */
7096
+ projectId: Schema$1.NullOr(ProjectId),
7097
+ /**
7098
+ * Canonical repository key, resolved server-side from `projectId` when the
7099
+ * task is written. This is the field that survives a move to another machine;
7100
+ * `projectId` above is a local pointer and is meaningless on a server that
7101
+ * did not mint it. A board reading a shared task matches on this and looks up
7102
+ * whatever local project shares the key.
7103
+ */
7104
+ repositoryKey: Schema$1.NullOr(TrimmedNonEmptyString),
7105
+ /**
7106
+ * The thread currently working this task, or `null`. A task links to at most
7107
+ * one thread at a time; re-running a task repoints this rather than appending
7108
+ * to a history.
7109
+ */
7110
+ threadId: Schema$1.NullOr(ThreadId),
7111
+ title: TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_TITLE_MAX_LENGTH)),
7112
+ body: Schema$1.String,
7113
+ status: TaskStatus,
7114
+ priority: TaskPriority,
7115
+ assignee: Schema$1.NullOr(TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSIGNEE_MAX_LENGTH))),
7116
+ labels: Schema$1.Array(TaskLabel).check(Schema$1.isMaxLength(TASK_LABELS_MAX_COUNT)),
7117
+ createdAt: IsoDateTime,
7118
+ updatedAt: IsoDateTime
7119
+ });
7120
+ const TaskCreateInput = Schema$1.Struct({
7121
+ projectId: Schema$1.optional(ProjectIdField),
7122
+ repositoryKey: Schema$1.optional(RepositoryKeyField),
7123
+ threadId: Schema$1.optional(ThreadIdField),
7124
+ title: TitleField,
7125
+ body: Schema$1.optional(BodyField),
7126
+ status: Schema$1.optional(StatusField),
7127
+ priority: Schema$1.optional(PriorityField),
7128
+ assignee: Schema$1.optional(AssigneeField),
7129
+ labels: Schema$1.optional(LabelsField)
7130
+ });
7131
+ /**
7132
+ * What an environment server sends the hub to file a task.
7133
+ *
7134
+ * Extends the public create input with the readable-id prefix, because the two
7135
+ * halves of the answer live on different machines: the prefix comes from the
7136
+ * project's checked-in `p4.json`, which only the submitting server can read,
7137
+ * while the sequence number must be allocated by the hub, which is the only
7138
+ * party that sees every machine's writes. Deliberately not part of
7139
+ * `TaskCreateInput` itself so an agent filing over MCP cannot pick a prefix -
7140
+ * the prefix belongs to the project, not to the caller.
7141
+ */
7142
+ const HubTaskCreateInput = Schema$1.Struct({
7143
+ ...TaskCreateInput.fields,
7144
+ readableIdPrefix: Schema$1.optional(TrimmedNonEmptyString)
7145
+ });
7146
+ /**
7147
+ * Every field is optional and an omitted field is left untouched. The nullable
7148
+ * fields accept an explicit `null` to clear them, which is why they are
7149
+ * `NullOr` inside `optional` rather than merely optional.
7150
+ */
7151
+ const TaskUpdateInput = Schema$1.Struct({
7152
+ taskId: TaskIdField,
7153
+ projectId: Schema$1.optional(Schema$1.NullOr(ProjectIdField)),
7154
+ repositoryKey: Schema$1.optional(Schema$1.NullOr(RepositoryKeyField)),
7155
+ threadId: Schema$1.optional(Schema$1.NullOr(ThreadIdField)),
7156
+ title: Schema$1.optional(TitleField),
7157
+ body: Schema$1.optional(BodyField),
7158
+ status: Schema$1.optional(StatusField),
7159
+ priority: Schema$1.optional(PriorityField),
7160
+ assignee: Schema$1.optional(Schema$1.NullOr(AssigneeField)),
7161
+ labels: Schema$1.optional(LabelsField)
7162
+ });
7163
+ /**
7164
+ * A task an agent has drafted but has not filed.
7165
+ *
7166
+ * Mirrors `TaskCreateInput` minus the fields the proposer has no business
7167
+ * setting. `status` is absent on purpose: a confirmed proposal always lands in
7168
+ * `backlog`, so an interviewing agent cannot queue its own work — the property
7169
+ * the board's pick-up relies on. `taskId`, `threadId` and `repositoryKey` are
7170
+ * absent because they are minted or derived when the proposal is accepted, not
7171
+ * offered by the model.
7172
+ */
7173
+ const TaskProposeInput = Schema$1.Struct({
7174
+ projectId: Schema$1.optional(ProjectIdField),
7175
+ title: TitleField,
7176
+ body: Schema$1.optional(BodyField),
7177
+ priority: Schema$1.optional(PriorityField),
7178
+ assignee: Schema$1.optional(AssigneeField),
7179
+ labels: Schema$1.optional(LabelsField)
7180
+ });
7181
+ /**
7182
+ * What `task_propose` returns. The tool writes nothing; its only effect is the
7183
+ * card the client renders from the call itself, so the result exists to tell
7184
+ * the model that nothing has been filed yet and it should stop and wait.
7185
+ */
7186
+ const TaskProposeResult = Schema$1.Struct({ awaitingConfirmation: Schema$1.Literal(true) });
7187
+ /**
7188
+ * Parameters for `task_current`. The thread defaults to the caller's own, which
7189
+ * is the point of the tool — an agent does not know its own thread id. It is
7190
+ * still a real parameter rather than an empty struct because an empty struct
7191
+ * generates a root `anyOf` JSON Schema that some providers reject outright.
7192
+ */
7193
+ const TaskCurrentInput = Schema$1.Struct({ threadId: Schema$1.optional(ThreadId.pipe(Schema$1.annotateEncoded({ description: "Thread whose linked task to read. Defaults to this agent session's own thread." }))) });
7194
+ /** Parameters for reading or targeting a single task by id. */
7195
+ const TaskGetInput = Schema$1.Struct({ taskId: TaskIdField });
7196
+ const TaskListFilter = Schema$1.Struct({
7197
+ projectId: Schema$1.optional(ProjectIdField),
7198
+ repositoryKey: Schema$1.optional(RepositoryKeyField),
7199
+ threadId: Schema$1.optional(ThreadIdField),
7200
+ status: Schema$1.optional(StatusField),
7201
+ assignee: Schema$1.optional(AssigneeField)
7202
+ });
7203
+ Schema$1.Union([Schema$1.Struct({
7204
+ type: Schema$1.Literal("upserted"),
7205
+ task: Task
7206
+ }), Schema$1.Struct({
7207
+ type: Schema$1.Literal("deleted"),
7208
+ taskId: TaskId
7209
+ })]);
7210
+ /**
7211
+ * What a board subscription carries: the current board, then every change.
7212
+ *
7213
+ * The snapshot rides on the stream rather than being fetched separately
7214
+ * because a list call followed by a subscribe has a gap — anything committed
7215
+ * between the two is lost, and the board then shows stale rows until something
7216
+ * else happens to touch them. The server takes the snapshot *after* it has
7217
+ * subscribed, so the worst case is an event the subscriber already has, which
7218
+ * is idempotent, rather than one it never sees.
7219
+ */
7220
+ const TaskStreamEvent = Schema$1.Union([
7221
+ Schema$1.Struct({
7222
+ type: Schema$1.Literal("snapshot"),
7223
+ tasks: Schema$1.Array(Task)
7224
+ }),
7225
+ Schema$1.Struct({
7226
+ type: Schema$1.Literal("upserted"),
7227
+ task: Task
7228
+ }),
7229
+ Schema$1.Struct({
7230
+ type: Schema$1.Literal("deleted"),
7231
+ taskId: TaskId
7232
+ })
7233
+ ]);
7234
+ Schema$1.Struct({
7235
+ patch: TaskUpdateInput,
7236
+ /** The status the caller believes the task is in. */
7237
+ expectedStatus: TaskStatus
7238
+ });
7239
+ Schema$1.Union([
7240
+ Schema$1.Struct({
7241
+ outcome: Schema$1.Literal("updated"),
7242
+ task: Task
7243
+ }),
7244
+ Schema$1.Struct({ outcome: Schema$1.Literal("missing") }),
7245
+ Schema$1.Struct({
7246
+ outcome: Schema$1.Literal("conflict"),
7247
+ current: Task
7248
+ })
7249
+ ]);
7250
+ /**
7251
+ * Start an agent on a task.
7252
+ *
7253
+ * The input is only the task, deliberately. Everything else the thread needs —
7254
+ * which project, which model, what prompt — is resolved server-side from the
7255
+ * task's own project, because the caller that matters next is not a person
7256
+ * clicking a button but Phase 12's orchestrator, and a client that supplies the
7257
+ * model is a client that can start a task on a provider the user does not use.
7258
+ *
7259
+ * Worktrees are absent for a harder reason: `prepareWorktree` needs a base
7260
+ * branch, a task carries none, and nothing resolves one server-side. A thread
7261
+ * started from a task therefore runs in the project's own checkout.
7262
+ */
7263
+ const TaskStartThreadInput = Schema$1.Struct({ taskId: TaskIdField });
7264
+ /**
7265
+ * The started thread and the task as it now stands.
7266
+ *
7267
+ * Both are returned because both changed: the caller navigates to the thread,
7268
+ * and the board would otherwise show the old status until the change event
7269
+ * arrived.
7270
+ */
7271
+ const TaskStartThreadResult = Schema$1.Struct({
7272
+ task: Task,
7273
+ threadId: ThreadId
7274
+ });
7275
+ /**
7276
+ * A task cannot be started, and the reason is a property of the task or the
7277
+ * server rather than a failure.
7278
+ *
7279
+ * Each reason is a different thing for the user to fix, which is why this is a
7280
+ * union and not one message. `no-model` is the one worth explaining: a task
7281
+ * carries no model selection, so the thread runs on the project's default. The
7282
+ * server-side fallback that exists for first-run bootstrap is deliberately not
7283
+ * used here — it hardcodes a provider, so reaching for it would silently start
7284
+ * someone who works in Claude on Codex. Failing is the honest answer.
7285
+ */
7286
+ const TaskNotStartableReason = Schema$1.Literals([
7287
+ "no-project",
7288
+ "project-missing",
7289
+ "no-model"
7290
+ ]);
7291
+ var TaskNotStartableError = class extends Schema$1.TaggedErrorClass()("TaskNotStartableError", {
7292
+ taskId: TaskId,
7293
+ reason: TaskNotStartableReason
7294
+ }) {
7295
+ get message() {
7296
+ switch (this.reason) {
7297
+ case "no-project": return "This task has no project, so there is nowhere to run it. Set a project on the task first.";
7298
+ case "project-missing": return "This task's project is not registered on this server. Point it at a project that is.";
7299
+ case "no-model": return "This task's project has no default model. Choose one in the project's settings, then start the task again.";
7300
+ }
7301
+ }
7302
+ };
7303
+ /** Result shape for the board's list RPC. */
7304
+ const TaskListResult$1 = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
7305
+ /** Result shape for every RPC that resolves to a single task. */
7306
+ const TaskResult = Schema$1.Struct({ task: Task });
7307
+ /** `get` distinguishes "no such task" from an error, so the task is nullable. */
7308
+ const TaskGetResult = Schema$1.Struct({ task: Schema$1.NullOr(Task) });
7309
+ var TaskToolUnavailableError = class extends Schema$1.TaggedErrorClass()("TaskToolUnavailableError", {
7310
+ capability: Schema$1.Literal("tasks"),
7311
+ environmentId: EnvironmentId,
7312
+ threadId: ThreadId,
7313
+ providerSessionId: TrimmedNonEmptyString,
7314
+ providerInstanceId: ProviderInstanceId
7315
+ }) {
7316
+ get message() {
7317
+ return `MCP credential does not grant the ${this.capability} capability.`;
7318
+ }
7319
+ };
7320
+ var TaskNotFoundError = class extends Schema$1.TaggedErrorClass()("TaskNotFoundError", { taskId: TaskId }) {
7321
+ get message() {
7322
+ return `No task exists with id ${this.taskId}.`;
7323
+ }
7324
+ };
7325
+ /**
7326
+ * A task tool hit the store and the store failed. The underlying persistence
7327
+ * error is deliberately not surfaced to the agent — it carries SQL detail that
7328
+ * is noise in a tool result — but it is logged server-side.
7329
+ */
7330
+ var TaskStoreError = class extends Schema$1.TaggedErrorClass()("TaskStoreError", { operation: TrimmedNonEmptyString }) {
7331
+ get message() {
7332
+ return `The task store failed during ${this.operation}.`;
7333
+ }
7334
+ };
7335
+ /**
7336
+ * No task is linked to the calling agent's thread.
7337
+ */
7338
+ var TaskNotLinkedError = class extends Schema$1.TaggedErrorClass()("TaskNotLinkedError", { threadId: ThreadId }) {
7339
+ get message() {
7340
+ return `No task is linked to thread ${this.threadId}.`;
7341
+ }
7342
+ };
7343
+ const TaskToolError = Schema$1.Union([
7344
+ TaskToolUnavailableError,
7345
+ TaskNotFoundError,
7346
+ TaskNotLinkedError,
7347
+ TaskStoreError
7348
+ ]);
7349
+ //#endregion
7019
7350
  //#region ../../packages/contracts/src/p4ProjectFile.ts
7020
7351
  /** File name of the checked-in P4 project file, resolved at the workspace root. */
7021
7352
  const P4_PROJECT_FILE_NAME = "p4.json";
@@ -7039,7 +7370,8 @@ const P4ProjectFileScript = Schema$1.Struct({
7039
7370
  const P4ProjectFile = Schema$1.Struct({
7040
7371
  $schema: Schema$1.optionalKey(Schema$1.String.annotate({ description: `URL of the JSON Schema for this file, typically "${P4_PROJECT_FILE_SCHEMA_URL}".` })),
7041
7372
  iconPath: Schema$1.optionalKey(trimmedNonEmpty({ description: "Workspace-relative path to the project icon (e.g. \"assets/logo.svg\"). Checked before P4Code's built-in icon locations." }, P4_PROJECT_FILE_PATH_MAX_LENGTH)),
7042
- scripts: Schema$1.optionalKey(Schema$1.Array(P4ProjectFileScript).annotate({ description: "Project scripts shared with everyone who opens this repository in P4Code." }).check(Schema$1.isMaxLength(P4_PROJECT_FILE_MAX_SCRIPTS)))
7373
+ scripts: Schema$1.optionalKey(Schema$1.Array(P4ProjectFileScript).annotate({ description: "Project scripts shared with everyone who opens this repository in P4Code." }).check(Schema$1.isMaxLength(P4_PROJECT_FILE_MAX_SCRIPTS))),
7374
+ taskPrefix: Schema$1.optionalKey(trimmedNonEmpty({ description: "Prefix for this project's readable task ids on the task board (e.g. \"P4\" numbers tasks P4-1, P4-2, ...). Letters and digits, starting with a letter, at most 10 characters; compared case-insensitively. Tasks without a project use \"T\"." }).check(Schema$1.isPattern(TASK_READABLE_ID_PREFIX_PATTERN)))
7043
7375
  }).annotate({
7044
7376
  title: "P4 project file",
7045
7377
  description: "Checked-in project configuration for P4Code (p4.json at the repository root). See https://p4.codes for documentation."
@@ -7185,305 +7517,6 @@ var ProjectWriteFileError = class extends Schema$1.TaggedErrorClass()("ProjectWr
7185
7517
  }
7186
7518
  };
7187
7519
  //#endregion
7188
- //#region ../../packages/contracts/src/task.ts
7189
- const TASK_TITLE_MAX_LENGTH = 512;
7190
- const TASK_LABEL_MAX_LENGTH = 64;
7191
- const TASK_LABELS_MAX_COUNT = 32;
7192
- const TASK_ASSIGNEE_MAX_LENGTH = 128;
7193
- /**
7194
- * Board columns, in board order. `backlog` is the default for newly created
7195
- * tasks — an agent filing a task mid-session is recording work that exists, not
7196
- * committing to doing it next.
7197
- */
7198
- const TaskStatus = Schema$1.Literals([
7199
- "backlog",
7200
- "todo",
7201
- "in_progress",
7202
- "in_review",
7203
- "done"
7204
- ]);
7205
- const TaskPriority = Schema$1.Literals([
7206
- "none",
7207
- "low",
7208
- "medium",
7209
- "high",
7210
- "urgent"
7211
- ]);
7212
- const TaskLabel = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_LABEL_MAX_LENGTH));
7213
- /**
7214
- * Agent-facing field descriptions. These ride on the *encoded* side because
7215
- * every one of these schemas is a brand or a check over a transformation, and
7216
- * the JSON Schema an agent actually reads is generated from the encoded schema
7217
- * — a plain `.annotate` on the decoded side is silently dropped.
7218
- */
7219
- const ProjectIdField = ProjectId.pipe(Schema$1.annotateEncoded({ description: "Id of the project this task belongs to." }));
7220
- const ThreadIdField = ThreadId.pipe(Schema$1.annotateEncoded({ description: "Id of the agent thread working this task." }));
7221
- const TitleField = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_TITLE_MAX_LENGTH)).pipe(Schema$1.annotateEncoded({ description: "One-line summary of the work. Required." }));
7222
- const BodyField = Schema$1.String.pipe(Schema$1.annotateEncoded({ description: "Markdown detail: context, acceptance criteria, links." }));
7223
- const StatusField = TaskStatus.pipe(Schema$1.annotateEncoded({ description: "Board column: backlog (recorded, not queued), todo (queued next), in_progress, in_review, done." }));
7224
- const PriorityField = TaskPriority.pipe(Schema$1.annotateEncoded({ description: "Relative urgency: none, low, medium, high or urgent." }));
7225
- const AssigneeField = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSIGNEE_MAX_LENGTH)).pipe(Schema$1.annotateEncoded({ description: "Who owns the task — a person's handle or an agent name." }));
7226
- /**
7227
- * The portable half of "which repository is this task about".
7228
- *
7229
- * `projectId` cannot answer that across machines: it is a UUID minted when a
7230
- * project is added, and the same checkout added on two servers gets two
7231
- * different ids. A canonical repository key — the normalized primary git remote
7232
- * — is the same string wherever the repo is cloned, so it survives the trip
7233
- * through a shared board.
7234
- *
7235
- * Null for a task that is not about a repository, and also for a checkout with
7236
- * no remote, where there is nothing stable to key on.
7237
- */
7238
- const RepositoryKeyField = TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "Canonical repository key (normalized git remote, e.g. github.com/owner/repo). Portable across machines, unlike projectId." }));
7239
- 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." }));
7240
- const TaskIdField = TaskId.pipe(Schema$1.annotateEncoded({ description: "Id of the task to act on." }));
7241
- const Task = Schema$1.Struct({
7242
- taskId: TaskId,
7243
- /**
7244
- * The project this task belongs to, or `null` for a task that is not scoped
7245
- * to one. This references the existing workspace project registry rather than
7246
- * a task-board-private notion of a project.
7247
- */
7248
- projectId: Schema$1.NullOr(ProjectId),
7249
- /**
7250
- * Canonical repository key, resolved server-side from `projectId` when the
7251
- * task is written. This is the field that survives a move to another machine;
7252
- * `projectId` above is a local pointer and is meaningless on a server that
7253
- * did not mint it. A board reading a shared task matches on this and looks up
7254
- * whatever local project shares the key.
7255
- */
7256
- repositoryKey: Schema$1.NullOr(TrimmedNonEmptyString),
7257
- /**
7258
- * The thread currently working this task, or `null`. A task links to at most
7259
- * one thread at a time; re-running a task repoints this rather than appending
7260
- * to a history.
7261
- */
7262
- threadId: Schema$1.NullOr(ThreadId),
7263
- title: TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_TITLE_MAX_LENGTH)),
7264
- body: Schema$1.String,
7265
- status: TaskStatus,
7266
- priority: TaskPriority,
7267
- assignee: Schema$1.NullOr(TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSIGNEE_MAX_LENGTH))),
7268
- labels: Schema$1.Array(TaskLabel).check(Schema$1.isMaxLength(TASK_LABELS_MAX_COUNT)),
7269
- createdAt: IsoDateTime,
7270
- updatedAt: IsoDateTime
7271
- });
7272
- const TaskCreateInput = Schema$1.Struct({
7273
- projectId: Schema$1.optional(ProjectIdField),
7274
- repositoryKey: Schema$1.optional(RepositoryKeyField),
7275
- threadId: Schema$1.optional(ThreadIdField),
7276
- title: TitleField,
7277
- body: Schema$1.optional(BodyField),
7278
- status: Schema$1.optional(StatusField),
7279
- priority: Schema$1.optional(PriorityField),
7280
- assignee: Schema$1.optional(AssigneeField),
7281
- labels: Schema$1.optional(LabelsField)
7282
- });
7283
- /**
7284
- * Every field is optional and an omitted field is left untouched. The nullable
7285
- * fields accept an explicit `null` to clear them, which is why they are
7286
- * `NullOr` inside `optional` rather than merely optional.
7287
- */
7288
- const TaskUpdateInput = Schema$1.Struct({
7289
- taskId: TaskIdField,
7290
- projectId: Schema$1.optional(Schema$1.NullOr(ProjectIdField)),
7291
- repositoryKey: Schema$1.optional(Schema$1.NullOr(RepositoryKeyField)),
7292
- threadId: Schema$1.optional(Schema$1.NullOr(ThreadIdField)),
7293
- title: Schema$1.optional(TitleField),
7294
- body: Schema$1.optional(BodyField),
7295
- status: Schema$1.optional(StatusField),
7296
- priority: Schema$1.optional(PriorityField),
7297
- assignee: Schema$1.optional(Schema$1.NullOr(AssigneeField)),
7298
- labels: Schema$1.optional(LabelsField)
7299
- });
7300
- /**
7301
- * A task an agent has drafted but has not filed.
7302
- *
7303
- * Mirrors `TaskCreateInput` minus the fields the proposer has no business
7304
- * setting. `status` is absent on purpose: a confirmed proposal always lands in
7305
- * `backlog`, so an interviewing agent cannot queue its own work — the property
7306
- * the board's pick-up relies on. `taskId`, `threadId` and `repositoryKey` are
7307
- * absent because they are minted or derived when the proposal is accepted, not
7308
- * offered by the model.
7309
- */
7310
- const TaskProposeInput = Schema$1.Struct({
7311
- projectId: Schema$1.optional(ProjectIdField),
7312
- title: TitleField,
7313
- body: Schema$1.optional(BodyField),
7314
- priority: Schema$1.optional(PriorityField),
7315
- assignee: Schema$1.optional(AssigneeField),
7316
- labels: Schema$1.optional(LabelsField)
7317
- });
7318
- /**
7319
- * What `task_propose` returns. The tool writes nothing; its only effect is the
7320
- * card the client renders from the call itself, so the result exists to tell
7321
- * the model that nothing has been filed yet and it should stop and wait.
7322
- */
7323
- const TaskProposeResult = Schema$1.Struct({ awaitingConfirmation: Schema$1.Literal(true) });
7324
- /**
7325
- * Parameters for `task_current`. The thread defaults to the caller's own, which
7326
- * is the point of the tool — an agent does not know its own thread id. It is
7327
- * still a real parameter rather than an empty struct because an empty struct
7328
- * generates a root `anyOf` JSON Schema that some providers reject outright.
7329
- */
7330
- const TaskCurrentInput = Schema$1.Struct({ threadId: Schema$1.optional(ThreadId.pipe(Schema$1.annotateEncoded({ description: "Thread whose linked task to read. Defaults to this agent session's own thread." }))) });
7331
- /** Parameters for reading or targeting a single task by id. */
7332
- const TaskGetInput = Schema$1.Struct({ taskId: TaskIdField });
7333
- const TaskListFilter = Schema$1.Struct({
7334
- projectId: Schema$1.optional(ProjectIdField),
7335
- repositoryKey: Schema$1.optional(RepositoryKeyField),
7336
- threadId: Schema$1.optional(ThreadIdField),
7337
- status: Schema$1.optional(StatusField),
7338
- assignee: Schema$1.optional(AssigneeField)
7339
- });
7340
- Schema$1.Union([Schema$1.Struct({
7341
- type: Schema$1.Literal("upserted"),
7342
- task: Task
7343
- }), Schema$1.Struct({
7344
- type: Schema$1.Literal("deleted"),
7345
- taskId: TaskId
7346
- })]);
7347
- /**
7348
- * What a board subscription carries: the current board, then every change.
7349
- *
7350
- * The snapshot rides on the stream rather than being fetched separately
7351
- * because a list call followed by a subscribe has a gap — anything committed
7352
- * between the two is lost, and the board then shows stale rows until something
7353
- * else happens to touch them. The server takes the snapshot *after* it has
7354
- * subscribed, so the worst case is an event the subscriber already has, which
7355
- * is idempotent, rather than one it never sees.
7356
- */
7357
- const TaskStreamEvent = Schema$1.Union([
7358
- Schema$1.Struct({
7359
- type: Schema$1.Literal("snapshot"),
7360
- tasks: Schema$1.Array(Task)
7361
- }),
7362
- Schema$1.Struct({
7363
- type: Schema$1.Literal("upserted"),
7364
- task: Task
7365
- }),
7366
- Schema$1.Struct({
7367
- type: Schema$1.Literal("deleted"),
7368
- taskId: TaskId
7369
- })
7370
- ]);
7371
- Schema$1.Struct({
7372
- patch: TaskUpdateInput,
7373
- /** The status the caller believes the task is in. */
7374
- expectedStatus: TaskStatus
7375
- });
7376
- Schema$1.Union([
7377
- Schema$1.Struct({
7378
- outcome: Schema$1.Literal("updated"),
7379
- task: Task
7380
- }),
7381
- Schema$1.Struct({ outcome: Schema$1.Literal("missing") }),
7382
- Schema$1.Struct({
7383
- outcome: Schema$1.Literal("conflict"),
7384
- current: Task
7385
- })
7386
- ]);
7387
- /**
7388
- * Start an agent on a task.
7389
- *
7390
- * The input is only the task, deliberately. Everything else the thread needs —
7391
- * which project, which model, what prompt — is resolved server-side from the
7392
- * task's own project, because the caller that matters next is not a person
7393
- * clicking a button but Phase 12's orchestrator, and a client that supplies the
7394
- * model is a client that can start a task on a provider the user does not use.
7395
- *
7396
- * Worktrees are absent for a harder reason: `prepareWorktree` needs a base
7397
- * branch, a task carries none, and nothing resolves one server-side. A thread
7398
- * started from a task therefore runs in the project's own checkout.
7399
- */
7400
- const TaskStartThreadInput = Schema$1.Struct({ taskId: TaskIdField });
7401
- /**
7402
- * The started thread and the task as it now stands.
7403
- *
7404
- * Both are returned because both changed: the caller navigates to the thread,
7405
- * and the board would otherwise show the old status until the change event
7406
- * arrived.
7407
- */
7408
- const TaskStartThreadResult = Schema$1.Struct({
7409
- task: Task,
7410
- threadId: ThreadId
7411
- });
7412
- /**
7413
- * A task cannot be started, and the reason is a property of the task or the
7414
- * server rather than a failure.
7415
- *
7416
- * Each reason is a different thing for the user to fix, which is why this is a
7417
- * union and not one message. `no-model` is the one worth explaining: a task
7418
- * carries no model selection, so the thread runs on the project's default. The
7419
- * server-side fallback that exists for first-run bootstrap is deliberately not
7420
- * used here — it hardcodes a provider, so reaching for it would silently start
7421
- * someone who works in Claude on Codex. Failing is the honest answer.
7422
- */
7423
- const TaskNotStartableReason = Schema$1.Literals([
7424
- "no-project",
7425
- "project-missing",
7426
- "no-model"
7427
- ]);
7428
- var TaskNotStartableError = class extends Schema$1.TaggedErrorClass()("TaskNotStartableError", {
7429
- taskId: TaskId,
7430
- reason: TaskNotStartableReason
7431
- }) {
7432
- get message() {
7433
- switch (this.reason) {
7434
- case "no-project": return "This task has no project, so there is nowhere to run it. Set a project on the task first.";
7435
- case "project-missing": return "This task's project is not registered on this server. Point it at a project that is.";
7436
- case "no-model": return "This task's project has no default model. Choose one in the project's settings, then start the task again.";
7437
- }
7438
- }
7439
- };
7440
- /** Result shape for the board's list RPC. */
7441
- const TaskListResult$1 = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
7442
- /** Result shape for every RPC that resolves to a single task. */
7443
- const TaskResult = Schema$1.Struct({ task: Task });
7444
- /** `get` distinguishes "no such task" from an error, so the task is nullable. */
7445
- const TaskGetResult = Schema$1.Struct({ task: Schema$1.NullOr(Task) });
7446
- var TaskToolUnavailableError = class extends Schema$1.TaggedErrorClass()("TaskToolUnavailableError", {
7447
- capability: Schema$1.Literal("tasks"),
7448
- environmentId: EnvironmentId,
7449
- threadId: ThreadId,
7450
- providerSessionId: TrimmedNonEmptyString,
7451
- providerInstanceId: ProviderInstanceId
7452
- }) {
7453
- get message() {
7454
- return `MCP credential does not grant the ${this.capability} capability.`;
7455
- }
7456
- };
7457
- var TaskNotFoundError = class extends Schema$1.TaggedErrorClass()("TaskNotFoundError", { taskId: TaskId }) {
7458
- get message() {
7459
- return `No task exists with id ${this.taskId}.`;
7460
- }
7461
- };
7462
- /**
7463
- * A task tool hit the store and the store failed. The underlying persistence
7464
- * error is deliberately not surfaced to the agent — it carries SQL detail that
7465
- * is noise in a tool result — but it is logged server-side.
7466
- */
7467
- var TaskStoreError = class extends Schema$1.TaggedErrorClass()("TaskStoreError", { operation: TrimmedNonEmptyString }) {
7468
- get message() {
7469
- return `The task store failed during ${this.operation}.`;
7470
- }
7471
- };
7472
- /**
7473
- * No task is linked to the calling agent's thread.
7474
- */
7475
- var TaskNotLinkedError = class extends Schema$1.TaggedErrorClass()("TaskNotLinkedError", { threadId: ThreadId }) {
7476
- get message() {
7477
- return `No task is linked to thread ${this.threadId}.`;
7478
- }
7479
- };
7480
- const TaskToolError = Schema$1.Union([
7481
- TaskToolUnavailableError,
7482
- TaskNotFoundError,
7483
- TaskNotLinkedError,
7484
- TaskStoreError
7485
- ]);
7486
- //#endregion
7487
7520
  //#region ../../packages/contracts/src/agentAsset.ts
7488
7521
  /**
7489
7522
  * User-scope agent assets — the things that make an agent behave the same on
@@ -12034,6 +12067,51 @@ var _036_TaskRepositoryKey_default = Effect.gen(function* () {
12034
12067
  `;
12035
12068
  });
12036
12069
  //#endregion
12070
+ //#region src/persistence/Migrations/037_TaskReadableIds.ts
12071
+ /**
12072
+ * Human-readable task ids (`T-1`, `P4-12`).
12073
+ *
12074
+ * `task_sequences` holds one row per prefix with the highest number ever
12075
+ * handed out. It only grows: deleting a task does not return its number, so a
12076
+ * readable id, once uttered, never comes to mean a different task.
12077
+ *
12078
+ * Existing rows are backfilled with the default `T` prefix in creation order.
12079
+ * Their projects' prefixes are deliberately not consulted — a prefix lives in
12080
+ * the project's checked-in `p4.json`, and a migration that shells out to read
12081
+ * workspaces (which may no longer be on disk) is exactly what a migration
12082
+ * should not be. New tasks pick up their project's prefix from creation on.
12083
+ */
12084
+ var _037_TaskReadableIds_default = Effect.gen(function* () {
12085
+ const sql = yield* SqlClient.SqlClient;
12086
+ yield* sql`
12087
+ CREATE TABLE IF NOT EXISTS task_sequences (
12088
+ prefix TEXT PRIMARY KEY,
12089
+ last_seq INTEGER NOT NULL
12090
+ )
12091
+ `;
12092
+ yield* sql`ALTER TABLE tasks ADD COLUMN readable_id TEXT`;
12093
+ yield* sql`
12094
+ UPDATE tasks
12095
+ SET readable_id = 'T-' || (
12096
+ SELECT COUNT(*)
12097
+ FROM tasks AS earlier
12098
+ WHERE earlier.created_at < tasks.created_at
12099
+ OR (earlier.created_at = tasks.created_at AND earlier.task_id <= tasks.task_id)
12100
+ )
12101
+ WHERE readable_id IS NULL
12102
+ `;
12103
+ yield* sql`
12104
+ INSERT INTO task_sequences (prefix, last_seq)
12105
+ SELECT 'T', COUNT(*) FROM tasks
12106
+ WHERE true
12107
+ ON CONFLICT (prefix) DO UPDATE SET last_seq = MAX(last_seq, excluded.last_seq)
12108
+ `;
12109
+ yield* sql`
12110
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_readable_id
12111
+ ON tasks(readable_id)
12112
+ `;
12113
+ });
12114
+ //#endregion
12037
12115
  //#region src/persistence/Migrations.ts
12038
12116
  /**
12039
12117
  * MigrationsLive - Migration runner with inline loader
@@ -12234,6 +12312,11 @@ const migrationEntries = [
12234
12312
  36,
12235
12313
  "TaskRepositoryKey",
12236
12314
  _036_TaskRepositoryKey_default
12315
+ ],
12316
+ [
12317
+ 37,
12318
+ "TaskReadableIds",
12319
+ _037_TaskReadableIds_default
12237
12320
  ]
12238
12321
  ];
12239
12322
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -13857,6 +13940,7 @@ const TaskListQuery = Schema$1.Struct({
13857
13940
  });
13858
13941
  const TASK_COLUMNS = `
13859
13942
  task_id AS "taskId",
13943
+ readable_id AS "readableId",
13860
13944
  project_id AS "projectId",
13861
13945
  repository_key AS "repositoryKey",
13862
13946
  thread_id AS "threadId",
@@ -13878,6 +13962,7 @@ const makeTaskRepository = Effect.gen(function* () {
13878
13962
  execute: (row) => sql`
13879
13963
  INSERT INTO tasks (
13880
13964
  task_id,
13965
+ readable_id,
13881
13966
  project_id,
13882
13967
  repository_key,
13883
13968
  thread_id,
@@ -13892,6 +13977,7 @@ const makeTaskRepository = Effect.gen(function* () {
13892
13977
  )
13893
13978
  VALUES (
13894
13979
  ${row.taskId},
13980
+ ${row.readableId},
13895
13981
  ${row.projectId},
13896
13982
  ${row.repositoryKey},
13897
13983
  ${row.threadId},
@@ -13906,6 +13992,7 @@ const makeTaskRepository = Effect.gen(function* () {
13906
13992
  )
13907
13993
  ON CONFLICT (task_id)
13908
13994
  DO UPDATE SET
13995
+ readable_id = excluded.readable_id,
13909
13996
  project_id = excluded.project_id,
13910
13997
  repository_key = excluded.repository_key,
13911
13998
  thread_id = excluded.thread_id,
@@ -13924,6 +14011,26 @@ const makeTaskRepository = Effect.gen(function* () {
13924
14011
  Result: TaskDbRow,
13925
14012
  execute: ({ taskId }) => sql`SELECT ${sql.literal(TASK_COLUMNS)} FROM tasks WHERE task_id = ${taskId}`
13926
14013
  });
14014
+ const getTaskRowByReadableId = SqlSchema.findOneOption({
14015
+ Request: Schema$1.String,
14016
+ Result: TaskDbRow,
14017
+ execute: (readableId) => sql`SELECT ${sql.literal(TASK_COLUMNS)} FROM tasks WHERE readable_id = ${readableId}`
14018
+ });
14019
+ /**
14020
+ * Take the next number for a prefix, creating the sequence at 1 on first
14021
+ * use. The row is never reset or decremented - a deleted task's number is
14022
+ * gone - which is what makes a readable id safe to say out loud.
14023
+ */
14024
+ const allocateReadableIdSeq = SqlSchema.findAll({
14025
+ Request: Schema$1.Struct({ prefix: Schema$1.String }),
14026
+ Result: Schema$1.Struct({ lastSeq: Schema$1.Int }),
14027
+ execute: ({ prefix }) => sql`
14028
+ INSERT INTO task_sequences (prefix, last_seq)
14029
+ VALUES (${prefix}, 1)
14030
+ ON CONFLICT (prefix) DO UPDATE SET last_seq = last_seq + 1
14031
+ RETURNING last_seq AS "lastSeq"
14032
+ `
14033
+ });
13927
14034
  const listTaskRows = SqlSchema.findAll({
13928
14035
  Request: TaskListQuery,
13929
14036
  Result: TaskDbRow,
@@ -13946,7 +14053,31 @@ const makeTaskRepository = Effect.gen(function* () {
13946
14053
  type: "upserted",
13947
14054
  task: row
13948
14055
  })));
14056
+ /**
14057
+ * The allocation and the insert share one transaction on purpose: a number
14058
+ * taken outside it would either be lost on a failed insert (a gap nobody
14059
+ * caused) or, worse, handed to two concurrent creates.
14060
+ */
14061
+ const create = (row, readableIdPrefix) => sql.withTransaction(Effect.gen(function* () {
14062
+ const prefix = readableIdPrefix.toUpperCase();
14063
+ const seq = (yield* allocateReadableIdSeq({ prefix }))[0]?.lastSeq;
14064
+ if (seq === void 0) return yield* new PersistenceSqlError({
14065
+ operation: "TaskRepository.create:allocate",
14066
+ detail: "the sequence upsert returned no row",
14067
+ cause: null
14068
+ });
14069
+ const task = {
14070
+ ...row,
14071
+ readableId: `${prefix}-${seq}`
14072
+ };
14073
+ yield* upsertTaskRow(task);
14074
+ return task;
14075
+ })).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.create:query")), Effect.tap((task) => publish({
14076
+ type: "upserted",
14077
+ task
14078
+ })));
13949
14079
  const getById = (input) => getTaskRow(input).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.getById:query")));
14080
+ const getByReadableId = (readableId) => getTaskRowByReadableId(readableId.toUpperCase()).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.getByReadableId:query")));
13950
14081
  const list = (filter) => listTaskRows({
13951
14082
  projectId: filter.projectId ?? null,
13952
14083
  repositoryKey: filter.repositoryKey ?? null,
@@ -14021,10 +14152,12 @@ const makeTaskRepository = Effect.gen(function* () {
14021
14152
  taskId: input.taskId
14022
14153
  })));
14023
14154
  return {
14155
+ create,
14024
14156
  upsert,
14025
14157
  patch,
14026
14158
  patchIfStatus,
14027
14159
  getById,
14160
+ getByReadableId,
14028
14161
  list,
14029
14162
  deleteById,
14030
14163
  streamChanges: Stream.fromPubSub(changes),
@@ -14113,12 +14246,13 @@ const getTaskRoute = HttpRouter.add("GET", "/tasks/:taskId", respondToHubFailure
14113
14246
  yield* authenticateHubRequest();
14114
14247
  const params = yield* HttpRouter.params;
14115
14248
  const taskId = TaskId.make(params.taskId ?? "");
14116
- const found = yield* (yield* TaskRepository).getById({ taskId });
14249
+ const tasks = yield* TaskRepository;
14250
+ const found = isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
14117
14251
  return Option.isNone(found) ? notFound(taskId) : HttpServerResponse.jsonUnsafe(found.value);
14118
14252
  })));
14119
14253
  const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Effect.gen(function* () {
14120
14254
  yield* authenticateHubRequest();
14121
- const input = yield* decodeOrInvalid(TaskCreateInput, "Request body does not match TaskCreateInput.")(yield* readJsonBody());
14255
+ const input = yield* decodeOrInvalid(HubTaskCreateInput, "Request body does not match HubTaskCreateInput.")(yield* readJsonBody());
14122
14256
  const tasks = yield* TaskRepository;
14123
14257
  const crypto = yield* Crypto.Crypto;
14124
14258
  const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
@@ -14137,8 +14271,8 @@ const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Ef
14137
14271
  createdAt: timestamp,
14138
14272
  updatedAt: timestamp
14139
14273
  };
14140
- yield* tasks.upsert(row);
14141
- return HttpServerResponse.jsonUnsafe(row, { status: 201 });
14274
+ const task = yield* tasks.create(row, input.readableIdPrefix ?? "T");
14275
+ return HttpServerResponse.jsonUnsafe(task, { status: 201 });
14142
14276
  })));
14143
14277
  const updateTaskRoute = HttpRouter.add("PATCH", "/tasks/:taskId", respondToHubFailures(Effect.gen(function* () {
14144
14278
  yield* authenticateHubRequest();
@@ -14376,6 +14510,50 @@ var _003_AgentAssets_default = Effect.gen(function* () {
14376
14510
  `;
14377
14511
  });
14378
14512
  //#endregion
14513
+ //#region src/hub/Migrations/004_TaskReadableIds.ts
14514
+ /**
14515
+ * Human-readable task ids (`T-1`, `P4-12`).
14516
+ *
14517
+ * `task_sequences` holds one row per prefix with the highest number ever
14518
+ * handed out. It only grows: deleting a task does not return its number, so a
14519
+ * readable id, once uttered, never comes to mean a different task.
14520
+ *
14521
+ * Existing rows are backfilled with the default `T` prefix in creation order.
14522
+ * Prefixes cannot be consulted here at all: they live in each project's
14523
+ * checked-in `p4.json`, and the hub has no checkouts. New tasks arrive with a
14524
+ * prefix resolved by the submitting environment server.
14525
+ */
14526
+ var _004_TaskReadableIds_default = Effect.gen(function* () {
14527
+ const sql = yield* SqlClient.SqlClient;
14528
+ yield* sql`
14529
+ CREATE TABLE IF NOT EXISTS task_sequences (
14530
+ prefix TEXT PRIMARY KEY,
14531
+ last_seq INTEGER NOT NULL
14532
+ )
14533
+ `;
14534
+ yield* sql`ALTER TABLE tasks ADD COLUMN readable_id TEXT`;
14535
+ yield* sql`
14536
+ UPDATE tasks
14537
+ SET readable_id = 'T-' || (
14538
+ SELECT COUNT(*)
14539
+ FROM tasks AS earlier
14540
+ WHERE earlier.created_at < tasks.created_at
14541
+ OR (earlier.created_at = tasks.created_at AND earlier.task_id <= tasks.task_id)
14542
+ )
14543
+ WHERE readable_id IS NULL
14544
+ `;
14545
+ yield* sql`
14546
+ INSERT INTO task_sequences (prefix, last_seq)
14547
+ SELECT 'T', COUNT(*) FROM tasks
14548
+ WHERE true
14549
+ ON CONFLICT (prefix) DO UPDATE SET last_seq = MAX(last_seq, excluded.last_seq)
14550
+ `;
14551
+ yield* sql`
14552
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_readable_id
14553
+ ON tasks(readable_id)
14554
+ `;
14555
+ });
14556
+ //#endregion
14379
14557
  //#region src/hub/Migrations.ts
14380
14558
  /**
14381
14559
  * Hub migrations.
@@ -14401,6 +14579,11 @@ const hubMigrationEntries = [
14401
14579
  3,
14402
14580
  "AgentAssets",
14403
14581
  _003_AgentAssets_default
14582
+ ],
14583
+ [
14584
+ 4,
14585
+ "TaskReadableIds",
14586
+ _004_TaskReadableIds_default
14404
14587
  ]
14405
14588
  ];
14406
14589
  const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -31482,6 +31665,287 @@ const make$39 = Effect.gen(function* () {
31482
31665
  });
31483
31666
  const layer$34 = Layer.effect(CheckpointDiffQuery, make$39);
31484
31667
  //#endregion
31668
+ //#region ../../packages/shared/src/toolCategory.ts
31669
+ const TOOL_CATEGORY_TITLES = {
31670
+ file_read: "File read",
31671
+ file_search: "File search",
31672
+ file_change: "File change",
31673
+ command: "Command run",
31674
+ version_control: "Version control",
31675
+ build_test: "Build or test",
31676
+ mcp_tool: "MCP tool call",
31677
+ subagent: "Subagent task",
31678
+ web_search: "Web search",
31679
+ web_fetch: "Web fetch",
31680
+ task_plan: "Task or plan",
31681
+ image_view: "Image view",
31682
+ tool: "Tool call"
31683
+ };
31684
+ new Set(Object.keys(TOOL_CATEGORY_TITLES));
31685
+ /** Exact tool names, lowercased, matched before the substring heuristics below. */
31686
+ const FILE_READ_TOOL_NAMES = /* @__PURE__ */ new Set([
31687
+ "read",
31688
+ "readfile",
31689
+ "read_file",
31690
+ "view",
31691
+ "viewfile",
31692
+ "view_file",
31693
+ "notebookread",
31694
+ "notebook_read",
31695
+ "openfile",
31696
+ "open_file"
31697
+ ]);
31698
+ const FILE_SEARCH_TOOL_NAMES = /* @__PURE__ */ new Set([
31699
+ "grep",
31700
+ "glob",
31701
+ "search",
31702
+ "filesearch",
31703
+ "file_search",
31704
+ "codebasesearch",
31705
+ "codebase_search",
31706
+ "listdir",
31707
+ "list_dir",
31708
+ "list_directory",
31709
+ "ls"
31710
+ ]);
31711
+ const FILE_CHANGE_TOOL_NAMES = /* @__PURE__ */ new Set([
31712
+ "edit",
31713
+ "write",
31714
+ "multiedit",
31715
+ "multi_edit",
31716
+ "notebookedit",
31717
+ "notebook_edit",
31718
+ "applypatch",
31719
+ "apply_patch",
31720
+ "patch",
31721
+ "createfile",
31722
+ "create_file",
31723
+ "strreplace",
31724
+ "str_replace",
31725
+ "deletefile",
31726
+ "delete_file"
31727
+ ]);
31728
+ const COMMAND_TOOL_NAMES = /* @__PURE__ */ new Set([
31729
+ "bash",
31730
+ "shell",
31731
+ "terminal",
31732
+ "exec",
31733
+ "execute",
31734
+ "execcommand",
31735
+ "exec_command",
31736
+ "runcommand",
31737
+ "run_command",
31738
+ "local_shell"
31739
+ ]);
31740
+ const SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
31741
+ "task",
31742
+ "agent",
31743
+ "subagent",
31744
+ "sub_agent"
31745
+ ]);
31746
+ const WEB_FETCH_TOOL_NAMES = /* @__PURE__ */ new Set([
31747
+ "webfetch",
31748
+ "web_fetch",
31749
+ "fetch",
31750
+ "fetchurl",
31751
+ "fetch_url",
31752
+ "browse",
31753
+ "openurl",
31754
+ "open_url"
31755
+ ]);
31756
+ /**
31757
+ * Matched before the subagent rules, so `task_create` is a board write while the
31758
+ * bare `Task` tool stays a subagent.
31759
+ */
31760
+ const TASK_PLAN_TOOL_NAMES = /* @__PURE__ */ new Set([
31761
+ "todowrite",
31762
+ "todo_write",
31763
+ "todoread",
31764
+ "todo_read",
31765
+ "exitplanmode",
31766
+ "exit_plan_mode",
31767
+ "updateplan",
31768
+ "update_plan",
31769
+ "task_create",
31770
+ "task_update",
31771
+ "task_list",
31772
+ "task_get",
31773
+ "task_current",
31774
+ "task_propose",
31775
+ "taskcreate",
31776
+ "taskupdate",
31777
+ "tasklist",
31778
+ "taskget"
31779
+ ]);
31780
+ /** Shell builtins that say nothing about what the command as a whole does. */
31781
+ const NEUTRAL_SHELL_COMMANDS = /* @__PURE__ */ new Set([
31782
+ "cd",
31783
+ "echo",
31784
+ "printf",
31785
+ "pwd",
31786
+ "true",
31787
+ "false",
31788
+ "time"
31789
+ ]);
31790
+ /** `sed`/`awk` are stream editors (`sed -i` rewrites files), so they stay commands. */
31791
+ const FILE_READ_SHELL_COMMANDS = /* @__PURE__ */ new Set([
31792
+ "cat",
31793
+ "head",
31794
+ "tail",
31795
+ "less",
31796
+ "more",
31797
+ "bat",
31798
+ "nl",
31799
+ "jq",
31800
+ "wc"
31801
+ ]);
31802
+ const FILE_SEARCH_SHELL_COMMANDS = /* @__PURE__ */ new Set([
31803
+ "grep",
31804
+ "egrep",
31805
+ "fgrep",
31806
+ "rg",
31807
+ "ag",
31808
+ "ack",
31809
+ "find",
31810
+ "fd",
31811
+ "ls",
31812
+ "tree",
31813
+ "which"
31814
+ ]);
31815
+ /** `git status` and `git diff` sort here too: the verb is what a reader scans for. */
31816
+ const VERSION_CONTROL_SHELL_COMMANDS = /* @__PURE__ */ new Set([
31817
+ "git",
31818
+ "gh",
31819
+ "jj",
31820
+ "hg",
31821
+ "svn",
31822
+ "glab"
31823
+ ]);
31824
+ /**
31825
+ * Builds, tests, type checks, lint and install share one category: a test run is
31826
+ * what a person scans for when a turn goes long, and the rest is the same noise.
31827
+ */
31828
+ const BUILD_TEST_SHELL_COMMANDS = /* @__PURE__ */ new Set([
31829
+ "pnpm",
31830
+ "npm",
31831
+ "npx",
31832
+ "yarn",
31833
+ "bun",
31834
+ "bunx",
31835
+ "vitest",
31836
+ "jest",
31837
+ "vp",
31838
+ "tsc",
31839
+ "tsgo",
31840
+ "eslint",
31841
+ "oxlint",
31842
+ "biome",
31843
+ "prettier",
31844
+ "ruff",
31845
+ "pytest",
31846
+ "make",
31847
+ "cargo",
31848
+ "gradle",
31849
+ "mvn",
31850
+ "xcodebuild",
31851
+ "swift",
31852
+ "pip",
31853
+ "uv",
31854
+ "poetry"
31855
+ ]);
31856
+ const WEB_FETCH_SHELL_COMMANDS = /* @__PURE__ */ new Set([
31857
+ "curl",
31858
+ "wget",
31859
+ "http",
31860
+ "httpie"
31861
+ ]);
31862
+ const SHELL_SEGMENT_SEPARATOR = /\|\||&&|;|\||\n/u;
31863
+ const LEADING_ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/u;
31864
+ const REDIRECT_TARGET = /\d?>>?\s*(?<target>[^\s|;&]+)/gu;
31865
+ /** Redirect targets that discard output instead of writing a file. */
31866
+ const DISCARDED_REDIRECT_TARGETS = /* @__PURE__ */ new Set([
31867
+ "/dev/null",
31868
+ "&1",
31869
+ "&2"
31870
+ ]);
31871
+ function toolCategoryTitle(category) {
31872
+ return TOOL_CATEGORY_TITLES[category];
31873
+ }
31874
+ function readCommandInput(toolInput) {
31875
+ const raw = toolInput?.command ?? toolInput?.cmd;
31876
+ if (typeof raw !== "string") return;
31877
+ const trimmed = raw.trim();
31878
+ return trimmed.length > 0 ? trimmed : void 0;
31879
+ }
31880
+ /** `cat x > y` reads and writes; only the write matters for the row heading. */
31881
+ function redirectsToFile(segment) {
31882
+ for (const match of segment.matchAll(REDIRECT_TARGET)) {
31883
+ const target = match.groups?.target;
31884
+ if (target && !DISCARDED_REDIRECT_TARGETS.has(target)) return true;
31885
+ }
31886
+ return false;
31887
+ }
31888
+ function classifyShellSegment(segment) {
31889
+ if (redirectsToFile(segment)) return "command";
31890
+ const head = segment.trim().replace(LEADING_ENV_ASSIGNMENT, "").split(/\s+/u)[0]?.replace(/^.*\//u, "").toLowerCase();
31891
+ if (!head || NEUTRAL_SHELL_COMMANDS.has(head)) return;
31892
+ if (FILE_READ_SHELL_COMMANDS.has(head)) return "file_read";
31893
+ if (FILE_SEARCH_SHELL_COMMANDS.has(head)) return "file_search";
31894
+ if (VERSION_CONTROL_SHELL_COMMANDS.has(head)) return "version_control";
31895
+ if (BUILD_TEST_SHELL_COMMANDS.has(head)) return "build_test";
31896
+ if (WEB_FETCH_SHELL_COMMANDS.has(head)) return "web_fetch";
31897
+ return "command";
31898
+ }
31899
+ /**
31900
+ * A shell call only counts as anything narrower than a command when every
31901
+ * non-neutral segment agrees; anything unrecognized (or write-ish) keeps the
31902
+ * whole call a command, and so does a pipeline that mixes two categories — the
31903
+ * one exception being a read piped into a search, which is still a search.
31904
+ * Quoted separators split segments too, which can only downgrade to "command".
31905
+ */
31906
+ function classifyShellCommand(command) {
31907
+ const categories = new Set(command.split(SHELL_SEGMENT_SEPARATOR).map(classifyShellSegment).filter((category) => category !== void 0));
31908
+ if (categories.size === 0 || categories.has("command")) return "command";
31909
+ const [only] = categories;
31910
+ if (categories.size === 1 && only) return only;
31911
+ return [...categories].every((category) => category === "file_read" || category === "file_search") ? "file_search" : "command";
31912
+ }
31913
+ function classifyToolCategory(input) {
31914
+ const normalized = input.toolName.trim().toLowerCase();
31915
+ if (normalized.startsWith("mcp__") || normalized.includes("mcp")) return "mcp_tool";
31916
+ if (TASK_PLAN_TOOL_NAMES.has(normalized)) return "task_plan";
31917
+ if (WEB_FETCH_TOOL_NAMES.has(normalized)) return "web_fetch";
31918
+ if (SUBAGENT_TOOL_NAMES.has(normalized) || normalized.includes("agent")) return "subagent";
31919
+ if (FILE_READ_TOOL_NAMES.has(normalized)) return "file_read";
31920
+ if (FILE_SEARCH_TOOL_NAMES.has(normalized)) return "file_search";
31921
+ if (FILE_CHANGE_TOOL_NAMES.has(normalized)) return "file_change";
31922
+ if (COMMAND_TOOL_NAMES.has(normalized) || normalized.includes("bash") || normalized.includes("shell") || normalized.includes("terminal") || normalized.includes("command")) {
31923
+ const command = readCommandInput(input.toolInput);
31924
+ return command ? classifyShellCommand(command) : "command";
31925
+ }
31926
+ if (normalized.includes("websearch") || normalized.includes("web_search")) return "web_search";
31927
+ if (normalized.includes("webfetch") || normalized.includes("web_fetch")) return "web_fetch";
31928
+ if (normalized.includes("todo") || normalized.includes("plan")) return "task_plan";
31929
+ if (normalized.includes("edit") || normalized.includes("write") || normalized.includes("patch") || normalized.includes("replace") || normalized.includes("create") || normalized.includes("delete")) return "file_change";
31930
+ if (normalized.includes("read") || normalized.includes("view")) return "file_read";
31931
+ if (normalized.includes("grep") || normalized.includes("glob")) return "file_search";
31932
+ if (normalized.includes("image")) return "image_view";
31933
+ return "tool";
31934
+ }
31935
+ function asRecord$2(value) {
31936
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
31937
+ }
31938
+ /** Classify from a runtime item payload's `data` (`{ toolName, input }`). */
31939
+ function classifyToolCategoryFromToolData(data) {
31940
+ const record = asRecord$2(data);
31941
+ const toolName = record?.toolName;
31942
+ if (typeof toolName !== "string" || toolName.trim().length === 0) return;
31943
+ return classifyToolCategory({
31944
+ toolName,
31945
+ toolInput: asRecord$2(record?.input)
31946
+ });
31947
+ }
31948
+ //#endregion
31485
31949
  //#region src/orchestration/ActivityPayloadProjection.ts
31486
31950
  function asRecord$1(value) {
31487
31951
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -31583,9 +32047,13 @@ function projectActivityPayload(activity) {
31583
32047
  const item = projectCommandData(data);
31584
32048
  if (item) projectedData.item = item;
31585
32049
  if ("command" in data) projectedData.command = data.command;
32050
+ const input = asRecord$1(data.input);
32051
+ if (input && "command" in input) projectedData.input = { command: input.command };
31586
32052
  const changedFiles = [];
31587
32053
  collectChangedFiles(data, changedFiles, /* @__PURE__ */ new Set(), 0);
31588
32054
  if (changedFiles.length > 0) projectedData.files = changedFiles.map((path) => ({ path }));
32055
+ const toolCategory = classifyToolCategoryFromToolData(data);
32056
+ if (toolCategory) projectedData.toolCategory = toolCategory;
31589
32057
  if ("toolCallId" in data) projectedData.toolCallId = data.toolCallId;
31590
32058
  if ("kind" in data) projectedData.kind = data.kind;
31591
32059
  const rawOutput = projectRawOutput(data.rawOutput);
@@ -31757,6 +32225,8 @@ const normalizeDispatchCommand = (command) => Effect.gen(function* () {
31757
32225
  * presentation and is not.
31758
32226
  *
31759
32227
  * The id and timestamp are passed in so this stays pure and holds no clock.
32228
+ * The readable id is absent on purpose: the store allocates it at insert time,
32229
+ * inside the same transaction as the sequence bump.
31760
32230
  */
31761
32231
  const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
31762
32232
  taskId,
@@ -31792,6 +32262,23 @@ const resolveTaskRepositoryKey = Effect.fn("taskBoard.resolveRepositoryKey")(fun
31792
32262
  return (yield* (yield* RepositoryIdentityResolver).resolve(project.value.workspaceRoot))?.canonicalKey ?? null;
31793
32263
  });
31794
32264
  /**
32265
+ * Resolve the readable-id prefix for a task's project.
32266
+ *
32267
+ * The prefix lives in the project's checked-in `p4.json` (`taskPrefix`), not in
32268
+ * this server's database: it is a property of the repository, agreed on by
32269
+ * everyone who clones it, exactly like the scripts beside it. A task with no
32270
+ * project, a removed project, or a `p4.json` without the field all fall back to
32271
+ * the default `T` - filing the task always beats refusing it over a label.
32272
+ */
32273
+ const resolveTaskReadableIdPrefix = Effect.fn("taskBoard.resolveReadableIdPrefix")(function* (projectId) {
32274
+ if (projectId == null) return "T";
32275
+ const project = yield* (yield* ProjectionProjectRepository).getById({ projectId }).pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
32276
+ if (Option.isNone(project)) return "T";
32277
+ const file = yield* (yield* P4ProjectFileLoader).load(project.value.workspaceRoot);
32278
+ if (Option.isNone(file) || file.value.taskPrefix === void 0) return "T";
32279
+ return file.value.taskPrefix.toUpperCase();
32280
+ });
32281
+ /**
31795
32282
  * Everything that decides whether a task can be started, as one pure function.
31796
32283
  *
31797
32284
  * Kept out of the RPC handler because the three refusals are the whole of the
@@ -42416,9 +42903,12 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
42416
42903
  [WS_METHODS.tasksList]: (filter) => observeRpcEffect$1(WS_METHODS.tasksList, taskRepository.list(filter).pipe(Effect.catch(toTaskStoreError("tasks.list")), Effect.map((tasks) => ({ tasks }))), { "rpc.aggregate": "tasks" }),
42417
42904
  [WS_METHODS.tasksGet]: ({ taskId }) => observeRpcEffect$1(WS_METHODS.tasksGet, taskRepository.getById({ taskId }).pipe(Effect.catch(toTaskStoreError("tasks.get")), Effect.map((found) => ({ task: Option.getOrNull(found) }))), { "rpc.aggregate": "tasks" }),
42418
42905
  [WS_METHODS.tasksCreate]: (input) => observeRpcEffect$1(WS_METHODS.tasksCreate, Effect.gen(function* () {
42419
- const row = buildTaskRow(input, TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)), yield* nowIso$5, yield* resolveTaskRepositoryKey(input.projectId));
42420
- yield* taskRepository.upsert(row).pipe(Effect.catch(toTaskStoreError("tasks.create")));
42421
- return { task: row };
42906
+ const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
42907
+ const timestamp = yield* nowIso$5;
42908
+ const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
42909
+ const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
42910
+ const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
42911
+ return { task: yield* taskRepository.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("tasks.create"))) };
42422
42912
  }), { "rpc.aggregate": "tasks" }),
42423
42913
  [WS_METHODS.tasksUpdate]: (input) => observeRpcEffect$1(WS_METHODS.tasksUpdate, Effect.gen(function* () {
42424
42914
  const timestamp = yield* nowIso$5;
@@ -42729,12 +43219,13 @@ const websocketRpcRouteLayer = Layer.unwrap(Effect.gen(function* () {
42729
43219
  const serverSelfUpdate = yield* ServerSelfUpdate;
42730
43220
  const taskRepository = yield* TaskRepository;
42731
43221
  const projectionProjects = yield* ProjectionProjectRepository;
43222
+ const p4ProjectFileLoader = yield* P4ProjectFileLoader;
42732
43223
  return HttpRouter.add("GET", "/ws", Effect.gen(function* () {
42733
43224
  const request = yield* HttpServerRequest.HttpServerRequest;
42734
43225
  const serverAuth = yield* EnvironmentAuth;
42735
43226
  const sessions = yield* SessionStore;
42736
43227
  const session = yield* serverAuth.authenticateWebSocketUpgrade(request).pipe(Effect.catchIf(isServerAuthCredentialError, (error) => failEnvironmentAuthInvalid(serverAuthCredentialReason(error))), Effect.catchIf(isServerAuthInternalError, (error) => failEnvironmentInternal("internal_error", error)));
42737
- const rpcWebSocketHttpEffect = yield* RpcServer.toHttpEffectWebsocket(WsRpcGroup, { disableTracing: true }).pipe(Effect.provide(makeWsRpcLayer(session, previewAutomationBroker).pipe(Layer.provideMerge(RpcSerialization.layerJson), Layer.provide(layer$33), Layer.provide(Layer.succeed(ServerSelfUpdate, serverSelfUpdate)), Layer.provide(Layer.succeed(TaskRepository, taskRepository)), Layer.provide(Layer.succeed(ProjectionProjectRepository, projectionProjects)), Layer.provide(layer$5.pipe(Layer.provide(layer$14.pipe(Layer.provide(Layer.mergeAll(layer$22, layer$20, layer$18, layer$16)), Layer.provideMerge(layer$38), Layer.provide(layer$36.pipe(Layer.provide(layer$37))))), Layer.provide(layer$39))))));
43228
+ const rpcWebSocketHttpEffect = yield* RpcServer.toHttpEffectWebsocket(WsRpcGroup, { disableTracing: true }).pipe(Effect.provide(makeWsRpcLayer(session, previewAutomationBroker).pipe(Layer.provideMerge(RpcSerialization.layerJson), Layer.provide(layer$33), Layer.provide(Layer.succeed(ServerSelfUpdate, serverSelfUpdate)), Layer.provide(Layer.succeed(TaskRepository, taskRepository)), Layer.provide(Layer.succeed(ProjectionProjectRepository, projectionProjects)), Layer.provide(Layer.succeed(P4ProjectFileLoader, p4ProjectFileLoader)), Layer.provide(layer$5.pipe(Layer.provide(layer$14.pipe(Layer.provide(Layer.mergeAll(layer$22, layer$20, layer$18, layer$16)), Layer.provideMerge(layer$38), Layer.provide(layer$36.pipe(Layer.provide(layer$37))))), Layer.provide(layer$39))))));
42738
43229
  return yield* Effect.acquireUseRelease(sessions.markConnected(session.sessionId), () => rpcWebSocketHttpEffect, () => sessions.markDisconnected(session.sessionId));
42739
43230
  }).pipe(Effect.catchTags({
42740
43231
  EnvironmentAuthInvalidError: HttpServerRespondable.toResponse,
@@ -42839,17 +43330,16 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
42839
43330
  return Option.some(yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("getById", "the response did not match the contract"))));
42840
43331
  });
42841
43332
  /**
42842
- * Upsert is a create here, and that is a real difference from the local
42843
- * store.
42844
- *
42845
- * The hub mints its own ids and timestamps, so a row written through this
42846
- * comes back with the hub's version of both. The caller's `taskId` is not
42847
- * carried: honouring it would need a hub route that takes one, and inventing
42848
- * that quietly would let two servers mint colliding ids.
43333
+ * Both writes here POST the hub's create route, and that is a real
43334
+ * difference from the local store: the hub mints its own ids, timestamps and
43335
+ * readable id, so a row written through this comes back with the hub's
43336
+ * version of all three. The caller's `taskId` is not carried - honouring it
43337
+ * would need a hub route that takes one, and inventing that quietly would
43338
+ * let two servers mint colliding ids.
42849
43339
  */
42850
- const upsert = (row) => Effect.gen(function* () {
42851
- const settings = yield* requireSettings("upsert");
42852
- const response = yield* send("upsert", settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(`${settings.baseUrl}/tasks`), {
43340
+ const postCreate = (operation, row, readableIdPrefix) => Effect.gen(function* () {
43341
+ const settings = yield* requireSettings(operation);
43342
+ const response = yield* send(operation, settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(`${settings.baseUrl}/tasks`), {
42853
43343
  ...row.projectId === null ? {} : { projectId: row.projectId },
42854
43344
  ...row.repositoryKey === null ? {} : { repositoryKey: row.repositoryKey },
42855
43345
  ...row.threadId === null ? {} : { threadId: row.threadId },
@@ -42858,15 +43348,24 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
42858
43348
  status: row.status,
42859
43349
  priority: row.priority,
42860
43350
  ...row.assignee === null ? {} : { assignee: row.assignee },
42861
- labels: row.labels
43351
+ labels: row.labels,
43352
+ ...readableIdPrefix === void 0 ? {} : { readableIdPrefix }
42862
43353
  }));
42863
- if (response.status !== 201 && response.status !== 200) return yield* hubUnavailable("upsert", `status ${response.status}`);
42864
- const created = yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("upsert", "the response did not match the contract")));
43354
+ if (response.status !== 201 && response.status !== 200) return yield* hubUnavailable(operation, `status ${response.status}`);
43355
+ const created = yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable(operation, "the response did not match the contract")));
42865
43356
  yield* publish({
42866
43357
  type: "upserted",
42867
43358
  task: created
42868
43359
  });
43360
+ return created;
42869
43361
  });
43362
+ const create = (row, readableIdPrefix) => postCreate("create", row, readableIdPrefix);
43363
+ const upsert = (row) => postCreate("upsert", row).pipe(Effect.asVoid);
43364
+ /**
43365
+ * Served by the same hub route as `getById`: readable ids and UUIDs are
43366
+ * disjoint by shape, so the hub dispatches on what it is handed.
43367
+ */
43368
+ const getByReadableId = (readableId) => getById({ taskId: TaskId.make(readableId) });
42870
43369
  const patch = (input) => Effect.gen(function* () {
42871
43370
  const settings = yield* requireSettings("patch");
42872
43371
  const { taskId, ...fields } = input;
@@ -42941,10 +43440,12 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
42941
43440
  }).pipe(Effect.ignoreCause({ log: true }));
42942
43441
  yield* Effect.forever(pollOnce.pipe(Effect.andThen(Effect.sleep(POLL_INTERVAL)))).pipe(Effect.forkScoped);
42943
43442
  return {
43443
+ create,
42944
43444
  upsert,
42945
43445
  patch,
42946
43446
  patchIfStatus,
42947
43447
  getById,
43448
+ getByReadableId,
42948
43449
  list,
42949
43450
  deleteById,
42950
43451
  streamChanges: Stream.fromPubSub(changes),
@@ -42994,10 +43495,12 @@ const makeRoutedTaskRepository = Effect.gen(function* () {
42994
43495
  */
42995
43496
  const pick = usingHub.pipe(Effect.map((hub) => hub ? remote : local));
42996
43497
  return {
43498
+ create: (row, readableIdPrefix) => pick.pipe(Effect.flatMap((repository) => repository.create(row, readableIdPrefix))),
42997
43499
  upsert: (row) => pick.pipe(Effect.flatMap((repository) => repository.upsert(row))),
42998
43500
  patch: (input, updatedAt) => pick.pipe(Effect.flatMap((repository) => repository.patch(input, updatedAt))),
42999
43501
  patchIfStatus: (input, updatedAt) => pick.pipe(Effect.flatMap((repository) => repository.patchIfStatus(input, updatedAt))),
43000
43502
  getById: (input) => pick.pipe(Effect.flatMap((repository) => repository.getById(input))),
43503
+ getByReadableId: (readableId) => pick.pipe(Effect.flatMap((repository) => repository.getByReadableId(readableId))),
43001
43504
  list: (filter) => pick.pipe(Effect.flatMap((repository) => repository.list(filter))),
43002
43505
  deleteById: (input) => pick.pipe(Effect.flatMap((repository) => repository.deleteById(input))),
43003
43506
  get streamChanges() {
@@ -46483,177 +46986,6 @@ function parseCliArgs(args, options) {
46483
46986
  };
46484
46987
  }
46485
46988
  //#endregion
46486
- //#region ../../packages/shared/src/toolCategory.ts
46487
- const TOOL_CATEGORY_TITLES = {
46488
- file_read: "File read",
46489
- file_search: "File search",
46490
- file_change: "File change",
46491
- command: "Command run",
46492
- mcp_tool: "MCP tool call",
46493
- subagent: "Subagent task",
46494
- web_search: "Web search",
46495
- image_view: "Image view",
46496
- tool: "Tool call"
46497
- };
46498
- /** Exact tool names, lowercased, matched before the substring heuristics below. */
46499
- const FILE_READ_TOOL_NAMES = /* @__PURE__ */ new Set([
46500
- "read",
46501
- "readfile",
46502
- "read_file",
46503
- "view",
46504
- "viewfile",
46505
- "view_file",
46506
- "notebookread",
46507
- "notebook_read",
46508
- "openfile",
46509
- "open_file"
46510
- ]);
46511
- const FILE_SEARCH_TOOL_NAMES = /* @__PURE__ */ new Set([
46512
- "grep",
46513
- "glob",
46514
- "search",
46515
- "filesearch",
46516
- "file_search",
46517
- "codebasesearch",
46518
- "codebase_search",
46519
- "listdir",
46520
- "list_dir",
46521
- "list_directory",
46522
- "ls"
46523
- ]);
46524
- const FILE_CHANGE_TOOL_NAMES = /* @__PURE__ */ new Set([
46525
- "edit",
46526
- "write",
46527
- "multiedit",
46528
- "multi_edit",
46529
- "notebookedit",
46530
- "notebook_edit",
46531
- "applypatch",
46532
- "apply_patch",
46533
- "patch",
46534
- "createfile",
46535
- "create_file",
46536
- "strreplace",
46537
- "str_replace",
46538
- "deletefile",
46539
- "delete_file"
46540
- ]);
46541
- const COMMAND_TOOL_NAMES = /* @__PURE__ */ new Set([
46542
- "bash",
46543
- "shell",
46544
- "terminal",
46545
- "exec",
46546
- "execute",
46547
- "execcommand",
46548
- "exec_command",
46549
- "runcommand",
46550
- "run_command",
46551
- "local_shell"
46552
- ]);
46553
- const SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
46554
- "task",
46555
- "agent",
46556
- "subagent",
46557
- "sub_agent"
46558
- ]);
46559
- /** Shell builtins that say nothing about what the command as a whole does. */
46560
- const NEUTRAL_SHELL_COMMANDS = /* @__PURE__ */ new Set([
46561
- "cd",
46562
- "echo",
46563
- "printf",
46564
- "pwd",
46565
- "true",
46566
- "false",
46567
- "time"
46568
- ]);
46569
- /** `sed`/`awk` are stream editors (`sed -i` rewrites files), so they stay commands. */
46570
- const FILE_READ_SHELL_COMMANDS = /* @__PURE__ */ new Set([
46571
- "cat",
46572
- "head",
46573
- "tail",
46574
- "less",
46575
- "more",
46576
- "bat",
46577
- "nl",
46578
- "jq",
46579
- "wc"
46580
- ]);
46581
- const FILE_SEARCH_SHELL_COMMANDS = /* @__PURE__ */ new Set([
46582
- "grep",
46583
- "egrep",
46584
- "fgrep",
46585
- "rg",
46586
- "ag",
46587
- "ack",
46588
- "find",
46589
- "fd",
46590
- "ls",
46591
- "tree",
46592
- "which"
46593
- ]);
46594
- const SHELL_SEGMENT_SEPARATOR = /\|\||&&|;|\||\n/u;
46595
- const LEADING_ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/u;
46596
- const REDIRECT_TARGET = /\d?>>?\s*(?<target>[^\s|;&]+)/gu;
46597
- /** Redirect targets that discard output instead of writing a file. */
46598
- const DISCARDED_REDIRECT_TARGETS = /* @__PURE__ */ new Set([
46599
- "/dev/null",
46600
- "&1",
46601
- "&2"
46602
- ]);
46603
- function toolCategoryTitle(category) {
46604
- return TOOL_CATEGORY_TITLES[category];
46605
- }
46606
- function readCommandInput(toolInput) {
46607
- const raw = toolInput?.command ?? toolInput?.cmd;
46608
- if (typeof raw !== "string") return;
46609
- const trimmed = raw.trim();
46610
- return trimmed.length > 0 ? trimmed : void 0;
46611
- }
46612
- /** `cat x > y` reads and writes; only the write matters for the row heading. */
46613
- function redirectsToFile(segment) {
46614
- for (const match of segment.matchAll(REDIRECT_TARGET)) {
46615
- const target = match.groups?.target;
46616
- if (target && !DISCARDED_REDIRECT_TARGETS.has(target)) return true;
46617
- }
46618
- return false;
46619
- }
46620
- function classifyShellSegment(segment) {
46621
- if (redirectsToFile(segment)) return "command";
46622
- const head = segment.trim().replace(LEADING_ENV_ASSIGNMENT, "").split(/\s+/u)[0]?.replace(/^.*\//u, "").toLowerCase();
46623
- if (!head || NEUTRAL_SHELL_COMMANDS.has(head)) return;
46624
- if (FILE_READ_SHELL_COMMANDS.has(head)) return "file_read";
46625
- if (FILE_SEARCH_SHELL_COMMANDS.has(head)) return "file_search";
46626
- return "command";
46627
- }
46628
- /**
46629
- * A shell call only counts as a read or a search when every non-neutral segment
46630
- * agrees; anything unrecognized (or write-ish) keeps the whole call a command.
46631
- * Quoted separators split segments too, which can only downgrade to "command".
46632
- */
46633
- function classifyShellCommand(command) {
46634
- const categories = command.split(SHELL_SEGMENT_SEPARATOR).map(classifyShellSegment).filter((category) => category !== void 0);
46635
- if (categories.length === 0 || categories.includes("command")) return "command";
46636
- return categories.includes("file_search") ? "file_search" : "file_read";
46637
- }
46638
- function classifyToolCategory(input) {
46639
- const normalized = input.toolName.trim().toLowerCase();
46640
- if (normalized.startsWith("mcp__") || normalized.includes("mcp")) return "mcp_tool";
46641
- if (SUBAGENT_TOOL_NAMES.has(normalized) || normalized.includes("agent")) return "subagent";
46642
- if (FILE_READ_TOOL_NAMES.has(normalized)) return "file_read";
46643
- if (FILE_SEARCH_TOOL_NAMES.has(normalized)) return "file_search";
46644
- if (FILE_CHANGE_TOOL_NAMES.has(normalized)) return "file_change";
46645
- if (COMMAND_TOOL_NAMES.has(normalized) || normalized.includes("bash") || normalized.includes("shell") || normalized.includes("terminal") || normalized.includes("command")) {
46646
- const command = readCommandInput(input.toolInput);
46647
- return command ? classifyShellCommand(command) : "command";
46648
- }
46649
- if (normalized.includes("websearch") || normalized.includes("web_search")) return "web_search";
46650
- if (normalized.includes("edit") || normalized.includes("write") || normalized.includes("patch") || normalized.includes("replace") || normalized.includes("create") || normalized.includes("delete")) return "file_change";
46651
- if (normalized.includes("read") || normalized.includes("view")) return "file_read";
46652
- if (normalized.includes("grep") || normalized.includes("glob")) return "file_search";
46653
- if (normalized.includes("image")) return "image_view";
46654
- return "tool";
46655
- }
46656
- //#endregion
46657
46989
  //#region src/provider/askUserQuestionAnswers.ts
46658
46990
  /**
46659
46991
  * What separates the options of one multi-select answer.
@@ -47249,6 +47581,8 @@ const SUPPORTED_CLAUDE_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
47249
47581
  * preset drops the interactive CLI's terminal-tone sections, so without this the
47250
47582
  * model opens a turn with a tool call rather than a line saying what it is doing.
47251
47583
  */
47584
+ /** Emitted by the CLI but absent from the SDK's `SDKMessage` union. */
47585
+ const UNTYPED_COMMAND_LIFECYCLE_MESSAGE = "command_lifecycle";
47252
47586
  const NARRATE_BEFORE_TOOLS_PROMPT = "Before your first tool call in a turn, write one short sentence saying what you are about to do. Keep it to a single line and skip it when you are answering without tools.";
47253
47587
  const CLAUDE_SETTING_SOURCES = [
47254
47588
  "user",
@@ -48694,6 +49028,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
48694
49028
  const handleSdkMessage = Effect.fn("handleSdkMessage")(function* (context, message) {
48695
49029
  yield* logNativeSdkMessage(context, message);
48696
49030
  yield* ensureThreadId(context, message);
49031
+ if (message.type === UNTYPED_COMMAND_LIFECYCLE_MESSAGE) return;
48697
49032
  switch (message.type) {
48698
49033
  case "stream_event":
48699
49034
  yield* handleStreamEvent(context, message);
@@ -49076,6 +49411,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
49076
49411
  };
49077
49412
  const mcpSession = readMcpProviderSession(input.threadId);
49078
49413
  const externalMcpServers = options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers;
49414
+ const narrateBeforeTools = options?.resolveToolCallNarration === void 0 ? DEFAULT_SERVER_SETTINGS.enableToolCallNarration : yield* options.resolveToolCallNarration;
49079
49415
  const queryOptions = {
49080
49416
  ...input.cwd ? { cwd: input.cwd } : {},
49081
49417
  ...apiModelId ? { model: apiModelId } : {},
@@ -49083,7 +49419,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
49083
49419
  systemPrompt: {
49084
49420
  type: "preset",
49085
49421
  preset: "claude_code",
49086
- ...claudeSettings.narrateBeforeTools ? { append: NARRATE_BEFORE_TOOLS_PROMPT } : {}
49422
+ ...narrateBeforeTools ? { append: NARRATE_BEFORE_TOOLS_PROMPT } : {}
49087
49423
  },
49088
49424
  settingSources: [...CLAUDE_SETTING_SOURCES],
49089
49425
  ...effectiveEffort ? { effort: effectiveEffort } : {},
@@ -49125,7 +49461,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
49125
49461
  "claude.query.include_partial_messages": true,
49126
49462
  "claude.query.additional_directories": input.cwd ? [input.cwd] : [],
49127
49463
  "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES],
49128
- "claude.query.narrate_before_tools": claudeSettings.narrateBeforeTools,
49464
+ "claude.query.narrate_before_tools": narrateBeforeTools,
49129
49465
  "claude.query.settings_json": encodeJsonStringForDiagnostics$2(settings) ?? "",
49130
49466
  "claude.query.extra_args_json": encodeJsonStringForDiagnostics$2(extraArgs) ?? "",
49131
49467
  "claude.query.path_to_executable": claudeBinaryPath
@@ -49536,11 +49872,13 @@ const ClaudeDriver = {
49536
49872
  });
49537
49873
  const mcpRegistry = yield* McpRegistry;
49538
49874
  const resolveDisabledSkills = serverSettings.getSettings.pipe(Effect.map((settings) => settings.disabledSkills), Effect.orElseSucceed(() => []));
49875
+ const resolveToolCallNarration = serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableToolCallNarration), Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS.enableToolCallNarration));
49539
49876
  const adapter = yield* makeClaudeAdapter(effectiveConfig, {
49540
49877
  instanceId,
49541
49878
  environment: processEnv,
49542
49879
  resolveMcpServers: mcpRegistry.resolveForSession,
49543
49880
  resolveDisabledSkills,
49881
+ resolveToolCallNarration,
49544
49882
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
49545
49883
  });
49546
49884
  const textGeneration = yield* makeClaudeTextGeneration(effectiveConfig, processEnv);
@@ -80967,8 +81305,15 @@ const writeDependencies = [
80967
81305
  ProjectionProjectRepository,
80968
81306
  RepositoryIdentityResolver
80969
81307
  ];
80970
- /** `task_create` also mints the task id, so it needs the crypto service. */
80971
- const createDependencies = [...writeDependencies, Crypto.Crypto];
81308
+ /**
81309
+ * `task_create` also mints the task id (crypto) and resolves the readable-id
81310
+ * prefix from the project's checked-in `p4.json` (the file loader).
81311
+ */
81312
+ const createDependencies = [
81313
+ ...writeDependencies,
81314
+ Crypto.Crypto,
81315
+ P4ProjectFileLoader
81316
+ ];
80972
81317
  const TaskListResult = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
80973
81318
  const TaskListTool = Tool.make("task_list", {
80974
81319
  description: "List tasks on the board, most recently updated first. Every filter is optional and they combine with AND; omit all of them to list every task. Use this to find work rather than guessing task ids.",
@@ -80978,7 +81323,7 @@ const TaskListTool = Tool.make("task_list", {
80978
81323
  dependencies
80979
81324
  }).annotate(Tool.Title, "List tasks").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
80980
81325
  const TaskGetTool = Tool.make("task_get", {
80981
- description: "Read a single task by id, including its body, labels and linked thread.",
81326
+ description: "Read a single task by id, including its body, labels and linked thread. Accepts either the UUID or the readable id (e.g. T-3, P4-12).",
80982
81327
  parameters: TaskGetInput,
80983
81328
  success: Task,
80984
81329
  failure: TaskToolError,
@@ -81028,6 +81373,15 @@ const TaskToolkit = Toolkit.make(TaskListTool, TaskGetTool, TaskCurrentTool, Tas
81028
81373
  //#endregion
81029
81374
  //#region src/mcp/toolkits/tasks/handlers.ts
81030
81375
  const nowIso$1 = Effect.map(DateTime.now, DateTime.formatIso);
81376
+ /**
81377
+ * Agents reference tasks by whichever id they have seen, so any `taskId`
81378
+ * parameter accepts the readable id (`P4-12`) as well as the UUID. The two are
81379
+ * disjoint by shape, which is what makes the dispatch safe.
81380
+ */
81381
+ const findTask = (taskId) => Effect.gen(function* () {
81382
+ const tasks = yield* TaskRepository;
81383
+ return isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
81384
+ });
81031
81385
  const TaskToolkitHandlersLive = TaskToolkit.toLayer({
81032
81386
  task_list: (input) => Effect.gen(function* () {
81033
81387
  yield* requireTaskCapability();
@@ -81035,7 +81389,7 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
81035
81389
  }),
81036
81390
  task_get: (input) => Effect.gen(function* () {
81037
81391
  yield* requireTaskCapability();
81038
- const found = yield* (yield* TaskRepository).getById({ taskId: input.taskId }).pipe(Effect.catch(toTaskStoreError("task_get")));
81392
+ const found = yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_get")));
81039
81393
  if (Option.isNone(found)) return yield* new TaskNotFoundError({ taskId: input.taskId });
81040
81394
  return found.value;
81041
81395
  }),
@@ -81051,16 +81405,25 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
81051
81405
  yield* requireTaskCapability();
81052
81406
  const tasks = yield* TaskRepository;
81053
81407
  const crypto = yield* Crypto.Crypto;
81054
- const row = buildTaskRow(input, TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)), yield* nowIso$1, yield* resolveTaskRepositoryKey(input.projectId));
81055
- yield* tasks.upsert(row).pipe(Effect.catch(toTaskStoreError("task_create")));
81056
- return row;
81408
+ const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
81409
+ const timestamp = yield* nowIso$1;
81410
+ const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
81411
+ const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
81412
+ const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
81413
+ return yield* tasks.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("task_create")));
81057
81414
  }),
81058
81415
  task_update: (input) => Effect.gen(function* () {
81059
81416
  yield* requireTaskCapability();
81060
81417
  const tasks = yield* TaskRepository;
81061
81418
  const timestamp = yield* nowIso$1;
81062
- const patch = input.projectId === void 0 ? input : {
81419
+ const taskId = isReadableTaskId(input.taskId) ? Option.getOrNull(yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_update"))))?.taskId : input.taskId;
81420
+ if (taskId === void 0) return yield* new TaskNotFoundError({ taskId: input.taskId });
81421
+ const patch = input.projectId === void 0 ? {
81422
+ ...input,
81423
+ taskId
81424
+ } : {
81063
81425
  ...input,
81426
+ taskId,
81064
81427
  repositoryKey: yield* resolveTaskRepositoryKey(input.projectId)
81065
81428
  };
81066
81429
  const patched = yield* tasks.patch(patch, timestamp).pipe(Effect.catch(toTaskStoreError("task_update")));
@@ -82429,6 +82792,31 @@ const make$2 = Effect.gen(function* () {
82429
82792
  },
82430
82793
  createdAt: input.createdAt
82431
82794
  })));
82795
+ /**
82796
+ * A successful interrupt otherwise leaves nothing behind: the only row the
82797
+ * timeline ever got was the failure case above, and the turn fold's "You
82798
+ * stopped after Xs" is invisible while the fold is open and says nothing in
82799
+ * the work log. The turn id comes from the request rather than the provider
82800
+ * because orchestration turn ids are not provider turn ids.
82801
+ */
82802
+ const appendTurnInterruptedActivity = (input) => Effect.all({
82803
+ commandId: serverCommandId("provider-interrupt-activity"),
82804
+ eventId: serverEventId()
82805
+ }).pipe(Effect.flatMap(({ commandId, eventId }) => orchestrationEngine.dispatch({
82806
+ type: "thread.activity.append",
82807
+ commandId,
82808
+ threadId: input.threadId,
82809
+ activity: {
82810
+ id: eventId,
82811
+ tone: "info",
82812
+ kind: "provider.turn.interrupted",
82813
+ summary: "Interrupted by user",
82814
+ payload: {},
82815
+ turnId: input.turnId,
82816
+ createdAt: input.createdAt
82817
+ },
82818
+ createdAt: input.createdAt
82819
+ })));
82432
82820
  const formatFailureDetail = (cause) => {
82433
82821
  const failReason = cause.reasons.find(Cause.isFailReason);
82434
82822
  const providerError = isProviderAdapterRequestError(failReason?.error) ? failReason.error : void 0;
@@ -82815,6 +83203,11 @@ const make$2 = Effect.gen(function* () {
82815
83203
  createdAt: event.payload.createdAt
82816
83204
  });
82817
83205
  yield* providerService.interruptTurn({ threadId: event.payload.threadId });
83206
+ yield* appendTurnInterruptedActivity({
83207
+ threadId: event.payload.threadId,
83208
+ turnId: event.payload.turnId ?? null,
83209
+ createdAt: event.payload.createdAt
83210
+ });
82818
83211
  });
82819
83212
  const processApprovalResponseRequested = Effect.fn("processApprovalResponseRequested")(function* (event) {
82820
83213
  const thread = yield* resolveThread(event.payload.threadId);
@@ -84744,7 +85137,7 @@ const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(Checkpo
84744
85137
  */
84745
85138
  const AssetSyncLive = layer$54.pipe(Layer.provideMerge(layer$55), Layer.provideMerge(layer$56));
84746
85139
  const RuntimeServicesLive = layer$44.pipe(Layer.provideMerge(AssetSyncLive), Layer.provideMerge(RuntimeDependenciesLive));
84747
- const makeRoutesLayer = Layer.mergeAll(Layer.mergeAll(HttpApiBuilder.layer(EnvironmentHttpApi).pipe(Layer.provide(authHttpApiLayer), Layer.provide(orchestrationHttpApiLayer), Layer.provide(serverEnvironmentHttpApiLayer), Layer.provide(environmentAuthenticatedAuthLayer)), otlpTracesProxyRouteLayer, assetRouteLayer, staticAndDevRouteLayer, websocketRpcRouteLayer), layer.pipe(Layer.provide(layer$2))).pipe(Layer.provide(RoutedTaskRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(ProjectionProjectRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(layer$28), Layer.provide(layer$46), Layer.provide(browserApiCorsLayer), Layer.provide(httpCompressionLayer));
85140
+ const makeRoutesLayer = Layer.mergeAll(Layer.mergeAll(HttpApiBuilder.layer(EnvironmentHttpApi).pipe(Layer.provide(authHttpApiLayer), Layer.provide(orchestrationHttpApiLayer), Layer.provide(serverEnvironmentHttpApiLayer), Layer.provide(environmentAuthenticatedAuthLayer)), otlpTracesProxyRouteLayer, assetRouteLayer, staticAndDevRouteLayer, websocketRpcRouteLayer), layer.pipe(Layer.provide(layer$2))).pipe(Layer.provide(RoutedTaskRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(ProjectionProjectRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(layer$42), Layer.provide(layer$28), Layer.provide(layer$46), Layer.provide(browserApiCorsLayer), Layer.provide(httpCompressionLayer));
84748
85141
  const makeServerLayer = Layer.unwrap(Effect.gen(function* () {
84749
85142
  const config = yield* ServerConfig$1;
84750
85143
  yield* fixPath();