@p4code/cli 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/bin.mjs +448 -201
  2. package/dist/client/assets/{DiffPanel-B_RUbYdJ.js → DiffPanel-D4tNpljU.js} +2 -2
  3. package/dist/client/assets/{FilePreviewPanel-D_TE6anL.js → FilePreviewPanel-xICJSzZ6.js} +2 -2
  4. package/dist/client/assets/{PreviewPanel-Dx5mB3w6.js → PreviewPanel-CK9d1fc5.js} +2 -2
  5. package/dist/client/assets/{angular-html-Z6GyEAg_.js → angular-html-BgzZxO9w.js} +2 -2
  6. package/dist/client/assets/{angular-ts-BdawRcND.js → angular-ts-YFEXVhe6.js} +2 -2
  7. package/dist/client/assets/{apl-0Nzwg3c9.js → apl-BoBwF9HI.js} +2 -2
  8. package/dist/client/assets/arrow-right-CXxWzaql.js +2 -0
  9. package/dist/client/assets/{astro-iJpYXO_m.js → astro-Ds5TXooD.js} +2 -2
  10. package/dist/client/assets/{blade-BeMtIYzp.js → blade-CDtJf5YR.js} +2 -2
  11. package/dist/client/assets/{c-n5Rj77uj.js → c-JH0umSmP.js} +2 -2
  12. package/dist/client/assets/{cobol-CfsSyjzP.js → cobol-gMss6BA3.js} +2 -2
  13. package/dist/client/assets/{coffee-LZB1mvnz.js → coffee-DESPXYwo.js} +2 -2
  14. package/dist/client/assets/{cpp-C689OQY_.js → cpp-CZztA1XN.js} +2 -2
  15. package/dist/client/assets/{crystal-CC7u-hEs.js → crystal-CDWlJpLE.js} +2 -2
  16. package/dist/client/assets/{css-eVqGM3Yx.js → css-CNPTTtX1.js} +2 -2
  17. package/dist/client/assets/{edge-CvU3NO69.js → edge-DSlrkPAr.js} +2 -2
  18. package/dist/client/assets/{elixir-gblyDHJ2.js → elixir-DrLl_wv7.js} +2 -2
  19. package/dist/client/assets/{elm-BEDREZ-r.js → elm-CwB92FlT.js} +2 -2
  20. package/dist/client/assets/{erb-C_pVTICR.js → erb-DXHrZ2ye.js} +2 -2
  21. package/dist/client/assets/{fileCommentAnnotations-Z0wcn9Z2.js → fileCommentAnnotations-CuJ4hnrK.js} +2 -2
  22. package/dist/client/assets/{git-rebase-BvBzTqE6.js → git-rebase-uRnCoWg0.js} +2 -2
  23. package/dist/client/assets/{glimmer-js-y6xvI0dn.js → glimmer-js-DU9fMoZx.js} +2 -2
  24. package/dist/client/assets/{glimmer-ts-BrWleVTO.js → glimmer-ts-Bftqha5Z.js} +2 -2
  25. package/dist/client/assets/{glsl-Bkrdoinn.js → glsl-CVVphTeN.js} +2 -2
  26. package/dist/client/assets/{graphql-CSx1KLSt.js → graphql-CwED2DHh.js} +2 -2
  27. package/dist/client/assets/{hack-CEPENt6B.js → hack-CwiQSrzl.js} +2 -2
  28. package/dist/client/assets/{haml-CR4MMWD-.js → haml-C2iiqAZE.js} +2 -2
  29. package/dist/client/assets/{handlebars-BvZ81eM2.js → handlebars-BtvKqvl0.js} +2 -2
  30. package/dist/client/assets/{html-CJJ_Yk-p.js → html-BRvZZmrv.js} +2 -2
  31. package/dist/client/assets/{html-derivative-DyGTUpT_.js → html-derivative-rVjUDzCo.js} +2 -2
  32. package/dist/client/assets/{http-CthnayZT.js → http-Cvb4plLT.js} +2 -2
  33. package/dist/client/assets/{hurl-CySpRHwn.js → hurl-TX6OEub-.js} +2 -2
  34. package/dist/client/assets/{index-CstNfQU9.css → index-CTDey6k4.css} +1 -1
  35. package/dist/client/assets/index-DJPm8QPz.js +664 -0
  36. package/dist/client/assets/{java-eLetcmnB.js → java-CudutWch.js} +2 -2
  37. package/dist/client/assets/{javascript-bze-DcnN.js → javascript-D7gB3c_x.js} +2 -2
  38. package/dist/client/assets/{jinja-BwBfXVzv.js → jinja-Ce0WqJfV.js} +2 -2
  39. package/dist/client/assets/{jison-BG-6HBeN.js → jison-2U6TlYgq.js} +2 -2
  40. package/dist/client/assets/{json-D04kql_u.js → json-oe5h_2FV.js} +2 -2
  41. package/dist/client/assets/{jsx-B3RhaC78.js → jsx-B0f4Z6sx.js} +2 -2
  42. package/dist/client/assets/{julia-CLYU2NBT.js → julia-By6UDEAG.js} +2 -2
  43. package/dist/client/assets/{just-CQGSktLF.js → just-YTIO1wok.js} +2 -2
  44. package/dist/client/assets/{latex-B2r18a0i.js → latex-B9HLUatB.js} +2 -2
  45. package/dist/client/assets/{liquid-DSPVRc0S.js → liquid-DajcsI9e.js} +2 -2
  46. package/dist/client/assets/{lua-CBxs-my9.js → lua-CINm0eWo.js} +2 -2
  47. package/dist/client/assets/{marko-FkXUABHT.js → marko-DXvL73SK.js} +2 -2
  48. package/dist/client/assets/{mdc-BW-qJ07Y.js → mdc-CYsP-lJY.js} +2 -2
  49. package/dist/client/assets/{nginx-r-F4ISTc.js → nginx-CPKmhI0k.js} +2 -2
  50. package/dist/client/assets/{nim-CqGONDki.js → nim-DodS9Uy_.js} +2 -2
  51. package/dist/client/assets/{perl-YqxSDkbB.js → perl-COac5jCW.js} +2 -2
  52. package/dist/client/assets/{php-o53m7ISl.js → php-DLugOmp_.js} +2 -2
  53. package/dist/client/assets/{pug-DvC1IxmZ.js → pug-ukoxsRP-.js} +2 -2
  54. package/dist/client/assets/{qml-B_UHgwrf.js → qml-DEoQyDDD.js} +2 -2
  55. package/dist/client/assets/{r-BslRtXqA.js → r-CePyxHEB.js} +2 -2
  56. package/dist/client/assets/{razor-DbFRY8EJ.js → razor-uhGlTCzi.js} +2 -2
  57. package/dist/client/assets/{regexp-LUjvkiV5.js → regexp-BYNm_E6v.js} +2 -2
  58. package/dist/client/assets/{rst-DjMRC4dl.js → rst-BMMyVK7q.js} +2 -2
  59. package/dist/client/assets/{ruby-D1SSXqCW.js → ruby-BBl_q7lV.js} +2 -2
  60. package/dist/client/assets/{sas-8pH9ZpYC.js → sas-q_pdbMhY.js} +2 -2
  61. package/dist/client/assets/{scss-CLXZYmiL.js → scss-Bf5qLSyj.js} +2 -2
  62. package/dist/client/assets/{shellscript-C3x4iGUN.js → shellscript-CnkO-FF6.js} +2 -2
  63. package/dist/client/assets/{shellsession-4iw-70AB.js → shellsession-CGTIDec0.js} +2 -2
  64. package/dist/client/assets/{soy-BmuiXqhy.js → soy-D_X4Ckow.js} +2 -2
  65. package/dist/client/assets/{sql-CYtr1JGt.js → sql-wOawmdfM.js} +2 -2
  66. package/dist/client/assets/{stata-D-p-ifu4.js → stata-CKQN4lyV.js} +2 -2
  67. package/dist/client/assets/{surrealql-BQtaSVD9.js → surrealql-tjpKGjMP.js} +2 -2
  68. package/dist/client/assets/{svelte-BZ0IuCtg.js → svelte-mCzdghlf.js} +2 -2
  69. package/dist/client/assets/{templ-D_0C9uOG.js → templ-COV4wNUr.js} +2 -2
  70. package/dist/client/assets/{tex-Da564oMl.js → tex-CC1f6JM7.js} +2 -2
  71. package/dist/client/assets/textarea-BDADSCjn.js +46 -0
  72. package/dist/client/assets/{ts-tags-CCvt3g0S.js → ts-tags-CrIJ77ir.js} +2 -2
  73. package/dist/client/assets/{tsx-C1pKkLrg.js → tsx-IX8SGkp3.js} +2 -2
  74. package/dist/client/assets/{twig-ulf4RPD9.js → twig-DXR_a9UJ.js} +2 -2
  75. package/dist/client/assets/{typescript-Cm2BDYNJ.js → typescript-BgLtzzgs.js} +2 -2
  76. package/dist/client/assets/{vue-D38YVq_o.js → vue-Dzfv___3.js} +2 -2
  77. package/dist/client/assets/{vue-html-Dw9Iwm-j.js → vue-html-BHyzHcFt.js} +2 -2
  78. package/dist/client/assets/{vue-vine-DGhLcVZx.js → vue-vine-DOSBq7Kw.js} +2 -2
  79. package/dist/client/assets/{xml-Bm9IDqQ-.js → xml-BI5bRQfx.js} +2 -2
  80. package/dist/client/assets/{xsl-CLGFKBCX.js → xsl-B4FzWFML.js} +2 -2
  81. package/dist/client/assets/{yaml-C5Olrwb1.js → yaml-CHgKneKM.js} +2 -2
  82. package/dist/client/index.html +3 -3
  83. package/package.json +1 -1
  84. package/dist/client/assets/arrow-right-BVDRuCFE.js +0 -2
  85. package/dist/client/assets/index-CTDRPB7h.js +0 -664
  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.8";
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
@@ -7233,10 +7077,18 @@ const AssigneeField = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSI
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." }));
7235
7079
  const LabelsField = Schema$1.Array(TaskLabel).check(Schema$1.isMaxLength(TASK_LABELS_MAX_COUNT)).pipe(Schema$1.annotateEncoded({ description: "Free-form tags used to group and filter tasks." }));
