@p4code/cli 0.2.30 → 0.2.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -238,7 +238,7 @@ const make$90 = () => {
238
238
  const layer$81 = Layer.sync(NetService, make$90);
239
239
  //#endregion
240
240
  //#region package.json
241
- var version = "0.2.30";
241
+ var version = "0.2.31";
242
242
  //#endregion
243
243
  //#region src/config.ts
244
244
  /**
@@ -7343,6 +7343,7 @@ const STATIC_KEYBINDING_COMMANDS = [
7343
7343
  "terminal.close",
7344
7344
  "rightPanel.toggle",
7345
7345
  "board.toggle",
7346
+ "todos.toggle",
7346
7347
  "diff.toggle",
7347
7348
  "preview.toggle",
7348
7349
  "preview.refresh",
@@ -7646,6 +7647,12 @@ const Task = Schema$1.Struct({
7646
7647
  priority: TaskPriority,
7647
7648
  assignee: Schema$1.NullOr(TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSIGNEE_MAX_LENGTH))),
7648
7649
  labels: Schema$1.Array(TaskLabel).check(Schema$1.isMaxLength(TASK_LABELS_MAX_COUNT)),
7650
+ /**
7651
+ * When `status` last changed. Nullable-with-default for rows written by an
7652
+ * older server; current stores always stamp it. A dedicated timestamp keeps
7653
+ * completion and archive dates stable when title or labels change later.
7654
+ */
7655
+ statusChangedAt: Schema$1.NullOr(IsoDateTime).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
7649
7656
  createdAt: IsoDateTime,
7650
7657
  updatedAt: IsoDateTime
7651
7658
  });
@@ -7712,6 +7719,7 @@ const HubTaskPutInput = Schema$1.Struct({
7712
7719
  labels: LabelsField,
7713
7720
  orderKey: Schema$1.NullOr(OrderKeyField),
7714
7721
  createdAt: IsoDateTime,
7722
+ statusChangedAt: Schema$1.optional(IsoDateTime),
7715
7723
  readableIdPrefix: Schema$1.optional(TrimmedNonEmptyString)
7716
7724
  });
7717
7725
  /**
@@ -8483,7 +8491,9 @@ const ServerProviderSkill = Schema$1.Struct({
8483
8491
  scope: Schema$1.optional(TrimmedNonEmptyString),
8484
8492
  enabled: Schema$1.Boolean,
8485
8493
  displayName: Schema$1.optional(TrimmedNonEmptyString),
8486
- shortDescription: Schema$1.optional(TrimmedNonEmptyString)
8494
+ shortDescription: Schema$1.optional(TrimmedNonEmptyString),
8495
+ /** Interaction mode P4Code selects when the user inserts this skill. */
8496
+ requiredInteractionMode: Schema$1.optional(ProviderInteractionMode)
8487
8497
  });
8488
8498
  /**
8489
8499
  * Availability of a configured provider instance from the runtime's POV.
@@ -15574,6 +15584,14 @@ var _048_ProjectionThreadsPinned_default = Effect.gen(function* () {
15574
15584
  `;
15575
15585
  });
15576
15586
  //#endregion
15587
+ //#region src/persistence/Migrations/049_TaskStatusChangedAt.ts
15588
+ /** Preserve status transition time independently from ordinary task edits. */
15589
+ var _049_TaskStatusChangedAt_default = Effect.gen(function* () {
15590
+ const sql = yield* SqlClient.SqlClient;
15591
+ yield* sql`ALTER TABLE tasks ADD COLUMN status_changed_at TEXT`;
15592
+ yield* sql`UPDATE tasks SET status_changed_at = updated_at WHERE status_changed_at IS NULL`;
15593
+ });
15594
+ //#endregion
15577
15595
  //#region src/persistence/Migrations.ts
