@p4code/cli 0.1.34 → 0.1.36

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 CHANGED
@@ -236,7 +236,7 @@ const make$76 = () => {
236
236
  const layer$72 = Layer.sync(NetService, make$76);
237
237
  //#endregion
238
238
  //#region package.json
239
- var version = "0.1.34";
239
+ var version = "0.1.36";
240
240
  //#endregion
241
241
  //#region src/config.ts
242
242
  /**
@@ -5664,6 +5664,509 @@ var KeybindingsConfigError = class extends Schema$1.TaggedErrorClass()("Keybindi
5664
5664
  }
5665
5665
  };
5666
5666
  //#endregion
5667
+ //#region ../../packages/contracts/src/task.ts
5668
+ const TASK_TITLE_MAX_LENGTH = 512;
5669
+ const TASK_LABEL_MAX_LENGTH = 64;
5670
+ const TASK_LABELS_MAX_COUNT = 32;
5671
+ const TASK_ASSIGNEE_MAX_LENGTH = 128;
5672
+ /**
5673
+ * Board columns, in board order. `backlog` is the default for newly created
5674
+ * tasks — an agent filing a task mid-session is recording work that exists, not
5675
+ * committing to doing it next.
5676
+ *
5677
+ * `cancelled` is the other way a task closes: decided against, superseded or
5678
+ * merged into another task. Marking such a task `done` is the workaround it
5679
+ * replaces, and it lies - a board that says five things finished when two of
5680
+ * them were dropped is not worth reading.
5681
+ */
5682
+ const TaskStatus = Schema$1.Literals([
5683
+ "backlog",
5684
+ "todo",
5685
+ "in_progress",
5686
+ "in_review",
5687
+ "done",
5688
+ "cancelled"
5689
+ ]);
5690
+ /**
5691
+ * What a status this build has never heard of decodes to when read back.
5692
+ *
5693
+ * `backlog` is the least dishonest guess: it says "recorded, nothing claimed
5694
+ * about it", which is true of a status we cannot interpret. Guessing `done` or
5695
+ * `cancelled` would claim the task is closed.
5696
+ */
5697
+ const UNKNOWN_TASK_STATUS_FALLBACK = "backlog";
5698
+ const TaskPriority = Schema$1.Literals([
5699
+ "none",
5700
+ "low",
5701
+ "medium",
5702
+ "high",
5703
+ "urgent"
5704
+ ]);
5705
+ /**
5706
+ * Which board a call is about.
5707
+ *
5708
+ * `board` is p4code's own: this machine's rows, mirrored to the hub when one is
5709
+ * linked. It is one board rather than two because the local store is a replica
5710
+ * of the hub and not a rival copy of it, which is why there is no `hub` member
5711
+ * here — asking for "the hub board" and "the local board" separately would be
5712
+ * asking for the same rows twice.
5713
+ *
5714
+ * `linear` is the user's Linear workspace, read and written through the Linear
5715
+ * MCP server they already registered. p4code holds no Linear credential of its
5716
+ * own; see `mcp/TicketResolver` for why that is the only way in.
5717
+ *
5718
+ * A per-call parameter rather than a setting: a person switches boards to look
5719
+ * at something and switches back, and a server-wide mode would make that a
5720
+ * configuration change with a blast radius across every open client.
5721
+ */
5722
+ const TaskSource = Schema$1.Literals(["board", "linear"]);
5723
+ /**
5724
+ * The board a call means when it does not say. Every existing caller predates
5725
+ * the parameter and means p4code's own board, so the default has to be `board`
5726
+ * for them to keep working unchanged.
5727
+ */
5728
+ const DEFAULT_TASK_SOURCE = "board";
5729
+ const TaskSourceField = TaskSource.pipe(Schema$1.annotateEncoded({ description: "Which board to act on: 'board' for p4code's own (the default), 'linear' for the linked Linear workspace." }));
5730
+ /**
5731
+ * Mixed into the RPC payloads rather than into `TaskCreateInput` and friends.
5732
+ *
5733
+ * Those inputs are also the hub's wire format, and a source does not belong
5734
+ * there because the hub *is* the board. Client RPCs and the task toolkit mix
5735
+ * this selector into their own request schemas explicitly.
5736
+ */
5737
+ const TaskSourceSelector = Schema$1.Struct({ source: Schema$1.optional(TaskSourceField) });
5738
+ /**
5739
+ * A per-prefix sequence prefix, e.g. `T` or `P4`. Declared by a project in its
5740
+ * `p4.json` (`taskPrefix`); always stored and compared uppercase.
5741
+ */
5742
+ const TASK_READABLE_ID_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,9}$/;
5743
+ /** A validated task-id prefix such as `T`, `P4`, or `MOBILE`. */
5744
+ const TaskReadableIdPrefix = TrimmedNonEmptyString.check(Schema$1.isPattern(TASK_READABLE_ID_PREFIX_PATTERN));
5745
+ /**
5746
+ * The shape of a readable task id: prefix, dash, 1-based sequence number.
5747
+ * Deliberately disjoint from the UUID `taskId`, which is what lets a lookup
5748
+ * accept either and tell them apart by shape alone.
5749
+ */
5750
+ const TASK_READABLE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,9}-[1-9][0-9]*$/;
5751
+ /** Whether a task reference is a readable id (`P4-12`) rather than a UUID. */
5752
+ const isReadableTaskId = (reference) => TASK_READABLE_ID_PATTERN.test(reference);
5753
+ const TaskLabel = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_LABEL_MAX_LENGTH));
5754
+ /**
5755
+ * Agent-facing field descriptions. These ride on the *encoded* side because
5756
+ * every one of these schemas is a brand or a check over a transformation, and
5757
+ * the JSON Schema an agent actually reads is generated from the encoded schema
5758
+ * — a plain `.annotate` on the decoded side is silently dropped.
5759
+ */
5760
+ const ProjectIdField = ProjectId.pipe(Schema$1.annotateEncoded({ description: "Id of the project this task belongs to." }));
5761
+ const ThreadIdField = ThreadId.pipe(Schema$1.annotateEncoded({ description: "Id of the agent thread working this task." }));
5762
+ const TitleField = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_TITLE_MAX_LENGTH)).pipe(Schema$1.annotateEncoded({ description: "One-line summary of the work. Required." }));
5763
+ const BodyField = Schema$1.String.pipe(Schema$1.annotateEncoded({ description: "Markdown detail: context, acceptance criteria, links." }));
5764
+ const StatusField = TaskStatus.pipe(Schema$1.annotateEncoded({ description: "Board column: backlog (recorded, not queued), todo (queued next), in_progress, in_review, done, cancelled (closed without doing the work)." }));
5765
+ /**
5766
+ * The status as read back from a store, tolerant of values this build predates.
5767
+ *
5768
+ * A board can be shared between machines running different versions, and a
5769
+ * newer peer may write a status added after this client shipped. A strict
5770
+ * literal would fail the decode of the whole page, so one unknown row would
5771
+ * blank the board behind a "hub is unavailable" error rather than degrade. An
5772
+ * unrecognized status therefore decodes to {@link UNKNOWN_TASK_STATUS_FALLBACK},
5773
+ * same spirit as the nullable `readableId` and `orderKey` below. Encoding stays
5774
+ * strict: this client can only ever write a status it knows.
5775
+ */
5776
+ const StoredStatusField = TaskStatus.pipe(Schema$1.catchDecoding(() => Effect.succeedSome(UNKNOWN_TASK_STATUS_FALLBACK)));
5777
+ const PriorityField = TaskPriority.pipe(Schema$1.annotateEncoded({ description: "Relative urgency: none, low, medium, high or urgent." }));
5778
+ 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." }));
5779
+ /**
5780
+ * The portable half of "which repository is this task about".
5781
+ *
5782
+ * `projectId` cannot answer that across machines: it is a UUID minted when a
5783
+ * project is added, and the same checkout added on two servers gets two
5784
+ * different ids. A canonical repository key — the normalized primary git remote
5785
+ * — is the same string wherever the repo is cloned, so it survives the trip
5786
+ * through a shared board.
5787
+ *
5788
+ * Null for a task that is not about a repository, and also for a checkout with
5789
+ * no remote, where there is nothing stable to key on.
5790
+ */
5791
+ 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." }));
5792
+ /**
5793
+ * Manual board position within a column, as a lexicographic fractional key.
5794
+ *
5795
+ * Compared as a plain string: smaller sorts higher. Keys are minted by the
5796
+ * board between the neighbours of a drop, so moving one card writes one row
5797
+ * rather than renumbering the column. `null` means the task has never been
5798
+ * placed by hand and falls back to the priority/recency order.
5799
+ */
5800
+ const OrderKeyField = TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "Manual board position key. Lexicographic: smaller sorts higher in the column. Null means no manual position." }));
5801
+ /**
5802
+ * Which tracker issue a row *is*, for a row read out of a tracker.
5803
+ *
5804
+ * The identifier is the tracker's own, not ours: `MOBILE-12262` reads the same
5805
+ * in p4code as it does in Linear, in a commit message, and in conversation,
5806
+ * which is the whole point of carrying it. `url` is what the tracker itself
5807
+ * reported, never a guess assembled from the identifier - a Linear URL embeds
5808
+ * the workspace slug, so a constructed one would 404.
5809
+ *
5810
+ * Read-only, and only ever set by the `linear` source. p4code's own board does
5811
+ * not copy tracker issues into rows of its own: a ticket is read where it lives
5812
+ * and shown under the id its tracker gave it, which is why nothing here appears
5813
+ * in a create or update input.
5814
+ */
5815
+ const TaskExternalRef = Schema$1.Struct({
5816
+ source: Schema$1.Literals(["linear"]),
5817
+ identifier: TrimmedNonEmptyString,
5818
+ url: TrimmedNonEmptyString
5819
+ });
5820
+ /**
5821
+ * The task this one was split out of.
5822
+ *
5823
+ * Carried as a field rather than as a sentence in the body because the plan
5824
+ * phase writes it and the board reads it: a parent chip, a subtask list and
5825
+ * "does this still have unfinished children" are all questions about the graph,
5826
+ * and prose cannot be asked them. Stored as the parent's UUID, but accepted as
5827
+ * either a UUID or a readable id on the way in - a planning agent works in
5828
+ * `P4-12`, and making it look up a UUID first is a round trip for nothing.
5829
+ *
5830
+ * One level is all this promises. Nothing forbids a subtask of a subtask, and
5831
+ * nothing walks the chain either; the board renders the direct children of a
5832
+ * task and stops.
5833
+ */
5834
+ const ParentTaskIdField = TaskId.pipe(Schema$1.annotateEncoded({ description: "The task this one is a subtask of - the parent's UUID or readable id (e.g. P4-12). Null for a task that is not part of a larger one." }));
5835
+ 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." }));
5836
+ 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)." }));
5837
+ const Task = Schema$1.Struct({
5838
+ taskId: TaskId,
5839
+ /**
5840
+ * The human-readable id, e.g. `T-1` or `P4-12`: a per-prefix sequence
5841
+ * allocated by the store that owns the row, never reused, with gaps where
5842
+ * tasks were deleted. Nullable only for version skew - a row read from a
5843
+ * store that has not run the backfill migration decodes to `null` rather
5844
+ * than failing the whole board.
5845
+ */
5846
+ readableId: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
5847
+ /**
5848
+ * Manual board position: a lexicographic fractional key, compared as a plain
5849
+ * string, smaller first. Written only when a card is dragged to a position;
5850
+ * `null` means "never placed by hand" and the board falls back to its
5851
+ * priority/recency order. Nullable-with-default for the same version-skew
5852
+ * reason as `readableId`: a row from a store without the migration decodes to
5853
+ * `null` rather than failing the whole board.
5854
+ */
5855
+ orderKey: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
5856
+ /**
5857
+ * The project this task belongs to, or `null` for a task that is not scoped
5858
+ * to one. This references the existing workspace project registry rather than
5859
+ * a task-board-private notion of a project.
5860
+ */
5861
+ projectId: Schema$1.NullOr(ProjectId),
5862
+ /**
5863
+ * Canonical repository key, resolved server-side from `projectId` when the
5864
+ * task is written. This is the field that survives a move to another machine;
5865
+ * `projectId` above is a local pointer and is meaningless on a server that
5866
+ * did not mint it. A board reading a shared task matches on this and looks up
5867
+ * whatever local project shares the key.
5868
+ */
5869
+ repositoryKey: Schema$1.NullOr(TrimmedNonEmptyString),
5870
+ /**
5871
+ * The thread currently working this task, or `null`. A task links to at most
5872
+ * one thread at a time; re-running a task repoints this rather than appending
5873
+ * to a history.
5874
+ */
5875
+ threadId: Schema$1.NullOr(ThreadId),
5876
+ /**
5877
+ * The tracker issue this row is, for a row read from a tracker, and `null`
5878
+ * for every row on p4code's own board. Nullable-with-default because the
5879
+ * board's own store has no such column at all - a row read from SQLite
5880
+ * decodes to `null` rather than failing the whole board.
5881
+ */
5882
+ externalRef: Schema$1.NullOr(TaskExternalRef).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
5883
+ /**
5884
+ * The task this one was split out of, always as a UUID once stored - a
5885
+ * readable id given on the way in is resolved before the row is written, so
5886
+ * the link survives a board where readable ids were allocated elsewhere.
5887
+ * Nullable-with-default for the same version-skew reason as `readableId`.
5888
+ */
5889
+ parentTaskId: Schema$1.NullOr(TaskId).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
5890
+ title: TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_TITLE_MAX_LENGTH)),
5891
+ body: Schema$1.String,
5892
+ status: StoredStatusField,
5893
+ priority: TaskPriority,
5894
+ assignee: Schema$1.NullOr(TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSIGNEE_MAX_LENGTH))),
5895
+ labels: Schema$1.Array(TaskLabel).check(Schema$1.isMaxLength(TASK_LABELS_MAX_COUNT)),
5896
+ createdAt: IsoDateTime,
5897
+ updatedAt: IsoDateTime
5898
+ });
5899
+ const TaskCreateInput = Schema$1.Struct({
5900
+ projectId: Schema$1.optional(ProjectIdField),
5901
+ repositoryKey: Schema$1.optional(RepositoryKeyField),
5902
+ threadId: Schema$1.optional(ThreadIdField),
5903
+ parentTaskId: Schema$1.optional(ParentTaskIdField),
5904
+ title: TitleField,
5905
+ body: Schema$1.optional(BodyField),
5906
+ status: Schema$1.optional(StatusField),
5907
+ priority: Schema$1.optional(PriorityField),
5908
+ assignee: Schema$1.optional(AssigneeField),
5909
+ labels: Schema$1.optional(LabelsField)
5910
+ });
5911
+ /**
5912
+ * What an environment server sends the hub to file a task.
5913
+ *
5914
+ * Extends the public create input with the readable-id prefix, because the two
5915
+ * halves of the answer live on different machines: the prefix comes from the
5916
+ * project's checked-in `p4.json`, which only the submitting server can read,
5917
+ * while the sequence number must be allocated by the hub, which is the only
5918
+ * party that sees every machine's writes. Deliberately not part of
5919
+ * `TaskCreateInput` itself so an agent filing over MCP cannot pick a prefix -
5920
+ * the prefix belongs to the project, not to the caller.
5921
+ */
5922
+ const HubTaskCreateInput = Schema$1.Struct({
5923
+ ...TaskCreateInput.fields,
5924
+ readableIdPrefix: Schema$1.optional(TrimmedNonEmptyString),
5925
+ /**
5926
+ * Carried on the hub's create route (but not the public one) so a server
5927
+ * mirroring an already-placed row to the hub does not lose its position.
5928
+ * An agent filing a new task has no business picking a slot.
5929
+ */
5930
+ orderKey: Schema$1.optional(OrderKeyField)
5931
+ });
5932
+ /**
5933
+ * A whole row, written to the hub at an id the sending machine chose.
5934
+ *
5935
+ * Every field is present and nullable rather than optional, because this is a
5936
+ * replace and not a patch: a task whose assignee was cleared locally has to
5937
+ * arrive as an explicit `null`, and under an optional field it would arrive as
5938
+ * an absence indistinguishable from "unchanged" — so the clear would never
5939
+ * propagate and the two machines would disagree forever about who owns it.
5940
+ *
5941
+ * `readableId` and `updatedAt` are absent for the opposite reason: they are
5942
+ * the hub's to assign. The hub allocates the readable id the first time it
5943
+ * sees an id and preserves it afterwards, which is what lets a task created
5944
+ * on an offline machine get its `P4-12` on the first push and keep it.
5945
+ *
5946
+ * `createdAt` does come from the sender. It is a fact about when the person
5947
+ * filed the task, and the hub learning about it late does not make it newer.
5948
+ */
5949
+ const HubTaskPutInput = Schema$1.Struct({
5950
+ projectId: Schema$1.NullOr(ProjectIdField),
5951
+ repositoryKey: Schema$1.NullOr(RepositoryKeyField),
5952
+ threadId: Schema$1.NullOr(ThreadIdField),
5953
+ parentTaskId: Schema$1.NullOr(ParentTaskIdField),
5954
+ title: TitleField,
5955
+ body: BodyField,
5956
+ status: StatusField,
5957
+ priority: PriorityField,
5958
+ assignee: Schema$1.NullOr(AssigneeField),
5959
+ labels: LabelsField,
5960
+ orderKey: Schema$1.NullOr(OrderKeyField),
5961
+ createdAt: IsoDateTime,
5962
+ readableIdPrefix: Schema$1.optional(TrimmedNonEmptyString)
5963
+ });
5964
+ /**
5965
+ * Every field is optional and an omitted field is left untouched. The nullable
5966
+ * fields accept an explicit `null` to clear them, which is why they are
5967
+ * `NullOr` inside `optional` rather than merely optional.
5968
+ */
5969
+ const TaskUpdateInput = Schema$1.Struct({
5970
+ taskId: TaskIdField,
5971
+ projectId: Schema$1.optional(Schema$1.NullOr(ProjectIdField)),
5972
+ repositoryKey: Schema$1.optional(Schema$1.NullOr(RepositoryKeyField)),
5973
+ threadId: Schema$1.optional(Schema$1.NullOr(ThreadIdField)),
5974
+ parentTaskId: Schema$1.optional(Schema$1.NullOr(ParentTaskIdField)),
5975
+ title: Schema$1.optional(TitleField),
5976
+ body: Schema$1.optional(BodyField),
5977
+ status: Schema$1.optional(StatusField),
5978
+ priority: Schema$1.optional(PriorityField),
5979
+ assignee: Schema$1.optional(Schema$1.NullOr(AssigneeField)),
5980
+ labels: Schema$1.optional(LabelsField),
5981
+ orderKey: Schema$1.optional(Schema$1.NullOr(OrderKeyField))
5982
+ });
5983
+ /**
5984
+ * A task an agent has drafted but has not filed.
5985
+ *
5986
+ * Mirrors `TaskCreateInput` minus the fields the proposer has no business
5987
+ * setting. `status` is absent on purpose: a confirmed proposal always lands in
5988
+ * `backlog`, so an interviewing agent cannot queue its own work — the property
5989
+ * the board's pick-up relies on. `taskId`, `threadId` and `repositoryKey` are
5990
+ * absent because they are minted or derived when the proposal is accepted, not
5991
+ * offered by the model.
5992
+ */
5993
+ const TaskProposeInput = Schema$1.Struct({
5994
+ projectId: Schema$1.optional(ProjectIdField),
5995
+ title: TitleField,
5996
+ body: Schema$1.optional(BodyField),
5997
+ priority: Schema$1.optional(PriorityField),
5998
+ assignee: Schema$1.optional(AssigneeField),
5999
+ labels: Schema$1.optional(LabelsField)
6000
+ });
6001
+ /**
6002
+ * What `task_propose` returns. The tool writes nothing; its only effect is the
6003
+ * card the client renders from the call itself, so the result exists to tell
6004
+ * the model that nothing has been filed yet and it should stop and wait.
6005
+ */
6006
+ const TaskProposeResult = Schema$1.Struct({ awaitingConfirmation: Schema$1.Literal(true) });
6007
+ /**
6008
+ * Parameters for `task_current`. The thread defaults to the caller's own, which
6009
+ * is the point of the tool — an agent does not know its own thread id. It is
6010
+ * still a real parameter rather than an empty struct because an empty struct
6011
+ * generates a root `anyOf` JSON Schema that some providers reject outright.
6012
+ */
6013
+ 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." }))) });
6014
+ /** Parameters for reading or targeting a single task by id. */
6015
+ const TaskGetInput = Schema$1.Struct({ taskId: TaskIdField });
6016
+ const TaskListFilter = Schema$1.Struct({
6017
+ projectId: Schema$1.optional(ProjectIdField),
6018
+ repositoryKey: Schema$1.optional(RepositoryKeyField),
6019
+ threadId: Schema$1.optional(ThreadIdField),
6020
+ status: Schema$1.optional(StatusField),
6021
+ assignee: Schema$1.optional(AssigneeField)
6022
+ });
6023
+ Schema$1.Union([Schema$1.Struct({
6024
+ type: Schema$1.Literal("upserted"),
6025
+ task: Task
6026
+ }), Schema$1.Struct({
6027
+ type: Schema$1.Literal("deleted"),
6028
+ taskId: TaskId
6029
+ })]);
6030
+ /**
6031
+ * What a board subscription carries: the current board, then every change.
6032
+ *
6033
+ * The snapshot rides on the stream rather than being fetched separately
6034
+ * because a list call followed by a subscribe has a gap — anything committed
6035
+ * between the two is lost, and the board then shows stale rows until something
6036
+ * else happens to touch them. The server takes the snapshot *after* it has
6037
+ * subscribed, so the worst case is an event the subscriber already has, which
6038
+ * is idempotent, rather than one it never sees.
6039
+ */
6040
+ const TaskStreamEvent = Schema$1.Union([
6041
+ Schema$1.Struct({
6042
+ type: Schema$1.Literal("snapshot"),
6043
+ tasks: Schema$1.Array(Task)
6044
+ }),
6045
+ Schema$1.Struct({
6046
+ type: Schema$1.Literal("upserted"),
6047
+ task: Task
6048
+ }),
6049
+ Schema$1.Struct({
6050
+ type: Schema$1.Literal("deleted"),
6051
+ taskId: TaskId
6052
+ })
6053
+ ]);
6054
+ Schema$1.Struct({
6055
+ patch: TaskUpdateInput,
6056
+ /** The status the caller believes the task is in. */
6057
+ expectedStatus: TaskStatus
6058
+ });
6059
+ Schema$1.Union([
6060
+ Schema$1.Struct({
6061
+ outcome: Schema$1.Literal("updated"),
6062
+ task: Task
6063
+ }),
6064
+ Schema$1.Struct({ outcome: Schema$1.Literal("missing") }),
6065
+ Schema$1.Struct({
6066
+ outcome: Schema$1.Literal("conflict"),
6067
+ current: Task
6068
+ })
6069
+ ]);
6070
+ /**
6071
+ * Start an agent on a task.
6072
+ *
6073
+ * The input is only the task, deliberately. Everything else the thread needs —
6074
+ * which project, which model, what prompt — is resolved server-side from the
6075
+ * task's own project, because the caller that matters next is not a person
6076
+ * clicking a button but Phase 12's orchestrator, and a client that supplies the
6077
+ * model is a client that can start a task on a provider the user does not use.
6078
+ *
6079
+ * Worktrees are absent for a harder reason: `prepareWorktree` needs a base
6080
+ * branch, a task carries none, and nothing resolves one server-side. A thread
6081
+ * started from a task therefore runs in the project's own checkout.
6082
+ */
6083
+ const TaskStartThreadInput = Schema$1.Struct({ taskId: TaskIdField });
6084
+ /**
6085
+ * The started thread and the task as it now stands.
6086
+ *
6087
+ * Both are returned because both changed: the caller navigates to the thread,
6088
+ * and the board would otherwise show the old status until the change event
6089
+ * arrived.
6090
+ */
6091
+ const TaskStartThreadResult = Schema$1.Struct({
6092
+ task: Task,
6093
+ threadId: ThreadId
6094
+ });
6095
+ /**
6096
+ * A task cannot be started, and the reason is a property of the task or the
6097
+ * server rather than a failure.
6098
+ *
6099
+ * Each reason is a different thing for the user to fix, which is why this is a
6100
+ * union and not one message. `no-model` is the one worth explaining: a task
6101
+ * carries no model selection, so the thread runs on the project's default. The
6102
+ * server-side fallback that exists for first-run bootstrap is deliberately not
6103
+ * used here — it hardcodes a provider, so reaching for it would silently start
6104
+ * someone who works in Claude on Codex. Failing is the honest answer.
6105
+ */
6106
+ const TaskNotStartableReason = Schema$1.Literals([
6107
+ "no-project",
6108
+ "project-missing",
6109
+ "no-model"
6110
+ ]);
6111
+ var TaskNotStartableError = class extends Schema$1.TaggedErrorClass()("TaskNotStartableError", {
6112
+ taskId: TaskId,
6113
+ reason: TaskNotStartableReason
6114
+ }) {
6115
+ get message() {
6116
+ switch (this.reason) {
6117
+ case "no-project": return "This task has no project, so there is nowhere to run it. Set a project on the task first.";
6118
+ case "project-missing": return "This task's project is not registered on this server. Point it at a project that is.";
6119
+ case "no-model": return "This task's project has no default model. Choose one in the project's settings, then start the task again.";
6120
+ }
6121
+ }
6122
+ };
6123
+ /** Result shape for the board's list RPC. */
6124
+ const TaskListResult$1 = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
6125
+ /** Result shape for every RPC that resolves to a single task. */
6126
+ const TaskResult = Schema$1.Struct({ task: Task });
6127
+ /** `get` distinguishes "no such task" from an error, so the task is nullable. */
6128
+ const TaskGetResult = Schema$1.Struct({ task: Schema$1.NullOr(Task) });
6129
+ var TaskToolUnavailableError = class extends Schema$1.TaggedErrorClass()("TaskToolUnavailableError", {
6130
+ capability: Schema$1.Literal("tasks"),
6131
+ environmentId: EnvironmentId,
6132
+ threadId: ThreadId,
6133
+ providerSessionId: TrimmedNonEmptyString,
6134
+ providerInstanceId: ProviderInstanceId
6135
+ }) {
6136
+ get message() {
6137
+ return `MCP credential does not grant the ${this.capability} capability.`;
6138
+ }
6139
+ };
6140
+ var TaskNotFoundError = class extends Schema$1.TaggedErrorClass()("TaskNotFoundError", { taskId: TaskId }) {
6141
+ get message() {
6142
+ return `No task exists with id ${this.taskId}.`;
6143
+ }
6144
+ };
6145
+ /**
6146
+ * A task tool hit the store and the store failed. The underlying persistence
6147
+ * error is deliberately not surfaced to the agent — it carries SQL detail that
6148
+ * is noise in a tool result — but it is logged server-side.
6149
+ */
6150
+ var TaskStoreError = class extends Schema$1.TaggedErrorClass()("TaskStoreError", { operation: TrimmedNonEmptyString }) {
6151
+ get message() {
6152
+ return `The task store failed during ${this.operation}.`;
6153
+ }
6154
+ };
6155
+ /**
6156
+ * No task is linked to the calling agent's thread.
6157
+ */
6158
+ var TaskNotLinkedError = class extends Schema$1.TaggedErrorClass()("TaskNotLinkedError", { threadId: ThreadId }) {
6159
+ get message() {
6160
+ return `No task is linked to thread ${this.threadId}.`;
6161
+ }
6162
+ };
6163
+ const TaskToolError = Schema$1.Union([
6164
+ TaskToolUnavailableError,
6165
+ TaskNotFoundError,
6166
+ TaskNotLinkedError,
6167
+ TaskStoreError
6168
+ ]);
6169
+ //#endregion
5667
6170
  //#region ../../packages/contracts/src/settings.ts
