@p4code/cli 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/bin.mjs +517 -202
  2. package/dist/client/assets/{DiffPanel-B_RUbYdJ.js → DiffPanel-Bb72saoS.js} +2 -2
  3. package/dist/client/assets/{FilePreviewPanel-D_TE6anL.js → FilePreviewPanel-6mJ-Vsub.js} +2 -2
  4. package/dist/client/assets/{PreviewPanel-Dx5mB3w6.js → PreviewPanel-ogPpU8Ev.js} +2 -2
  5. package/dist/client/assets/{angular-html-Z6GyEAg_.js → angular-html-BIzUKsg7.js} +2 -2
  6. package/dist/client/assets/{angular-ts-BdawRcND.js → angular-ts-CSIA-4Zf.js} +2 -2
  7. package/dist/client/assets/{apl-0Nzwg3c9.js → apl-Du1uuPDy.js} +2 -2
  8. package/dist/client/assets/arrow-right-qNd2lFlZ.js +2 -0
  9. package/dist/client/assets/{astro-iJpYXO_m.js → astro-bVpCfC8I.js} +2 -2
  10. package/dist/client/assets/{blade-BeMtIYzp.js → blade-KOd_hdA5.js} +2 -2
  11. package/dist/client/assets/{c-n5Rj77uj.js → c-BUOUjYKH.js} +2 -2
  12. package/dist/client/assets/{cobol-CfsSyjzP.js → cobol-DVr6VPEj.js} +2 -2
  13. package/dist/client/assets/{coffee-LZB1mvnz.js → coffee-CaWTiLWu.js} +2 -2
  14. package/dist/client/assets/{cpp-C689OQY_.js → cpp-DOVxTtfq.js} +2 -2
  15. package/dist/client/assets/{crystal-CC7u-hEs.js → crystal-D_Q817Qu.js} +2 -2
  16. package/dist/client/assets/{css-eVqGM3Yx.js → css-Rez0Ry86.js} +2 -2
  17. package/dist/client/assets/{edge-CvU3NO69.js → edge-C5CKZJyo.js} +2 -2
  18. package/dist/client/assets/{elixir-gblyDHJ2.js → elixir-D_eVXxgH.js} +2 -2
  19. package/dist/client/assets/{elm-BEDREZ-r.js → elm-BLkMkUJ0.js} +2 -2
  20. package/dist/client/assets/{erb-C_pVTICR.js → erb-DGG0CxNR.js} +2 -2
  21. package/dist/client/assets/{fileCommentAnnotations-Z0wcn9Z2.js → fileCommentAnnotations-D0gz9Nvl.js} +2 -2
  22. package/dist/client/assets/{git-rebase-BvBzTqE6.js → git-rebase-D7H5ArnN.js} +2 -2
  23. package/dist/client/assets/{glimmer-js-y6xvI0dn.js → glimmer-js-DOuFsyYy.js} +2 -2
  24. package/dist/client/assets/{glimmer-ts-BrWleVTO.js → glimmer-ts-C_dgMrVH.js} +2 -2
  25. package/dist/client/assets/{glsl-Bkrdoinn.js → glsl-BJU0wvLz.js} +2 -2
  26. package/dist/client/assets/{graphql-CSx1KLSt.js → graphql-CXUg7dVm.js} +2 -2
  27. package/dist/client/assets/{hack-CEPENt6B.js → hack-DEwTpBBH.js} +2 -2
  28. package/dist/client/assets/{haml-CR4MMWD-.js → haml-Br-Q1jfj.js} +2 -2
  29. package/dist/client/assets/{handlebars-BvZ81eM2.js → handlebars-U-qCMjLv.js} +2 -2
  30. package/dist/client/assets/{html-CJJ_Yk-p.js → html-CBhkruiu.js} +2 -2
  31. package/dist/client/assets/{html-derivative-DyGTUpT_.js → html-derivative-CtOWonuc.js} +2 -2
  32. package/dist/client/assets/{http-CthnayZT.js → http-B-anb7K2.js} +2 -2
  33. package/dist/client/assets/{hurl-CySpRHwn.js → hurl-UbSvk6zv.js} +2 -2
  34. package/dist/client/assets/{index-CTDRPB7h.js → index-CADZUNMb.js} +203 -203
  35. package/dist/client/assets/index-CPnR_Tju.css +1 -0
  36. package/dist/client/assets/{java-eLetcmnB.js → java-d4Ae9Kq8.js} +2 -2
  37. package/dist/client/assets/{javascript-bze-DcnN.js → javascript-DENutVTd.js} +2 -2
  38. package/dist/client/assets/{jinja-BwBfXVzv.js → jinja-Bugh_6jh.js} +2 -2
  39. package/dist/client/assets/{jison-BG-6HBeN.js → jison-5v-Z1oHG.js} +2 -2
  40. package/dist/client/assets/{json-D04kql_u.js → json-BhFmIQh2.js} +2 -2
  41. package/dist/client/assets/{jsx-B3RhaC78.js → jsx-ByAWu7n0.js} +2 -2
  42. package/dist/client/assets/{julia-CLYU2NBT.js → julia-DE4JKJ_I.js} +2 -2
  43. package/dist/client/assets/{just-CQGSktLF.js → just-Ck-bQzBW.js} +2 -2
  44. package/dist/client/assets/{latex-B2r18a0i.js → latex-0aopGn_u.js} +2 -2
  45. package/dist/client/assets/{liquid-DSPVRc0S.js → liquid-BOV6YY-4.js} +2 -2
  46. package/dist/client/assets/{lua-CBxs-my9.js → lua-BS79SG5v.js} +2 -2
  47. package/dist/client/assets/{marko-FkXUABHT.js → marko-CxCHVwvS.js} +2 -2
  48. package/dist/client/assets/{mdc-BW-qJ07Y.js → mdc-D8ArMX_3.js} +2 -2
  49. package/dist/client/assets/{nginx-r-F4ISTc.js → nginx-54lPKP4l.js} +2 -2
  50. package/dist/client/assets/{nim-CqGONDki.js → nim-CQW1n-W6.js} +2 -2
  51. package/dist/client/assets/{perl-YqxSDkbB.js → perl-TGEf9BSy.js} +2 -2
  52. package/dist/client/assets/{php-o53m7ISl.js → php-CRSs0ljw.js} +2 -2
  53. package/dist/client/assets/{pug-DvC1IxmZ.js → pug-B94qB-T3.js} +2 -2
  54. package/dist/client/assets/{qml-B_UHgwrf.js → qml-Ce-jpaqY.js} +2 -2
  55. package/dist/client/assets/{r-BslRtXqA.js → r-Di-hq60i.js} +2 -2
  56. package/dist/client/assets/{razor-DbFRY8EJ.js → razor-VXC_c_VY.js} +2 -2
  57. package/dist/client/assets/{regexp-LUjvkiV5.js → regexp-DCuWLst8.js} +2 -2
  58. package/dist/client/assets/{rst-DjMRC4dl.js → rst-CaleKCxt.js} +2 -2
  59. package/dist/client/assets/{ruby-D1SSXqCW.js → ruby-BNvjj0Xx.js} +2 -2
  60. package/dist/client/assets/{sas-8pH9ZpYC.js → sas-CN3Jyhxd.js} +2 -2
  61. package/dist/client/assets/{scss-CLXZYmiL.js → scss-iGZoIRyN.js} +2 -2
  62. package/dist/client/assets/{shellscript-C3x4iGUN.js → shellscript-B3Nk58br.js} +2 -2
  63. package/dist/client/assets/{shellsession-4iw-70AB.js → shellsession-aV0EL5Me.js} +2 -2
  64. package/dist/client/assets/{soy-BmuiXqhy.js → soy-CK-MnjGK.js} +2 -2
  65. package/dist/client/assets/{sql-CYtr1JGt.js → sql-WiqY-pg6.js} +2 -2
  66. package/dist/client/assets/{stata-D-p-ifu4.js → stata-L2KCRIsj.js} +2 -2
  67. package/dist/client/assets/{surrealql-BQtaSVD9.js → surrealql-y8rFKpHp.js} +2 -2
  68. package/dist/client/assets/{svelte-BZ0IuCtg.js → svelte-BUjzGJjC.js} +2 -2
  69. package/dist/client/assets/{templ-D_0C9uOG.js → templ-Lwa6_2Pi.js} +2 -2
  70. package/dist/client/assets/{tex-Da564oMl.js → tex-C6_2o8N6.js} +2 -2
  71. package/dist/client/assets/textarea-C26NLrE0.js +46 -0
  72. package/dist/client/assets/{ts-tags-CCvt3g0S.js → ts-tags-DHGXrdlY.js} +2 -2
  73. package/dist/client/assets/{tsx-C1pKkLrg.js → tsx-D7a2s_rX.js} +2 -2
  74. package/dist/client/assets/{twig-ulf4RPD9.js → twig-rodlH6R_.js} +2 -2
  75. package/dist/client/assets/{typescript-Cm2BDYNJ.js → typescript-BSVnlc3E.js} +2 -2
  76. package/dist/client/assets/{vue-D38YVq_o.js → vue-CPqhBG4f.js} +2 -2
  77. package/dist/client/assets/{vue-html-Dw9Iwm-j.js → vue-html-DlcYUp9j.js} +2 -2
  78. package/dist/client/assets/{vue-vine-DGhLcVZx.js → vue-vine-C33vB5eU.js} +2 -2
  79. package/dist/client/assets/{xml-Bm9IDqQ-.js → xml-Dk7HbObH.js} +2 -2
  80. package/dist/client/assets/{xsl-CLGFKBCX.js → xsl-Ce_0ufBe.js} +2 -2
  81. package/dist/client/assets/{yaml-C5Olrwb1.js → yaml-hWCzaJrB.js} +2 -2
  82. package/dist/client/index.html +3 -3
  83. package/package.json +1 -1
  84. package/dist/client/assets/arrow-right-BVDRuCFE.js +0 -2
  85. package/dist/client/assets/index-CstNfQU9.css +0 -1
  86. package/dist/client/assets/textarea-CAyleL6s.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.7";