15578
15596
  /**
15579
15597
  * MigrationsLive - Migration runner with inline loader
@@ -15834,6 +15852,11 @@ const migrationEntries = [
15834
15852
  48,
15835
15853
  "ProjectionThreadsPinned",
15836
15854
  _048_ProjectionThreadsPinned_default
15855
+ ],
15856
+ [
15857
+ 49,
15858
+ "TaskStatusChangedAt",
15859
+ _049_TaskStatusChangedAt_default
15837
15860
  ]
15838
15861
  ];
15839
15862
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -18471,6 +18494,7 @@ const TASK_COLUMNS$1 = `
18471
18494
  assignee,
18472
18495
  labels_json AS "labels",
18473
18496
  order_key AS "orderKey",
18497
+ status_changed_at AS "statusChangedAt",
18474
18498
  created_at AS "createdAt",
18475
18499
  updated_at AS "updatedAt"
18476
18500
  `;
@@ -18495,6 +18519,7 @@ const makeTaskRepository = Effect.gen(function* () {
18495
18519
  assignee,
18496
18520
  labels_json,
18497
18521
  order_key,
18522
+ status_changed_at,
18498
18523
  created_at,
18499
18524
  updated_at
18500
18525
  )
@@ -18512,6 +18537,7 @@ const makeTaskRepository = Effect.gen(function* () {
18512
18537
  ${row.assignee},
18513
18538
  ${JSON.stringify(row.labels)},
18514
18539
  ${row.orderKey},
18540
+ ${row.statusChangedAt},
18515
18541
  ${row.createdAt},
18516
18542
  ${row.updatedAt}
18517
18543
  )
@@ -18529,6 +18555,7 @@ const makeTaskRepository = Effect.gen(function* () {
18529
18555
  assignee = excluded.assignee,
18530
18556
  labels_json = excluded.labels_json,
18531
18557
  order_key = excluded.order_key,
18558
+ status_changed_at = excluded.status_changed_at,
18532
18559
  created_at = excluded.created_at,
18533
18560
  updated_at = excluded.updated_at
18534
18561
  `
@@ -18628,6 +18655,7 @@ const makeTaskRepository = Effect.gen(function* () {
18628
18655
  ...input.assignee !== void 0 ? { assignee: input.assignee } : {},
18629
18656
  ...input.labels !== void 0 ? { labels: input.labels } : {},
18630
18657
  ...input.orderKey !== void 0 ? { orderKey: input.orderKey } : {},
18658
+ statusChangedAt: input.status !== void 0 && input.status !== existing.value.status ? updatedAt : existing.value.statusChangedAt,
18631
18659
  updatedAt
18632
18660
  };
18633
18661
  yield* upsertTaskRow(updated);
@@ -18667,6 +18695,7 @@ const makeTaskRepository = Effect.gen(function* () {
18667
18695
  ...input.patch.assignee !== void 0 ? { assignee: input.patch.assignee } : {},
18668
18696
  ...input.patch.labels !== void 0 ? { labels: input.patch.labels } : {},
18669
18697
  ...input.patch.orderKey !== void 0 ? { orderKey: input.patch.orderKey } : {},
18698
+ statusChangedAt: input.patch.status !== void 0 && input.patch.status !== existing.value.status ? updatedAt : existing.value.statusChangedAt,
18670
18699
  updatedAt
18671
18700
  };
18672
18701
  yield* upsertTaskRow(updated);
@@ -18802,6 +18831,7 @@ const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Ef
18802
18831
  assignee: input.assignee ?? null,
18803
18832
  labels: input.labels ?? [],
18804
18833
  orderKey: input.orderKey ?? null,
18834
+ statusChangedAt: timestamp,
18805
18835
  createdAt: timestamp,
18806
18836
  updatedAt: timestamp
18807
18837
  };
@@ -18842,7 +18872,8 @@ const putTaskRoute = HttpRouter.add("PUT", "/tasks/:taskId", respondToHubFailure
18842
18872
  priority: input.priority,
18843
18873
  assignee: input.assignee,
18844
18874
  labels: input.labels,
18845
- orderKey: input.orderKey
18875
+ orderKey: input.orderKey,
18876
+ statusChangedAt: input.statusChangedAt ?? input.createdAt
18846
18877
  };
18847
18878
  const existing = yield* tasks.getById({ taskId });
18848
18879
  if (Option.isNone(existing)) {
@@ -18857,6 +18888,7 @@ const putTaskRoute = HttpRouter.add("PUT", "/tasks/:taskId", respondToHubFailure
18857
18888
  const replaced = {
18858
18889
  ...existing.value,
18859
18890
  ...fields,
18891
+ statusChangedAt: input.statusChangedAt ?? (input.status !== existing.value.status ? timestamp : existing.value.statusChangedAt ?? existing.value.updatedAt),
18860
18892
  updatedAt: timestamp
18861
18893
  };
18862
18894
  yield* tasks.upsert(replaced);
@@ -19546,6 +19578,14 @@ var _013_FeedArticleCategories_default = Effect.gen(function* () {
19546
19578
  yield* (yield* SqlClient.SqlClient)`ALTER TABLE feed_articles ADD COLUMN category TEXT`;
19547
19579
  });
19548
19580
  //#endregion
19581
+ //#region src/hub/Migrations/014_TaskStatusChangedAt.ts
19582
+ /** Keep completion and archive time stable across server replicas. */
19583
+ var _014_TaskStatusChangedAt_default = Effect.gen(function* () {
19584
+ const sql = yield* SqlClient.SqlClient;
19585
+ yield* sql`ALTER TABLE tasks ADD COLUMN status_changed_at TEXT`;
19586
+ yield* sql`UPDATE tasks SET status_changed_at = updated_at WHERE status_changed_at IS NULL`;
19587
+ });
19588
+ //#endregion
19549
19589
  //#region src/hub/Migrations.ts