5668
6171
  const TimestampFormat = Schema$1.Literals([
5669
6172
  "locale",
@@ -5978,6 +6481,12 @@ const ServerSettings = Schema$1.Struct({
5978
6481
  }))),
5979
6482
  sourceControlWritingStyle: SourceControlWritingStyleSettings.pipe(Schema$1.withDecodingDefault(Effect.succeed({}))),
5980
6483
  sourceControlWriterModelSelection: Schema$1.NullOr(ModelSelection).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
6484
+ /** Prefix used for tasks without a project or project-specific override. */
6485
+ defaultTaskPrefix: TaskReadableIdPrefix.pipe(Schema$1.withDecodingDefault(Effect.succeed("T"))),
6486
+ /** Linear issue identifier prefixes. Empty leaves Linear references unclaimed. */
6487
+ linearTaskPrefixes: Schema$1.Array(TaskReadableIdPrefix).pipe(Schema$1.withDecodingDefault(Effect.succeed([]))),
6488
+ /** Local task-id prefix overrides keyed by this environment's project id. */
6489
+ projectTaskPrefixes: Schema$1.Record(ProjectId, TaskReadableIdPrefix).pipe(Schema$1.withDecodingDefault(Effect.succeed({}))),
5981
6490
  /**
5982
6491
  * The Linear team new issues are filed into, by name or id.
5983
6492
  *
@@ -6113,6 +6622,9 @@ const ServerSettingsPatch = Schema$1.Struct({
6113
6622
  followChangeRequestTemplates: Schema$1.optionalKey(Schema$1.Boolean)
6114
6623
  })),
6115
6624
  sourceControlWriterModelSelection: Schema$1.optionalKey(Schema$1.NullOr(ModelSelection)),
6625
+ defaultTaskPrefix: Schema$1.optionalKey(TaskReadableIdPrefix),
6626
+ linearTaskPrefixes: Schema$1.optionalKey(Schema$1.Array(TaskReadableIdPrefix)),
6627
+ projectTaskPrefixes: Schema$1.optionalKey(Schema$1.Record(ProjectId, TaskReadableIdPrefix)),
6116
6628
  disabledSkills: Schema$1.optionalKey(Schema$1.Array(TrimmedNonEmptyString)),
6117
6629
  linearTeam: Schema$1.optionalKey(TrimmedString),
6118
6630
  observability: Schema$1.optionalKey(Schema$1.Struct({
@@ -7263,509 +7775,6 @@ const GitActionProgressEvent = Schema$1.Union([
7263
7775
  GitActionFailedEvent
7264
7776
  ]);
7265
7777
  //#endregion
7266
- //#region ../../packages/contracts/src/task.ts
7267
- const TASK_TITLE_MAX_LENGTH = 512;
7268
- const TASK_LABEL_MAX_LENGTH = 64;
7269
- const TASK_LABELS_MAX_COUNT = 32;
7270
- const TASK_ASSIGNEE_MAX_LENGTH = 128;
7271
- /**
7272
- * Board columns, in board order. `backlog` is the default for newly created
7273
- * tasks — an agent filing a task mid-session is recording work that exists, not
7274
- * committing to doing it next.
7275
- *
7276
- * `cancelled` is the other way a task closes: decided against, superseded or
7277
- * merged into another task. Marking such a task `done` is the workaround it
7278
- * replaces, and it lies - a board that says five things finished when two of
7279
- * them were dropped is not worth reading.
7280
- */
7281
- const TaskStatus = Schema$1.Literals([
7282
- "backlog",
7283
- "todo",
7284
- "in_progress",
7285
- "in_review",
7286
- "done",
7287
- "cancelled"
7288
- ]);
7289
- /**
7290
- * What a status this build has never heard of decodes to when read back.
7291
- *
7292
- * `backlog` is the least dishonest guess: it says "recorded, nothing claimed
7293
- * about it", which is true of a status we cannot interpret. Guessing `done` or
7294
- * `cancelled` would claim the task is closed.
7295
- */
7296
- const UNKNOWN_TASK_STATUS_FALLBACK = "backlog";
7297
- const TaskPriority = Schema$1.Literals([
7298
- "none",
7299
- "low",
7300
- "medium",
7301
- "high",
7302
- "urgent"
7303
- ]);
7304
- /**
7305
- * Which board a call is about.
7306
- *
7307
- * `board` is p4code's own: this machine's rows, mirrored to the hub when one is
7308
- * linked. It is one board rather than two because the local store is a replica
7309
- * of the hub and not a rival copy of it, which is why there is no `hub` member
7310
- * here — asking for "the hub board" and "the local board" separately would be
7311
- * asking for the same rows twice.
7312
- *
7313
- * `linear` is the user's Linear workspace, read and written through the Linear
7314
- * MCP server they already registered. p4code holds no Linear credential of its
7315
- * own; see `mcp/TicketResolver` for why that is the only way in.
7316
- *
7317
- * A per-call parameter rather than a setting: a person switches boards to look
7318
- * at something and switches back, and a server-wide mode would make that a
7319
- * configuration change with a blast radius across every open client.
7320
- */
7321
- const TaskSource = Schema$1.Literals(["board", "linear"]);
7322
- /**
7323
- * The board a call means when it does not say. Every existing caller predates
7324
- * the parameter and means p4code's own board, so the default has to be `board`
7325
- * for them to keep working unchanged.
7326
- */
7327
- const DEFAULT_TASK_SOURCE = "board";
7328
- const TaskSourceField = TaskSource.pipe(Schema$1.annotateEncoded({ description: "Which board to act on: 'board' for p4code's own (the default), 'linear' for the linked Linear workspace." }));
7329
- /**
7330
- * Mixed into the RPC payloads rather than into `TaskCreateInput` and friends.
7331
- *
7332
- * Those inputs are also the hub's wire format and the MCP tools' argument
7333
- * schemas, and a source belongs to neither: the hub *is* the board, so a
7334
- * `source` field on its create route would be a field it must reject, and an
7335
- * agent picking a tracker per call is a capability to add deliberately rather
7336
- * than one to leak in by inheritance.
7337
- */
7338
- const TaskSourceSelector = Schema$1.Struct({ source: Schema$1.optional(TaskSourceField) });
7339
- /**
7340
- * A per-prefix sequence prefix, e.g. `T` or `P4`. Declared by a project in its
7341
- * `p4.json` (`taskPrefix`); always stored and compared uppercase.
7342
- */
7343
- const TASK_READABLE_ID_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,9}$/;
7344
- /**
7345
- * The shape of a readable task id: prefix, dash, 1-based sequence number.
7346
- * Deliberately disjoint from the UUID `taskId`, which is what lets a lookup
7347
- * accept either and tell them apart by shape alone.
7348
- */
7349
- const TASK_READABLE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,9}-[1-9][0-9]*$/;
7350
- /** Whether a task reference is a readable id (`P4-12`) rather than a UUID. */
7351
- const isReadableTaskId = (reference) => TASK_READABLE_ID_PATTERN.test(reference);
7352
- const TaskLabel = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_LABEL_MAX_LENGTH));
7353
- /**
7354
- * Agent-facing field descriptions. These ride on the *encoded* side because
7355
- * every one of these schemas is a brand or a check over a transformation, and
7356
- * the JSON Schema an agent actually reads is generated from the encoded schema
7357
- * — a plain `.annotate` on the decoded side is silently dropped.
7358
- */
7359
- const ProjectIdField = ProjectId.pipe(Schema$1.annotateEncoded({ description: "Id of the project this task belongs to." }));
7360
- const ThreadIdField = ThreadId.pipe(Schema$1.annotateEncoded({ description: "Id of the agent thread working this task." }));
7361
- const TitleField = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_TITLE_MAX_LENGTH)).pipe(Schema$1.annotateEncoded({ description: "One-line summary of the work. Required." }));
7362
- const BodyField = Schema$1.String.pipe(Schema$1.annotateEncoded({ description: "Markdown detail: context, acceptance criteria, links." }));
7363
- const StatusField = TaskStatus.pipe(Schema$1.annotateEncoded({ description: "Board column: backlog (recorded, not queued), todo (queued next), in_progress, in_review, done, cancelled (closed without doing the work)." }));
7364
- /**
7365
- * The status as read back from a store, tolerant of values this build predates.
7366
- *
7367
- * A board can be shared between machines running different versions, and a
7368
- * newer peer may write a status added after this client shipped. A strict
7369
- * literal would fail the decode of the whole page, so one unknown row would
7370
- * blank the board behind a "hub is unavailable" error rather than degrade. An
7371
- * unrecognized status therefore decodes to {@link UNKNOWN_TASK_STATUS_FALLBACK},
7372
- * same spirit as the nullable `readableId` and `orderKey` below. Encoding stays
7373
- * strict: this client can only ever write a status it knows.
7374
- */
7375
- const StoredStatusField = TaskStatus.pipe(Schema$1.catchDecoding(() => Effect.succeedSome(UNKNOWN_TASK_STATUS_FALLBACK)));
7376
- const PriorityField = TaskPriority.pipe(Schema$1.annotateEncoded({ description: "Relative urgency: none, low, medium, high or urgent." }));
7377
- 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." }));
7378
- /**
7379
- * The portable half of "which repository is this task about".
7380
- *
7381
- * `projectId` cannot answer that across machines: it is a UUID minted when a
7382
- * project is added, and the same checkout added on two servers gets two
7383
- * different ids. A canonical repository key — the normalized primary git remote
7384
- * — is the same string wherever the repo is cloned, so it survives the trip
7385
- * through a shared board.
7386
- *
7387
- * Null for a task that is not about a repository, and also for a checkout with
7388
- * no remote, where there is nothing stable to key on.
7389
- */
7390
- 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." }));
7391
- /**
7392
- * Manual board position within a column, as a lexicographic fractional key.
7393
- *
7394
- * Compared as a plain string: smaller sorts higher. Keys are minted by the
7395
- * board between the neighbours of a drop, so moving one card writes one row
7396
- * rather than renumbering the column. `null` means the task has never been
7397
- * placed by hand and falls back to the priority/recency order.
7398
- */
7399
- const OrderKeyField = TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "Manual board position key. Lexicographic: smaller sorts higher in the column. Null means no manual position." }));
7400
- /**
7401
- * Which tracker issue a row *is*, for a row read out of a tracker.
7402
- *
7403
- * The identifier is the tracker's own, not ours: `MOBILE-12262` reads the same
7404
- * in p4code as it does in Linear, in a commit message, and in conversation,
7405
- * which is the whole point of carrying it. `url` is what the tracker itself
7406
- * reported, never a guess assembled from the identifier - a Linear URL embeds
7407
- * the workspace slug, so a constructed one would 404.
7408
- *
7409
- * Read-only, and only ever set by the `linear` source. p4code's own board does
7410
- * not copy tracker issues into rows of its own: a ticket is read where it lives
7411
- * and shown under the id its tracker gave it, which is why nothing here appears
7412
- * in a create or update input.
7413
- */
7414
- const TaskExternalRef = Schema$1.Struct({
7415
- source: Schema$1.Literals(["linear"]),
7416
- identifier: TrimmedNonEmptyString,
7417
- url: TrimmedNonEmptyString
7418
- });
7419
- /**
7420
- * The task this one was split out of.
7421
- *
7422
- * Carried as a field rather than as a sentence in the body because the plan
7423
- * phase writes it and the board reads it: a parent chip, a subtask list and
7424
- * "does this still have unfinished children" are all questions about the graph,
7425
- * and prose cannot be asked them. Stored as the parent's UUID, but accepted as
7426
- * either a UUID or a readable id on the way in - a planning agent works in
7427
- * `P4-12`, and making it look up a UUID first is a round trip for nothing.
7428
- *
7429
- * One level is all this promises. Nothing forbids a subtask of a subtask, and
7430
- * nothing walks the chain either; the board renders the direct children of a
7431
- * task and stops.
7432
- */
7433
- const ParentTaskIdField = TaskId.pipe(Schema$1.annotateEncoded({ description: "The task this one is a subtask of - the parent's UUID or readable id (e.g. P4-12). Null for a task that is not part of a larger one." }));
7434
- 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." }));
7435
- 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)." }));
7436
- const Task = Schema$1.Struct({
7437
- taskId: TaskId,
7438
- /**
7439
- * The human-readable id, e.g. `T-1` or `P4-12`: a per-prefix sequence
7440
- * allocated by the store that owns the row, never reused, with gaps where
7441
- * tasks were deleted. Nullable only for version skew - a row read from a
7442
- * store that has not run the backfill migration decodes to `null` rather
7443
- * than failing the whole board.
7444
- */
7445
- readableId: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7446
- /**
7447
- * Manual board position: a lexicographic fractional key, compared as a plain
7448
- * string, smaller first. Written only when a card is dragged to a position;
7449
- * `null` means "never placed by hand" and the board falls back to its
7450
- * priority/recency order. Nullable-with-default for the same version-skew
7451
- * reason as `readableId`: a row from a store without the migration decodes to
7452
- * `null` rather than failing the whole board.
7453
- */
7454
- orderKey: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7455
- /**
7456
- * The project this task belongs to, or `null` for a task that is not scoped
7457
- * to one. This references the existing workspace project registry rather than
7458
- * a task-board-private notion of a project.
7459
- */
7460
- projectId: Schema$1.NullOr(ProjectId),
7461
- /**
7462
- * Canonical repository key, resolved server-side from `projectId` when the
7463
- * task is written. This is the field that survives a move to another machine;
7464
- * `projectId` above is a local pointer and is meaningless on a server that
7465
- * did not mint it. A board reading a shared task matches on this and looks up
7466
- * whatever local project shares the key.
7467
- */
7468
- repositoryKey: Schema$1.NullOr(TrimmedNonEmptyString),
7469
- /**
7470
- * The thread currently working this task, or `null`. A task links to at most
7471
- * one thread at a time; re-running a task repoints this rather than appending
7472
- * to a history.
7473
- */
7474
- threadId: Schema$1.NullOr(ThreadId),
7475
- /**
7476
- * The tracker issue this row is, for a row read from a tracker, and `null`
7477
- * for every row on p4code's own board. Nullable-with-default because the
7478
- * board's own store has no such column at all - a row read from SQLite
7479
- * decodes to `null` rather than failing the whole board.
7480
- */
7481
- externalRef: Schema$1.NullOr(TaskExternalRef).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7482
- /**
7483
- * The task this one was split out of, always as a UUID once stored - a
7484
- * readable id given on the way in is resolved before the row is written, so
7485
- * the link survives a board where readable ids were allocated elsewhere.
7486
- * Nullable-with-default for the same version-skew reason as `readableId`.
7487
- */
7488
- parentTaskId: Schema$1.NullOr(TaskId).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7489
- title: TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_TITLE_MAX_LENGTH)),
7490
- body: Schema$1.String,
7491
- status: StoredStatusField,
7492
- priority: TaskPriority,
7493
- assignee: Schema$1.NullOr(TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSIGNEE_MAX_LENGTH))),
7494
- labels: Schema$1.Array(TaskLabel).check(Schema$1.isMaxLength(TASK_LABELS_MAX_COUNT)),
7495
- createdAt: IsoDateTime,
7496
- updatedAt: IsoDateTime
7497
- });
7498
- const TaskCreateInput = Schema$1.Struct({
7499
- projectId: Schema$1.optional(ProjectIdField),
7500
- repositoryKey: Schema$1.optional(RepositoryKeyField),
7501
- threadId: Schema$1.optional(ThreadIdField),
7502
- parentTaskId: Schema$1.optional(ParentTaskIdField),
7503
- title: TitleField,
7504
- body: Schema$1.optional(BodyField),
7505
- status: Schema$1.optional(StatusField),
7506
- priority: Schema$1.optional(PriorityField),
7507
- assignee: Schema$1.optional(AssigneeField),
7508
- labels: Schema$1.optional(LabelsField)
7509
- });
7510
- /**
7511
- * What an environment server sends the hub to file a task.
7512
- *
7513
- * Extends the public create input with the readable-id prefix, because the two
7514
- * halves of the answer live on different machines: the prefix comes from the
7515
- * project's checked-in `p4.json`, which only the submitting server can read,
7516
- * while the sequence number must be allocated by the hub, which is the only
7517
- * party that sees every machine's writes. Deliberately not part of
7518
- * `TaskCreateInput` itself so an agent filing over MCP cannot pick a prefix -
7519
- * the prefix belongs to the project, not to the caller.
7520
- */
7521
- const HubTaskCreateInput = Schema$1.Struct({
7522
- ...TaskCreateInput.fields,
7523
- readableIdPrefix: Schema$1.optional(TrimmedNonEmptyString),
7524
- /**
7525
- * Carried on the hub's create route (but not the public one) so a server
7526
- * mirroring an already-placed row to the hub does not lose its position.
7527
- * An agent filing a new task has no business picking a slot.
7528
- */
7529
- orderKey: Schema$1.optional(OrderKeyField)
7530
- });
7531
- /**
7532
- * A whole row, written to the hub at an id the sending machine chose.
7533
- *
7534
- * Every field is present and nullable rather than optional, because this is a
7535
- * replace and not a patch: a task whose assignee was cleared locally has to
7536
- * arrive as an explicit `null`, and under an optional field it would arrive as
7537
- * an absence indistinguishable from "unchanged" — so the clear would never
7538
- * propagate and the two machines would disagree forever about who owns it.
7539
- *
7540
- * `readableId` and `updatedAt` are absent for the opposite reason: they are
7541
- * the hub's to assign. The hub allocates the readable id the first time it
7542
- * sees an id and preserves it afterwards, which is what lets a task created
7543
- * on an offline machine get its `P4-12` on the first push and keep it.
7544
- *
7545
- * `createdAt` does come from the sender. It is a fact about when the person
7546
- * filed the task, and the hub learning about it late does not make it newer.
7547
- */
7548
- const HubTaskPutInput = Schema$1.Struct({
7549
- projectId: Schema$1.NullOr(ProjectIdField),
7550
- repositoryKey: Schema$1.NullOr(RepositoryKeyField),
7551
- threadId: Schema$1.NullOr(ThreadIdField),
7552
- parentTaskId: Schema$1.NullOr(ParentTaskIdField),
7553
- title: TitleField,
7554
- body: BodyField,
7555
- status: StatusField,
7556
- priority: PriorityField,
7557
- assignee: Schema$1.NullOr(AssigneeField),
7558
- labels: LabelsField,
7559
- orderKey: Schema$1.NullOr(OrderKeyField),
7560
- createdAt: IsoDateTime,
7561
- readableIdPrefix: Schema$1.optional(TrimmedNonEmptyString)
7562
- });
7563
- /**
7564
- * Every field is optional and an omitted field is left untouched. The nullable
7565
- * fields accept an explicit `null` to clear them, which is why they are
7566
- * `NullOr` inside `optional` rather than merely optional.
7567
- */
7568
- const TaskUpdateInput = Schema$1.Struct({
7569
- taskId: TaskIdField,
7570
- projectId: Schema$1.optional(Schema$1.NullOr(ProjectIdField)),
7571
- repositoryKey: Schema$1.optional(Schema$1.NullOr(RepositoryKeyField)),
7572
- threadId: Schema$1.optional(Schema$1.NullOr(ThreadIdField)),
7573
- parentTaskId: Schema$1.optional(Schema$1.NullOr(ParentTaskIdField)),
7574
- title: Schema$1.optional(TitleField),
7575
- body: Schema$1.optional(BodyField),
7576
- status: Schema$1.optional(StatusField),
7577
- priority: Schema$1.optional(PriorityField),
7578
- assignee: Schema$1.optional(Schema$1.NullOr(AssigneeField)),
7579
- labels: Schema$1.optional(LabelsField),
7580
- orderKey: Schema$1.optional(Schema$1.NullOr(OrderKeyField))
7581
- });
7582
- /**
7583
- * A task an agent has drafted but has not filed.
7584
- *
7585
- * Mirrors `TaskCreateInput` minus the fields the proposer has no business
7586
- * setting. `status` is absent on purpose: a confirmed proposal always lands in
7587
- * `backlog`, so an interviewing agent cannot queue its own work — the property
7588
- * the board's pick-up relies on. `taskId`, `threadId` and `repositoryKey` are
7589
- * absent because they are minted or derived when the proposal is accepted, not
7590
- * offered by the model.
7591
- */
7592
- const TaskProposeInput = Schema$1.Struct({
7593
- projectId: Schema$1.optional(ProjectIdField),
7594
- title: TitleField,
7595
- body: Schema$1.optional(BodyField),
7596
- priority: Schema$1.optional(PriorityField),
7597
- assignee: Schema$1.optional(AssigneeField),
7598
- labels: Schema$1.optional(LabelsField)
7599
- });
7600
- /**
7601
- * What `task_propose` returns. The tool writes nothing; its only effect is the
7602
- * card the client renders from the call itself, so the result exists to tell
7603
- * the model that nothing has been filed yet and it should stop and wait.
7604
- */
7605
- const TaskProposeResult = Schema$1.Struct({ awaitingConfirmation: Schema$1.Literal(true) });
7606
- /**
7607
- * Parameters for `task_current`. The thread defaults to the caller's own, which
7608
- * is the point of the tool — an agent does not know its own thread id. It is
7609
- * still a real parameter rather than an empty struct because an empty struct
7610
- * generates a root `anyOf` JSON Schema that some providers reject outright.
7611
- */
7612
- 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." }))) });
7613
- /** Parameters for reading or targeting a single task by id. */
7614
- const TaskGetInput = Schema$1.Struct({ taskId: TaskIdField });
7615
- const TaskListFilter = Schema$1.Struct({
7616
- projectId: Schema$1.optional(ProjectIdField),
7617
- repositoryKey: Schema$1.optional(RepositoryKeyField),
7618
- threadId: Schema$1.optional(ThreadIdField),
7619
- status: Schema$1.optional(StatusField),
7620
- assignee: Schema$1.optional(AssigneeField)
7621
- });
7622
- Schema$1.Union([Schema$1.Struct({
7623
- type: Schema$1.Literal("upserted"),
7624
- task: Task
7625
- }), Schema$1.Struct({
7626
- type: Schema$1.Literal("deleted"),
7627
- taskId: TaskId
7628
- })]);
7629
- /**
7630
- * What a board subscription carries: the current board, then every change.
7631
- *
7632
- * The snapshot rides on the stream rather than being fetched separately
7633
- * because a list call followed by a subscribe has a gap — anything committed
7634
- * between the two is lost, and the board then shows stale rows until something
7635
- * else happens to touch them. The server takes the snapshot *after* it has
7636
- * subscribed, so the worst case is an event the subscriber already has, which
7637
- * is idempotent, rather than one it never sees.
7638
- */
7639
- const TaskStreamEvent = Schema$1.Union([
7640
- Schema$1.Struct({
7641
- type: Schema$1.Literal("snapshot"),
7642
- tasks: Schema$1.Array(Task)
7643
- }),
7644
- Schema$1.Struct({
7645
- type: Schema$1.Literal("upserted"),
7646
- task: Task
7647
- }),
7648
- Schema$1.Struct({
7649
- type: Schema$1.Literal("deleted"),
7650
- taskId: TaskId
7651
- })
7652
- ]);
7653
- Schema$1.Struct({
7654
- patch: TaskUpdateInput,
7655
- /** The status the caller believes the task is in. */
7656
- expectedStatus: TaskStatus
7657
- });
7658
- Schema$1.Union([
7659
- Schema$1.Struct({
7660
- outcome: Schema$1.Literal("updated"),
7661
- task: Task
7662
- }),
7663
- Schema$1.Struct({ outcome: Schema$1.Literal("missing") }),
7664
- Schema$1.Struct({
7665
- outcome: Schema$1.Literal("conflict"),
7666
- current: Task
7667
- })
7668
- ]);
7669
- /**
7670
- * Start an agent on a task.
7671
- *
7672
- * The input is only the task, deliberately. Everything else the thread needs —
7673
- * which project, which model, what prompt — is resolved server-side from the
7674
- * task's own project, because the caller that matters next is not a person
7675
- * clicking a button but Phase 12's orchestrator, and a client that supplies the
7676
- * model is a client that can start a task on a provider the user does not use.
7677
- *
7678
- * Worktrees are absent for a harder reason: `prepareWorktree` needs a base
7679
- * branch, a task carries none, and nothing resolves one server-side. A thread
7680
- * started from a task therefore runs in the project's own checkout.
7681
- */
7682
- const TaskStartThreadInput = Schema$1.Struct({ taskId: TaskIdField });
7683
- /**
7684
- * The started thread and the task as it now stands.
7685
- *
7686
- * Both are returned because both changed: the caller navigates to the thread,
7687
- * and the board would otherwise show the old status until the change event
7688
- * arrived.
7689
- */
7690
- const TaskStartThreadResult = Schema$1.Struct({
7691
- task: Task,
7692
- threadId: ThreadId
7693
- });
7694
- /**
7695
- * A task cannot be started, and the reason is a property of the task or the
7696
- * server rather than a failure.
7697
- *
7698
- * Each reason is a different thing for the user to fix, which is why this is a
7699
- * union and not one message. `no-model` is the one worth explaining: a task
7700
- * carries no model selection, so the thread runs on the project's default. The
7701
- * server-side fallback that exists for first-run bootstrap is deliberately not
7702
- * used here — it hardcodes a provider, so reaching for it would silently start
7703
- * someone who works in Claude on Codex. Failing is the honest answer.
7704
- */
7705
- const TaskNotStartableReason = Schema$1.Literals([
7706
- "no-project",
7707
- "project-missing",
7708
- "no-model"
7709
- ]);
7710
- var TaskNotStartableError = class extends Schema$1.TaggedErrorClass()("TaskNotStartableError", {
7711
- taskId: TaskId,
7712
- reason: TaskNotStartableReason
7713
- }) {
7714
- get message() {
7715
- switch (this.reason) {
7716
- case "no-project": return "This task has no project, so there is nowhere to run it. Set a project on the task first.";
7717
- case "project-missing": return "This task's project is not registered on this server. Point it at a project that is.";
7718
- case "no-model": return "This task's project has no default model. Choose one in the project's settings, then start the task again.";
7719
- }
7720
- }
7721
- };
7722
- /** Result shape for the board's list RPC. */
7723
- const TaskListResult$1 = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
7724
- /** Result shape for every RPC that resolves to a single task. */
7725
- const TaskResult = Schema$1.Struct({ task: Task });
7726
- /** `get` distinguishes "no such task" from an error, so the task is nullable. */
7727
- const TaskGetResult = Schema$1.Struct({ task: Schema$1.NullOr(Task) });
7728
- var TaskToolUnavailableError = class extends Schema$1.TaggedErrorClass()("TaskToolUnavailableError", {
7729
- capability: Schema$1.Literal("tasks"),
7730
- environmentId: EnvironmentId,
7731
- threadId: ThreadId,
7732
- providerSessionId: TrimmedNonEmptyString,
7733
- providerInstanceId: ProviderInstanceId
7734
- }) {
7735
- get message() {
7736
- return `MCP credential does not grant the ${this.capability} capability.`;
7737
- }
7738
- };
7739
- var TaskNotFoundError = class extends Schema$1.TaggedErrorClass()("TaskNotFoundError", { taskId: TaskId }) {
7740
- get message() {
7741
- return `No task exists with id ${this.taskId}.`;
7742
- }
7743
- };
7744
- /**
7745
- * A task tool hit the store and the store failed. The underlying persistence
7746
- * error is deliberately not surfaced to the agent — it carries SQL detail that
7747
- * is noise in a tool result — but it is logged server-side.
7748
- */
7749
- var TaskStoreError = class extends Schema$1.TaggedErrorClass()("TaskStoreError", { operation: TrimmedNonEmptyString }) {
7750
- get message() {
7751
- return `The task store failed during ${this.operation}.`;
7752
- }
7753
- };
7754
- /**
7755
- * No task is linked to the calling agent's thread.
7756
- */
7757
- var TaskNotLinkedError = class extends Schema$1.TaggedErrorClass()("TaskNotLinkedError", { threadId: ThreadId }) {
7758
- get message() {
7759
- return `No task is linked to thread ${this.threadId}.`;
7760
- }
7761
- };
7762
- const TaskToolError = Schema$1.Union([
7763
- TaskToolUnavailableError,
7764
- TaskNotFoundError,
7765
- TaskNotLinkedError,
7766
- TaskStoreError
7767
- ]);
7768
- //#endregion
7769
7778
  //#region ../../packages/contracts/src/p4ProjectFile.ts