236
+ var version = "0.1.9";
237
237
  //#endregion
238
238
  //#region src/config.ts
239
239
  /**
@@ -7012,175 +7012,6 @@ const GitActionProgressEvent = Schema$1.Union([
7012
7012
  GitActionFailedEvent
7013
7013
  ]);
7014
7014
  //#endregion
7015
- //#region ../../packages/contracts/src/p4ProjectFile.ts
7016
- /** File name of the checked-in P4 project file, resolved at the workspace root. */
7017
- const P4_PROJECT_FILE_NAME = "p4.json";
7018
- /** Public URL of the published JSON Schema for {@link P4ProjectFile}. */
7019
- const P4_PROJECT_FILE_SCHEMA_URL = "https://p4.codes/schema/p4.json";
7020
- const P4_PROJECT_FILE_PATH_MAX_LENGTH = 512;
7021
- const P4_PROJECT_FILE_MAX_SCRIPTS = 50;
7022
- const trimmedNonEmpty = (annotations, maxLength) => {
7023
- const annotated = Schema$1.String.annotate(annotations);
7024
- const encoded = maxLength === void 0 ? annotated.check(Schema$1.isNonEmpty()) : annotated.check(Schema$1.isNonEmpty(), Schema$1.isMaxLength(maxLength));
7025
- return encoded.pipe(Schema$1.decodeTo(encoded, SchemaTransformation.trim()));
7026
- };
7027
- const P4ProjectFileScript = Schema$1.Struct({
7028
- name: trimmedNonEmpty({ description: "Display name for the script, shown in the P4Code scripts menu." }),
7029
- command: trimmedNonEmpty({ description: "Shell command executed in a P4Code terminal at the project root." }),
7030
- icon: Schema$1.optionalKey(ProjectScriptIcon.annotate({ description: "Icon shown next to the script in the scripts menu. Defaults to \"play\"." })),
7031
- runOnWorktreeCreate: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, the script runs automatically after a worktree is created for a new thread." })),
7032
- previewUrl: Schema$1.optionalKey(trimmedNonEmpty({ description: "URL opened in the in-app browser preview when this script runs. Only honored on the desktop build." })),
7033
- autoOpenPreview: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, automatically open the preview panel at `previewUrl` the moment the script starts." }))
7034
- }).annotate({ description: "A project script that team members can import into P4Code." });
7035
- const P4ProjectFile = Schema$1.Struct({
7036
- $schema: Schema$1.optionalKey(Schema$1.String.annotate({ description: `URL of the JSON Schema for this file, typically "${P4_PROJECT_FILE_SCHEMA_URL}".` })),
7037
- 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)),
7038
- 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)))
7039
- }).annotate({
7040
- title: "P4 project file",
7041
- description: "Checked-in project configuration for P4Code (p4.json at the repository root). See https://p4.codes for documentation."
7042
- });
7043
- //#endregion
7044
- //#region ../../packages/contracts/src/project.ts
7045
- const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200;
7046
- const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512;
7047
- const PROJECT_READ_FILE_PATH_MAX_LENGTH = 512;
7048
- const ProjectSearchEntriesInput = Schema$1.Struct({
7049
- cwd: TrimmedNonEmptyString,
7050
- query: TrimmedNonEmptyString.check(Schema$1.isMaxLength(256)),
7051
- limit: PositiveInt.check(Schema$1.isLessThanOrEqualTo(PROJECT_SEARCH_ENTRIES_MAX_LIMIT))
7052
- });
7053
- const ProjectEntryKind = Schema$1.Literals(["file", "directory"]);
7054
- const ProjectEntry = Schema$1.Struct({
7055
- path: TrimmedNonEmptyString,
7056
- kind: ProjectEntryKind
7057
- });
7058
- const ProjectSearchEntriesResult = Schema$1.Struct({
7059
- entries: Schema$1.Array(ProjectEntry),
7060
- truncated: Schema$1.Boolean
7061
- });
7062
- const ProjectListEntriesInput = Schema$1.Struct({ cwd: TrimmedNonEmptyString });
7063
- const ProjectListEntriesResult = Schema$1.Struct({
7064
- entries: Schema$1.Array(ProjectEntry),
7065
- truncated: Schema$1.Boolean
7066
- });
7067
- const ProjectEntriesFailure = Schema$1.Literals([
7068
- "workspace_root_not_found",
7069
- "workspace_root_create_failed",
7070
- "workspace_root_stat_failed",
7071
- "workspace_root_not_directory",
7072
- "search_index_create_failed",
7073
- "search_index_scan_timed_out",
7074
- "search_index_search_failed"
7075
- ]);
7076
- function decodedProjectErrorMessage(props) {
7077
- if (!("message" in props)) return void 0;
7078
- return typeof props.message === "string" ? props.message : void 0;
7079
- }
7080
- var ProjectSearchEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectSearchEntriesError", {
7081
- cwd: Schema$1.optional(TrimmedNonEmptyString),
7082
- queryLength: Schema$1.optional(NonNegativeInt),
7083
- limit: Schema$1.optional(PositiveInt),
7084
- failure: Schema$1.optional(ProjectEntriesFailure),
7085
- normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
7086
- timeout: Schema$1.optional(TrimmedNonEmptyString),
7087
- detail: Schema$1.optional(TrimmedNonEmptyString),
7088
- message: TrimmedNonEmptyString,
7089
- cause: Schema$1.optional(Schema$1.Defect())
7090
- }) {
7091
- constructor(props) {
7092
- super({
7093
- ...props,
7094
- message: decodedProjectErrorMessage(props) ?? `Failed to search workspace entries in '${props.cwd}'.`
7095
- });
7096
- }
7097
- };
7098
- var ProjectListEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectListEntriesError", {
7099
- cwd: Schema$1.optional(TrimmedNonEmptyString),
7100
- failure: Schema$1.optional(ProjectEntriesFailure),
7101
- normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
7102
- timeout: Schema$1.optional(TrimmedNonEmptyString),
7103
- detail: Schema$1.optional(TrimmedNonEmptyString),
7104
- message: TrimmedNonEmptyString,
7105
- cause: Schema$1.optional(Schema$1.Defect())
7106
- }) {
7107
- constructor(props) {
7108
- super({
7109
- ...props,
7110
- message: decodedProjectErrorMessage(props) ?? `Failed to list workspace entries in '${props.cwd}'.`
7111
- });
7112
- }
7113
- };
7114
- const ProjectReadFileInput = Schema$1.Struct({
7115
- cwd: TrimmedNonEmptyString,
7116
- relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_READ_FILE_PATH_MAX_LENGTH))
7117
- });
7118
- const ProjectReadFileResult = Schema$1.Struct({
7119
- relativePath: TrimmedNonEmptyString,
7120
- contents: Schema$1.String,
7121
- byteLength: NonNegativeInt,
7122
- truncated: Schema$1.Boolean
7123
- });
7124
- const ProjectFileFailure = Schema$1.Literals([
7125
- "workspace_path_outside_root",
7126
- "resolved_path_outside_root",
7127
- "path_not_file",
7128
- "binary_file",
7129
- "operation_failed"
7130
- ]);
7131
- const ProjectFileOperation = Schema$1.Literals([
7132
- "realpath-workspace-root",
7133
- "realpath-target",
7134
- "open",
7135
- "stat",
7136
- "read",
7137
- "close",
7138
- "make-directory",
7139
- "write-file"
7140
- ]);
7141
- var ProjectReadFileError = class extends Schema$1.TaggedErrorClass()("ProjectReadFileError", {
7142
- cwd: Schema$1.optional(TrimmedNonEmptyString),
7143
- relativePath: Schema$1.optional(TrimmedNonEmptyString),
7144
- failure: Schema$1.optional(ProjectFileFailure),
7145
- resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
7146
- resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
7147
- operation: Schema$1.optional(ProjectFileOperation),
7148
- operationPath: Schema$1.optional(TrimmedNonEmptyString),
7149
- message: TrimmedNonEmptyString,
7150
- cause: Schema$1.optional(Schema$1.Defect())
7151
- }) {
7152
- constructor(props) {
7153
- super({
7154
- ...props,
7155
- message: decodedProjectErrorMessage(props) ?? `Failed to read workspace file '${props.relativePath}' in '${props.cwd}'.`
7156
- });
7157
- }
7158
- };
7159
- const ProjectWriteFileInput = Schema$1.Struct({
7160
- cwd: TrimmedNonEmptyString,
7161
- relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_WRITE_FILE_PATH_MAX_LENGTH)),
7162
- contents: Schema$1.String
7163
- });
7164
- const ProjectWriteFileResult = Schema$1.Struct({ relativePath: TrimmedNonEmptyString });
7165
- var ProjectWriteFileError = class extends Schema$1.TaggedErrorClass()("ProjectWriteFileError", {
7166
- cwd: Schema$1.optional(TrimmedNonEmptyString),
7167
- relativePath: Schema$1.optional(TrimmedNonEmptyString),
7168
- failure: Schema$1.optional(ProjectFileFailure),
7169
- resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
7170
- resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
7171
- operation: Schema$1.optional(ProjectFileOperation),
7172
- operationPath: Schema$1.optional(TrimmedNonEmptyString),
7173
- message: TrimmedNonEmptyString,
7174
- cause: Schema$1.optional(Schema$1.Defect())
7175
- }) {
7176
- constructor(props) {
7177
- super({
7178
- ...props,
7179
- message: decodedProjectErrorMessage(props) ?? `Failed to write workspace file '${props.relativePath}' in '${props.cwd}'.`
7180
- });
7181
- }
7182
- };
7183
- //#endregion
7184
7015
  //#region ../../packages/contracts/src/task.ts
