@p4code/cli 0.2.29 → 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.29";
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
  /**
@@ -8238,9 +8246,8 @@ const ServerSettings = Schema$1.Struct({
8238
8246
  model: DEFAULT_TEXT_GENERATION_MODEL
8239
8247
  }))),
8240
8248
  /**
8241
- * The supervisor model a Fusion pair starts with. Null until the user picks
8242
- * one; kept in settings rather than per-draft state so the choice survives
8243
- * the next thread instead of resetting to none each time.
8249
+ * Recommended supervisor model used when opening the Fusion picker. This
8250
+ * does not activate Fusion for a draft by itself.
8244
8251
  */
8245
8252
  fusionWatcherModelSelection: Schema$1.NullOr(ModelSelection).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
8246
8253
  sourceControlWritingStyle: SourceControlWritingStyleSettings.pipe(Schema$1.withDecodingDefault(Effect.succeed({}))),
@@ -8484,7 +8491,9 @@ const ServerProviderSkill = Schema$1.Struct({
8484
8491
  scope: Schema$1.optional(TrimmedNonEmptyString),
8485
8492
  enabled: Schema$1.Boolean,
8486
8493
  displayName: Schema$1.optional(TrimmedNonEmptyString),
8487
- 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)
8488
8497
  });
8489
8498
  /**
8490
8499
  * Availability of a configured provider instance from the runtime's POV.
@@ -15575,6 +15584,14 @@ var _048_ProjectionThreadsPinned_default = Effect.gen(function* () {
15575
15584
  `;
15576
15585
  });
15577
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
15578
15595
  //#region src/persistence/Migrations.ts
15579
15596
  /**
15580
15597
  * MigrationsLive - Migration runner with inline loader
@@ -15835,6 +15852,11 @@ const migrationEntries = [
15835
15852
  48,
15836
15853
  "ProjectionThreadsPinned",
15837
15854
  _048_ProjectionThreadsPinned_default
15855
+ ],
15856
+ [
15857
+ 49,
15858
+ "TaskStatusChangedAt",
15859
+ _049_TaskStatusChangedAt_default
15838
15860
  ]
15839
15861
  ];
15840
15862
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -18472,6 +18494,7 @@ const TASK_COLUMNS$1 = `
18472
18494
  assignee,
18473
18495
  labels_json AS "labels",
18474
18496
  order_key AS "orderKey",
18497
+ status_changed_at AS "statusChangedAt",
18475
18498
  created_at AS "createdAt",
18476
18499
  updated_at AS "updatedAt"
18477
18500
  `;
@@ -18496,6 +18519,7 @@ const makeTaskRepository = Effect.gen(function* () {
18496
18519
  assignee,
18497
18520
  labels_json,
18498
18521
  order_key,
18522
+ status_changed_at,
18499
18523
  created_at,
18500
18524
  updated_at
18501
18525
  )
@@ -18513,6 +18537,7 @@ const makeTaskRepository = Effect.gen(function* () {
18513
18537
  ${row.assignee},
18514
18538
  ${JSON.stringify(row.labels)},
18515
18539
  ${row.orderKey},
18540
+ ${row.statusChangedAt},
18516
18541
  ${row.createdAt},
18517
18542
  ${row.updatedAt}
18518
18543
  )
@@ -18530,6 +18555,7 @@ const makeTaskRepository = Effect.gen(function* () {
18530
18555
  assignee = excluded.assignee,
18531
18556
  labels_json = excluded.labels_json,
18532
18557
  order_key = excluded.order_key,
18558
+ status_changed_at = excluded.status_changed_at,
18533
18559
  created_at = excluded.created_at,
18534
18560
  updated_at = excluded.updated_at
18535
18561
  `
@@ -18629,6 +18655,7 @@ const makeTaskRepository = Effect.gen(function* () {
18629
18655
  ...input.assignee !== void 0 ? { assignee: input.assignee } : {},
18630
18656
  ...input.labels !== void 0 ? { labels: input.labels } : {},
18631
18657
  ...input.orderKey !== void 0 ? { orderKey: input.orderKey } : {},
18658
+ statusChangedAt: input.status !== void 0 && input.status !== existing.value.status ? updatedAt : existing.value.statusChangedAt,
18632
18659
  updatedAt
18633
18660
  };
18634
18661
  yield* upsertTaskRow(updated);
@@ -18668,6 +18695,7 @@ const makeTaskRepository = Effect.gen(function* () {
18668
18695
  ...input.patch.assignee !== void 0 ? { assignee: input.patch.assignee } : {},
18669
18696
  ...input.patch.labels !== void 0 ? { labels: input.patch.labels } : {},
18670
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,
18671
18699
  updatedAt
18672
18700
  };
18673
18701
  yield* upsertTaskRow(updated);
@@ -18803,6 +18831,7 @@ const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Ef
18803
18831
  assignee: input.assignee ?? null,
18804
18832
  labels: input.labels ?? [],
18805
18833
  orderKey: input.orderKey ?? null,
18834
+ statusChangedAt: timestamp,
18806
18835
  createdAt: timestamp,
18807
18836
  updatedAt: timestamp
18808
18837
  };
@@ -18843,7 +18872,8 @@ const putTaskRoute = HttpRouter.add("PUT", "/tasks/:taskId", respondToHubFailure
18843
18872
  priority: input.priority,
18844
18873
  assignee: input.assignee,
18845
18874
  labels: input.labels,
18846
- orderKey: input.orderKey
18875
+ orderKey: input.orderKey,
18876
+ statusChangedAt: input.statusChangedAt ?? input.createdAt
18847
18877
  };
18848
18878
  const existing = yield* tasks.getById({ taskId });
18849
18879
  if (Option.isNone(existing)) {
@@ -18858,6 +18888,7 @@ const putTaskRoute = HttpRouter.add("PUT", "/tasks/:taskId", respondToHubFailure
18858
18888
  const replaced = {
18859
18889
  ...existing.value,
18860
18890
  ...fields,
18891
+ statusChangedAt: input.statusChangedAt ?? (input.status !== existing.value.status ? timestamp : existing.value.statusChangedAt ?? existing.value.updatedAt),
18861
18892
  updatedAt: timestamp
18862
18893
  };
18863
18894
  yield* tasks.upsert(replaced);
@@ -19547,6 +19578,14 @@ var _013_FeedArticleCategories_default = Effect.gen(function* () {
19547
19578
  yield* (yield* SqlClient.SqlClient)`ALTER TABLE feed_articles ADD COLUMN category TEXT`;
19548
19579
  });
19549
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
19550
19589
  //#region src/hub/Migrations.ts
19551
19590
  /**
19552
19591
  * Hub migrations.
@@ -19622,6 +19661,11 @@ const hubMigrationEntries = [
19622
19661
  13,
19623
19662
  "FeedArticleCategories",
19624
19663
  _013_FeedArticleCategories_default
19664
+ ],
19665
+ [
19666
+ 14,
19667
+ "TaskStatusChangedAt",
19668
+ _014_TaskStatusChangedAt_default
19625
19669
  ]
19626
19670
  ];
19627
19671
  const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -20217,11 +20261,13 @@ function parseMarkdownFrontmatter(contents) {
20217
20261
  const name = typeof record.name === "string" ? record.name.trim() : "";
20218
20262
  const description = typeof record.description === "string" ? record.description.trim() : "";
20219
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;
20220
20265
  return {
20221
20266
  kind: "parsed",
20222
20267
  ...name ? { name } : {},
20223
20268
  ...description ? { description } : {},
20224
- ...disableModelInvocation === void 0 ? {} : { disableModelInvocation }
20269
+ ...disableModelInvocation === void 0 ? {} : { disableModelInvocation },
20270
+ ...requiredInteractionMode === void 0 ? {} : { requiredInteractionMode }
20225
20271
  };
20226
20272
  }
20227
20273
  /**
@@ -20305,7 +20351,8 @@ const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* (config
20305
20351
  path: skillPath
20306
20352
  }, disabledSkills ?? []),
20307
20353
  scope: root.scope,
20308
- ...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 } : {}
20309
20356
  });
20310
20357
  }
20311
20358
  }
@@ -24035,6 +24082,7 @@ const ReadFromSequenceRequestSchema = Schema$1.Struct({
24035
24082
  sequenceExclusive: NonNegativeInt,
24036
24083
  limit: Schema$1.Number
24037
24084
  });
24085
+ const ReadByCommandIdRequestSchema = Schema$1.Struct({ commandId: CommandId });
24038
24086
  const DEFAULT_READ_FROM_SEQUENCE_LIMIT = 1e3;
24039
24087
  const READ_PAGE_SIZE = 500;
24040
24088
  function inferActorKind(event) {
@@ -24125,6 +24173,27 @@ const makeEventStore = Effect.gen(function* () {
24125
24173
  WHERE sequence > ${request.sequenceExclusive}
24126
24174
  ORDER BY sequence ASC
24127
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
24128
24197
  `
24129
24198
  });
24130
24199
  const append = (event) => appendEventRow({
@@ -24154,8 +24223,10 @@ const makeEventStore = Effect.gen(function* () {
24154
24223
  }));
24155
24224
  return readPage(sequenceExclusive, normalizedLimit);
24156
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"))))));
24157
24227
  return {
24158
24228
  append,
24229
+ readByCommandId,
24159
24230
  readFromSequence,
24160
24231
  readAll: () => readFromSequence(0, Number.MAX_SAFE_INTEGER)
24161
24232
  };
@@ -24856,6 +24927,9 @@ function projectEvent(model, event) {
24856
24927
  //#endregion
24857
24928
  //#region src/orchestration/decider.ts
24858
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
+ }
24859
24933
  const QUEUED_TURN_START_GRACE_MS = 120 * 1e3;
24860
24934
  /**
24861
24935
  * Blocked-on-you work derived from the thread's retained activities: an
@@ -25086,7 +25160,7 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
25086
25160
  command,
25087
25161
  threadId: command.threadId
25088
25162
  });
25089
- const activePair = (readModel.threadPairs ?? []).find((pair) => pair.detachedAt === null && (pair.implementerThreadId === command.threadId || pair.watcherThreadId === command.threadId));
25163
+ const activePair = findActiveThreadPair(readModel, command.threadId);
25090
25164
  if (activePair !== void 0) return yield* decideCommandSequence({
25091
25165
  readModel,
25092
25166
  commands: [{
@@ -25338,47 +25412,118 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
25338
25412
  };
25339
25413
  }
25340
25414
  case "thread.archive": {
25341
- 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({
25342
25440
  readModel,
25343
25441
  command,
25344
- threadId: command.threadId
25345
- });
25346
- const occurredAt = yield* nowIso$8;
25347
- return {
25348
- ...yield* withEventBase({
25349
- aggregateKind: "thread",
25350
- aggregateId: command.threadId,
25351
- occurredAt,
25352
- commandId: command.commandId
25353
- }),
25354
- type: "thread.archived",
25355
- payload: {
25356
- threadId: command.threadId,
25357
- archivedAt: occurredAt,
25358
- updatedAt: occurredAt
25359
- }
25360
- };
25361
- }
25362
- case "thread.unarchive": {
25363
- yield* requireThreadArchived({
25442
+ threadId: activePair.implementerThreadId
25443
+ }), yield* requireThread({
25364
25444
  readModel,
25365
25445
  command,
25366
- threadId: command.threadId
25367
- });
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
+ }
25368
25504
  const occurredAt = yield* nowIso$8;
25369
- return {
25370
- ...yield* withEventBase({
25371
- aggregateKind: "thread",
25372
- aggregateId: command.threadId,
25373
- occurredAt,
25374
- commandId: command.commandId
25375
- }),
25376
- type: "thread.unarchived",
25377
- payload: {
25378
- threadId: command.threadId,
25379
- updatedAt: occurredAt
25380
- }
25381
- };
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;
25382
25527
  }
25383
25528
  case "thread.settle": {
25384
25529
  const thread = yield* requireThreadNotArchived({
@@ -26195,7 +26340,13 @@ const makeOrchestrationEngine = Effect.gen(function* () {
26195
26340
  });
26196
26341
  const existingReceipt = yield* commandReceiptRepository.getByCommandId({ commandId: envelope.command.commandId });
26197
26342
  if (Option.isSome(existingReceipt)) {
26198
- 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
+ }
26199
26350
  return yield* new OrchestrationCommandPreviouslyRejectedError({
26200
26351
  commandId: envelope.command.commandId,
26201
26352
  detail: existingReceipt.value.error ?? "Previously rejected."
@@ -26247,7 +26398,10 @@ const makeOrchestrationEngine = Effect.gen(function* () {
26247
26398
  ackEventType: event.type
26248
26399
  })), Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - envelope.startedAtMs)));
26249
26400
  }
26250
- return { sequence: committedCommand.lastSequence };
26401
+ return {
26402
+ sequence: committedCommand.lastSequence,
26403
+ events: committedCommand.committedEvents
26404
+ };
26251
26405
  }).pipe(Effect.withSpan(`orchestration.command.${envelope.command.type}`))).pipe(Effect.flatMap((exit) => Effect.gen(function* () {
26252
26406
  const outcome = Exit.isSuccess(exit) ? "success" : Cause.hasInterruptsOnly(exit.cause) ? "interrupt" : "failure";
26253
26407
  yield* Metric.update(Metric.withAttributes(orchestrationCommandDuration, metricAttributes(baseMetricAttributes)), Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - processingStartedAtMs)));
@@ -30903,8 +31057,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
30903
31057
  listArchivedThreadRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getArchivedShellSnapshot:listThreads:query", "ProjectionSnapshotQuery.getArchivedShellSnapshot:listThreads:decodeRows"))),
30904
31058
  listArchivedThreadSessionRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getArchivedShellSnapshot:listThreadSessions:query", "ProjectionSnapshotQuery.getArchivedShellSnapshot:listThreadSessions:decodeRows"))),
30905
31059
  listArchivedLatestTurnRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getArchivedShellSnapshot:listLatestTurns:query", "ProjectionSnapshotQuery.getArchivedShellSnapshot:listLatestTurns:decodeRows"))),
30906
- listProjectionStateRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getArchivedShellSnapshot:listProjectionState:query", "ProjectionSnapshotQuery.getArchivedShellSnapshot:listProjectionState:decodeRows")))
30907
- ])).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* () {
30908
31063
  let updatedAt = null;
30909
31064
  for (const row of projectRows) updatedAt = maxIso(updatedAt, row.updatedAt);
30910
31065
  for (const row of threadRows) updatedAt = maxIso(updatedAt, row.updatedAt);
@@ -30919,6 +31074,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
30919
31074
  const repositoryIdentities = yield* resolveRepositoryIdentitiesForProjects(projectRows.filter((row) => activeProjectIds.has(row.projectId)));
30920
31075
  const latestTurnByThread = new Map(latestTurnRows.map((row) => [row.threadId, mapLatestTurn(row)]));
30921
31076
  const sessionByThread = new Map(sessionRows.map((row) => [row.threadId, mapSessionRow(row)]));
31077
+ const archivedThreadIds = new Set(threadRows.map((row) => row.threadId));
30922
31078
  const snapshot = {
30923
31079
  snapshotSequence: computeSnapshotSequence(stateRows),
30924
31080
  projects: Arr.filterMap(projectRows, (row) => row.deletedAt === null && activeProjectIds.has(row.projectId) ? Result.succeed(mapProjectShellRow(row, repositoryIdentities.get(row.projectId) ?? null)) : Result.failVoid),
@@ -30952,6 +31108,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
30952
31108
  hasBackgroundTasks: row.backgroundTaskCount > 0,
30953
31109
  scheduledWakeAt: row.scheduledWakeAt
30954
31110
  })),
31111
+ threadPairs: threadPairRows.filter((pair) => pair.detachedAt === null && (archivedThreadIds.has(pair.implementerThreadId) || archivedThreadIds.has(pair.watcherThreadId))),
30955
31112
  updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z"
30956
31113
  };
30957
31114
  return yield* decodeShellSnapshot(snapshot).pipe(Effect.mapError(toPersistenceDecodeError("ProjectionSnapshotQuery.getArchivedShellSnapshot:decodeShellSnapshot")));
@@ -31256,6 +31413,10 @@ const DEFAULT_KEYBINDINGS = [
31256
31413
  key: "mod+shift+b",
31257
31414
  command: "board.toggle"
31258
31415
  },
31416
+ {
31417
+ key: "mod+alt+t",
31418
+ command: "todos.toggle"
31419
+ },
31259
31420
  {
31260
31421
  key: "mod+d",
31261
31422
  command: "terminal.split",
@@ -41230,6 +41391,7 @@ const taskFromLinearIssue = (issue, link = EMPTY_LINEAR_TASK_LINK) => {
41230
41391
  priority: priorityFromLinear(issue["priority"]),
41231
41392
  assignee: personName(issue["assignee"]),
41232
41393
  labels: labelNames(issue["labels"]),
41394
+ statusChangedAt: updatedAt ?? createdAt ?? "1970-01-01T00:00:00.000Z",
41233
41395
  createdAt: createdAt ?? updatedAt ?? "1970-01-01T00:00:00.000Z",
41234
41396
  updatedAt: updatedAt ?? createdAt ?? "1970-01-01T00:00:00.000Z"
41235
41397
  };
@@ -41621,6 +41783,7 @@ const makeHubTaskClient = Effect.gen(function* () {
41621
41783
  labels: row.labels,
41622
41784
  orderKey: row.orderKey,
41623
41785
  createdAt: row.createdAt,
41786
+ statusChangedAt: row.statusChangedAt ?? row.updatedAt,
41624
41787
  readableIdPrefix
41625
41788
  }));
41626
41789
  if (response.status !== 200 && response.status !== 201) return yield* hubUnavailable("put", `status ${response.status}`);
@@ -41692,6 +41855,7 @@ const TASK_COLUMNS = `
41692
41855
  assignee,
41693
41856
  labels_json AS "labels",
41694
41857
  order_key AS "orderKey",
41858
+ status_changed_at AS "statusChangedAt",
41695
41859
  created_at AS "createdAt",
41696
41860
  updated_at AS "updatedAt"
41697
41861
  `;
@@ -42071,6 +42235,7 @@ const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
42071
42235
  assignee: input.assignee ?? null,
42072
42236
  labels: input.labels ?? [],
42073
42237
  orderKey: null,
42238
+ statusChangedAt: timestamp,
42074
42239
  createdAt: timestamp,
42075
42240
  updatedAt: timestamp
42076
42241
  });
@@ -62303,7 +62468,12 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
62303
62468
  })), Effect.orElseSucceed(() => false)) : false;
62304
62469
  const result = yield* dispatchNormalizedCommand(normalizedCommand);
62305
62470
  if (normalizedCommand.type === "thread.archive") {
62306
- 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* () {
62307
62477
  const stopCommand = yield* normalizeDispatchCommand({
62308
62478
  type: "thread.session.stop",
62309
62479
  commandId: CommandId.make(`session-stop-for-archive:${normalizedCommand.commandId}`),
@@ -62315,12 +62485,12 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
62315
62485
  threadId: normalizedCommand.threadId,
62316
62486
  cause
62317
62487
  })));
62318
- yield* terminalManager.close({ threadId: normalizedCommand.threadId }).pipe(Effect.catch((error) => Effect.logWarning("failed to close thread terminals after archive", {
62319
- 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,
62320
62490
  error: error.message
62321
- })));
62491
+ }))), { discard: true });
62322
62492
  }
62323
- return result;
62493
+ return { sequence: result.sequence };
62324
62494
  }).pipe(Effect.mapError((cause) => isOrchestrationDispatchCommandError(cause) ? cause : new OrchestrationDispatchCommandError({
62325
62495
  message: "Failed to dispatch orchestration command",
62326
62496
  cause
@@ -87876,6 +88046,17 @@ function parseCodexSkillsListResponse(response, cwd) {
87876
88046
  return parsedSkill;
87877
88047
  });
87878
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
+ });
87879
88060
  const requestAllCodexModels = Effect.fn("requestAllCodexModels")(function* (client) {
87880
88061
  const models = [];
87881
88062
  let cursor = void 0;
@@ -87938,11 +88119,12 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
87938
88119
  skills: []
87939
88120
  };
87940
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));
87941
88123
  return {
87942
88124
  account: accountResponse,
87943
88125
  version,
87944
88126
  models: applyPreferredCodexDefaultModel(appendCustomCodexModels(models, input.customModels ?? [])),
87945
- skills: parseCodexSkillsListResponse(skillsResponse, input.cwd)
88127
+ skills
87946
88128
  };
87947
88129
  });
87948
88130
  const emptyCodexModelsFromSettings = (codexSettings) => {
@@ -90583,6 +90765,7 @@ const CodexDriver = {
90583
90765
  defaultConfig: () => decodeCodexSettings({}),
90584
90766
  create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () {
90585
90767
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
90768
+ const fileSystem = yield* FileSystem.FileSystem;
90586
90769
  const httpClient = yield* HttpClient.HttpClient;
90587
90770
  const serverSettings = yield* ServerSettingsService;
90588
90771
  const eventLoggers = yield* ProviderEventLoggers;
@@ -90617,7 +90800,7 @@ const CodexDriver = {
90617
90800
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
90618
90801
  });
90619
90802
  const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv);
90620
- 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));
90621
90804
  const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
90622
90805
  const snapshot = yield* makeManagedServerProvider({
90623
90806
  maintenanceCapabilities,
@@ -105238,18 +105421,18 @@ const ProviderRuntimeIngestionLive = Layer.effect(ProviderRuntimeIngestionServic
105238
105421
  * @module provider/userInvokedSkills
105239
105422
  */