19550
19590
  /**
19551
19591
  * Hub migrations.
@@ -19621,6 +19661,11 @@ const hubMigrationEntries = [
19621
19661
  13,
19622
19662
  "FeedArticleCategories",
19623
19663
  _013_FeedArticleCategories_default
19664
+ ],
19665
+ [
19666
+ 14,
19667
+ "TaskStatusChangedAt",
19668
+ _014_TaskStatusChangedAt_default
19624
19669
  ]
19625
19670
  ];
19626
19671
  const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -20216,11 +20261,13 @@ function parseMarkdownFrontmatter(contents) {
20216
20261
  const name = typeof record.name === "string" ? record.name.trim() : "";
20217
20262
  const description = typeof record.description === "string" ? record.description.trim() : "";
20218
20263
  const disableModelInvocation = readFrontmatterBoolean(record["disable-model-invocation"]);
20264
+ const requiredInteractionMode = record["required-interaction-mode"] === "default" || record["required-interaction-mode"] === "plan" ? record["required-interaction-mode"] : void 0;
20219
20265
  return {
20220
20266
  kind: "parsed",
20221
20267
  ...name ? { name } : {},
20222
20268
  ...description ? { description } : {},
20223
- ...disableModelInvocation === void 0 ? {} : { disableModelInvocation }
20269
+ ...disableModelInvocation === void 0 ? {} : { disableModelInvocation },
20270
+ ...requiredInteractionMode === void 0 ? {} : { requiredInteractionMode }
20224
20271
  };
20225
20272
  }
20226
20273
  /**
@@ -20304,7 +20351,8 @@ const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* (config
20304
20351
  path: skillPath
20305
20352
  }, disabledSkills ?? []),
20306
20353
  scope: root.scope,
20307
- ...frontmatter.kind === "parsed" && frontmatter.description ? { description: frontmatter.description } : {}
20354
+ ...frontmatter.kind === "parsed" && frontmatter.description ? { description: frontmatter.description } : {},
20355
+ ...frontmatter.kind === "parsed" && frontmatter.requiredInteractionMode ? { requiredInteractionMode: frontmatter.requiredInteractionMode } : {}
20308
20356
  });
20309
20357
  }
20310
20358
  }
@@ -24034,6 +24082,7 @@ const ReadFromSequenceRequestSchema = Schema$1.Struct({
24034
24082
  sequenceExclusive: NonNegativeInt,
24035
24083
  limit: Schema$1.Number
24036
24084
  });
24085
+ const ReadByCommandIdRequestSchema = Schema$1.Struct({ commandId: CommandId });
24037
24086
  const DEFAULT_READ_FROM_SEQUENCE_LIMIT = 1e3;
24038
24087
  const READ_PAGE_SIZE = 500;
24039
24088
  function inferActorKind(event) {
@@ -24124,6 +24173,27 @@ const makeEventStore = Effect.gen(function* () {
24124
24173
  WHERE sequence > ${request.sequenceExclusive}
24125
24174
  ORDER BY sequence ASC
24126
24175
  LIMIT ${request.limit}
24176
+ `
24177
+ });
24178
+ const readEventRowsByCommandId = SqlSchema.findAll({
24179
+ Request: ReadByCommandIdRequestSchema,
24180
+ Result: OrchestrationEventPersistedRowSchema,
24181
+ execute: ({ commandId }) => sql`
24182
+ SELECT
24183
+ sequence,
24184
+ event_id AS "eventId",
24185
+ event_type AS "type",
24186
+ aggregate_kind AS "aggregateKind",
24187
+ stream_id AS "aggregateId",
24188
+ occurred_at AS "occurredAt",
24189
+ command_id AS "commandId",
24190
+ causation_event_id AS "causationEventId",
24191
+ correlation_id AS "correlationId",
24192
+ payload_json AS "payload",
24193
+ metadata_json AS "metadata"
24194
+ FROM orchestration_events
24195
+ WHERE command_id = ${commandId}
24196
+ ORDER BY sequence ASC
24127
24197
  `
24128
24198
  });
24129
24199
  const append = (event) => appendEventRow({
@@ -24153,8 +24223,10 @@ const makeEventStore = Effect.gen(function* () {
24153
24223
  }));
24154
24224
  return readPage(sequenceExclusive, normalizedLimit);
24155
24225
  };
24226
+ const readByCommandId = (commandId) => readEventRowsByCommandId({ commandId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$4("OrchestrationEventStore.readByCommandId:query", "OrchestrationEventStore.readByCommandId:decodeRows")), Effect.flatMap((rows) => Effect.forEach(rows, (row) => decodeEvent(row).pipe(Effect.mapError(toPersistenceDecodeError("OrchestrationEventStore.readByCommandId:rowToEvent"))))));
24156
24227
  return {
24157
24228
  append,
24229
+ readByCommandId,
24158
24230
  readFromSequence,
24159
24231
  readAll: () => readFromSequence(0, Number.MAX_SAFE_INTEGER)
24160
24232
  };
@@ -24855,6 +24927,9 @@ function projectEvent(model, event) {
24855
24927
  //#endregion
24856
24928
  //#region src/orchestration/decider.ts
24857
24929
  const nowIso$8 = Effect.map(DateTime.now, DateTime.formatIso);
24930
+ function findActiveThreadPair(readModel, threadId) {
24931
+ return (readModel.threadPairs ?? []).find((pair) => pair.detachedAt === null && (pair.implementerThreadId === threadId || pair.watcherThreadId === threadId));
24932
+ }
24858
24933
  const QUEUED_TURN_START_GRACE_MS = 120 * 1e3;
24859
24934
  /**
24860
24935
  * Blocked-on-you work derived from the thread's retained activities: an
@@ -25085,7 +25160,7 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
25085
25160
  command,
25086
25161
  threadId: command.threadId
25087
25162
  });
25088
- const activePair = (readModel.threadPairs ?? []).find((pair) => pair.detachedAt === null && (pair.implementerThreadId === command.threadId || pair.watcherThreadId === command.threadId));
25163
+ const activePair = findActiveThreadPair(readModel, command.threadId);
25089
25164
  if (activePair !== void 0) return yield* decideCommandSequence({
25090
25165
  readModel,
25091
25166
  commands: [{
@@ -25337,47 +25412,118 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
25337
25412
  };
25338
25413
  }
25339
25414
  case "thread.archive": {
25340
- yield* requireThreadNotArchived({
25415
+ const activePair = findActiveThreadPair(readModel, command.threadId);
25416
+ if (activePair === void 0) {
25417
+ yield* requireThreadNotArchived({
25418
+ readModel,
25419
+ command,
25420
+ threadId: command.threadId
25421
+ });
25422
+ const occurredAt = yield* nowIso$8;
25423
+ return {
25424
+ ...yield* withEventBase({
25425
+ aggregateKind: "thread",
25426
+ aggregateId: command.threadId,
25427
+ occurredAt,
25428
+ commandId: command.commandId
25429
+ }),
25430
+ type: "thread.archived",
25431
+ payload: {
25432
+ threadId: command.threadId,
25433
+ archivedAt: occurredAt,
25434
+ updatedAt: occurredAt
25435
+ }
25436
+ };
25437
+ }
25438
+ const occurredAt = yield* nowIso$8;
25439
+ const pairedThreads = [yield* requireThread({
25341
25440
  readModel,
25342
25441
  command,
25343
- threadId: command.threadId
25344
- });
25345
- const occurredAt = yield* nowIso$8;
25346
- return {
25347
- ...yield* withEventBase({
25348
- aggregateKind: "thread",
25349
- aggregateId: command.threadId,
25350
- occurredAt,
25351
- commandId: command.commandId
25352
- }),
25353
- type: "thread.archived",
25354
- payload: {
25355
- threadId: command.threadId,
25356
- archivedAt: occurredAt,
25357
- updatedAt: occurredAt
25358
- }
25359
- };
25360
- }
25361
- case "thread.unarchive": {
25362
- yield* requireThreadArchived({
25442
+ threadId: activePair.implementerThreadId
25443
+ }), yield* requireThread({
25363
25444
  readModel,
25364
25445
  command,
25365
- threadId: command.threadId
25366
- });
25446
+ threadId: activePair.watcherThreadId
25447
+ })];
25448
+ const stopEvents = [];
25449
+ const archiveEvents = [];
25450
+ for (const thread of pairedThreads) {
25451
+ if (thread.session !== null && thread.session.status !== "stopped") stopEvents.push({
25452
+ ...yield* withEventBase({
25453
+ aggregateKind: "thread",
25454
+ aggregateId: thread.id,
25455
+ occurredAt,
25456
+ commandId: command.commandId
25457
+ }),
25458
+ type: "thread.session-stop-requested",
25459
+ payload: {
25460
+ threadId: thread.id,
25461
+ createdAt: occurredAt
25462
+ }
25463
+ });
25464
+ archiveEvents.push({
25465
+ ...yield* withEventBase({
25466
+ aggregateKind: "thread",
25467
+ aggregateId: thread.id,
25468
+ occurredAt,
25469
+ commandId: command.commandId
25470
+ }),
25471
+ type: "thread.archived",
25472
+ payload: {
25473
+ threadId: thread.id,
25474
+ archivedAt: thread.archivedAt ?? occurredAt,
25475
+ updatedAt: thread.archivedAt === null ? occurredAt : thread.updatedAt
25476
+ }
25477
+ });
25478
+ }
25479
+ return [...stopEvents, ...archiveEvents];
25480
+ }
25481
+ case "thread.unarchive": {
25482
+ const activePair = findActiveThreadPair(readModel, command.threadId);
25483
+ if (activePair === void 0) {
25484
+ yield* requireThreadArchived({
25485
+ readModel,
25486
+ command,
25487
+ threadId: command.threadId
25488
+ });
25489
+ const occurredAt = yield* nowIso$8;
25490
+ return {
25491
+ ...yield* withEventBase({
25492
+ aggregateKind: "thread",
25493
+ aggregateId: command.threadId,
25494
+ occurredAt,
25495
+ commandId: command.commandId
25496
+ }),
25497
+ type: "thread.unarchived",
25498
+ payload: {
25499
+ threadId: command.threadId,
25500
+ updatedAt: occurredAt
25501
+ }
25502
+ };
25503
+ }
25367
25504
  const occurredAt = yield* nowIso$8;
25368
- return {
25369
- ...yield* withEventBase({
25370
- aggregateKind: "thread",
25371
- aggregateId: command.threadId,
25372
- occurredAt,
25373
- commandId: command.commandId
25374
- }),
25375
- type: "thread.unarchived",
25376
- payload: {
25377
- threadId: command.threadId,
25378
- updatedAt: occurredAt
25379
- }
25380
- };
25505
+ const events = [];
25506
+ for (const threadId of [activePair.implementerThreadId, activePair.watcherThreadId]) {
25507
+ const thread = yield* requireThread({
25508
+ readModel,
25509
+ command,
25510
+ threadId
25511
+ });
25512
+ events.push({
25513
+ ...yield* withEventBase({
25514
+ aggregateKind: "thread",
25515
+ aggregateId: thread.id,
25516
+ occurredAt,
25517
+ commandId: command.commandId
25518
+ }),
25519
+ type: "thread.unarchived",
25520
+ payload: {
25521
+ threadId: thread.id,
25522
+ updatedAt: thread.archivedAt === null ? thread.updatedAt : occurredAt
25523
+ }
25524
+ });
25525
+ }
25526
+ return events;
25381
25527
  }
25382
25528
  case "thread.settle": {
25383
25529
  const thread = yield* requireThreadNotArchived({
@@ -26194,7 +26340,13 @@ const makeOrchestrationEngine = Effect.gen(function* () {
26194
26340
  });
26195
26341
  const existingReceipt = yield* commandReceiptRepository.getByCommandId({ commandId: envelope.command.commandId });
26196
26342
  if (Option.isSome(existingReceipt)) {
26197
- if (existingReceipt.value.status === "accepted") return { sequence: existingReceipt.value.resultSequence };
26343
+ if (existingReceipt.value.status === "accepted") {
26344
+ const events = yield* eventStore.readByCommandId(envelope.command.commandId);
26345
+ return {
26346
+ sequence: existingReceipt.value.resultSequence,
26347
+ events
26348
+ };
26349
+ }
26198
26350
  return yield* new OrchestrationCommandPreviouslyRejectedError({
26199
26351
  commandId: envelope.command.commandId,
26200
26352
  detail: existingReceipt.value.error ?? "Previously rejected."
@@ -26246,7 +26398,10 @@ const makeOrchestrationEngine = Effect.gen(function* () {
26246
26398
  ackEventType: event.type
26247
26399
  })), Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - envelope.startedAtMs)));
26248
26400
  }
26249
- return { sequence: committedCommand.lastSequence };
26401
+ return {
26402
+ sequence: committedCommand.lastSequence,
26403
+ events: committedCommand.committedEvents
26404
+ };
26250
26405
  }).pipe(Effect.withSpan(`orchestration.command.${envelope.command.type}`))).pipe(Effect.flatMap((exit) => Effect.gen(function* () {
26251
26406
  const outcome = Exit.isSuccess(exit) ? "success" : Cause.hasInterruptsOnly(exit.cause) ? "interrupt" : "failure";
26252
26407
  yield* Metric.update(Metric.withAttributes(orchestrationCommandDuration, metricAttributes(baseMetricAttributes)), Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - processingStartedAtMs)));
@@ -30902,8 +31057,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
30902
31057
  listArchivedThreadRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getArchivedShellSnapshot:listThreads:query", "ProjectionSnapshotQuery.getArchivedShellSnapshot:listThreads:decodeRows"))),
30903
31058
  listArchivedThreadSessionRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getArchivedShellSnapshot:listThreadSessions:query", "ProjectionSnapshotQuery.getArchivedShellSnapshot:listThreadSessions:decodeRows"))),
30904
31059
  listArchivedLatestTurnRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getArchivedShellSnapshot:listLatestTurns:query", "ProjectionSnapshotQuery.getArchivedShellSnapshot:listLatestTurns:decodeRows"))),
30905
- listProjectionStateRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getArchivedShellSnapshot:listProjectionState:query", "ProjectionSnapshotQuery.getArchivedShellSnapshot:listProjectionState:decodeRows")))
30906
- ])).pipe(Effect.flatMap(([projectRows, threadRows, sessionRows, latestTurnRows, stateRows]) => Effect.gen(function* () {
31060
+ listProjectionStateRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getArchivedShellSnapshot:listProjectionState:query", "ProjectionSnapshotQuery.getArchivedShellSnapshot:listProjectionState:decodeRows"))),
31061
+ listThreadPairRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getArchivedShellSnapshot:listThreadPairs:query", "ProjectionSnapshotQuery.getArchivedShellSnapshot:listThreadPairs:decodeRows")))
31062
+ ])).pipe(Effect.flatMap(([projectRows, threadRows, sessionRows, latestTurnRows, stateRows, threadPairRows]) => Effect.gen(function* () {
30907
31063
  let updatedAt = null;
30908
31064
  for (const row of projectRows) updatedAt = maxIso(updatedAt, row.updatedAt);
30909
31065
  for (const row of threadRows) updatedAt = maxIso(updatedAt, row.updatedAt);
@@ -30918,6 +31074,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
30918
31074
  const repositoryIdentities = yield* resolveRepositoryIdentitiesForProjects(projectRows.filter((row) => activeProjectIds.has(row.projectId)));
30919
31075
  const latestTurnByThread = new Map(latestTurnRows.map((row) => [row.threadId, mapLatestTurn(row)]));
30920
31076
  const sessionByThread = new Map(sessionRows.map((row) => [row.threadId, mapSessionRow(row)]));
31077
+ const archivedThreadIds = new Set(threadRows.map((row) => row.threadId));
30921
31078
  const snapshot = {
30922
31079
  snapshotSequence: computeSnapshotSequence(stateRows),
30923
31080
  projects: Arr.filterMap(projectRows, (row) => row.deletedAt === null && activeProjectIds.has(row.projectId) ? Result.succeed(mapProjectShellRow(row, repositoryIdentities.get(row.projectId) ?? null)) : Result.failVoid),
@@ -30951,6 +31108,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
30951
31108
  hasBackgroundTasks: row.backgroundTaskCount > 0,
30952
31109
  scheduledWakeAt: row.scheduledWakeAt
30953
31110
  })),
31111
+ threadPairs: threadPairRows.filter((pair) => pair.detachedAt === null && (archivedThreadIds.has(pair.implementerThreadId) || archivedThreadIds.has(pair.watcherThreadId))),
30954
31112
  updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z"
30955
31113
  };
30956
31114
  return yield* decodeShellSnapshot(snapshot).pipe(Effect.mapError(toPersistenceDecodeError("ProjectionSnapshotQuery.getArchivedShellSnapshot:decodeShellSnapshot")));
@@ -31255,6 +31413,10 @@ const DEFAULT_KEYBINDINGS = [
31255
31413
  key: "mod+shift+b",
31256
31414
  command: "board.toggle"
31257
31415
  },
31416
+ {
31417
+ key: "mod+alt+t",
31418
+ command: "todos.toggle"
31419
+ },
31258
31420
  {
31259
31421
  key: "mod+d",
31260
31422
  command: "terminal.split",
@@ -41229,6 +41391,7 @@ const taskFromLinearIssue = (issue, link = EMPTY_LINEAR_TASK_LINK) => {
41229
41391
  priority: priorityFromLinear(issue["priority"]),
41230
41392
  assignee: personName(issue["assignee"]),
41231
41393
  labels: labelNames(issue["labels"]),
41394
+ statusChangedAt: updatedAt ?? createdAt ?? "1970-01-01T00:00:00.000Z",
41232
41395
  createdAt: createdAt ?? updatedAt ?? "1970-01-01T00:00:00.000Z",
41233
41396
  updatedAt: updatedAt ?? createdAt ?? "1970-01-01T00:00:00.000Z"
41234
41397
  };
@@ -41620,6 +41783,7 @@ const makeHubTaskClient = Effect.gen(function* () {
41620
41783
  labels: row.labels,
41621
41784
  orderKey: row.orderKey,
41622
41785
  createdAt: row.createdAt,
41786
+ statusChangedAt: row.statusChangedAt ?? row.updatedAt,
41623
41787
  readableIdPrefix
41624
41788
  }));
41625
41789
  if (response.status !== 200 && response.status !== 201) return yield* hubUnavailable("put", `status ${response.status}`);
@@ -41691,6 +41855,7 @@ const TASK_COLUMNS = `
41691
41855
  assignee,
41692
41856
  labels_json AS "labels",
41693
41857
  order_key AS "orderKey",
41858
+ status_changed_at AS "statusChangedAt",
41694
41859
  created_at AS "createdAt",
41695
41860
  updated_at AS "updatedAt"
41696
41861
  `;
@@ -42070,6 +42235,7 @@ const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
42070
42235
  assignee: input.assignee ?? null,
42071
42236
  labels: input.labels ?? [],
42072
42237
  orderKey: null,
42238
+ statusChangedAt: timestamp,
42073
42239
  createdAt: timestamp,
42074
42240
  updatedAt: timestamp
42075
42241
  });
@@ -62302,7 +62468,12 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
62302
62468
  })), Effect.orElseSucceed(() => false)) : false;
62303
62469
  const result = yield* dispatchNormalizedCommand(normalizedCommand);
62304
62470
  if (normalizedCommand.type === "thread.archive") {
62305
- if (shouldStopSessionAfterArchive) yield* Effect.gen(function* () {
62471
+ const archivedThreadIds = result.events?.filter((event) => event.type === "thread.archived").map((event) => event.payload.threadId);
62472
+ if (archivedThreadIds === void 0 || archivedThreadIds.length === 0) return yield* new OrchestrationDispatchCommandError({ message: "Archive command completed without authoritative archive events." });
62473
+ const uniqueArchivedThreadIds = Array.from(new Set(archivedThreadIds));
62474
+ const pairedArchive = uniqueArchivedThreadIds.length > 1;
62475
+ if (!uniqueArchivedThreadIds.includes(normalizedCommand.threadId)) return yield* new OrchestrationDispatchCommandError({ message: "Archive command result did not include the requested thread." });
62476
+ if (shouldStopSessionAfterArchive && !pairedArchive) yield* Effect.gen(function* () {
62306
62477
  const stopCommand = yield* normalizeDispatchCommand({
62307
62478
  type: "thread.session.stop",
62308
62479
  commandId: CommandId.make(`session-stop-for-archive:${normalizedCommand.commandId}`),
@@ -62314,12 +62485,12 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
62314
62485
  threadId: normalizedCommand.threadId,
62315
62486
  cause
62316
62487
  })));
62317
- yield* terminalManager.close({ threadId: normalizedCommand.threadId }).pipe(Effect.catch((error) => Effect.logWarning("failed to close thread terminals after archive", {
62318
- threadId: normalizedCommand.threadId,
62488
+ yield* Effect.forEach(uniqueArchivedThreadIds, (threadId) => terminalManager.close({ threadId }).pipe(Effect.catch((error) => Effect.logWarning("failed to close thread terminals after archive", {
62489
+ threadId,
62319
62490
  error: error.message
62320
- })));
62491
+ }))), { discard: true });
62321
62492
  }
62322
- return result;
62493
+ return { sequence: result.sequence };
62323
62494
  }).pipe(Effect.mapError((cause) => isOrchestrationDispatchCommandError(cause) ? cause : new OrchestrationDispatchCommandError({
62324
62495
  message: "Failed to dispatch orchestration command",
62325
62496
  cause
@@ -87875,6 +88046,17 @@ function parseCodexSkillsListResponse(response, cwd) {
87875
88046
  return parsedSkill;
87876
88047
  });
87877
88048
  }
88049
+ /** Codex omits unknown SKILL.md frontmatter, so enrich its snapshot from each advertised path. */
88050
+ const enrichCodexSkillsWithFrontmatter = Effect.fnUntraced(function* (skills) {
88051
+ const fileSystem = yield* FileSystem.FileSystem;
88052
+ return yield* Effect.forEach(skills, (skill) => fileSystem.readFileString(skill.path).pipe(Effect.map((contents) => {
88053
+ const frontmatter = parseMarkdownFrontmatter(contents);
88054
+ return frontmatter.kind === "parsed" && frontmatter.requiredInteractionMode ? {
88055
+ ...skill,
88056
+ requiredInteractionMode: frontmatter.requiredInteractionMode
88057
+ } : skill;
88058
+ }), Effect.orElseSucceed(() => skill)), { concurrency: "unbounded" });
88059
+ });
87878
88060
  const requestAllCodexModels = Effect.fn("requestAllCodexModels")(function* (client) {
87879
88061
  const models = [];
87880
88062
  let cursor = void 0;
@@ -87937,11 +88119,12 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
87937
88119
  skills: []
87938
88120
  };
87939
88121
  const [skillsResponse, models] = yield* Effect.all([client.request("skills/list", { cwds: [input.cwd] }), requestAllCodexModels(client)], { concurrency: "unbounded" });
88122
+ const skills = yield* enrichCodexSkillsWithFrontmatter(parseCodexSkillsListResponse(skillsResponse, input.cwd));
87940
88123
  return {
87941
88124
  account: accountResponse,
87942
88125
  version,
87943
88126
  models: applyPreferredCodexDefaultModel(appendCustomCodexModels(models, input.customModels ?? [])),
87944
- skills: parseCodexSkillsListResponse(skillsResponse, input.cwd)
88127
+ skills
87945
88128
  };
87946
88129
  });