7236
- const TaskIdField = TaskId.pipe(Schema$1.annotateEncoded({ description: "Id of the task to act on." }));
7080
+ const TaskIdField = TaskId.pipe(Schema$1.annotateEncoded({ description: "Id of the task to act on - the UUID or the readable id (e.g. P4-12)." }));
7237
7081
  const Task = Schema$1.Struct({
7238
7082
  taskId: TaskId,
7239
7083
  /**
7084
+ * The human-readable id, e.g. `T-1` or `P4-12`: a per-prefix sequence
7085
+ * allocated by the store that owns the row, never reused, with gaps where
7086
+ * tasks were deleted. Nullable only for version skew - a row read from a
7087
+ * store that has not run the backfill migration decodes to `null` rather
7088
+ * than failing the whole board.
7089
+ */
7090
+ readableId: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7091
+ /**
7240
7092
  * The project this task belongs to, or `null` for a task that is not scoped
7241
7093
  * to one. This references the existing workspace project registry rather than
7242
7094
  * a task-board-private notion of a project.
@@ -7277,6 +7129,21 @@ const TaskCreateInput = Schema$1.Struct({
7277
7129
  labels: Schema$1.optional(LabelsField)
7278
7130
  });
7279
7131
  /**
7132
+ * What an environment server sends the hub to file a task.
7133
+ *
7134
+ * Extends the public create input with the readable-id prefix, because the two
7135
+ * halves of the answer live on different machines: the prefix comes from the
7136
+ * project's checked-in `p4.json`, which only the submitting server can read,
7137
+ * while the sequence number must be allocated by the hub, which is the only
7138
+ * party that sees every machine's writes. Deliberately not part of
7139
+ * `TaskCreateInput` itself so an agent filing over MCP cannot pick a prefix -
7140
+ * the prefix belongs to the project, not to the caller.
7141
+ */
7142
+ const HubTaskCreateInput = Schema$1.Struct({
7143
+ ...TaskCreateInput.fields,
7144
+ readableIdPrefix: Schema$1.optional(TrimmedNonEmptyString)
7145
+ });
7146
+ /**
7280
7147
  * Every field is optional and an omitted field is left untouched. The nullable
7281
7148
  * fields accept an explicit `null` to clear them, which is why they are
7282
7149
  * `NullOr` inside `optional` rather than merely optional.
@@ -7480,6 +7347,176 @@ const TaskToolError = Schema$1.Union([
7480
7347
  TaskStoreError
7481
7348
  ]);
7482
7349
  //#endregion
7350
+ //#region ../../packages/contracts/src/p4ProjectFile.ts
7351
+ /** File name of the checked-in P4 project file, resolved at the workspace root. */
7352
+ const P4_PROJECT_FILE_NAME = "p4.json";
7353
+ /** Public URL of the published JSON Schema for {@link P4ProjectFile}. */
7354
+ const P4_PROJECT_FILE_SCHEMA_URL = "https://p4.codes/schema/p4.json";
7355
+ const P4_PROJECT_FILE_PATH_MAX_LENGTH = 512;
7356
+ const P4_PROJECT_FILE_MAX_SCRIPTS = 50;
7357
+ const trimmedNonEmpty = (annotations, maxLength) => {
7358
+ const annotated = Schema$1.String.annotate(annotations);
7359
+ const encoded = maxLength === void 0 ? annotated.check(Schema$1.isNonEmpty()) : annotated.check(Schema$1.isNonEmpty(), Schema$1.isMaxLength(maxLength));
7360
+ return encoded.pipe(Schema$1.decodeTo(encoded, SchemaTransformation.trim()));
7361
+ };
7362
+ const P4ProjectFileScript = Schema$1.Struct({
7363
+ name: trimmedNonEmpty({ description: "Display name for the script, shown in the P4Code scripts menu." }),
7364
+ command: trimmedNonEmpty({ description: "Shell command executed in a P4Code terminal at the project root." }),
7365
+ icon: Schema$1.optionalKey(ProjectScriptIcon.annotate({ description: "Icon shown next to the script in the scripts menu. Defaults to \"play\"." })),
7366
+ runOnWorktreeCreate: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, the script runs automatically after a worktree is created for a new thread." })),
7367
+ previewUrl: Schema$1.optionalKey(trimmedNonEmpty({ description: "URL opened in the in-app browser preview when this script runs. Only honored on the desktop build." })),
7368
+ autoOpenPreview: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, automatically open the preview panel at `previewUrl` the moment the script starts." }))
7369
+ }).annotate({ description: "A project script that team members can import into P4Code." });
7370
+ const P4ProjectFile = Schema$1.Struct({
7371
+ $schema: Schema$1.optionalKey(Schema$1.String.annotate({ description: `URL of the JSON Schema for this file, typically "${P4_PROJECT_FILE_SCHEMA_URL}".` })),
7372
+ iconPath: Schema$1.optionalKey(trimmedNonEmpty({ description: "Workspace-relative path to the project icon (e.g. \"assets/logo.svg\"). Checked before P4Code's built-in icon locations." }, P4_PROJECT_FILE_PATH_MAX_LENGTH)),
7373
+ scripts: Schema$1.optionalKey(Schema$1.Array(P4ProjectFileScript).annotate({ description: "Project scripts shared with everyone who opens this repository in P4Code." }).check(Schema$1.isMaxLength(P4_PROJECT_FILE_MAX_SCRIPTS))),
7374
+ taskPrefix: Schema$1.optionalKey(trimmedNonEmpty({ description: "Prefix for this project's readable task ids on the task board (e.g. \"P4\" numbers tasks P4-1, P4-2, ...). Letters and digits, starting with a letter, at most 10 characters; compared case-insensitively. Tasks without a project use \"T\"." }).check(Schema$1.isPattern(TASK_READABLE_ID_PREFIX_PATTERN)))
7375
+ }).annotate({
7376
+ title: "P4 project file",
7377
+ description: "Checked-in project configuration for P4Code (p4.json at the repository root). See https://p4.codes for documentation."
7378
+ });
7379
+ //#endregion
7380
+ //#region ../../packages/contracts/src/project.ts
7381
+ const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200;
7382
+ const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512;
7383
+ const PROJECT_READ_FILE_PATH_MAX_LENGTH = 512;
7384
+ const ProjectSearchEntriesInput = Schema$1.Struct({
7385
+ cwd: TrimmedNonEmptyString,
7386
+ query: TrimmedNonEmptyString.check(Schema$1.isMaxLength(256)),
7387
+ limit: PositiveInt.check(Schema$1.isLessThanOrEqualTo(PROJECT_SEARCH_ENTRIES_MAX_LIMIT))
7388
+ });
7389
+ const ProjectEntryKind = Schema$1.Literals(["file", "directory"]);
7390
+ const ProjectEntry = Schema$1.Struct({
7391
+ path: TrimmedNonEmptyString,
7392
+ kind: ProjectEntryKind
7393
+ });
7394
+ const ProjectSearchEntriesResult = Schema$1.Struct({
7395
+ entries: Schema$1.Array(ProjectEntry),
7396
+ truncated: Schema$1.Boolean
7397
+ });
7398
+ const ProjectListEntriesInput = Schema$1.Struct({ cwd: TrimmedNonEmptyString });
7399
+ const ProjectListEntriesResult = Schema$1.Struct({
7400
+ entries: Schema$1.Array(ProjectEntry),
7401
+ truncated: Schema$1.Boolean
7402
+ });
7403
+ const ProjectEntriesFailure = Schema$1.Literals([
7404
+ "workspace_root_not_found",
7405
+ "workspace_root_create_failed",
7406
+ "workspace_root_stat_failed",
7407
+ "workspace_root_not_directory",
7408
+ "search_index_create_failed",
7409
+ "search_index_scan_timed_out",
7410
+ "search_index_search_failed"
7411
+ ]);
7412
+ function decodedProjectErrorMessage(props) {
7413
+ if (!("message" in props)) return void 0;
7414
+ return typeof props.message === "string" ? props.message : void 0;
7415
+ }
7416
+ var ProjectSearchEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectSearchEntriesError", {
7417
+ cwd: Schema$1.optional(TrimmedNonEmptyString),
7418
+ queryLength: Schema$1.optional(NonNegativeInt),
7419
+ limit: Schema$1.optional(PositiveInt),
7420
+ failure: Schema$1.optional(ProjectEntriesFailure),
7421
+ normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
7422
+ timeout: Schema$1.optional(TrimmedNonEmptyString),
7423
+ detail: Schema$1.optional(TrimmedNonEmptyString),
7424
+ message: TrimmedNonEmptyString,
7425
+ cause: Schema$1.optional(Schema$1.Defect())
7426
+ }) {
7427
+ constructor(props) {
7428
+ super({
7429
+ ...props,
7430
+ message: decodedProjectErrorMessage(props) ?? `Failed to search workspace entries in '${props.cwd}'.`
7431
+ });
7432
+ }
7433
+ };
7434
+ var ProjectListEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectListEntriesError", {
7435
+ cwd: Schema$1.optional(TrimmedNonEmptyString),
7436
+ failure: Schema$1.optional(ProjectEntriesFailure),
7437
+ normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
7438
+ timeout: Schema$1.optional(TrimmedNonEmptyString),
7439
+ detail: Schema$1.optional(TrimmedNonEmptyString),
7440
+ message: TrimmedNonEmptyString,
7441
+ cause: Schema$1.optional(Schema$1.Defect())
7442
+ }) {
7443
+ constructor(props) {
7444
+ super({
7445
+ ...props,
7446
+ message: decodedProjectErrorMessage(props) ?? `Failed to list workspace entries in '${props.cwd}'.`
7447
+ });
7448
+ }
7449
+ };
7450
+ const ProjectReadFileInput = Schema$1.Struct({
7451
+ cwd: TrimmedNonEmptyString,
7452
+ relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_READ_FILE_PATH_MAX_LENGTH))
7453
+ });
7454
+ const ProjectReadFileResult = Schema$1.Struct({
7455
+ relativePath: TrimmedNonEmptyString,
7456
+ contents: Schema$1.String,
7457
+ byteLength: NonNegativeInt,
7458
+ truncated: Schema$1.Boolean
7459
+ });
7460
+ const ProjectFileFailure = Schema$1.Literals([
7461
+ "workspace_path_outside_root",
7462
+ "resolved_path_outside_root",
7463
+ "path_not_file",
7464
+ "binary_file",
7465
+ "operation_failed"
7466
+ ]);
7467
+ const ProjectFileOperation = Schema$1.Literals([
7468
+ "realpath-workspace-root",
7469
+ "realpath-target",
7470
+ "open",
7471
+ "stat",
7472
+ "read",
7473
+ "close",
7474
+ "make-directory",
7475
+ "write-file"
7476
+ ]);
7477
+ var ProjectReadFileError = class extends Schema$1.TaggedErrorClass()("ProjectReadFileError", {
7478
+ cwd: Schema$1.optional(TrimmedNonEmptyString),
7479
+ relativePath: Schema$1.optional(TrimmedNonEmptyString),
7480
+ failure: Schema$1.optional(ProjectFileFailure),
7481
+ resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
7482
+ resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
7483
+ operation: Schema$1.optional(ProjectFileOperation),
7484
+ operationPath: Schema$1.optional(TrimmedNonEmptyString),
7485
+ message: TrimmedNonEmptyString,
7486
+ cause: Schema$1.optional(Schema$1.Defect())
7487
+ }) {
7488
+ constructor(props) {
7489
+ super({
7490
+ ...props,
7491
+ message: decodedProjectErrorMessage(props) ?? `Failed to read workspace file '${props.relativePath}' in '${props.cwd}'.`
7492
+ });
7493
+ }
7494
+ };
7495
+ const ProjectWriteFileInput = Schema$1.Struct({
7496
+ cwd: TrimmedNonEmptyString,
7497
+ relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_WRITE_FILE_PATH_MAX_LENGTH)),
7498
+ contents: Schema$1.String
7499
+ });
7500
+ const ProjectWriteFileResult = Schema$1.Struct({ relativePath: TrimmedNonEmptyString });
7501
+ var ProjectWriteFileError = class extends Schema$1.TaggedErrorClass()("ProjectWriteFileError", {
7502
+ cwd: Schema$1.optional(TrimmedNonEmptyString),
7503
+ relativePath: Schema$1.optional(TrimmedNonEmptyString),
7504
+ failure: Schema$1.optional(ProjectFileFailure),
7505
+ resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
7506
+ resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
7507
+ operation: Schema$1.optional(ProjectFileOperation),
7508
+ operationPath: Schema$1.optional(TrimmedNonEmptyString),
7509
+ message: TrimmedNonEmptyString,
7510
+ cause: Schema$1.optional(Schema$1.Defect())
7511
+ }) {
7512
+ constructor(props) {
7513
+ super({
7514
+ ...props,
7515
+ message: decodedProjectErrorMessage(props) ?? `Failed to write workspace file '${props.relativePath}' in '${props.cwd}'.`
7516
+ });
7517
+ }
7518
+ };
7519
+ //#endregion
7483
7520
  //#region ../../packages/contracts/src/agentAsset.ts
