@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.
- package/dist/bin.mjs +911 -518
- package/dist/client/assets/{DiffPanel-CcuhH24b.js → DiffPanel-D4tNpljU.js} +2 -2
- package/dist/client/assets/{FilePreviewPanel-IupIzTGt.js → FilePreviewPanel-xICJSzZ6.js} +2 -2
- package/dist/client/assets/{PreviewPanel-CMRKi8Jr.js → PreviewPanel-CK9d1fc5.js} +2 -2
- package/dist/client/assets/{angular-html-D4VEGoU_.js → angular-html-BgzZxO9w.js} +2 -2
- package/dist/client/assets/{angular-ts-DDbN_Pcg.js → angular-ts-YFEXVhe6.js} +2 -2
- package/dist/client/assets/{apl-BH7XGSiX.js → apl-BoBwF9HI.js} +2 -2
- package/dist/client/assets/arrow-right-CXxWzaql.js +2 -0
- package/dist/client/assets/{astro-DKjLEfvq.js → astro-Ds5TXooD.js} +2 -2
- package/dist/client/assets/{blade-mO6atsi4.js → blade-CDtJf5YR.js} +2 -2
- package/dist/client/assets/{c-drphy09y.js → c-JH0umSmP.js} +2 -2
- package/dist/client/assets/{cobol-Do365H-h.js → cobol-gMss6BA3.js} +2 -2
- package/dist/client/assets/{coffee-CJ9y15YY.js → coffee-DESPXYwo.js} +2 -2
- package/dist/client/assets/{cpp-C1FVZ39w.js → cpp-CZztA1XN.js} +2 -2
- package/dist/client/assets/{crystal-D1Y9mCOy.js → crystal-CDWlJpLE.js} +2 -2
- package/dist/client/assets/{css-C1chQpfJ.js → css-CNPTTtX1.js} +2 -2
- package/dist/client/assets/{edge-Cir90YUi.js → edge-DSlrkPAr.js} +2 -2
- package/dist/client/assets/{elixir-D8U_xdUO.js → elixir-DrLl_wv7.js} +2 -2
- package/dist/client/assets/{elm-DFyeiMH7.js → elm-CwB92FlT.js} +2 -2
- package/dist/client/assets/{erb-C6TvSrSi.js → erb-DXHrZ2ye.js} +2 -2
- package/dist/client/assets/{fileCommentAnnotations-BotL9n2H.js → fileCommentAnnotations-CuJ4hnrK.js} +2 -2
- package/dist/client/assets/{git-rebase-C7PDtezn.js → git-rebase-uRnCoWg0.js} +2 -2
- package/dist/client/assets/{glimmer-js-CXugsloP.js → glimmer-js-DU9fMoZx.js} +2 -2
- package/dist/client/assets/{glimmer-ts-IiHVp7il.js → glimmer-ts-Bftqha5Z.js} +2 -2
- package/dist/client/assets/{glsl-eBdXjOgx.js → glsl-CVVphTeN.js} +2 -2
- package/dist/client/assets/{graphql-CBwWowq8.js → graphql-CwED2DHh.js} +2 -2
- package/dist/client/assets/{hack-DoKoRNA-.js → hack-CwiQSrzl.js} +2 -2
- package/dist/client/assets/{haml-B2cA0ELm.js → haml-C2iiqAZE.js} +2 -2
- package/dist/client/assets/{handlebars-OQNujFnM.js → handlebars-BtvKqvl0.js} +2 -2
- package/dist/client/assets/{html-B9l55IY7.js → html-BRvZZmrv.js} +2 -2
- package/dist/client/assets/{html-derivative-C24d_8BY.js → html-derivative-rVjUDzCo.js} +2 -2
- package/dist/client/assets/{http-Ca2KxOLT.js → http-Cvb4plLT.js} +2 -2
- package/dist/client/assets/{hurl-BuLZo6wH.js → hurl-TX6OEub-.js} +2 -2
- package/dist/client/assets/index-CTDey6k4.css +1 -0
- package/dist/client/assets/{index-B-nUm8w7.js → index-DJPm8QPz.js} +203 -203
- package/dist/client/assets/{java-BEVFbCvA.js → java-CudutWch.js} +2 -2
- package/dist/client/assets/{javascript-AWDSrR1H.js → javascript-D7gB3c_x.js} +2 -2
- package/dist/client/assets/{jinja-CFK-VDc5.js → jinja-Ce0WqJfV.js} +2 -2
- package/dist/client/assets/{jison-CSmyyQ0V.js → jison-2U6TlYgq.js} +2 -2
- package/dist/client/assets/{json-D0qpEJUc.js → json-oe5h_2FV.js} +2 -2
- package/dist/client/assets/{jsx-smdyqdvA.js → jsx-B0f4Z6sx.js} +2 -2
- package/dist/client/assets/{julia-DWo_5QH9.js → julia-By6UDEAG.js} +2 -2
- package/dist/client/assets/{just-74GGYpCk.js → just-YTIO1wok.js} +2 -2
- package/dist/client/assets/{latex-d8vfKbSv.js → latex-B9HLUatB.js} +2 -2
- package/dist/client/assets/{liquid-CsWNZJPs.js → liquid-DajcsI9e.js} +2 -2
- package/dist/client/assets/{lua-x3z7TbKl.js → lua-CINm0eWo.js} +2 -2
- package/dist/client/assets/{marko-CJyhwQKw.js → marko-DXvL73SK.js} +2 -2
- package/dist/client/assets/{mdc-CtbrrOYL.js → mdc-CYsP-lJY.js} +2 -2
- package/dist/client/assets/{nginx-CY-uw8dq.js → nginx-CPKmhI0k.js} +2 -2
- package/dist/client/assets/{nim-DqT4w7rK.js → nim-DodS9Uy_.js} +2 -2
- package/dist/client/assets/{perl-CE3_DH1L.js → perl-COac5jCW.js} +2 -2
- package/dist/client/assets/{php-Zxsq1XMP.js → php-DLugOmp_.js} +2 -2
- package/dist/client/assets/{pug-DnCImvlN.js → pug-ukoxsRP-.js} +2 -2
- package/dist/client/assets/{qml-DLfoubjW.js → qml-DEoQyDDD.js} +2 -2
- package/dist/client/assets/{r-BtfLsgk8.js → r-CePyxHEB.js} +2 -2
- package/dist/client/assets/{razor-CzAGh9fe.js → razor-uhGlTCzi.js} +2 -2
- package/dist/client/assets/{regexp-Bw17HBbP.js → regexp-BYNm_E6v.js} +2 -2
- package/dist/client/assets/{rst-DhRr0i5h.js → rst-BMMyVK7q.js} +2 -2
- package/dist/client/assets/{ruby-BnQcHU-H.js → ruby-BBl_q7lV.js} +2 -2
- package/dist/client/assets/{sas-mMUu1htl.js → sas-q_pdbMhY.js} +2 -2
- package/dist/client/assets/{scss-0_lOQ1qp.js → scss-Bf5qLSyj.js} +2 -2
- package/dist/client/assets/{shellscript-BAdG8zJQ.js → shellscript-CnkO-FF6.js} +2 -2
- package/dist/client/assets/{shellsession-upfFHc_2.js → shellsession-CGTIDec0.js} +2 -2
- package/dist/client/assets/{soy-Dzncl5z6.js → soy-D_X4Ckow.js} +2 -2
- package/dist/client/assets/{sql-Ca2FgbAh.js → sql-wOawmdfM.js} +2 -2
- package/dist/client/assets/{stata-D8IymSlL.js → stata-CKQN4lyV.js} +2 -2
- package/dist/client/assets/{surrealql-CZsXvHAi.js → surrealql-tjpKGjMP.js} +2 -2
- package/dist/client/assets/{svelte-nM61Il-m.js → svelte-mCzdghlf.js} +2 -2
- package/dist/client/assets/{templ-DzjlBYVo.js → templ-COV4wNUr.js} +2 -2
- package/dist/client/assets/{tex-BPwIdn9G.js → tex-CC1f6JM7.js} +2 -2
- package/dist/client/assets/textarea-BDADSCjn.js +46 -0
- package/dist/client/assets/{ts-tags-BL9D6Ojm.js → ts-tags-CrIJ77ir.js} +2 -2
- package/dist/client/assets/{tsx-AKDVGVPx.js → tsx-IX8SGkp3.js} +2 -2
- package/dist/client/assets/{twig-B79f_Jz8.js → twig-DXR_a9UJ.js} +2 -2
- package/dist/client/assets/{typescript-BsyojNKj.js → typescript-BgLtzzgs.js} +2 -2
- package/dist/client/assets/{vue-BTqtDJT-.js → vue-Dzfv___3.js} +2 -2
- package/dist/client/assets/{vue-html-B_UfyILk.js → vue-html-BHyzHcFt.js} +2 -2
- package/dist/client/assets/{vue-vine-B8EK-Iln.js → vue-vine-DOSBq7Kw.js} +2 -2
- package/dist/client/assets/{xml-Do-3lCfL.js → xml-BI5bRQfx.js} +2 -2
- package/dist/client/assets/{xsl-CwgwYGXC.js → xsl-B4FzWFML.js} +2 -2
- package/dist/client/assets/{yaml-C4XlY-p5.js → yaml-CHgKneKM.js} +2 -2
- package/dist/client/index.html +3 -3
- package/package.json +1 -1
- package/dist/client/assets/arrow-right-B-_6vDIF.js +0 -2
- package/dist/client/assets/index-DfKhU-QO.css +0 -1
- 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.
|
|
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
|
|
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(
|
|
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.
|
|
14141
|
-
return HttpServerResponse.jsonUnsafe(
|
|
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
|
|
42420
|
-
yield*
|
|
42421
|
-
|
|
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
|
-
*
|
|
42843
|
-
* store
|
|
42844
|
-
*
|
|
42845
|
-
*
|
|
42846
|
-
*
|
|
42847
|
-
*
|
|
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
|
|
42851
|
-
const settings = yield* requireSettings(
|
|
42852
|
-
const response = yield* send(
|
|
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(
|
|
42864
|
-
const created = yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable(
|
|
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
|
-
...
|
|
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":
|
|
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
|
-
/**
|
|
80971
|
-
|
|
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* (
|
|
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
|
|
81055
|
-
yield*
|
|
81056
|
-
|
|
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
|
|
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();
|