105240
105423
  /**
105241
- * A leading `/name`, with whatever followed it.
105424
+ * A leading `/name` or `$name`, with whatever followed it.
105242
105425
  *
105243
- * Anchored at the start: a slash somewhere inside a sentence is a path, a date
105244
- * 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
105245
105428
  * messages. The name pattern matches what a skill directory can be called,
105246
105429
  * including the `plugin:skill` form.
105247
105430
  */
105248
- 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]*)$/;
105249
105432
  /** `SKILL.md` is the only file a skill is required to have. */
105250
105433
  const SKILL_FILENAME = "SKILL.md";
105251
105434
  const parseSlashInvocation = (text) => {
105252
- const match = SLASH_INVOCATION.exec(text.trim());
105435
+ const match = SKILL_INVOCATION.exec(text.trim());
105253
105436
  const name = match?.[1];
105254
105437
  if (name === void 0) return void 0;
105255
105438
  return {
@@ -105533,7 +105716,7 @@ const make$3 = Effect.gen(function* () {
105533
105716
  return yield* projectionSnapshotQuery.getProjectShellById(projectId).pipe(Effect.map(Option.getOrUndefined));
105534
105717
  });
105535
105718
  /**
105536
- * 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
105537
105720
  * workspace's.
105538
105721
  *
105539
105722
  * Project last so it wins, matching the CLI's most-specific-wins resolution.