7770
7779
  /** File name of the checked-in P4 project file, resolved at the workspace root. */
7771
7780
  const P4_PROJECT_FILE_NAME = "p4.json";
@@ -7790,7 +7799,7 @@ const P4ProjectFile = Schema$1.Struct({
7790
7799
  $schema: Schema$1.optionalKey(Schema$1.String.annotate({ description: `URL of the JSON Schema for this file, typically "${P4_PROJECT_FILE_SCHEMA_URL}".` })),
7791
7800
  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)),
7792
7801
  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))),
7793
- 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)))
7802
+ taskPrefix: Schema$1.optionalKey(trimmedNonEmpty({ description: "Checked-in 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. A project-specific Settings override takes precedence." }).check(Schema$1.isPattern(TASK_READABLE_ID_PREFIX_PATTERN)))
7794
7803
  }).annotate({
7795
7804
  title: "P4 project file",
7796
7805
  description: "Checked-in project configuration for P4Code (p4.json at the repository root). See https://p4.codes for documentation."
@@ -37552,22 +37561,36 @@ const resolveTaskRepositoryKey = Effect.fn("taskBoard.resolveRepositoryKey")(fun
37552
37561
  if (Option.isNone(project)) return null;
37553
37562
  return (yield* (yield* RepositoryIdentityResolver).resolve(project.value.workspaceRoot))?.canonicalKey ?? null;
37554
37563
  });
37564
+ /** Resolve configured prefix precedence without filesystem or store effects. */
37565
+ function selectTaskReadableIdPrefix(input) {
37566
+ return (input.projectOverride ?? input.projectFilePrefix ?? input.defaultPrefix).toUpperCase();
37567
+ }
37555
37568
  /**
37556
37569
  * Resolve the readable-id prefix for a task's project.
37557
37570
  *
37558
- * The prefix lives in the project's checked-in `p4.json` (`taskPrefix`), not in
37559
- * this server's database: it is a property of the repository, agreed on by
37560
- * everyone who clones it, exactly like the scripts beside it. A task with no
37561
- * project, a removed project, or a `p4.json` without the field all fall back to
37562
- * the default `T` - filing the task always beats refusing it over a label.
37571
+ * A server setting may override each project. Without one, the checked-in
37572
+ * `p4.json` value remains portable across clones. The configured default is
37573
+ * used when neither exists, including tasks with no project.
37563
37574
  */
37564
37575
  const resolveTaskReadableIdPrefix = Effect.fn("taskBoard.resolveReadableIdPrefix")(function* (projectId) {
37565
- if (projectId == null) return "T";
37576
+ const current = yield* (yield* ServerSettingsService).getSettings.pipe(Effect.orElseSucceed(() => ({
37577
+ defaultTaskPrefix: "T",
37578
+ projectTaskPrefixes: {}
37579
+ })));
37580
+ if (projectId == null) return selectTaskReadableIdPrefix({ defaultPrefix: current.defaultTaskPrefix });
37581
+ const override = current.projectTaskPrefixes[projectId];
37582
+ if (override !== void 0) return selectTaskReadableIdPrefix({
37583
+ defaultPrefix: current.defaultTaskPrefix,
37584
+ projectOverride: override
37585
+ });
37566
37586
  const project = yield* (yield* ProjectionProjectRepository).getById({ projectId }).pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
37567
- if (Option.isNone(project)) return "T";
37587
+ if (Option.isNone(project)) return selectTaskReadableIdPrefix({ defaultPrefix: current.defaultTaskPrefix });
37568
37588
  const file = yield* (yield* P4ProjectFileLoader).load(project.value.workspaceRoot);
37569
- if (Option.isNone(file) || file.value.taskPrefix === void 0) return "T";
37570
- return file.value.taskPrefix.toUpperCase();
37589
+ if (Option.isNone(file) || file.value.taskPrefix === void 0) return selectTaskReadableIdPrefix({ defaultPrefix: current.defaultTaskPrefix });
37590
+ return selectTaskReadableIdPrefix({
37591
+ defaultPrefix: current.defaultTaskPrefix,
37592
+ projectFilePrefix: file.value.taskPrefix
37593
+ });
37571
37594
  });
37572
37595
  /** Project settings + live snapshots into the two lookups a start needs. */
37573
37596
  const buildTaskStartProviderDefaults = (input) => ({
@@ -88308,14 +88331,22 @@ const writeDependencies = [
88308
88331
  RepositoryIdentityResolver
88309
88332
  ];
88310
88333
  /**
88311
- * `task_create` also mints the task id (crypto) and resolves the readable-id
88312
- * prefix from the project's checked-in `p4.json` (the file loader).
88334
+ * `task_create` also mints the task id, routes to the requested board, and
88335
+ * resolves the readable-id prefix from settings or `p4.json`.
88313
88336
  */
88314
88337
  const createDependencies = [
88315
88338
  ...writeDependencies,
88316
88339
  Crypto.Crypto,
88317
- P4ProjectFileLoader
88340
+ P4ProjectFileLoader,
88341
+ ServerSettingsService,
88342
+ TaskRepositoryRegistry,
88343
+ ProjectionThreadRepository
88318
88344
  ];
88345
+ const TaskCreateToolInput = Schema$1.Struct({
88346
+ ...TaskCreateInput.fields,
88347
+ ...TaskSourceSelector.fields,
88348
+ linkCurrentThread: Schema$1.optional(Schema$1.Boolean.annotate({ description: "Link the new task or ticket to this conversation. Use this when the user asks to capture the current chat as work." }))
88349
+ });
88319
88350
  const TaskListResult = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
88320
88351
  const TaskListTool = Tool.make("task_list", {
88321
88352
  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.",
@@ -88339,8 +88370,8 @@ const TaskCurrentTool = Tool.make("task_current", {
88339
88370
  dependencies
88340
88371
  }).annotate(Tool.Title, "Get the current task").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
88341
88372
  const TaskCreateTool = Tool.make("task_create", {
88342
- description: "File a new task on the board. Defaults to the 'backlog' status, so use this freely to record follow-up work you noticed but are not doing now. Set status to 'todo' only when the work is genuinely queued next.",
88343
- parameters: TaskCreateInput,
88373
+ description: "File a new task on the p4code board, or a Linear ticket with source 'linear'. Defaults to the 'backlog' status. Set linkCurrentThread to true when the user asks to capture this conversation as a task or ticket; the new row then appears in this chat header. Set status to 'todo' only when the work is genuinely queued next.",
88374
+ parameters: TaskCreateToolInput,
88344
88375
  success: Task,
88345
88376
  failure: TaskToolError,
88346
88377
  dependencies: createDependencies
@@ -88449,16 +88480,25 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
88449
88480
  return current;
88450
88481
  }),
88451
88482
  task_create: (input) => Effect.gen(function* () {
88452
- yield* requireTaskCapability();
88453
- const tasks = yield* TaskRepository;
88483
+ const invocation = yield* requireTaskCapability();
88484
+ const taskRepositories = yield* TaskRepositoryRegistry;
88485
+ const threads = yield* ProjectionThreadRepository;
88486
+ const { source, linkCurrentThread, ...createInput } = input;
88487
+ const tasks = taskRepositories.forSource(source);
88488
+ const currentProjectId = linkCurrentThread === true && createInput.projectId === void 0 ? Option.getOrUndefined(yield* threads.getById({ threadId: invocation.threadId }).pipe(Effect.catch(toTaskStoreError("task_create:current_thread"))))?.projectId : void 0;
88454
88489
  const crypto = yield* Crypto.Crypto;
88455
88490
  const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
88456
88491
  const timestamp = yield* nowIso$1;
88457
- const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
88458
- const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
88459
- const parentTaskId = yield* resolveParentTaskId(input.parentTaskId, "task_create");
88460
- const row = buildTaskRow(parentTaskId === void 0 || parentTaskId === null ? input : {
88461
- ...input,
88492
+ const linkedInput = linkCurrentThread === true ? {
88493
+ ...createInput,
88494
+ ...currentProjectId === void 0 ? {} : { projectId: currentProjectId },
88495
+ threadId: invocation.threadId
88496
+ } : createInput;
88497
+ const repositoryKey = yield* resolveTaskRepositoryKey(linkedInput.projectId);
88498
+ const readableIdPrefix = yield* resolveTaskReadableIdPrefix(linkedInput.projectId);
88499
+ const parentTaskId = yield* resolveParentTaskId(linkedInput.parentTaskId, "task_create");
88500
+ const row = buildTaskRow(parentTaskId === void 0 || parentTaskId === null ? linkedInput : {
88501
+ ...linkedInput,
88462
88502
  parentTaskId
88463
88503
  }, taskId, timestamp, repositoryKey);
88464
88504
  return yield* tasks.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("task_create")));