7484
7521
  /**
7485
7522
  * User-scope agent assets — the things that make an agent behave the same on
@@ -12030,6 +12067,51 @@ var _036_TaskRepositoryKey_default = Effect.gen(function* () {
12030
12067
  `;
12031
12068
  });
12032
12069
  //#endregion
12070
+ //#region src/persistence/Migrations/037_TaskReadableIds.ts
12071
+ /**
12072
+ * Human-readable task ids (`T-1`, `P4-12`).
12073
+ *
12074
+ * `task_sequences` holds one row per prefix with the highest number ever
12075
+ * handed out. It only grows: deleting a task does not return its number, so a
12076
+ * readable id, once uttered, never comes to mean a different task.
12077
+ *
12078
+ * Existing rows are backfilled with the default `T` prefix in creation order.
12079
+ * Their projects' prefixes are deliberately not consulted — a prefix lives in
12080
+ * the project's checked-in `p4.json`, and a migration that shells out to read
12081
+ * workspaces (which may no longer be on disk) is exactly what a migration
12082
+ * should not be. New tasks pick up their project's prefix from creation on.
12083
+ */
12084
+ var _037_TaskReadableIds_default = Effect.gen(function* () {
12085
+ const sql = yield* SqlClient.SqlClient;
12086
+ yield* sql`
12087
+ CREATE TABLE IF NOT EXISTS task_sequences (
12088
+ prefix TEXT PRIMARY KEY,
12089
+ last_seq INTEGER NOT NULL
12090
+ )
12091
+ `;
12092
+ yield* sql`ALTER TABLE tasks ADD COLUMN readable_id TEXT`;
12093
+ yield* sql`
12094
+ UPDATE tasks
12095
+ SET readable_id = 'T-' || (
12096
+ SELECT COUNT(*)
12097
+ FROM tasks AS earlier
12098
+ WHERE earlier.created_at < tasks.created_at
12099
+ OR (earlier.created_at = tasks.created_at AND earlier.task_id <= tasks.task_id)
12100
+ )
12101
+ WHERE readable_id IS NULL
12102
+ `;
12103
+ yield* sql`
12104
+ INSERT INTO task_sequences (prefix, last_seq)
12105
+ SELECT 'T', COUNT(*) FROM tasks
12106
+ WHERE true
12107
+ ON CONFLICT (prefix) DO UPDATE SET last_seq = MAX(last_seq, excluded.last_seq)
12108
+ `;
12109
+ yield* sql`
12110
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_readable_id
12111
+ ON tasks(readable_id)
12112
+ `;
12113
+ });
12114
+ //#endregion
12033
12115
  //#region src/persistence/Migrations.ts