7185
7016
  const TASK_TITLE_MAX_LENGTH = 512;
7186
7017
  const TASK_LABEL_MAX_LENGTH = 64;
@@ -7205,6 +7036,19 @@ const TaskPriority = Schema$1.Literals([
7205
7036
  "high",
7206
7037
  "urgent"
7207
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);
7208
7052
  const TaskLabel = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_LABEL_MAX_LENGTH));
7209
7053
  /**
7210
7054
  * Agent-facing field descriptions. These ride on the *encoded* side because
@@ -7232,11 +7076,37 @@ const AssigneeField = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSI
7232
7076
  * no remote, where there is nothing stable to key on.
7233
7077
  */
7234
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
+ /**
7080
+ * Manual board position within a column, as a lexicographic fractional key.
7081
+ *
7082
+ * Compared as a plain string: smaller sorts higher. Keys are minted by the
7083
+ * board between the neighbours of a drop, so moving one card writes one row
7084
+ * rather than renumbering the column. `null` means the task has never been
7085
+ * placed by hand and falls back to the priority/recency order.
7086
+ */
7087
+ const OrderKeyField = TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "Manual board position key. Lexicographic: smaller sorts higher in the column. Null means no manual position." }));
7235
7088
  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." }));
7236
- const TaskIdField = TaskId.pipe(Schema$1.annotateEncoded({ description: "Id of the task to act on." }));
7089
+ 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)." }));
7237
7090
  const Task = Schema$1.Struct({
7238
7091
  taskId: TaskId,
7239
7092
  /**
7093
+ * The human-readable id, e.g. `T-1` or `P4-12`: a per-prefix sequence
7094
+ * allocated by the store that owns the row, never reused, with gaps where
7095
+ * tasks were deleted. Nullable only for version skew - a row read from a
7096
+ * store that has not run the backfill migration decodes to `null` rather
7097
+ * than failing the whole board.
7098
+ */
7099
+ readableId: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7100
+ /**
7101
+ * Manual board position: a lexicographic fractional key, compared as a plain
7102
+ * string, smaller first. Written only when a card is dragged to a position;
7103
+ * `null` means "never placed by hand" and the board falls back to its
7104
+ * priority/recency order. Nullable-with-default for the same version-skew
7105
+ * reason as `readableId`: a row from a store without the migration decodes to
7106
+ * `null` rather than failing the whole board.
7107
+ */
7108
+ orderKey: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7109
+ /**
7240
7110
  * The project this task belongs to, or `null` for a task that is not scoped
7241
7111
  * to one. This references the existing workspace project registry rather than
7242
7112
  * a task-board-private notion of a project.
@@ -7277,6 +7147,27 @@ const TaskCreateInput = Schema$1.Struct({
7277
7147
  labels: Schema$1.optional(LabelsField)
7278
7148
  });
7279
7149
  /**
7150
+ * What an environment server sends the hub to file a task.
7151
+ *
7152
+ * Extends the public create input with the readable-id prefix, because the two
7153
+ * halves of the answer live on different machines: the prefix comes from the
7154
+ * project's checked-in `p4.json`, which only the submitting server can read,
7155
+ * while the sequence number must be allocated by the hub, which is the only
7156
+ * party that sees every machine's writes. Deliberately not part of
7157
+ * `TaskCreateInput` itself so an agent filing over MCP cannot pick a prefix -
7158
+ * the prefix belongs to the project, not to the caller.
7159
+ */
7160
+ const HubTaskCreateInput = Schema$1.Struct({
7161
+ ...TaskCreateInput.fields,
7162
+ readableIdPrefix: Schema$1.optional(TrimmedNonEmptyString),
7163
+ /**
7164
+ * Carried on the hub's create route (but not the public one) so a server
7165
+ * mirroring an already-placed row to the hub does not lose its position.
7166
+ * An agent filing a new task has no business picking a slot.
7167
+ */
7168
+ orderKey: Schema$1.optional(OrderKeyField)
7169
+ });
7170
+ /**
7280
7171
  * Every field is optional and an omitted field is left untouched. The nullable
7281
7172
  * fields accept an explicit `null` to clear them, which is why they are
7282
7173
  * `NullOr` inside `optional` rather than merely optional.
@@ -7291,7 +7182,8 @@ const TaskUpdateInput = Schema$1.Struct({
7291
7182
  status: Schema$1.optional(StatusField),
7292
7183
  priority: Schema$1.optional(PriorityField),
7293
7184
  assignee: Schema$1.optional(Schema$1.NullOr(AssigneeField)),
7294
- labels: Schema$1.optional(LabelsField)
7185
+ labels: Schema$1.optional(LabelsField),
7186
+ orderKey: Schema$1.optional(Schema$1.NullOr(OrderKeyField))
7295
7187
  });
7296
7188
  /**
7297
7189
  * A task an agent has drafted but has not filed.
@@ -7480,6 +7372,176 @@ const TaskToolError = Schema$1.Union([
7480
7372
  TaskStoreError
7481
7373
  ]);
7482
7374
  //#endregion
7375
+ //#region ../../packages/contracts/src/p4ProjectFile.ts
7376
+ /** File name of the checked-in P4 project file, resolved at the workspace root. */
7377
+ const P4_PROJECT_FILE_NAME = "p4.json";
7378
+ /** Public URL of the published JSON Schema for {@link P4ProjectFile}. */
7379
+ const P4_PROJECT_FILE_SCHEMA_URL = "https://p4.codes/schema/p4.json";
7380
+ const P4_PROJECT_FILE_PATH_MAX_LENGTH = 512;
7381
+ const P4_PROJECT_FILE_MAX_SCRIPTS = 50;
7382
+ const trimmedNonEmpty = (annotations, maxLength) => {
7383
+ const annotated = Schema$1.String.annotate(annotations);
7384
+ const encoded = maxLength === void 0 ? annotated.check(Schema$1.isNonEmpty()) : annotated.check(Schema$1.isNonEmpty(), Schema$1.isMaxLength(maxLength));
7385
+ return encoded.pipe(Schema$1.decodeTo(encoded, SchemaTransformation.trim()));
7386
+ };
7387
+ const P4ProjectFileScript = Schema$1.Struct({
7388
+ name: trimmedNonEmpty({ description: "Display name for the script, shown in the P4Code scripts menu." }),
7389
+ command: trimmedNonEmpty({ description: "Shell command executed in a P4Code terminal at the project root." }),
7390
+ icon: Schema$1.optionalKey(ProjectScriptIcon.annotate({ description: "Icon shown next to the script in the scripts menu. Defaults to \"play\"." })),
7391
+ runOnWorktreeCreate: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, the script runs automatically after a worktree is created for a new thread." })),
7392
+ previewUrl: Schema$1.optionalKey(trimmedNonEmpty({ description: "URL opened in the in-app browser preview when this script runs. Only honored on the desktop build." })),
7393
+ autoOpenPreview: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, automatically open the preview panel at `previewUrl` the moment the script starts." }))
7394
+ }).annotate({ description: "A project script that team members can import into P4Code." });
7395
+ const P4ProjectFile = Schema$1.Struct({
7396
+ $schema: Schema$1.optionalKey(Schema$1.String.annotate({ description: `URL of the JSON Schema for this file, typically "${P4_PROJECT_FILE_SCHEMA_URL}".` })),
7397
+ 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)),
7398
+ 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))),
7399
+ 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)))
7400
+ }).annotate({
7401
+ title: "P4 project file",
7402
+ description: "Checked-in project configuration for P4Code (p4.json at the repository root). See https://p4.codes for documentation."
7403
+ });
7404
+ //#endregion
7405
+ //#region ../../packages/contracts/src/project.ts
7406
+ const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200;
7407
+ const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512;
7408
+ const PROJECT_READ_FILE_PATH_MAX_LENGTH = 512;
7409
+ const ProjectSearchEntriesInput = Schema$1.Struct({
7410
+ cwd: TrimmedNonEmptyString,
7411
+ query: TrimmedNonEmptyString.check(Schema$1.isMaxLength(256)),
7412
+ limit: PositiveInt.check(Schema$1.isLessThanOrEqualTo(PROJECT_SEARCH_ENTRIES_MAX_LIMIT))
7413
+ });
7414
+ const ProjectEntryKind = Schema$1.Literals(["file", "directory"]);
7415
+ const ProjectEntry = Schema$1.Struct({
7416
+ path: TrimmedNonEmptyString,
7417
+ kind: ProjectEntryKind
7418
+ });
7419
+ const ProjectSearchEntriesResult = Schema$1.Struct({
7420
+ entries: Schema$1.Array(ProjectEntry),
7421
+ truncated: Schema$1.Boolean
7422
+ });
7423
+ const ProjectListEntriesInput = Schema$1.Struct({ cwd: TrimmedNonEmptyString });
7424
+ const ProjectListEntriesResult = Schema$1.Struct({
7425
+ entries: Schema$1.Array(ProjectEntry),
7426
+ truncated: Schema$1.Boolean
7427
+ });
7428
+ const ProjectEntriesFailure = Schema$1.Literals([
7429
+ "workspace_root_not_found",
7430
+ "workspace_root_create_failed",
7431
+ "workspace_root_stat_failed",
7432
+ "workspace_root_not_directory",
7433
+ "search_index_create_failed",
7434
+ "search_index_scan_timed_out",
7435
+ "search_index_search_failed"
7436
+ ]);
7437
+ function decodedProjectErrorMessage(props) {
7438
+ if (!("message" in props)) return void 0;
7439
+ return typeof props.message === "string" ? props.message : void 0;
7440
+ }
7441
+ var ProjectSearchEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectSearchEntriesError", {
7442
+ cwd: Schema$1.optional(TrimmedNonEmptyString),
7443
+ queryLength: Schema$1.optional(NonNegativeInt),
7444
+ limit: Schema$1.optional(PositiveInt),
7445
+ failure: Schema$1.optional(ProjectEntriesFailure),
7446
+ normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
7447
+ timeout: Schema$1.optional(TrimmedNonEmptyString),
7448
+ detail: Schema$1.optional(TrimmedNonEmptyString),
7449
+ message: TrimmedNonEmptyString,
7450
+ cause: Schema$1.optional(Schema$1.Defect())
7451
+ }) {
7452
+ constructor(props) {
7453
+ super({
7454
+ ...props,
7455
+ message: decodedProjectErrorMessage(props) ?? `Failed to search workspace entries in '${props.cwd}'.`
7456
+ });
7457
+ }
7458
+ };
7459
+ var ProjectListEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectListEntriesError", {
7460
+ cwd: Schema$1.optional(TrimmedNonEmptyString),
7461
+ failure: Schema$1.optional(ProjectEntriesFailure),
7462
+ normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
7463
+ timeout: Schema$1.optional(TrimmedNonEmptyString),
7464
+ detail: Schema$1.optional(TrimmedNonEmptyString),
7465
+ message: TrimmedNonEmptyString,
7466
+ cause: Schema$1.optional(Schema$1.Defect())
7467
+ }) {
7468
+ constructor(props) {
7469
+ super({
7470
+ ...props,
7471
+ message: decodedProjectErrorMessage(props) ?? `Failed to list workspace entries in '${props.cwd}'.`
7472
+ });
7473
+ }
7474
+ };
7475
+ const ProjectReadFileInput = Schema$1.Struct({
7476
+ cwd: TrimmedNonEmptyString,
7477
+ relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_READ_FILE_PATH_MAX_LENGTH))
7478
+ });
7479
+ const ProjectReadFileResult = Schema$1.Struct({
7480
+ relativePath: TrimmedNonEmptyString,
7481
+ contents: Schema$1.String,
7482
+ byteLength: NonNegativeInt,
7483
+ truncated: Schema$1.Boolean
7484
+ });
7485
+ const ProjectFileFailure = Schema$1.Literals([
7486
+ "workspace_path_outside_root",
7487
+ "resolved_path_outside_root",
7488
+ "path_not_file",
7489
+ "binary_file",
7490
+ "operation_failed"
7491
+ ]);
7492
+ const ProjectFileOperation = Schema$1.Literals([
7493
+ "realpath-workspace-root",
7494
+ "realpath-target",
7495
+ "open",
7496
+ "stat",
7497
+ "read",
7498
+ "close",
7499
+ "make-directory",
7500
+ "write-file"
7501
+ ]);
7502
+ var ProjectReadFileError = class extends Schema$1.TaggedErrorClass()("ProjectReadFileError", {
7503
+ cwd: Schema$1.optional(TrimmedNonEmptyString),
7504
+ relativePath: Schema$1.optional(TrimmedNonEmptyString),
7505
+ failure: Schema$1.optional(ProjectFileFailure),
7506
+ resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
7507
+ resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
7508
+ operation: Schema$1.optional(ProjectFileOperation),
7509
+ operationPath: Schema$1.optional(TrimmedNonEmptyString),
7510
+ message: TrimmedNonEmptyString,
7511
+ cause: Schema$1.optional(Schema$1.Defect())
7512
+ }) {
7513
+ constructor(props) {
7514
+ super({
7515
+ ...props,
7516
+ message: decodedProjectErrorMessage(props) ?? `Failed to read workspace file '${props.relativePath}' in '${props.cwd}'.`
7517
+ });
7518
+ }
7519
+ };
7520
+ const ProjectWriteFileInput = Schema$1.Struct({
7521
+ cwd: TrimmedNonEmptyString,
7522
+ relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_WRITE_FILE_PATH_MAX_LENGTH)),
7523
+ contents: Schema$1.String
7524
+ });
7525
+ const ProjectWriteFileResult = Schema$1.Struct({ relativePath: TrimmedNonEmptyString });
7526
+ var ProjectWriteFileError = class extends Schema$1.TaggedErrorClass()("ProjectWriteFileError", {
7527
+ cwd: Schema$1.optional(TrimmedNonEmptyString),
7528
+ relativePath: Schema$1.optional(TrimmedNonEmptyString),
7529
+ failure: Schema$1.optional(ProjectFileFailure),
7530
+ resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
7531
+ resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
7532
+ operation: Schema$1.optional(ProjectFileOperation),
7533
+ operationPath: Schema$1.optional(TrimmedNonEmptyString),
7534
+ message: TrimmedNonEmptyString,
7535
+ cause: Schema$1.optional(Schema$1.Defect())
7536
+ }) {
7537
+ constructor(props) {
7538
+ super({
7539
+ ...props,
7540
+ message: decodedProjectErrorMessage(props) ?? `Failed to write workspace file '${props.relativePath}' in '${props.cwd}'.`
7541
+ });
7542
+ }
7543
+ };
7544
+ //#endregion
7483
7545
  //#region ../../packages/contracts/src/agentAsset.ts