87947
88130
  const emptyCodexModelsFromSettings = (codexSettings) => {
@@ -90582,6 +90765,7 @@ const CodexDriver = {
90582
90765
  defaultConfig: () => decodeCodexSettings({}),
90583
90766
  create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () {
90584
90767
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
90768
+ const fileSystem = yield* FileSystem.FileSystem;
90585
90769
  const httpClient = yield* HttpClient.HttpClient;
90586
90770
  const serverSettings = yield* ServerSettingsService;
90587
90771
  const eventLoggers = yield* ProviderEventLoggers;
@@ -90616,7 +90800,7 @@ const CodexDriver = {
90616
90800
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
90617
90801
  });
90618
90802
  const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv);
90619
- const checkProvider = checkCodexProviderStatus(effectiveConfig, void 0, processEnv).pipe(Effect.map(stampIdentity), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner));
90803
+ const checkProvider = checkCodexProviderStatus(effectiveConfig, void 0, processEnv).pipe(Effect.map(stampIdentity), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem));
90620
90804
  const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
90621
90805
  const snapshot = yield* makeManagedServerProvider({
90622
90806
  maintenanceCapabilities,
@@ -105237,18 +105421,18 @@ const ProviderRuntimeIngestionLive = Layer.effect(ProviderRuntimeIngestionServic
105237
105421
  * @module provider/userInvokedSkills
105238
105422
  */
105239
105423
  /**
105240
- * A leading `/name`, with whatever followed it.
105424
+ * A leading `/name` or `$name`, with whatever followed it.
105241
105425
  *
105242
- * Anchored at the start: a slash somewhere inside a sentence is a path, a date
105243
- * or an "and/or", and treating those as commands would rewrite ordinary
105426
+ * Anchored at the start: a marker somewhere inside a sentence can be a path,
105427
+ * date, currency, or prose, and treating it as a command would rewrite ordinary
105244
105428
  * messages. The name pattern matches what a skill directory can be called,
105245
105429
  * including the `plugin:skill` form.
105246
105430
  */
105247
- const SLASH_INVOCATION = /^\/([A-Za-z0-9][A-Za-z0-9_:-]*)[ \t]*([\s\S]*)$/;
105431
+ const SKILL_INVOCATION = /^[/$]([A-Za-z0-9][A-Za-z0-9_:-]*)[ \t]*([\s\S]*)$/;
105248
105432
  /** `SKILL.md` is the only file a skill is required to have. */
105249
105433
  const SKILL_FILENAME = "SKILL.md";
105250
105434
  const parseSlashInvocation = (text) => {
105251
- const match = SLASH_INVOCATION.exec(text.trim());
105435
+ const match = SKILL_INVOCATION.exec(text.trim());
105252
105436
  const name = match?.[1];
105253
105437
  if (name === void 0) return void 0;
105254
105438
  return {
@@ -105532,7 +105716,7 @@ const make$3 = Effect.gen(function* () {
105532
105716
  return yield* projectionSnapshotQuery.getProjectShellById(projectId).pipe(Effect.map(Option.getOrUndefined));
105533
105717
  });
105534
105718
  /**
105535
- * Where a typed `/name` is looked up: the user's skills, then the
105719
+ * Where a typed `/name` or `$name` is looked up: the user's skills, then the
105536
105720
  * workspace's.
105537
105721
  *
105538
105722
  * Project last so it wins, matching the CLI's most-specific-wins resolution.