12034
12116
  /**
12035
12117
  * MigrationsLive - Migration runner with inline loader
@@ -12230,6 +12312,11 @@ const migrationEntries = [
12230
12312
  36,
12231
12313
  "TaskRepositoryKey",
12232
12314
  _036_TaskRepositoryKey_default
12315
+ ],
12316
+ [
12317
+ 37,
12318
+ "TaskReadableIds",
12319
+ _037_TaskReadableIds_default
12233
12320
  ]
12234
12321
  ];
12235
12322
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -13853,6 +13940,7 @@ const TaskListQuery = Schema$1.Struct({
13853
13940
  });
13854
13941
  const TASK_COLUMNS = `
13855
13942
  task_id AS "taskId",
13943
+ readable_id AS "readableId",
13856
13944
  project_id AS "projectId",
13857
13945
  repository_key AS "repositoryKey",
13858
13946
  thread_id AS "threadId",
@@ -13874,6 +13962,7 @@ const makeTaskRepository = Effect.gen(function* () {
13874
13962
  execute: (row) => sql`
13875
13963
  INSERT INTO tasks (
13876
13964
  task_id,
13965
+ readable_id,
13877
13966
  project_id,
13878
13967
  repository_key,
13879
13968
  thread_id,
@@ -13888,6 +13977,7 @@ const makeTaskRepository = Effect.gen(function* () {
13888
13977
  )
13889
13978
  VALUES (
13890
13979
  ${row.taskId},
13980
+ ${row.readableId},
13891
13981
  ${row.projectId},
13892
13982
  ${row.repositoryKey},
13893
13983
  ${row.threadId},
@@ -13902,6 +13992,7 @@ const makeTaskRepository = Effect.gen(function* () {
13902
13992
  )
13903
13993
  ON CONFLICT (task_id)
13904
13994
  DO UPDATE SET
13995
+ readable_id = excluded.readable_id,
13905
13996
  project_id = excluded.project_id,
13906
13997
  repository_key = excluded.repository_key,
13907
13998
  thread_id = excluded.thread_id,
@@ -13920,6 +14011,26 @@ const makeTaskRepository = Effect.gen(function* () {
13920
14011
  Result: TaskDbRow,
13921
14012
  execute: ({ taskId }) => sql`SELECT ${sql.literal(TASK_COLUMNS)} FROM tasks WHERE task_id = ${taskId}`
13922
14013
  });
14014
+ const getTaskRowByReadableId = SqlSchema.findOneOption({
14015
+ Request: Schema$1.String,
14016
+ Result: TaskDbRow,
14017
+ execute: (readableId) => sql`SELECT ${sql.literal(TASK_COLUMNS)} FROM tasks WHERE readable_id = ${readableId}`
14018
+ });
14019
+ /**
14020
+ * Take the next number for a prefix, creating the sequence at 1 on first
14021
+ * use. The row is never reset or decremented - a deleted task's number is
14022
+ * gone - which is what makes a readable id safe to say out loud.
14023
+ */
14024
+ const allocateReadableIdSeq = SqlSchema.findAll({
14025
+ Request: Schema$1.Struct({ prefix: Schema$1.String }),
14026
+ Result: Schema$1.Struct({ lastSeq: Schema$1.Int }),
14027
+ execute: ({ prefix }) => sql`
14028
+ INSERT INTO task_sequences (prefix, last_seq)
14029
+ VALUES (${prefix}, 1)
14030
+ ON CONFLICT (prefix) DO UPDATE SET last_seq = last_seq + 1
14031
+ RETURNING last_seq AS "lastSeq"
14032
+ `
14033
+ });
13923
14034
  const listTaskRows = SqlSchema.findAll({
13924
14035
  Request: TaskListQuery,
13925
14036
  Result: TaskDbRow,
@@ -13942,7 +14053,31 @@ const makeTaskRepository = Effect.gen(function* () {
13942
14053
  type: "upserted",
13943
14054
  task: row
13944
14055
  })));