7484
7546
  /**
7485
7547
  * User-scope agent assets — the things that make an agent behave the same on
@@ -12030,6 +12092,65 @@ var _036_TaskRepositoryKey_default = Effect.gen(function* () {
12030
12092
  `;
12031
12093
  });
12032
12094
  //#endregion
12095
+ //#region src/persistence/Migrations/037_TaskReadableIds.ts
12096
+ /**
12097
+ * Human-readable task ids (`T-1`, `P4-12`).
12098
+ *
12099
+ * `task_sequences` holds one row per prefix with the highest number ever
12100
+ * handed out. It only grows: deleting a task does not return its number, so a
12101
+ * readable id, once uttered, never comes to mean a different task.
12102
+ *
12103
+ * Existing rows are backfilled with the default `T` prefix in creation order.
12104
+ * Their projects' prefixes are deliberately not consulted — a prefix lives in
12105
+ * the project's checked-in `p4.json`, and a migration that shells out to read
12106
+ * workspaces (which may no longer be on disk) is exactly what a migration
12107
+ * should not be. New tasks pick up their project's prefix from creation on.
12108
+ */
12109
+ var _037_TaskReadableIds_default = Effect.gen(function* () {
12110
+ const sql = yield* SqlClient.SqlClient;
12111
+ yield* sql`
12112
+ CREATE TABLE IF NOT EXISTS task_sequences (
12113
+ prefix TEXT PRIMARY KEY,
12114
+ last_seq INTEGER NOT NULL
12115
+ )
12116
+ `;
12117
+ yield* sql`ALTER TABLE tasks ADD COLUMN readable_id TEXT`;
12118
+ yield* sql`
12119
+ UPDATE tasks
12120
+ SET readable_id = 'T-' || (
12121
+ SELECT COUNT(*)
12122
+ FROM tasks AS earlier
12123
+ WHERE earlier.created_at < tasks.created_at
12124
+ OR (earlier.created_at = tasks.created_at AND earlier.task_id <= tasks.task_id)
12125
+ )
12126
+ WHERE readable_id IS NULL
12127
+ `;
12128
+ yield* sql`
12129
+ INSERT INTO task_sequences (prefix, last_seq)
12130
+ SELECT 'T', COUNT(*) FROM tasks
12131
+ WHERE true
12132
+ ON CONFLICT (prefix) DO UPDATE SET last_seq = MAX(last_seq, excluded.last_seq)
12133
+ `;
12134
+ yield* sql`
12135
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_readable_id
12136
+ ON tasks(readable_id)
12137
+ `;
12138
+ });
12139
+ //#endregion
12140
+ //#region src/persistence/Migrations/038_TaskOrderKeys.ts
12141
+ /**
12142
+ * Manual board position (`order_key`): a lexicographic fractional key written
12143
+ * when a card is dragged to a position, so one move rewrites one row.
12144
+ *
12145
+ * Existing rows are deliberately not backfilled. A null key means "never placed
12146
+ * by hand", and the board's comparator falls back to the old priority/recency
12147
+ * order for those - so a board nobody has dragged on looks exactly as it did
12148
+ * before this migration.
12149
+ */
12150
+ var _038_TaskOrderKeys_default = Effect.gen(function* () {
12151
+ yield* (yield* SqlClient.SqlClient)`ALTER TABLE tasks ADD COLUMN order_key TEXT`;
12152
+ });
12153
+ //#endregion
12033
12154
  //#region src/persistence/Migrations.ts