14056
+ /**
14057
+ * The allocation and the insert share one transaction on purpose: a number
14058
+ * taken outside it would either be lost on a failed insert (a gap nobody
14059
+ * caused) or, worse, handed to two concurrent creates.
14060
+ */
14061
+ const create = (row, readableIdPrefix) => sql.withTransaction(Effect.gen(function* () {
14062
+ const prefix = readableIdPrefix.toUpperCase();
14063
+ const seq = (yield* allocateReadableIdSeq({ prefix }))[0]?.lastSeq;
14064
+ if (seq === void 0) return yield* new PersistenceSqlError({
14065
+ operation: "TaskRepository.create:allocate",
14066
+ detail: "the sequence upsert returned no row",
14067
+ cause: null
14068
+ });
14069
+ const task = {
14070
+ ...row,
14071
+ readableId: `${prefix}-${seq}`
14072
+ };
14073
+ yield* upsertTaskRow(task);
14074
+ return task;
14075
+ })).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.create:query")), Effect.tap((task) => publish({
14076
+ type: "upserted",
14077
+ task
14078
+ })));
13945
14079
  const getById = (input) => getTaskRow(input).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.getById:query")));
14080
+ const getByReadableId = (readableId) => getTaskRowByReadableId(readableId.toUpperCase()).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.getByReadableId:query")));
13946
14081
  const list = (filter) => listTaskRows({
13947
14082
  projectId: filter.projectId ?? null,
13948
14083
  repositoryKey: filter.repositoryKey ?? null,
@@ -14017,10 +14152,12 @@ const makeTaskRepository = Effect.gen(function* () {
14017
14152
  taskId: input.taskId
14018
14153
  })));
14019
14154
  return {
14155
+ create,
14020
14156
  upsert,
14021
14157
  patch,
14022
14158
  patchIfStatus,
14023
14159
  getById,
14160
+ getByReadableId,
14024
14161
  list,
14025
14162
  deleteById,
14026
14163
  streamChanges: Stream.fromPubSub(changes),
@@ -14109,12 +14246,13 @@ const getTaskRoute = HttpRouter.add("GET", "/tasks/:taskId", respondToHubFailure
14109
14246
  yield* authenticateHubRequest();
14110
14247
  const params = yield* HttpRouter.params;
14111
14248
  const taskId = TaskId.make(params.taskId ?? "");
14112
- const found = yield* (yield* TaskRepository).getById({ taskId });
14249
+ const tasks = yield* TaskRepository;
14250
+ const found = isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
14113
14251
  return Option.isNone(found) ? notFound(taskId) : HttpServerResponse.jsonUnsafe(found.value);
14114
14252
  })));