12034
12155
  /**
12035
12156
  * MigrationsLive - Migration runner with inline loader
@@ -12230,6 +12351,16 @@ const migrationEntries = [
12230
12351
  36,
12231
12352
  "TaskRepositoryKey",
12232
12353
  _036_TaskRepositoryKey_default
12354
+ ],
12355
+ [
12356
+ 37,
12357
+ "TaskReadableIds",
12358
+ _037_TaskReadableIds_default
12359
+ ],
12360
+ [
12361
+ 38,
12362
+ "TaskOrderKeys",
12363
+ _038_TaskOrderKeys_default
12233
12364
  ]
12234
12365
  ];
12235
12366
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -13853,6 +13984,7 @@ const TaskListQuery = Schema$1.Struct({
13853
13984
  });
13854
13985
  const TASK_COLUMNS = `
13855
13986
  task_id AS "taskId",
13987
+ readable_id AS "readableId",
13856
13988
  project_id AS "projectId",
13857
13989
  repository_key AS "repositoryKey",
13858
13990
  thread_id AS "threadId",
@@ -13862,6 +13994,7 @@ const TASK_COLUMNS = `
13862
13994
  priority,
13863
13995
  assignee,
13864
13996
  labels_json AS "labels",
13997
+ order_key AS "orderKey",
13865
13998
  created_at AS "createdAt",
13866
13999
  updated_at AS "updatedAt"
13867
14000
  `;
@@ -13874,6 +14007,7 @@ const makeTaskRepository = Effect.gen(function* () {
13874
14007
  execute: (row) => sql`
13875
14008
  INSERT INTO tasks (
13876
14009
  task_id,
14010
+ readable_id,
13877
14011
  project_id,
13878
14012
  repository_key,
13879
14013
  thread_id,
@@ -13883,11 +14017,13 @@ const makeTaskRepository = Effect.gen(function* () {
13883
14017
  priority,
13884
14018
  assignee,
13885
14019
  labels_json,
14020
+ order_key,
13886
14021
  created_at,
13887
14022
  updated_at
13888
14023
  )
13889
14024
  VALUES (
13890
14025
  ${row.taskId},
14026
+ ${row.readableId},
13891
14027
  ${row.projectId},
13892
14028
  ${row.repositoryKey},
13893
14029
  ${row.threadId},
@@ -13897,11 +14033,13 @@ const makeTaskRepository = Effect.gen(function* () {
13897
14033
  ${row.priority},
13898
14034
  ${row.assignee},
13899
14035
  ${JSON.stringify(row.labels)},
14036
+ ${row.orderKey},
13900
14037
  ${row.createdAt},
13901
14038
  ${row.updatedAt}
13902
14039
  )
13903
14040
  ON CONFLICT (task_id)
13904
14041
  DO UPDATE SET
14042
+ readable_id = excluded.readable_id,
13905
14043
  project_id = excluded.project_id,
13906
14044
  repository_key = excluded.repository_key,
13907
14045
  thread_id = excluded.thread_id,
@@ -13911,6 +14049,7 @@ const makeTaskRepository = Effect.gen(function* () {
13911
14049
  priority = excluded.priority,
13912
14050
  assignee = excluded.assignee,
13913
14051
  labels_json = excluded.labels_json,
14052
+ order_key = excluded.order_key,
13914
14053
  created_at = excluded.created_at,
13915
14054
  updated_at = excluded.updated_at
13916
14055
  `
@@ -13920,6 +14059,26 @@ const makeTaskRepository = Effect.gen(function* () {
13920
14059
  Result: TaskDbRow,
13921
14060
  execute: ({ taskId }) => sql`SELECT ${sql.literal(TASK_COLUMNS)} FROM tasks WHERE task_id = ${taskId}`
13922
14061
  });
14062
+ const getTaskRowByReadableId = SqlSchema.findOneOption({
14063
+ Request: Schema$1.String,
14064
+ Result: TaskDbRow,
14065
+ execute: (readableId) => sql`SELECT ${sql.literal(TASK_COLUMNS)} FROM tasks WHERE readable_id = ${readableId}`
14066
+ });
14067
+ /**
14068
+ * Take the next number for a prefix, creating the sequence at 1 on first
14069
+ * use. The row is never reset or decremented - a deleted task's number is
14070
+ * gone - which is what makes a readable id safe to say out loud.
14071
+ */
14072
+ const allocateReadableIdSeq = SqlSchema.findAll({
14073
+ Request: Schema$1.Struct({ prefix: Schema$1.String }),
14074
+ Result: Schema$1.Struct({ lastSeq: Schema$1.Int }),
14075
+ execute: ({ prefix }) => sql`
14076
+ INSERT INTO task_sequences (prefix, last_seq)
14077
+ VALUES (${prefix}, 1)
14078
+ ON CONFLICT (prefix) DO UPDATE SET last_seq = last_seq + 1
14079
+ RETURNING last_seq AS "lastSeq"
14080
+ `
14081
+ });
13923
14082
  const listTaskRows = SqlSchema.findAll({
13924
14083
  Request: TaskListQuery,
13925
14084
  Result: TaskDbRow,
@@ -13942,7 +14101,31 @@ const makeTaskRepository = Effect.gen(function* () {
13942
14101
  type: "upserted",
13943
14102
  task: row
13944
14103
  })));
14104
+ /**
14105
+ * The allocation and the insert share one transaction on purpose: a number
14106
+ * taken outside it would either be lost on a failed insert (a gap nobody
14107
+ * caused) or, worse, handed to two concurrent creates.
14108
+ */
14109
+ const create = (row, readableIdPrefix) => sql.withTransaction(Effect.gen(function* () {
14110
+ const prefix = readableIdPrefix.toUpperCase();
14111
+ const seq = (yield* allocateReadableIdSeq({ prefix }))[0]?.lastSeq;
14112
+ if (seq === void 0) return yield* new PersistenceSqlError({
14113
+ operation: "TaskRepository.create:allocate",
14114
+ detail: "the sequence upsert returned no row",
14115
+ cause: null
14116
+ });
14117
+ const task = {
14118
+ ...row,
14119
+ readableId: `${prefix}-${seq}`
14120
+ };
14121
+ yield* upsertTaskRow(task);
14122
+ return task;
14123
+ })).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.create:query")), Effect.tap((task) => publish({
14124
+ type: "upserted",
14125
+ task
14126
+ })));
13945
14127
  const getById = (input) => getTaskRow(input).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.getById:query")));
14128
+ const getByReadableId = (readableId) => getTaskRowByReadableId(readableId.toUpperCase()).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.getByReadableId:query")));
13946
14129
  const list = (filter) => listTaskRows({
13947
14130
  projectId: filter.projectId ?? null,
13948
14131
  repositoryKey: filter.repositoryKey ?? null,
@@ -13964,6 +14147,7 @@ const makeTaskRepository = Effect.gen(function* () {
13964
14147
  ...input.priority !== void 0 ? { priority: input.priority } : {},
13965
14148
  ...input.assignee !== void 0 ? { assignee: input.assignee } : {},
13966
14149
  ...input.labels !== void 0 ? { labels: input.labels } : {},
14150
+ ...input.orderKey !== void 0 ? { orderKey: input.orderKey } : {},
13967
14151
  updatedAt
13968
14152
  };
13969
14153
  yield* upsertTaskRow(updated);
@@ -14001,6 +14185,7 @@ const makeTaskRepository = Effect.gen(function* () {
14001
14185
  ...input.patch.priority !== void 0 ? { priority: input.patch.priority } : {},
14002
14186
  ...input.patch.assignee !== void 0 ? { assignee: input.patch.assignee } : {},
14003
14187
  ...input.patch.labels !== void 0 ? { labels: input.patch.labels } : {},
14188
+ ...input.patch.orderKey !== void 0 ? { orderKey: input.patch.orderKey } : {},
14004
14189
  updatedAt
14005
14190
  };
14006
14191
  yield* upsertTaskRow(updated);
@@ -14017,10 +14202,12 @@ const makeTaskRepository = Effect.gen(function* () {
14017
14202
  taskId: input.taskId
14018
14203
  })));
14019
14204
  return {
14205
+ create,
14020
14206
  upsert,
14021
14207
  patch,
14022
14208
  patchIfStatus,
14023
14209
  getById,
14210
+ getByReadableId,
14024
14211
  list,
14025
14212
  deleteById,
14026
14213
  streamChanges: Stream.fromPubSub(changes),
@@ -14109,12 +14296,13 @@ const getTaskRoute = HttpRouter.add("GET", "/tasks/:taskId", respondToHubFailure
14109
14296
  yield* authenticateHubRequest();
14110
14297
  const params = yield* HttpRouter.params;
14111
14298
  const taskId = TaskId.make(params.taskId ?? "");
14112
- const found = yield* (yield* TaskRepository).getById({ taskId });
14299
+ const tasks = yield* TaskRepository;
14300
+ const found = isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
14113
14301
  return Option.isNone(found) ? notFound(taskId) : HttpServerResponse.jsonUnsafe(found.value);
14114
14302
  })));
14115
14303
  const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Effect.gen(function* () {
14116
14304
  yield* authenticateHubRequest();
14117
- const input = yield* decodeOrInvalid(TaskCreateInput, "Request body does not match TaskCreateInput.")(yield* readJsonBody());
14305
+ const input = yield* decodeOrInvalid(HubTaskCreateInput, "Request body does not match HubTaskCreateInput.")(yield* readJsonBody());
14118
14306
  const tasks = yield* TaskRepository;
14119
14307
  const crypto = yield* Crypto.Crypto;
14120
14308
  const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
@@ -14130,11 +14318,12 @@ const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Ef
14130
14318
  priority: input.priority ?? "none",
14131
14319
  assignee: input.assignee ?? null,
14132
14320
  labels: input.labels ?? [],
14321
+ orderKey: input.orderKey ?? null,
14133
14322
  createdAt: timestamp,
14134
14323
  updatedAt: timestamp
14135
14324
  };
14136
- yield* tasks.upsert(row);
14137
- return HttpServerResponse.jsonUnsafe(row, { status: 201 });
14325
+ const task = yield* tasks.create(row, input.readableIdPrefix ?? "T");
14326
+ return HttpServerResponse.jsonUnsafe(task, { status: 201 });
14138
14327
  })));