14115
14253
  const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Effect.gen(function* () {
14116
14254
  yield* authenticateHubRequest();
14117
- const input = yield* decodeOrInvalid(TaskCreateInput, "Request body does not match TaskCreateInput.")(yield* readJsonBody());
14255
+ const input = yield* decodeOrInvalid(HubTaskCreateInput, "Request body does not match HubTaskCreateInput.")(yield* readJsonBody());
14118
14256
  const tasks = yield* TaskRepository;
14119
14257
  const crypto = yield* Crypto.Crypto;
14120
14258
  const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
@@ -14133,8 +14271,8 @@ const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Ef
14133
14271
  createdAt: timestamp,
14134
14272
  updatedAt: timestamp
14135
14273
  };
14136
- yield* tasks.upsert(row);
14137
- return HttpServerResponse.jsonUnsafe(row, { status: 201 });
14274
+ const task = yield* tasks.create(row, input.readableIdPrefix ?? "T");
14275
+ return HttpServerResponse.jsonUnsafe(task, { status: 201 });
14138
14276
  })));
14139
14277
  const updateTaskRoute = HttpRouter.add("PATCH", "/tasks/:taskId", respondToHubFailures(Effect.gen(function* () {
14140
14278
  yield* authenticateHubRequest();
@@ -14372,6 +14510,50 @@ var _003_AgentAssets_default = Effect.gen(function* () {
14372
14510
  `;
14373
14511
  });
14374
14512
  //#endregion
14513
+ //#region src/hub/Migrations/004_TaskReadableIds.ts
14514
+ /**
14515
+ * Human-readable task ids (`T-1`, `P4-12`).
14516
+ *
14517
+ * `task_sequences` holds one row per prefix with the highest number ever
14518
+ * handed out. It only grows: deleting a task does not return its number, so a
14519
+ * readable id, once uttered, never comes to mean a different task.
14520
+ *
14521
+ * Existing rows are backfilled with the default `T` prefix in creation order.
14522
+ * Prefixes cannot be consulted here at all: they live in each project's
14523
+ * checked-in `p4.json`, and the hub has no checkouts. New tasks arrive with a
14524
+ * prefix resolved by the submitting environment server.
14525
+ */
14526
+ var _004_TaskReadableIds_default = Effect.gen(function* () {
14527
+ const sql = yield* SqlClient.SqlClient;
14528
+ yield* sql`
14529
+ CREATE TABLE IF NOT EXISTS task_sequences (
14530
+ prefix TEXT PRIMARY KEY,
14531
+ last_seq INTEGER NOT NULL
14532
+ )
14533
+ `;
14534
+ yield* sql`ALTER TABLE tasks ADD COLUMN readable_id TEXT`;
14535
+ yield* sql`
14536
+ UPDATE tasks
14537
+ SET readable_id = 'T-' || (
14538
+ SELECT COUNT(*)
14539
+ FROM tasks AS earlier
14540
+ WHERE earlier.created_at < tasks.created_at
14541
+ OR (earlier.created_at = tasks.created_at AND earlier.task_id <= tasks.task_id)
14542
+ )
14543
+ WHERE readable_id IS NULL
14544
+ `;
14545
+ yield* sql`
14546
+ INSERT INTO task_sequences (prefix, last_seq)
14547
+ SELECT 'T', COUNT(*) FROM tasks
14548
+ WHERE true
14549
+ ON CONFLICT (prefix) DO UPDATE SET last_seq = MAX(last_seq, excluded.last_seq)
14550
+ `;
14551
+ yield* sql`
14552
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_readable_id
14553
+ ON tasks(readable_id)
14554
+ `;
14555
+ });
14556
+ //#endregion
14375
14557
  //#region src/hub/Migrations.ts
14376
14558
  /**
14377
14559
  * Hub migrations.
@@ -14397,6 +14579,11 @@ const hubMigrationEntries = [
14397
14579
  3,
14398
14580
  "AgentAssets",
14399
14581
  _003_AgentAssets_default
14582
+ ],
14583
+ [
14584
+ 4,
14585
+ "TaskReadableIds",
14586
+ _004_TaskReadableIds_default
14400
14587
  ]
14401
14588
  ];
14402
14589
  const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -32038,6 +32225,8 @@ const normalizeDispatchCommand = (command) => Effect.gen(function* () {
32038
32225
  * presentation and is not.
32039
32226
  *
32040
32227
  * The id and timestamp are passed in so this stays pure and holds no clock.
32228
+ * The readable id is absent on purpose: the store allocates it at insert time,
32229
+ * inside the same transaction as the sequence bump.
32041
32230
  */
32042
32231
  const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
32043
32232
  taskId,
@@ -32073,6 +32262,23 @@ const resolveTaskRepositoryKey = Effect.fn("taskBoard.resolveRepositoryKey")(fun
32073
32262
  return (yield* (yield* RepositoryIdentityResolver).resolve(project.value.workspaceRoot))?.canonicalKey ?? null;
32074
32263
  });
32075
32264
  /**
32265
+ * Resolve the readable-id prefix for a task's project.
32266
+ *
32267
+ * The prefix lives in the project's checked-in `p4.json` (`taskPrefix`), not in
32268
+ * this server's database: it is a property of the repository, agreed on by
32269
+ * everyone who clones it, exactly like the scripts beside it. A task with no
32270
+ * project, a removed project, or a `p4.json` without the field all fall back to
32271
+ * the default `T` - filing the task always beats refusing it over a label.
32272
+ */
32273
+ const resolveTaskReadableIdPrefix = Effect.fn("taskBoard.resolveReadableIdPrefix")(function* (projectId) {
32274
+ if (projectId == null) return "T";
32275
+ const project = yield* (yield* ProjectionProjectRepository).getById({ projectId }).pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
32276
+ if (Option.isNone(project)) return "T";
32277
+ const file = yield* (yield* P4ProjectFileLoader).load(project.value.workspaceRoot);
32278
+ if (Option.isNone(file) || file.value.taskPrefix === void 0) return "T";
32279
+ return file.value.taskPrefix.toUpperCase();
32280
+ });
32281
+ /**
32076
32282
  * Everything that decides whether a task can be started, as one pure function.
32077
32283
  *
32078
32284
  * Kept out of the RPC handler because the three refusals are the whole of the
@@ -42697,9 +42903,12 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
42697
42903
  [WS_METHODS.tasksList]: (filter) => observeRpcEffect$1(WS_METHODS.tasksList, taskRepository.list(filter).pipe(Effect.catch(toTaskStoreError("tasks.list")), Effect.map((tasks) => ({ tasks }))), { "rpc.aggregate": "tasks" }),
42698
42904
  [WS_METHODS.tasksGet]: ({ taskId }) => observeRpcEffect$1(WS_METHODS.tasksGet, taskRepository.getById({ taskId }).pipe(Effect.catch(toTaskStoreError("tasks.get")), Effect.map((found) => ({ task: Option.getOrNull(found) }))), { "rpc.aggregate": "tasks" }),
42699
42905
  [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 };
42906
+ const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
42907
+ const timestamp = yield* nowIso$5;
42908
+ const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
42909
+ const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
42910
+ const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
42911
+ return { task: yield* taskRepository.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("tasks.create"))) };
42703
42912
  }), { "rpc.aggregate": "tasks" }),
42704
42913
  [WS_METHODS.tasksUpdate]: (input) => observeRpcEffect$1(WS_METHODS.tasksUpdate, Effect.gen(function* () {
42705
42914
  const timestamp = yield* nowIso$5;
@@ -43010,12 +43219,13 @@ const websocketRpcRouteLayer = Layer.unwrap(Effect.gen(function* () {
43010
43219
  const serverSelfUpdate = yield* ServerSelfUpdate;
43011
43220
  const taskRepository = yield* TaskRepository;
43012
43221
  const projectionProjects = yield* ProjectionProjectRepository;
43222
+ const p4ProjectFileLoader = yield* P4ProjectFileLoader;
43013
43223
  return HttpRouter.add("GET", "/ws", Effect.gen(function* () {
43014
43224
  const request = yield* HttpServerRequest.HttpServerRequest;
43015
43225
  const serverAuth = yield* EnvironmentAuth;
43016
43226
  const sessions = yield* SessionStore;
43017
43227
  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))))));
43228
+ const rpcWebSocketHttpEffect = yield* RpcServer.toHttpEffectWebsocket(WsRpcGroup, { disableTracing: true }).pipe(Effect.provide(makeWsRpcLayer(session, previewAutomationBroker).pipe(Layer.provideMerge(RpcSerialization.layerJson), Layer.provide(layer$33), Layer.provide(Layer.succeed(ServerSelfUpdate, serverSelfUpdate)), Layer.provide(Layer.succeed(TaskRepository, taskRepository)), Layer.provide(Layer.succeed(ProjectionProjectRepository, projectionProjects)), Layer.provide(Layer.succeed(P4ProjectFileLoader, p4ProjectFileLoader)), Layer.provide(layer$5.pipe(Layer.provide(layer$14.pipe(Layer.provide(Layer.mergeAll(layer$22, layer$20, layer$18, layer$16)), Layer.provideMerge(layer$38), Layer.provide(layer$36.pipe(Layer.provide(layer$37))))), Layer.provide(layer$39))))));
43019
43229
  return yield* Effect.acquireUseRelease(sessions.markConnected(session.sessionId), () => rpcWebSocketHttpEffect, () => sessions.markDisconnected(session.sessionId));
43020
43230
  }).pipe(Effect.catchTags({
43021
43231
  EnvironmentAuthInvalidError: HttpServerRespondable.toResponse,
@@ -43120,17 +43330,16 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
43120
43330
  return Option.some(yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("getById", "the response did not match the contract"))));
43121
43331
  });
43122
43332
  /**
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.
43333
+ * Both writes here POST the hub's create route, and that is a real
43334
+ * difference from the local store: the hub mints its own ids, timestamps and
43335
+ * readable id, so a row written through this comes back with the hub's
43336
+ * version of all three. The caller's `taskId` is not carried - honouring it
43337
+ * would need a hub route that takes one, and inventing that quietly would
43338
+ * let two servers mint colliding ids.
43130
43339
  */
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`), {
43340
+ const postCreate = (operation, row, readableIdPrefix) => Effect.gen(function* () {
43341
+ const settings = yield* requireSettings(operation);
43342
+ const response = yield* send(operation, settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(`${settings.baseUrl}/tasks`), {
43134
43343
  ...row.projectId === null ? {} : { projectId: row.projectId },
43135
43344
  ...row.repositoryKey === null ? {} : { repositoryKey: row.repositoryKey },
43136
43345
  ...row.threadId === null ? {} : { threadId: row.threadId },
@@ -43139,15 +43348,24 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
43139
43348
  status: row.status,
43140
43349
  priority: row.priority,
43141
43350
  ...row.assignee === null ? {} : { assignee: row.assignee },
43142
- labels: row.labels
43351
+ labels: row.labels,
43352
+ ...readableIdPrefix === void 0 ? {} : { readableIdPrefix }
43143
43353
  }));
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")));
43354
+ if (response.status !== 201 && response.status !== 200) return yield* hubUnavailable(operation, `status ${response.status}`);
43355
+ const created = yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable(operation, "the response did not match the contract")));
43146
43356
  yield* publish({
43147
43357
  type: "upserted",
43148
43358
  task: created
43149
43359
  });
43360
+ return created;
43150
43361
  });
43362
+ const create = (row, readableIdPrefix) => postCreate("create", row, readableIdPrefix);
43363
+ const upsert = (row) => postCreate("upsert", row).pipe(Effect.asVoid);
43364
+ /**
43365
+ * Served by the same hub route as `getById`: readable ids and UUIDs are
43366
+ * disjoint by shape, so the hub dispatches on what it is handed.
43367
+ */
43368
+ const getByReadableId = (readableId) => getById({ taskId: TaskId.make(readableId) });
43151
43369
  const patch = (input) => Effect.gen(function* () {
43152
43370
  const settings = yield* requireSettings("patch");
43153
43371
  const { taskId, ...fields } = input;
@@ -43222,10 +43440,12 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
43222
43440
  }).pipe(Effect.ignoreCause({ log: true }));
43223
43441
  yield* Effect.forever(pollOnce.pipe(Effect.andThen(Effect.sleep(POLL_INTERVAL)))).pipe(Effect.forkScoped);
43224
43442
  return {
43443
+ create,
43225
43444
  upsert,
43226
43445
  patch,
43227
43446
  patchIfStatus,
43228
43447
  getById,
43448
+ getByReadableId,
43229
43449
  list,
43230
43450
  deleteById,
43231
43451
  streamChanges: Stream.fromPubSub(changes),
@@ -43275,10 +43495,12 @@ const makeRoutedTaskRepository = Effect.gen(function* () {
43275
43495
  */
43276
43496
  const pick = usingHub.pipe(Effect.map((hub) => hub ? remote : local));
43277
43497
  return {
43498
+ create: (row, readableIdPrefix) => pick.pipe(Effect.flatMap((repository) => repository.create(row, readableIdPrefix))),
43278
43499
  upsert: (row) => pick.pipe(Effect.flatMap((repository) => repository.upsert(row))),
43279
43500
  patch: (input, updatedAt) => pick.pipe(Effect.flatMap((repository) => repository.patch(input, updatedAt))),
43280
43501
  patchIfStatus: (input, updatedAt) => pick.pipe(Effect.flatMap((repository) => repository.patchIfStatus(input, updatedAt))),
43281
43502
  getById: (input) => pick.pipe(Effect.flatMap((repository) => repository.getById(input))),
43503
+ getByReadableId: (readableId) => pick.pipe(Effect.flatMap((repository) => repository.getByReadableId(readableId))),
43282
43504
  list: (filter) => pick.pipe(Effect.flatMap((repository) => repository.list(filter))),
43283
43505
  deleteById: (input) => pick.pipe(Effect.flatMap((repository) => repository.deleteById(input))),
43284
43506
  get streamChanges() {
@@ -81083,8 +81305,15 @@ const writeDependencies = [
81083
81305
  ProjectionProjectRepository,
81084
81306
  RepositoryIdentityResolver
81085
81307
  ];
81086
- /** `task_create` also mints the task id, so it needs the crypto service. */
81087
- const createDependencies = [...writeDependencies, Crypto.Crypto];
81308
+ /**
81309
+ * `task_create` also mints the task id (crypto) and resolves the readable-id
81310
+ * prefix from the project's checked-in `p4.json` (the file loader).
81311
+ */
81312
+ const createDependencies = [
81313
+ ...writeDependencies,
81314
+ Crypto.Crypto,
81315
+ P4ProjectFileLoader
81316
+ ];
81088
81317
  const TaskListResult = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
81089
81318
  const TaskListTool = Tool.make("task_list", {
81090
81319
  description: "List tasks on the board, most recently updated first. Every filter is optional and they combine with AND; omit all of them to list every task. Use this to find work rather than guessing task ids.",
@@ -81094,7 +81323,7 @@ const TaskListTool = Tool.make("task_list", {
81094
81323
  dependencies
81095
81324
  }).annotate(Tool.Title, "List tasks").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
81096
81325
  const TaskGetTool = Tool.make("task_get", {
81097
- description: "Read a single task by id, including its body, labels and linked thread.",
81326
+ description: "Read a single task by id, including its body, labels and linked thread. Accepts either the UUID or the readable id (e.g. T-3, P4-12).",
81098
81327
  parameters: TaskGetInput,
81099
81328
  success: Task,
81100
81329
  failure: TaskToolError,
@@ -81144,6 +81373,15 @@ const TaskToolkit = Toolkit.make(TaskListTool, TaskGetTool, TaskCurrentTool, Tas
81144
81373
  //#endregion
81145
81374
  //#region src/mcp/toolkits/tasks/handlers.ts
81146
81375
  const nowIso$1 = Effect.map(DateTime.now, DateTime.formatIso);
81376
+ /**
81377
+ * Agents reference tasks by whichever id they have seen, so any `taskId`
81378
+ * parameter accepts the readable id (`P4-12`) as well as the UUID. The two are
81379
+ * disjoint by shape, which is what makes the dispatch safe.
81380
+ */
81381
+ const findTask = (taskId) => Effect.gen(function* () {
81382
+ const tasks = yield* TaskRepository;
81383
+ return isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
81384
+ });
81147
81385
  const TaskToolkitHandlersLive = TaskToolkit.toLayer({
81148
81386
  task_list: (input) => Effect.gen(function* () {
81149
81387
  yield* requireTaskCapability();
@@ -81151,7 +81389,7 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
81151
81389
  }),
81152
81390
  task_get: (input) => Effect.gen(function* () {
81153
81391
  yield* requireTaskCapability();
81154
- const found = yield* (yield* TaskRepository).getById({ taskId: input.taskId }).pipe(Effect.catch(toTaskStoreError("task_get")));
81392
+ const found = yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_get")));
81155
81393
  if (Option.isNone(found)) return yield* new TaskNotFoundError({ taskId: input.taskId });
81156
81394
  return found.value;
81157
81395
  }),
@@ -81167,16 +81405,25 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
81167
81405
  yield* requireTaskCapability();
81168
81406
  const tasks = yield* TaskRepository;
81169
81407
  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;
81408
+ const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
81409
+ const timestamp = yield* nowIso$1;
81410
+ const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
81411
+ const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
81412
+ const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
81413
+ return yield* tasks.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("task_create")));
81173
81414
  }),
81174
81415
  task_update: (input) => Effect.gen(function* () {
81175
81416
  yield* requireTaskCapability();
81176
81417
  const tasks = yield* TaskRepository;
81177
81418
  const timestamp = yield* nowIso$1;
81178
- const patch = input.projectId === void 0 ? input : {
81419
+ const taskId = isReadableTaskId(input.taskId) ? Option.getOrNull(yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_update"))))?.taskId : input.taskId;
81420
+ if (taskId === void 0) return yield* new TaskNotFoundError({ taskId: input.taskId });
81421
+ const patch = input.projectId === void 0 ? {
81422
+ ...input,
81423
+ taskId
81424
+ } : {
81179
81425
  ...input,
81426
+ taskId,
81180
81427
  repositoryKey: yield* resolveTaskRepositoryKey(input.projectId)
81181
81428
  };
81182
81429
  const patched = yield* tasks.patch(patch, timestamp).pipe(Effect.catch(toTaskStoreError("task_update")));
@@ -84890,7 +85137,7 @@ const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(Checkpo
84890
85137
  */
84891
85138
  const AssetSyncLive = layer$54.pipe(Layer.provideMerge(layer$55), Layer.provideMerge(layer$56));
84892
85139
  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));
85140
+ const makeRoutesLayer = Layer.mergeAll(Layer.mergeAll(HttpApiBuilder.layer(EnvironmentHttpApi).pipe(Layer.provide(authHttpApiLayer), Layer.provide(orchestrationHttpApiLayer), Layer.provide(serverEnvironmentHttpApiLayer), Layer.provide(environmentAuthenticatedAuthLayer)), otlpTracesProxyRouteLayer, assetRouteLayer, staticAndDevRouteLayer, websocketRpcRouteLayer), layer.pipe(Layer.provide(layer$2))).pipe(Layer.provide(RoutedTaskRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(ProjectionProjectRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(layer$42), Layer.provide(layer$28), Layer.provide(layer$46), Layer.provide(browserApiCorsLayer), Layer.provide(httpCompressionLayer));
84894
85141
  const makeServerLayer = Layer.unwrap(Effect.gen(function* () {
84895
85142
  const config = yield* ServerConfig$1;
84896
85143
  yield* fixPath();