14139
14328
  const updateTaskRoute = HttpRouter.add("PATCH", "/tasks/:taskId", respondToHubFailures(Effect.gen(function* () {
14140
14329
  yield* authenticateHubRequest();
@@ -14372,6 +14561,60 @@ var _003_AgentAssets_default = Effect.gen(function* () {
14372
14561
  `;
14373
14562
  });
14374
14563
  //#endregion
14564
+ //#region src/hub/Migrations/004_TaskReadableIds.ts
14565
+ /**
14566
+ * Human-readable task ids (`T-1`, `P4-12`).
14567
+ *
14568
+ * `task_sequences` holds one row per prefix with the highest number ever
14569
+ * handed out. It only grows: deleting a task does not return its number, so a
14570
+ * readable id, once uttered, never comes to mean a different task.
14571
+ *
14572
+ * Existing rows are backfilled with the default `T` prefix in creation order.
14573
+ * Prefixes cannot be consulted here at all: they live in each project's
14574
+ * checked-in `p4.json`, and the hub has no checkouts. New tasks arrive with a
14575
+ * prefix resolved by the submitting environment server.
14576
+ */
14577
+ var _004_TaskReadableIds_default = Effect.gen(function* () {
14578
+ const sql = yield* SqlClient.SqlClient;
14579
+ yield* sql`
14580
+ CREATE TABLE IF NOT EXISTS task_sequences (
14581
+ prefix TEXT PRIMARY KEY,
14582
+ last_seq INTEGER NOT NULL
14583
+ )
14584
+ `;
14585
+ yield* sql`ALTER TABLE tasks ADD COLUMN readable_id TEXT`;
14586
+ yield* sql`
14587
+ UPDATE tasks
14588
+ SET readable_id = 'T-' || (
14589
+ SELECT COUNT(*)
14590
+ FROM tasks AS earlier
14591
+ WHERE earlier.created_at < tasks.created_at
14592
+ OR (earlier.created_at = tasks.created_at AND earlier.task_id <= tasks.task_id)
14593
+ )
14594
+ WHERE readable_id IS NULL
14595
+ `;
14596
+ yield* sql`
14597
+ INSERT INTO task_sequences (prefix, last_seq)
14598
+ SELECT 'T', COUNT(*) FROM tasks
14599
+ WHERE true
14600
+ ON CONFLICT (prefix) DO UPDATE SET last_seq = MAX(last_seq, excluded.last_seq)
14601
+ `;
14602
+ yield* sql`
14603
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_readable_id
14604
+ ON tasks(readable_id)
14605
+ `;
14606
+ });
14607
+ //#endregion
14608
+ //#region src/hub/Migrations/005_TaskOrderKeys.ts
14609
+ /**
14610
+ * Manual board position (`order_key`), mirroring the environment server's
14611
+ * migration 038: a lexicographic fractional key written when a card is dragged
14612
+ * to a position. No backfill - null falls back to priority/recency order.
14613
+ */
14614
+ var _005_TaskOrderKeys_default = Effect.gen(function* () {
14615
+ yield* (yield* SqlClient.SqlClient)`ALTER TABLE tasks ADD COLUMN order_key TEXT`;
14616
+ });
14617
+ //#endregion
14375
14618
  //#region src/hub/Migrations.ts
14376
14619
  /**
14377
14620
  * Hub migrations.
@@ -14397,6 +14640,16 @@ const hubMigrationEntries = [
14397
14640
  3,
14398
14641
  "AgentAssets",
14399
14642
  _003_AgentAssets_default
14643
+ ],
14644
+ [
14645
+ 4,
14646
+ "TaskReadableIds",
14647
+ _004_TaskReadableIds_default
14648
+ ],
14649
+ [
14650
+ 5,
14651
+ "TaskOrderKeys",
14652
+ _005_TaskOrderKeys_default
14400
14653
  ]
14401
14654
  ];
14402
14655
  const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -32038,6 +32291,8 @@ const normalizeDispatchCommand = (command) => Effect.gen(function* () {
32038
32291
  * presentation and is not.
32039
32292
  *
32040
32293
  * The id and timestamp are passed in so this stays pure and holds no clock.
32294
+ * The readable id is absent on purpose: the store allocates it at insert time,
32295
+ * inside the same transaction as the sequence bump.
32041
32296
  */
32042
32297
  const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
32043
32298
  taskId,
@@ -32050,6 +32305,7 @@ const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
32050
32305
  priority: input.priority ?? "none",
32051
32306
  assignee: input.assignee ?? null,
32052
32307
  labels: input.labels ?? [],
32308
+ orderKey: null,
32053
32309
  createdAt: timestamp,
32054
32310
  updatedAt: timestamp
32055
32311
  });
@@ -32073,6 +32329,23 @@ const resolveTaskRepositoryKey = Effect.fn("taskBoard.resolveRepositoryKey")(fun
32073
32329
  return (yield* (yield* RepositoryIdentityResolver).resolve(project.value.workspaceRoot))?.canonicalKey ?? null;
32074
32330
  });
32075
32331
  /**
32332
+ * Resolve the readable-id prefix for a task's project.
32333
+ *
32334
+ * The prefix lives in the project's checked-in `p4.json` (`taskPrefix`), not in
32335
+ * this server's database: it is a property of the repository, agreed on by
32336
+ * everyone who clones it, exactly like the scripts beside it. A task with no
32337
+ * project, a removed project, or a `p4.json` without the field all fall back to
32338
+ * the default `T` - filing the task always beats refusing it over a label.
32339
+ */
32340
+ const resolveTaskReadableIdPrefix = Effect.fn("taskBoard.resolveReadableIdPrefix")(function* (projectId) {
32341
+ if (projectId == null) return "T";
32342
+ const project = yield* (yield* ProjectionProjectRepository).getById({ projectId }).pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
32343
+ if (Option.isNone(project)) return "T";
32344
+ const file = yield* (yield* P4ProjectFileLoader).load(project.value.workspaceRoot);
32345
+ if (Option.isNone(file) || file.value.taskPrefix === void 0) return "T";
32346
+ return file.value.taskPrefix.toUpperCase();
32347
+ });
32348
+ /**
32076
32349
  * Everything that decides whether a task can be started, as one pure function.
32077
32350
  *
32078
32351
  * Kept out of the RPC handler because the three refusals are the whole of the
@@ -42697,9 +42970,12 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
42697
42970
  [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" }),
42698
42971
  [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" }),
42699
42972
  [WS_METHODS.tasksCreate]: (input) => observeRpcEffect$1(WS_METHODS.tasksCreate, Effect.gen(function* () {
42700
- const row = buildTaskRow(input, TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)), yield* nowIso$5, yield* resolveTaskRepositoryKey(input.projectId));
42701
- yield* taskRepository.upsert(row).pipe(Effect.catch(toTaskStoreError("tasks.create")));
42702
- return { task: row };
42973
+ const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
42974
+ const timestamp = yield* nowIso$5;
42975
+ const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
42976
+ const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
42977
+ const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
42978
+ return { task: yield* taskRepository.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("tasks.create"))) };
42703
42979
  }), { "rpc.aggregate": "tasks" }),
42704
42980
  [WS_METHODS.tasksUpdate]: (input) => observeRpcEffect$1(WS_METHODS.tasksUpdate, Effect.gen(function* () {
42705
42981
  const timestamp = yield* nowIso$5;
@@ -43010,12 +43286,13 @@ const websocketRpcRouteLayer = Layer.unwrap(Effect.gen(function* () {
43010
43286
  const serverSelfUpdate = yield* ServerSelfUpdate;
43011
43287
  const taskRepository = yield* TaskRepository;
43012
43288
  const projectionProjects = yield* ProjectionProjectRepository;
43289
+ const p4ProjectFileLoader = yield* P4ProjectFileLoader;
43013
43290
  return HttpRouter.add("GET", "/ws", Effect.gen(function* () {
43014
43291
  const request = yield* HttpServerRequest.HttpServerRequest;
43015
43292
  const serverAuth = yield* EnvironmentAuth;
43016
43293
  const sessions = yield* SessionStore;
43017
43294
  const session = yield* serverAuth.authenticateWebSocketUpgrade(request).pipe(Effect.catchIf(isServerAuthCredentialError, (error) => failEnvironmentAuthInvalid(serverAuthCredentialReason(error))), Effect.catchIf(isServerAuthInternalError, (error) => failEnvironmentInternal("internal_error", error)));
43018
- 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))))));
43295
+ 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))))));
43019
43296
  return yield* Effect.acquireUseRelease(sessions.markConnected(session.sessionId), () => rpcWebSocketHttpEffect, () => sessions.markDisconnected(session.sessionId));
43020
43297
  }).pipe(Effect.catchTags({
43021
43298
  EnvironmentAuthInvalidError: HttpServerRespondable.toResponse,
@@ -43120,17 +43397,16 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
43120
43397
  return Option.some(yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("getById", "the response did not match the contract"))));
43121
43398
  });
43122
43399
  /**
43123
- * Upsert is a create here, and that is a real difference from the local
43124
- * store.
43125
- *
43126
- * The hub mints its own ids and timestamps, so a row written through this
43127
- * comes back with the hub's version of both. The caller's `taskId` is not
43128
- * carried: honouring it would need a hub route that takes one, and inventing
43129
- * that quietly would let two servers mint colliding ids.
43400
+ * Both writes here POST the hub's create route, and that is a real
43401
+ * difference from the local store: the hub mints its own ids, timestamps and
43402
+ * readable id, so a row written through this comes back with the hub's
43403
+ * version of all three. The caller's `taskId` is not carried - honouring it
43404
+ * would need a hub route that takes one, and inventing that quietly would
43405
+ * let two servers mint colliding ids.
43130
43406
  */
43131
- const upsert = (row) => Effect.gen(function* () {
43132
- const settings = yield* requireSettings("upsert");
43133
- const response = yield* send("upsert", settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(`${settings.baseUrl}/tasks`), {
43407
+ const postCreate = (operation, row, readableIdPrefix) => Effect.gen(function* () {
43408
+ const settings = yield* requireSettings(operation);
43409
+ const response = yield* send(operation, settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(`${settings.baseUrl}/tasks`), {
43134
43410
  ...row.projectId === null ? {} : { projectId: row.projectId },
43135
43411
  ...row.repositoryKey === null ? {} : { repositoryKey: row.repositoryKey },
43136
43412
  ...row.threadId === null ? {} : { threadId: row.threadId },
@@ -43139,15 +43415,25 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
43139
43415
  status: row.status,
43140
43416
  priority: row.priority,
43141
43417
  ...row.assignee === null ? {} : { assignee: row.assignee },
43142
- labels: row.labels
43418
+ labels: row.labels,
43419
+ ...row.orderKey === null ? {} : { orderKey: row.orderKey },
43420
+ ...readableIdPrefix === void 0 ? {} : { readableIdPrefix }
43143
43421
  }));
43144
- if (response.status !== 201 && response.status !== 200) return yield* hubUnavailable("upsert", `status ${response.status}`);
43145
- const created = yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("upsert", "the response did not match the contract")));
43422
+ if (response.status !== 201 && response.status !== 200) return yield* hubUnavailable(operation, `status ${response.status}`);
43423
+ const created = yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable(operation, "the response did not match the contract")));
43146
43424
  yield* publish({
43147
43425
  type: "upserted",
43148
43426
  task: created
43149
43427
  });
43428
+ return created;
43150
43429
  });
43430
+ const create = (row, readableIdPrefix) => postCreate("create", row, readableIdPrefix);
43431
+ const upsert = (row) => postCreate("upsert", row).pipe(Effect.asVoid);
43432
+ /**
43433
+ * Served by the same hub route as `getById`: readable ids and UUIDs are
43434
+ * disjoint by shape, so the hub dispatches on what it is handed.
43435
+ */
43436
+ const getByReadableId = (readableId) => getById({ taskId: TaskId.make(readableId) });
43151
43437
  const patch = (input) => Effect.gen(function* () {
43152
43438
  const settings = yield* requireSettings("patch");
43153
43439
  const { taskId, ...fields } = input;
@@ -43222,10 +43508,12 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
43222
43508
  }).pipe(Effect.ignoreCause({ log: true }));
43223
43509
  yield* Effect.forever(pollOnce.pipe(Effect.andThen(Effect.sleep(POLL_INTERVAL)))).pipe(Effect.forkScoped);
43224
43510
  return {
43511
+ create,
43225
43512
  upsert,
43226
43513
  patch,
43227
43514
  patchIfStatus,
43228
43515
  getById,
43516
+ getByReadableId,
43229
43517
  list,
43230
43518
  deleteById,
43231
43519
  streamChanges: Stream.fromPubSub(changes),
@@ -43275,10 +43563,12 @@ const makeRoutedTaskRepository = Effect.gen(function* () {
43275
43563
  */
43276
43564
  const pick = usingHub.pipe(Effect.map((hub) => hub ? remote : local));
43277
43565
  return {
43566
+ create: (row, readableIdPrefix) => pick.pipe(Effect.flatMap((repository) => repository.create(row, readableIdPrefix))),
43278
43567
  upsert: (row) => pick.pipe(Effect.flatMap((repository) => repository.upsert(row))),
43279
43568
  patch: (input, updatedAt) => pick.pipe(Effect.flatMap((repository) => repository.patch(input, updatedAt))),
43280
43569
  patchIfStatus: (input, updatedAt) => pick.pipe(Effect.flatMap((repository) => repository.patchIfStatus(input, updatedAt))),
43281
43570
  getById: (input) => pick.pipe(Effect.flatMap((repository) => repository.getById(input))),
43571
+ getByReadableId: (readableId) => pick.pipe(Effect.flatMap((repository) => repository.getByReadableId(readableId))),
43282
43572
  list: (filter) => pick.pipe(Effect.flatMap((repository) => repository.list(filter))),
43283
43573
  deleteById: (input) => pick.pipe(Effect.flatMap((repository) => repository.deleteById(input))),
43284
43574
  get streamChanges() {
@@ -81083,8 +81373,15 @@ const writeDependencies = [
81083
81373
  ProjectionProjectRepository,
81084
81374
  RepositoryIdentityResolver
81085
81375
  ];
81086
- /** `task_create` also mints the task id, so it needs the crypto service. */
81087
- const createDependencies = [...writeDependencies, Crypto.Crypto];
81376
+ /**
81377
+ * `task_create` also mints the task id (crypto) and resolves the readable-id
81378
+ * prefix from the project's checked-in `p4.json` (the file loader).
81379
+ */
81380
+ const createDependencies = [
81381
+ ...writeDependencies,
81382
+ Crypto.Crypto,
81383
+ P4ProjectFileLoader
81384
+ ];
81088
81385
  const TaskListResult = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
81089
81386
  const TaskListTool = Tool.make("task_list", {
81090
81387
  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.",
@@ -81094,7 +81391,7 @@ const TaskListTool = Tool.make("task_list", {
81094
81391
  dependencies
81095
81392
  }).annotate(Tool.Title, "List tasks").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
81096
81393
  const TaskGetTool = Tool.make("task_get", {
81097
- description: "Read a single task by id, including its body, labels and linked thread.",
81394
+ 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).",
81098
81395
  parameters: TaskGetInput,
81099
81396
  success: Task,
81100
81397
  failure: TaskToolError,
@@ -81144,6 +81441,15 @@ const TaskToolkit = Toolkit.make(TaskListTool, TaskGetTool, TaskCurrentTool, Tas
81144
81441
  //#endregion
81145
81442
  //#region src/mcp/toolkits/tasks/handlers.ts
81146
81443
  const nowIso$1 = Effect.map(DateTime.now, DateTime.formatIso);
81444
+ /**
81445
+ * Agents reference tasks by whichever id they have seen, so any `taskId`
81446
+ * parameter accepts the readable id (`P4-12`) as well as the UUID. The two are
81447
+ * disjoint by shape, which is what makes the dispatch safe.
81448
+ */
81449
+ const findTask = (taskId) => Effect.gen(function* () {
81450
+ const tasks = yield* TaskRepository;
81451
+ return isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
81452
+ });
81147
81453
  const TaskToolkitHandlersLive = TaskToolkit.toLayer({
81148
81454
  task_list: (input) => Effect.gen(function* () {
81149
81455
  yield* requireTaskCapability();
@@ -81151,7 +81457,7 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
81151
81457
  }),
81152
81458
  task_get: (input) => Effect.gen(function* () {
81153
81459
  yield* requireTaskCapability();
81154
- const found = yield* (yield* TaskRepository).getById({ taskId: input.taskId }).pipe(Effect.catch(toTaskStoreError("task_get")));
81460
+ const found = yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_get")));
81155
81461
  if (Option.isNone(found)) return yield* new TaskNotFoundError({ taskId: input.taskId });
81156
81462
  return found.value;
81157
81463
  }),
@@ -81167,16 +81473,25 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
81167
81473
  yield* requireTaskCapability();
81168
81474
  const tasks = yield* TaskRepository;
81169
81475
  const crypto = yield* Crypto.Crypto;
81170
- const row = buildTaskRow(input, TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)), yield* nowIso$1, yield* resolveTaskRepositoryKey(input.projectId));
81171
- yield* tasks.upsert(row).pipe(Effect.catch(toTaskStoreError("task_create")));
81172
- return row;
81476
+ const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
81477
+ const timestamp = yield* nowIso$1;
81478
+ const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
81479
+ const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
81480
+ const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
81481
+ return yield* tasks.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("task_create")));
81173
81482
  }),
81174
81483
  task_update: (input) => Effect.gen(function* () {
81175
81484
  yield* requireTaskCapability();
81176
81485
  const tasks = yield* TaskRepository;
81177
81486
  const timestamp = yield* nowIso$1;
81178
- const patch = input.projectId === void 0 ? input : {
81487
+ const taskId = isReadableTaskId(input.taskId) ? Option.getOrNull(yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_update"))))?.taskId : input.taskId;
81488
+ if (taskId === void 0) return yield* new TaskNotFoundError({ taskId: input.taskId });
81489
+ const patch = input.projectId === void 0 ? {
81490
+ ...input,
81491
+ taskId
81492
+ } : {
81179
81493
  ...input,
81494
+ taskId,
81180
81495
  repositoryKey: yield* resolveTaskRepositoryKey(input.projectId)
81181
81496
  };
81182
81497
  const patched = yield* tasks.patch(patch, timestamp).pipe(Effect.catch(toTaskStoreError("task_update")));
@@ -84890,7 +85205,7 @@ const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(Checkpo
84890
85205
  */
84891
85206
  const AssetSyncLive = layer$54.pipe(Layer.provideMerge(layer$55), Layer.provideMerge(layer$56));
84892
85207
  const RuntimeServicesLive = layer$44.pipe(Layer.provideMerge(AssetSyncLive), Layer.provideMerge(RuntimeDependenciesLive));
84893
- 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));
85208
+ 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));
84894
85209
  const makeServerLayer = Layer.unwrap(Effect.gen(function* () {
84895
85210
  const config = yield* ServerConfig$1;
84896
85211
  yield* fixPath();