syntaur 0.67.0 → 0.69.0

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 (73) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-CE2CIvur.js → _basePickBy-DKk6tHtk.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-BznLQqID.js → _baseUniq-DM-f7DWz.js} +1 -1
  4. package/dashboard/dist/assets/{arc-B6JqtWve.js → arc-ZBlA3YdV.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-P8UCT3rj.js → architectureDiagram-2XIMDMQ5-BUmvtGTF.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-C1ATZKSf.js → blockDiagram-WCTKOSBZ-B3qxWK6s.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-AvN1yayQ.js → c4Diagram-IC4MRINW-BEq_UJO-.js} +1 -1
  8. package/dashboard/dist/assets/channel-fypxffzQ.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-CyYz6mlJ.js → chunk-4BX2VUAB-C-Y9ryMm.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-QyOF7ox_.js → chunk-55IACEB6-CGdtbsjw.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-DVTHM99U.js → chunk-FMBD7UC4-DllxJhUp.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-DQfxaQtT.js → chunk-JSJVCQXG-jjMM8O5F.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-4R1oobH6.js → chunk-KX2RTZJC-B_6BPltQ.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-D8H_7yNS.js → chunk-NQ4KR5QH-D0hJiXHp.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-DLxDUSuo.js → chunk-QZHKN3VN-BCWo4hLS.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-CWuFDkVp.js → chunk-WL4C6EOR-DH_jEAwg.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-1KnjQvtL.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-1KnjQvtL.js +1 -0
  19. package/dashboard/dist/assets/clone-CKMabBhS.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-D23Dy_Za.js → cose-bilkent-S5V4N54A-5ld00TOH.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-CaKBk8eh.js → dagre-KLK3FWXG-Cnu6eQWy.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-BAPQPki-.js → diagram-E7M64L7V-_CBBKNP-.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-Cla6qyvn.js → diagram-IFDJBPK2-DE6WjIb1.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-DnTZq_y6.js → diagram-P4PSJMXO-DpW7UzNK.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-yc1_7ebn.js → erDiagram-INFDFZHY-BD2409fE.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-CMt2tF6O.js → flowDiagram-PKNHOUZH-1p0khhFI.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CXiK-5Gp.js → ganttDiagram-A5KZAMGK-B2zFyA4s.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-B4PbW06T.js → gitGraphDiagram-K3NZZRJ6-bH-4YH7h.js} +1 -1
  29. package/dashboard/dist/assets/{graph-CjdFy-q-.js → graph-BT24B6iQ.js} +1 -1
  30. package/dashboard/dist/assets/{index-DlUgV5eO.css → index-BZwPAi8K.css} +1 -1
  31. package/dashboard/dist/assets/index-Cxqr3rQB.js +670 -0
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-BeOWVt7p.js → infoDiagram-LFFYTUFH-CMzP4Hcg.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-LE7swZOH.js → ishikawaDiagram-PHBUUO56-DMEmFC7M.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-DG9-sksf.js → journeyDiagram-4ABVD52K-CAtzYQUm.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-7CKAw6eI.js → kanban-definition-K7BYSVSG-d1JVbtvX.js} +1 -1
  36. package/dashboard/dist/assets/{layout-CHChKPuz.js → layout-BVuI38I_.js} +1 -1
  37. package/dashboard/dist/assets/{linear-BWSj4au4.js → linear-Bc8PGMbp.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-CAwQaPqG.js → mermaid.core-UtwFYLNj.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-CtZWMkVN.js → mindmap-definition-YRQLILUH-DHc5RCDj.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CstCoaK7.js → pieDiagram-SKSYHLDU-9anIsdIA.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-DD8zRtaB.js → quadrantDiagram-337W2JSQ-FZ0D9HnU.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BWpKIK3b.js → requirementDiagram-Z7DCOOCP-BkjCvH_u.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-08gqQtFM.js → sankeyDiagram-WA2Y5GQK-DzqwYHDo.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-DsfIDBUH.js → sequenceDiagram-2WXFIKYE-BW4g5Ao-.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-Dqzfk_yy.js → stateDiagram-RAJIS63D-D0tKU3Z0.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DxWQhjNO.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DgH_p9jR.js → timeline-definition-YZTLITO2-Bu269QDX.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-7XKqMhq9.js → treemap-KZPCXAKY-BGu_rrva.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-BFtNLBWM.js → vennDiagram-LZ73GAT5-Cx_n5FRZ.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-CQPevMNl.js → xychartDiagram-JWTSCODW-BOJsKV_W.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +459 -63
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +1158 -702
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.d.ts +18 -0
  57. package/dist/launch/index.js +120 -12
  58. package/dist/launch/index.js.map +1 -1
  59. package/package.json +1 -1
  60. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  61. package/platforms/claude-code/hooks/hooks.json +1 -1
  62. package/platforms/claude-code/hooks/session-cleanup.sh +21 -0
  63. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  64. package/platforms/codex/hooks.json +1 -1
  65. package/platforms/codex/scripts/session-cleanup.sh +13 -0
  66. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  67. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  68. package/dashboard/dist/assets/channel-IZujZyS6.js +0 -1
  69. package/dashboard/dist/assets/classDiagram-VBA2DB6C-ChnJofe3.js +0 -1
  70. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-ChnJofe3.js +0 -1
  71. package/dashboard/dist/assets/clone-JjIbzsqJ.js +0 -1
  72. package/dashboard/dist/assets/index-COOcebN7.js +0 -659
  73. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-D542UPiR.js +0 -1
@@ -937,6 +937,38 @@ CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
937
937
  });
938
938
 
939
939
  // src/lifecycle/event-emit.ts
940
+ var event_emit_exports = {};
941
+ __export(event_emit_exports, {
942
+ emitEvent: () => emitEvent,
943
+ isSuppressingEvents: () => isSuppressingEvents,
944
+ recordStatusEvent: () => recordStatusEvent,
945
+ resolveActor: () => resolveActor,
946
+ setSuppressEvents: () => setSuppressEvents,
947
+ withSuppressedEvents: () => withSuppressedEvents
948
+ });
949
+ function setSuppressEvents(value) {
950
+ suppressEvents = value;
951
+ }
952
+ function isSuppressingEvents() {
953
+ return suppressEvents;
954
+ }
955
+ function withSuppressedEvents(fn) {
956
+ const prior = suppressEvents;
957
+ suppressEvents = true;
958
+ try {
959
+ const result = fn();
960
+ if (result instanceof Promise) {
961
+ return result.finally(() => {
962
+ suppressEvents = prior;
963
+ });
964
+ }
965
+ suppressEvents = prior;
966
+ return result;
967
+ } catch (e) {
968
+ suppressEvents = prior;
969
+ throw e;
970
+ }
971
+ }
940
972
  function resolveActor(by) {
941
973
  return by ?? "system";
942
974
  }
@@ -952,6 +984,10 @@ function recordStatusEvent(input) {
952
984
  details: { from: input.from, to: input.to, command: input.command }
953
985
  });
954
986
  }
987
+ function emitEvent(input) {
988
+ if (suppressEvents) return;
989
+ recordEvent(input);
990
+ }
955
991
  var suppressEvents;
956
992
  var init_event_emit = __esm({
957
993
  "src/lifecycle/event-emit.ts"() {
@@ -3764,7 +3800,9 @@ __export(config_exports, {
3764
3800
  getTerminal: () => getTerminal,
3765
3801
  normalizeFactDeclarations: () => normalizeFactDeclarations,
3766
3802
  parseAgentCommand: () => parseAgentCommand,
3803
+ parseDurationMs: () => parseDurationMs,
3767
3804
  parseSearchConfig: () => parseSearchConfig,
3805
+ parseStalenessConfig: () => parseStalenessConfig,
3768
3806
  parseStatusConfig: () => parseStatusConfig,
3769
3807
  parseTerminalConfig: () => parseTerminalConfig,
3770
3808
  readConfig: () => readConfig,
@@ -3780,6 +3818,7 @@ __export(config_exports, {
3780
3818
  validateDeriveConfig: () => validateDeriveConfig,
3781
3819
  validateDeriveShape: () => validateDeriveShape,
3782
3820
  validateFactDeclarations: () => validateFactDeclarations,
3821
+ validateStalenessConfig: () => validateStalenessConfig,
3783
3822
  writeAgentsConfig: () => writeAgentsConfig,
3784
3823
  writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
3785
3824
  writeSearchConfig: () => writeSearchConfig,
@@ -3791,6 +3830,13 @@ __export(config_exports, {
3791
3830
  import { readFile as readFile5 } from "fs/promises";
3792
3831
  import { spawnSync } from "child_process";
3793
3832
  import { resolve as resolve7, isAbsolute } from "path";
3833
+ function parseDurationMs(raw2) {
3834
+ const m = raw2.trim().match(DURATION_RE);
3835
+ if (!m) return null;
3836
+ const n = Number(m[1]);
3837
+ if (!Number.isFinite(n) || n <= 0) return null;
3838
+ return n * DURATION_UNIT_MS[m[2] ?? "ms"];
3839
+ }
3794
3840
  function parseAgentCommand(value, agentId) {
3795
3841
  if (typeof value !== "string" || value.trim() === "") {
3796
3842
  throw new AgentConfigError(
@@ -5027,6 +5073,65 @@ ${cleanedFm}
5027
5073
  ---${afterFrontmatter}`;
5028
5074
  await writeFileForce(configPath, newContent);
5029
5075
  }
5076
+ function parseStalenessConfig(content) {
5077
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
5078
+ if (!match) return null;
5079
+ const fmBlock = match[1];
5080
+ const blockStart = fmBlock.match(/^staleness:\s*$/m);
5081
+ if (!blockStart) return null;
5082
+ const startIdx = (blockStart.index ?? 0) + blockStart[0].length;
5083
+ const lines = fmBlock.slice(startIdx).split("\n");
5084
+ const out = {};
5085
+ for (const line of lines) {
5086
+ if (line.trim() === "") continue;
5087
+ const trimmed = line.trimStart();
5088
+ const indent = line.length - trimmed.length;
5089
+ if (indent === 0) break;
5090
+ const ci = trimmed.indexOf(":");
5091
+ if (ci <= 0) continue;
5092
+ const key = trimmed.slice(0, ci).trim();
5093
+ const field = STALENESS_KEY_TO_FIELD[key];
5094
+ if (!field) continue;
5095
+ let value = trimmed.slice(ci + 1).trim();
5096
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
5097
+ value = value.slice(1, -1);
5098
+ }
5099
+ const ms = parseDurationMs(value);
5100
+ if (ms !== null) out[field] = ms;
5101
+ }
5102
+ return Object.keys(out).length > 0 ? out : null;
5103
+ }
5104
+ function validateStalenessConfig(content) {
5105
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
5106
+ if (!match) return [];
5107
+ const fmBlock = match[1];
5108
+ const blockStart = fmBlock.match(/^staleness:\s*$/m);
5109
+ if (!blockStart) return [];
5110
+ const startIdx = (blockStart.index ?? 0) + blockStart[0].length;
5111
+ const lines = fmBlock.slice(startIdx).split("\n");
5112
+ const problems = [];
5113
+ for (const line of lines) {
5114
+ if (line.trim() === "") continue;
5115
+ const trimmed = line.trimStart();
5116
+ const indent = line.length - trimmed.length;
5117
+ if (indent === 0) break;
5118
+ const ci = trimmed.indexOf(":");
5119
+ if (ci <= 0) continue;
5120
+ const key = trimmed.slice(0, ci).trim();
5121
+ let value = trimmed.slice(ci + 1).trim();
5122
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
5123
+ value = value.slice(1, -1);
5124
+ }
5125
+ if (!(key in STALENESS_KEY_TO_FIELD)) {
5126
+ problems.push(`staleness.${key}: unknown key (expected one of ${Object.keys(STALENESS_KEY_TO_FIELD).join(", ")})`);
5127
+ continue;
5128
+ }
5129
+ if (parseDurationMs(value) === null) {
5130
+ problems.push(`staleness.${key}: "${value}" is not a positive duration (e.g. 7d, 12h, 30m, 90s, 500ms)`);
5131
+ }
5132
+ }
5133
+ return problems;
5134
+ }
5030
5135
  function parseSearchConfig(content) {
5031
5136
  const match = content.match(/^---\n([\s\S]*?)\n---/);
5032
5137
  if (!match) return null;
@@ -5309,7 +5414,9 @@ async function readConfig() {
5309
5414
  }
5310
5415
  })(),
5311
5416
  searchConfig: parseSearchConfig(content),
5312
- workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock)
5417
+ workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock),
5418
+ staleness: parseStalenessConfig(content),
5419
+ stalenessWatchdog: String(fm["stalenessWatchdog"]).toLowerCase() === "true"
5313
5420
  };
5314
5421
  }
5315
5422
  function getAssignmentTypes(config) {
@@ -5372,7 +5479,7 @@ async function updateAgentsConfig(mutation, options = {}) {
5372
5479
  await writeAgentsConfig(next);
5373
5480
  return { previous, next, written: true };
5374
5481
  }
5375
- var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, SESSION_AUTO_TRACK_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
5482
+ var STALENESS_KEY_TO_FIELD, DURATION_RE, DURATION_UNIT_MS, DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, SESSION_AUTO_TRACK_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
5376
5483
  var init_config2 = __esm({
5377
5484
  "src/utils/config.ts"() {
5378
5485
  "use strict";
@@ -5389,6 +5496,21 @@ var init_config2 = __esm({
5389
5496
  init_terminal_schema();
5390
5497
  init_search_schema();
5391
5498
  init_workspace_visibility_schema();
5499
+ STALENESS_KEY_TO_FIELD = {
5500
+ inProgressNoActivity: "inProgressNoActivityMs",
5501
+ readyUnclaimed: "readyUnclaimedMs",
5502
+ reviewAging: "reviewAgingMs",
5503
+ blockedAging: "blockedAgingMs",
5504
+ planApprovalAging: "planApprovalAgingMs"
5505
+ };
5506
+ DURATION_RE = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/;
5507
+ DURATION_UNIT_MS = {
5508
+ ms: 1,
5509
+ s: 1e3,
5510
+ m: 6e4,
5511
+ h: 36e5,
5512
+ d: 864e5
5513
+ };
5392
5514
  DEFAULT_ASSIGNMENT_TYPES = {
5393
5515
  definitions: [
5394
5516
  { id: "feature", label: "Feature" },
@@ -5431,7 +5553,9 @@ var init_config2 = __esm({
5431
5553
  searchConfig: null,
5432
5554
  workspaceVisibility: {
5433
5555
  hidden: []
5434
- }
5556
+ },
5557
+ staleness: null,
5558
+ stalenessWatchdog: false
5435
5559
  };
5436
5560
  AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
5437
5561
  SESSION_AUTO_TRACK_VALUES = ["all", "workspaces-only", "off"];
@@ -7793,6 +7917,74 @@ var init_overviewCopy = __esm({
7793
7917
  }
7794
7918
  });
7795
7919
 
7920
+ // src/staleness/classify.ts
7921
+ function resolveStaleThresholds(overrides) {
7922
+ const merged = { ...DEFAULT_STALE_THRESHOLDS };
7923
+ if (overrides) {
7924
+ for (const key of Object.keys(merged)) {
7925
+ const v = overrides[key];
7926
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) merged[key] = v;
7927
+ }
7928
+ }
7929
+ return merged;
7930
+ }
7931
+ function classifyNeedsAttention(input, thresholds = DEFAULT_STALE_THRESHOLDS) {
7932
+ if (input.isTerminal) return [];
7933
+ const reasons = [];
7934
+ const age = input.statusAgeMs;
7935
+ const aged = (gate) => age !== null && age >= gate;
7936
+ const blocked = input.disposition === "blocked" || input.blockedReason !== null;
7937
+ if (input.phase === IN_PROGRESS_PHASE && !blocked && aged(thresholds.inProgressNoActivityMs) && input.lastActivityMs !== null && input.lastActivityMs >= thresholds.inProgressNoActivityMs) {
7938
+ reasons.push({
7939
+ kind: "in_progress_no_activity",
7940
+ label: "In progress, but no recent activity",
7941
+ severity: "medium"
7942
+ });
7943
+ }
7944
+ if (input.phase === READY_PHASE && input.assignee === null && aged(thresholds.readyUnclaimedMs)) {
7945
+ reasons.push({
7946
+ kind: "ready_unclaimed",
7947
+ label: "Ready to implement, unclaimed",
7948
+ severity: "medium"
7949
+ });
7950
+ }
7951
+ if (input.phase === REVIEW_PHASE && aged(thresholds.reviewAgingMs)) {
7952
+ reasons.push({ kind: "review_aging", label: "Awaiting review", severity: "high" });
7953
+ }
7954
+ if (blocked && aged(thresholds.blockedAgingMs)) {
7955
+ reasons.push({ kind: "blocked_aging", label: "Blocked and aging", severity: "high" });
7956
+ }
7957
+ if (input.phase === PLANNING_PHASE && input.planExists && !input.planApproved && aged(thresholds.planApprovalAgingMs)) {
7958
+ reasons.push({
7959
+ kind: "plan_awaiting_approval",
7960
+ label: "Plan awaiting approval",
7961
+ severity: "medium"
7962
+ });
7963
+ }
7964
+ if (input.depsSatisfied === false && (input.phase === READY_PHASE || input.phase === IN_PROGRESS_PHASE)) {
7965
+ reasons.push({ kind: "deps_unsatisfied", label: "Unmet dependencies", severity: "high" });
7966
+ }
7967
+ return reasons;
7968
+ }
7969
+ var DAY, DEFAULT_STALE_THRESHOLDS, PLANNING_PHASE, READY_PHASE, IN_PROGRESS_PHASE, REVIEW_PHASE;
7970
+ var init_classify = __esm({
7971
+ "src/staleness/classify.ts"() {
7972
+ "use strict";
7973
+ DAY = 24 * 60 * 60 * 1e3;
7974
+ DEFAULT_STALE_THRESHOLDS = {
7975
+ inProgressNoActivityMs: 7 * DAY,
7976
+ readyUnclaimedMs: 3 * DAY,
7977
+ reviewAgingMs: 3 * DAY,
7978
+ blockedAgingMs: 3 * DAY,
7979
+ planApprovalAgingMs: 3 * DAY
7980
+ };
7981
+ PLANNING_PHASE = "ready_for_planning";
7982
+ READY_PHASE = "ready_to_implement";
7983
+ IN_PROGRESS_PHASE = "in_progress";
7984
+ REVIEW_PHASE = "review";
7985
+ }
7986
+ });
7987
+
7796
7988
  // src/dashboard/servers.ts
7797
7989
  import { readdir as readdir9, readFile as readFile12, unlink as unlink2 } from "fs/promises";
7798
7990
  import { resolve as resolve15 } from "path";
@@ -8427,7 +8619,38 @@ var init_scanner = __esm({
8427
8619
  });
8428
8620
 
8429
8621
  // src/dashboard/api.ts
8430
- import { readdir as readdir10, readFile as readFile13, writeFile as writeFile3 } from "fs/promises";
8622
+ var api_exports = {};
8623
+ __export(api_exports, {
8624
+ WorkspaceBlockedError: () => WorkspaceBlockedError,
8625
+ clearStatusConfigCache: () => clearStatusConfigCache,
8626
+ collectStaleCandidates: () => collectStaleCandidates,
8627
+ createWorkspace: () => createWorkspace,
8628
+ deleteWorkspace: () => deleteWorkspace,
8629
+ getAssignmentDetail: () => getAssignmentDetail,
8630
+ getAssignmentDetailById: () => getAssignmentDetailById,
8631
+ getEditableDocument: () => getEditableDocument,
8632
+ getEditableDocumentById: () => getEditableDocumentById,
8633
+ getHelp: () => getHelp,
8634
+ getMemoryDetail: () => getMemoryDetail,
8635
+ getOverview: () => getOverview,
8636
+ getPlaybookDetail: () => getPlaybookDetail,
8637
+ getProjectDetail: () => getProjectDetail,
8638
+ getResourceDetail: () => getResourceDetail,
8639
+ getStatusConfig: () => getStatusConfig,
8640
+ installRecordsInvalidation: () => installRecordsInvalidation,
8641
+ invalidateRecordsCache: () => invalidateRecordsCache,
8642
+ listAllMemories: () => listAllMemories,
8643
+ listAllResources: () => listAllResources,
8644
+ listArchived: () => listArchived,
8645
+ listAssignmentsBoard: () => listAssignmentsBoard,
8646
+ listPlaybooks: () => listPlaybooks,
8647
+ listProjects: () => listProjects,
8648
+ listWorkspaceRecords: () => listWorkspaceRecords,
8649
+ listWorkspaces: () => listWorkspaces,
8650
+ resolveProjectPath: () => resolveProjectPath,
8651
+ resolveWorkspaceMembers: () => resolveWorkspaceMembers
8652
+ });
8653
+ import { readdir as readdir10, readFile as readFile13, writeFile as writeFile3, stat as stat2 } from "fs/promises";
8431
8654
  import { resolve as resolve17, dirname as dirname2, basename } from "path";
8432
8655
  function clearFrontmatterField(content, key) {
8433
8656
  const fieldRegex = new RegExp(`^(${escapeRegExp2(key)}:)\\s*.*$`, "m");
@@ -8818,10 +9041,10 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
8818
9041
  (total, record) => total + (record.summary.progress["failed"] ?? 0),
8819
9042
  0
8820
9043
  ),
8821
- staleAssignments: activeProjectRecords.reduce(
8822
- (total, record) => total + activeAssignments(record.assignments).filter((assignment) => isStale(assignment.updated)).length,
8823
- 0
8824
- ) + activeStandaloneRecords.filter((sr) => isStale(sr.record.updated)).length
9044
+ // Derived from the SAME classifier verdict as the stale segment (via the
9045
+ // pre-cap segment total) so the badge count can never diverge from the
9046
+ // listed rows.
9047
+ staleAssignments: segments.stale.total
8825
9048
  },
8826
9049
  hero,
8827
9050
  segments,
@@ -9973,9 +10196,77 @@ function segmentSeverity(segment) {
9973
10196
  return "medium";
9974
10197
  }
9975
10198
  }
10199
+ function topStaleReason(reasons) {
10200
+ if (reasons.length === 0) return null;
10201
+ return reasons.slice().sort((a, b) => STALE_SEVERITY_RANK[b.severity] - STALE_SEVERITY_RANK[a.severity])[0];
10202
+ }
10203
+ async function readProgressActivityMs(progressPath, now) {
10204
+ try {
10205
+ const s = await stat2(progressPath);
10206
+ return Math.max(0, now - s.mtimeMs);
10207
+ } catch {
10208
+ return null;
10209
+ }
10210
+ }
10211
+ function classifyAssignmentRecord(assignment, terminalStatuses, depsSatisfied, lastActivityMs, thresholds) {
10212
+ const virtuals = deriveStatusVirtuals(assignment, terminalStatuses);
10213
+ return classifyNeedsAttention(
10214
+ {
10215
+ phase: virtuals.phase,
10216
+ disposition: virtuals.disposition,
10217
+ isTerminal: terminalStatuses.has(assignment.status),
10218
+ assignee: assignment.assignee ?? null,
10219
+ blockedReason: assignment.blockedReason,
10220
+ depsSatisfied,
10221
+ // plan_awaiting_approval is deferred to the decision inbox's plan-approval
10222
+ // category for now; pass values that keep that reason dormant.
10223
+ planExists: false,
10224
+ planApproved: true,
10225
+ statusAgeMs: virtuals.statusAge,
10226
+ lastActivityMs
10227
+ },
10228
+ thresholds
10229
+ );
10230
+ }
10231
+ async function collectStaleCandidates(projectsDir, assignmentsDir2) {
10232
+ const [projectRecords, standaloneRecords] = await Promise.all([
10233
+ listProjectRecords(projectsDir),
10234
+ listStandaloneRecords(assignmentsDir2)
10235
+ ]);
10236
+ const { terminalStatuses } = await getStatusConfig();
10237
+ const thresholds = resolveStaleThresholds((await readConfig()).staleness);
10238
+ const now = Date.now();
10239
+ const out = [];
10240
+ for (const record of projectRecords) {
10241
+ if (isProjectArchived(record.summary)) continue;
10242
+ const projectPath = resolve17(projectsDir, record.summary.slug);
10243
+ const depMap = /* @__PURE__ */ new Map();
10244
+ for (const a of record.assignments) depMap.set(a.slug, a.status);
10245
+ for (const assignment of activeAssignments(record.assignments)) {
10246
+ const depsSatisfied = assignment.dependsOn.length === 0 ? true : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap)).length === 0;
10247
+ const lastActivityMs = await readProgressActivityMs(
10248
+ resolve17(projectPath, "assignments", assignment.slug, "progress.md"),
10249
+ now
10250
+ );
10251
+ const reasons = classifyAssignmentRecord(assignment, terminalStatuses, depsSatisfied, lastActivityMs, thresholds);
10252
+ if (reasons.length > 0) {
10253
+ out.push({ assignmentId: assignment.id, projectSlug: record.summary.slug, reasons });
10254
+ }
10255
+ }
10256
+ }
10257
+ for (const sr of standaloneRecords) {
10258
+ if (sr.record.archived === true) continue;
10259
+ const lastActivityMs = await readProgressActivityMs(resolve17(sr.assignmentDir, "progress.md"), now);
10260
+ const reasons = classifyAssignmentRecord(sr.record, terminalStatuses, true, lastActivityMs, thresholds);
10261
+ if (reasons.length > 0) out.push({ assignmentId: sr.record.id, projectSlug: null, reasons });
10262
+ }
10263
+ return out;
10264
+ }
9976
10265
  async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords, traces) {
9977
10266
  const now = Date.now();
9978
10267
  const buckets = emptyBuckets();
10268
+ const { terminalStatuses } = await getStatusConfig();
10269
+ const staleThresholds = resolveStaleThresholds((await readConfig()).staleness);
9979
10270
  const newestPool = [];
9980
10271
  for (const record of projectRecords) {
9981
10272
  if (isProjectArchived(record.summary)) continue;
@@ -9984,6 +10275,7 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
9984
10275
  depMap.set(a.slug, a.status);
9985
10276
  }
9986
10277
  const visibleAssignments = activeAssignments(record.assignments);
10278
+ const projectPath = resolve17(projectsDir, record.summary.slug);
9987
10279
  const resolvedTransitions = await Promise.all(
9988
10280
  visibleAssignments.map(async (assignment) => {
9989
10281
  const t0 = traces ? performance.now() : 0;
@@ -9995,13 +10287,25 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
9995
10287
  { traces, dependencyStatusMap: depMap }
9996
10288
  );
9997
10289
  if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
9998
- return { assignment, availableTransitions };
10290
+ const depsSatisfied = assignment.dependsOn.length === 0 ? true : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap)).length === 0;
10291
+ const lastActivityMs = await readProgressActivityMs(
10292
+ resolve17(projectPath, "assignments", assignment.slug, "progress.md"),
10293
+ now
10294
+ );
10295
+ return { assignment, availableTransitions, depsSatisfied, lastActivityMs };
9999
10296
  })
10000
10297
  );
10001
- for (const { assignment, availableTransitions } of resolvedTransitions) {
10298
+ for (const { assignment, availableTransitions, depsSatisfied, lastActivityMs } of resolvedTransitions) {
10002
10299
  const segmentId = STATUS_TO_SEGMENT[assignment.status];
10003
- const stale = isStale(assignment.updated);
10004
- const isTerminal = TERMINAL_STATUSES2.has(assignment.status);
10300
+ const isTerminal = terminalStatuses.has(assignment.status);
10301
+ const staleReasons = classifyAssignmentRecord(
10302
+ assignment,
10303
+ terminalStatuses,
10304
+ depsSatisfied,
10305
+ lastActivityMs,
10306
+ staleThresholds
10307
+ );
10308
+ const stale = staleReasons.length > 0;
10005
10309
  const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));
10006
10310
  const baseId = `${record.summary.slug}:${assignment.slug}`;
10007
10311
  const shared = {
@@ -10030,11 +10334,12 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
10030
10334
  buckets[segmentId].push(primary);
10031
10335
  }
10032
10336
  if (stale && !isTerminal) {
10337
+ const top = topStaleReason(staleReasons);
10033
10338
  const staleItem = {
10034
10339
  ...shared,
10035
10340
  id: `${baseId}:stale`,
10036
10341
  severity: "low",
10037
- reason: SEGMENT_REASON.stale,
10342
+ reason: top?.label ?? SEGMENT_REASON.stale,
10038
10343
  segment: "stale"
10039
10344
  };
10040
10345
  buckets.stale.push(staleItem);
@@ -10058,14 +10363,22 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
10058
10363
  const t0 = traces ? performance.now() : 0;
10059
10364
  const availableTransitions = await getStandaloneAvailableTransitions(sr.record);
10060
10365
  if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
10061
- return { sr, availableTransitions };
10366
+ const lastActivityMs = await readProgressActivityMs(resolve17(sr.assignmentDir, "progress.md"), now);
10367
+ return { sr, availableTransitions, lastActivityMs };
10062
10368
  })
10063
10369
  );
10064
- for (const { sr, availableTransitions } of resolvedStandaloneTransitions) {
10370
+ for (const { sr, availableTransitions, lastActivityMs } of resolvedStandaloneTransitions) {
10065
10371
  const assignment = sr.record;
10066
10372
  const segmentId = STATUS_TO_SEGMENT[assignment.status];
10067
- const stale = isStale(assignment.updated);
10068
- const isTerminal = TERMINAL_STATUSES2.has(assignment.status);
10373
+ const isTerminal = terminalStatuses.has(assignment.status);
10374
+ const staleReasons = classifyAssignmentRecord(
10375
+ assignment,
10376
+ terminalStatuses,
10377
+ true,
10378
+ lastActivityMs,
10379
+ staleThresholds
10380
+ );
10381
+ const stale = staleReasons.length > 0;
10069
10382
  const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));
10070
10383
  const baseId = `standalone:${sr.id}`;
10071
10384
  const shared = {
@@ -10093,11 +10406,12 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
10093
10406
  });
10094
10407
  }
10095
10408
  if (stale && !isTerminal) {
10409
+ const top = topStaleReason(staleReasons);
10096
10410
  buckets.stale.push({
10097
10411
  ...shared,
10098
10412
  id: `${baseId}:stale`,
10099
10413
  severity: "low",
10100
- reason: SEGMENT_REASON.stale,
10414
+ reason: top?.label ?? SEGMENT_REASON.stale,
10101
10415
  segment: "stale"
10102
10416
  });
10103
10417
  }
@@ -10218,13 +10532,6 @@ function parseTimestamp(timestamp) {
10218
10532
  const parsed = Date.parse(timestamp);
10219
10533
  return Number.isFinite(parsed) ? parsed : 0;
10220
10534
  }
10221
- function isStale(updated) {
10222
- const timestamp = parseTimestamp(updated);
10223
- if (timestamp === 0) {
10224
- return false;
10225
- }
10226
- return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
10227
- }
10228
10535
  async function countOpenQuestions(projectPath, assignmentSlug) {
10229
10536
  const commentsPath = resolve17(
10230
10537
  projectPath,
@@ -10343,7 +10650,7 @@ async function getPlaybookDetail(playbooksDir2, slug) {
10343
10650
  enabled
10344
10651
  };
10345
10652
  }
10346
- var WorkspaceBlockedError, STALE_ASSIGNMENT_MS, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, TERMINAL_STATUSES2, STATUS_TO_SEGMENT, HERO_PRIORITY, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
10653
+ var WorkspaceBlockedError, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, STATUS_TO_SEGMENT, HERO_PRIORITY, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS, STALE_SEVERITY_RANK;
10347
10654
  var init_api = __esm({
10348
10655
  "src/dashboard/api.ts"() {
10349
10656
  "use strict";
@@ -10361,6 +10668,7 @@ var init_api = __esm({
10361
10668
  init_help();
10362
10669
  init_agent_sessions();
10363
10670
  init_overviewCopy();
10671
+ init_classify();
10364
10672
  WorkspaceBlockedError = class extends Error {
10365
10673
  blockedBy;
10366
10674
  constructor(blockedBy) {
@@ -10371,7 +10679,6 @@ var init_api = __esm({
10371
10679
  this.blockedBy = blockedBy;
10372
10680
  }
10373
10681
  };
10374
- STALE_ASSIGNMENT_MS = 7 * 24 * 60 * 60 * 1e3;
10375
10682
  RECENT_PROJECTS_LIMIT = 6;
10376
10683
  RECENT_ACTIVITY_LIMIT = 12;
10377
10684
  RECENT_SESSIONS_LIMIT = 10;
@@ -10379,7 +10686,6 @@ var init_api = __esm({
10379
10686
  SEGMENT_DISPLAY_CAP = 5;
10380
10687
  STALE_LIMIT_DEFAULT = 50;
10381
10688
  STALE_LIMIT_MAX = 200;
10382
- TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["completed", "failed", "archived"]);
10383
10689
  STATUS_TO_SEGMENT = {
10384
10690
  review: "readyForReview",
10385
10691
  ready_to_implement: "readyToImplement",
@@ -10472,6 +10778,7 @@ var init_api = __esm({
10472
10778
  failed: "fill:#9f2d2d,stroke:#651616,color:#ffffff",
10473
10779
  review: "fill:#c6911e,stroke:#7a5a10,color:#ffffff"
10474
10780
  };
10781
+ STALE_SEVERITY_RANK = { high: 3, medium: 2, low: 1 };
10475
10782
  }
10476
10783
  });
10477
10784
 
@@ -10907,12 +11214,13 @@ __export(recompute_exports, {
10907
11214
  markDeriveMigrated: () => markDeriveMigrated,
10908
11215
  recomputeAll: () => recomputeAll,
10909
11216
  recomputeAndWrite: () => recomputeAndWrite,
11217
+ recomputeAssignmentDir: () => recomputeAssignmentDir,
10910
11218
  recomputeDependents: () => recomputeDependents,
10911
11219
  resolveDeriveContext: () => resolveDeriveContext
10912
11220
  });
10913
11221
  import { createHash as createHash2 } from "crypto";
10914
- import { open, readdir as readdir12, readFile as readFile17, unlink as unlink5, stat as stat2 } from "fs/promises";
10915
- import { dirname as dirname4, resolve as resolve22 } from "path";
11222
+ import { open, readdir as readdir12, readFile as readFile17, unlink as unlink5, stat as stat3 } from "fs/promises";
11223
+ import { basename as basename3, dirname as dirname4, resolve as resolve22 } from "path";
10916
11224
  async function isDeriveMigrated() {
10917
11225
  return fileExists(resolve22(syntaurRoot(), MIGRATION_MARKER));
10918
11226
  }
@@ -10952,7 +11260,7 @@ async function acquireLock(assignmentDir) {
10952
11260
  const code = err.code;
10953
11261
  if (code !== "EEXIST") throw err;
10954
11262
  try {
10955
- const info = await stat2(lockPath);
11263
+ const info = await stat3(lockPath);
10956
11264
  if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
10957
11265
  await unlink5(lockPath).catch(() => {
10958
11266
  });
@@ -11144,6 +11452,18 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
11144
11452
  }
11145
11453
  return summary;
11146
11454
  }
11455
+ async function recomputeAssignmentDir(assignmentDir, cause, by) {
11456
+ try {
11457
+ const assignmentPath = resolve22(assignmentDir, "assignment.md");
11458
+ if (!await fileExists(assignmentPath)) return null;
11459
+ const parent = dirname4(assignmentDir);
11460
+ const projectDir = basename3(parent) === "assignments" ? dirname4(parent) : null;
11461
+ const context = await resolveDeriveContext();
11462
+ return await recomputeAndWrite(assignmentPath, { cause, by, projectDir, context });
11463
+ } catch {
11464
+ return null;
11465
+ }
11466
+ }
11147
11467
  var LOCK_FILE, LOCK_STALE_MS, LOCK_WAIT_MS, LOCK_MAX_WAITS, CAS_RETRIES, MIGRATION_MARKER;
11148
11468
  var init_recompute = __esm({
11149
11469
  "src/lifecycle/recompute.ts"() {
@@ -11251,14 +11571,14 @@ var init_process_info = __esm({
11251
11571
  });
11252
11572
 
11253
11573
  // src/usage/cwd-extractor.ts
11254
- import { open as open3, readdir as readdir13, stat as stat3 } from "fs/promises";
11574
+ import { open as open3, readdir as readdir13, stat as stat4 } from "fs/promises";
11255
11575
  import { join as join4 } from "path";
11256
11576
  import { homedir as homedir3 } from "os";
11257
11577
  async function extractClaudeSessionMeta(jsonlPath) {
11258
11578
  const cwd = await derivePathFromTranscript(jsonlPath);
11259
11579
  if (!cwd) return null;
11260
- const basename6 = jsonlPath.split("/").pop() ?? "";
11261
- const sessionId = basename6.replace(/\.jsonl$/, "");
11580
+ const basename7 = jsonlPath.split("/").pop() ?? "";
11581
+ const sessionId = basename7.replace(/\.jsonl$/, "");
11262
11582
  if (!sessionId) return null;
11263
11583
  const startTs = await readFirstTimestamp(jsonlPath);
11264
11584
  const endTs = await readLastTimestamp(jsonlPath);
@@ -11353,8 +11673,8 @@ async function* walkClaudeProjects(opts = {}) {
11353
11673
  async function* walkCodexSessions(opts = {}) {
11354
11674
  const root = resolveCodexSessionsRoot(opts.root);
11355
11675
  for await (const filePath of walkJsonlRecursive(root)) {
11356
- const basename6 = filePath.split("/").pop() ?? "";
11357
- if (!basename6.endsWith(".jsonl")) continue;
11676
+ const basename7 = filePath.split("/").pop() ?? "";
11677
+ if (!basename7.endsWith(".jsonl")) continue;
11358
11678
  if (opts.sinceMtimeMs !== void 0) {
11359
11679
  const mtime = await mtimeMs(filePath);
11360
11680
  if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
@@ -11372,10 +11692,10 @@ function resolveCodexSessionsRoot(override) {
11372
11692
  return join4(homedir3(), ".codex", "sessions");
11373
11693
  }
11374
11694
  async function extractPiSessionMeta(jsonlPath) {
11375
- const basename6 = jsonlPath.split("/").pop() ?? "";
11376
- const underscoreIdx = basename6.lastIndexOf("_");
11695
+ const basename7 = jsonlPath.split("/").pop() ?? "";
11696
+ const underscoreIdx = basename7.lastIndexOf("_");
11377
11697
  if (underscoreIdx === -1) return null;
11378
- const sessionId = basename6.slice(underscoreIdx + 1).replace(/\.jsonl$/, "");
11698
+ const sessionId = basename7.slice(underscoreIdx + 1).replace(/\.jsonl$/, "");
11379
11699
  if (!sessionId) return null;
11380
11700
  let handle;
11381
11701
  try {
@@ -11490,7 +11810,7 @@ async function* walkJsonlRecursive(root) {
11490
11810
  }
11491
11811
  async function mtimeMs(path) {
11492
11812
  try {
11493
- const s = await stat3(path);
11813
+ const s = await stat4(path);
11494
11814
  return s.mtimeMs;
11495
11815
  } catch {
11496
11816
  return null;
@@ -12096,6 +12416,40 @@ var init_scanner2 = __esm({
12096
12416
  }
12097
12417
  });
12098
12418
 
12419
+ // src/staleness/watchdog.ts
12420
+ var watchdog_exports = {};
12421
+ __export(watchdog_exports, {
12422
+ runStalenessWatchdogTick: () => runStalenessWatchdogTick
12423
+ });
12424
+ function runStalenessWatchdogTick(candidates, seen, emit) {
12425
+ const staleNow = /* @__PURE__ */ new Map();
12426
+ for (const c of candidates) {
12427
+ if (c.reasons.length > 0) staleNow.set(c.assignmentId, c);
12428
+ }
12429
+ let newlyStale = 0;
12430
+ for (const [id, c] of staleNow) {
12431
+ if (!seen.has(id)) {
12432
+ seen.add(id);
12433
+ newlyStale++;
12434
+ emit({ assignmentId: id, projectSlug: c.projectSlug, type: "staleness-detected", reasons: c.reasons });
12435
+ }
12436
+ }
12437
+ let cleared = 0;
12438
+ for (const id of [...seen]) {
12439
+ if (!staleNow.has(id)) {
12440
+ seen.delete(id);
12441
+ cleared++;
12442
+ emit({ assignmentId: id, projectSlug: null, type: "staleness-cleared", reasons: [] });
12443
+ }
12444
+ }
12445
+ return { scanned: candidates.length, stale: staleNow.size, newlyStale, cleared };
12446
+ }
12447
+ var init_watchdog = __esm({
12448
+ "src/staleness/watchdog.ts"() {
12449
+ "use strict";
12450
+ }
12451
+ });
12452
+
12099
12453
  // src/dashboard/server.ts
12100
12454
  init_paths();
12101
12455
  init_api();
@@ -13264,7 +13618,7 @@ init_timestamp();
13264
13618
  init_fs();
13265
13619
  init_git_worktree();
13266
13620
  import { Router as Router2 } from "express";
13267
- import { resolve as resolve23, basename as basename3, isAbsolute as isAbsolute4 } from "path";
13621
+ import { resolve as resolve23, basename as basename4, isAbsolute as isAbsolute4 } from "path";
13268
13622
  import { rm, readFile as readFile18, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
13269
13623
  import { spawnSync as spawnSync3 } from "child_process";
13270
13624
 
@@ -14485,7 +14839,7 @@ function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
14485
14839
  res.status(404).json({ error: `Project "${slug}" not found` });
14486
14840
  return;
14487
14841
  }
14488
- const document = await getEditableDocument(projectsDir, "memory", basename3(projectDir), itemSlug);
14842
+ const document = await getEditableDocument(projectsDir, "memory", basename4(projectDir), itemSlug);
14489
14843
  if (!document) {
14490
14844
  res.status(404).json({ error: "Memory not found" });
14491
14845
  return;
@@ -14504,7 +14858,7 @@ function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
14504
14858
  res.status(404).json({ error: `Project "${slug}" not found` });
14505
14859
  return;
14506
14860
  }
14507
- const document = await getEditableDocument(projectsDir, "resource", basename3(projectDir), itemSlug);
14861
+ const document = await getEditableDocument(projectsDir, "resource", basename4(projectDir), itemSlug);
14508
14862
  if (!document) {
14509
14863
  res.status(404).json({ error: "Resource not found" });
14510
14864
  return;
@@ -14589,7 +14943,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
14589
14943
  let content = renderItemStub(kind, {
14590
14944
  slug: requestedSlug,
14591
14945
  name,
14592
- projectSlug: basename3(projectDir),
14946
+ projectSlug: basename4(projectDir),
14593
14947
  timestamp
14594
14948
  });
14595
14949
  const customBody = typeof body.body === "string" ? body.body : "";
@@ -14606,13 +14960,13 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
14606
14960
  } catch (err) {
14607
14961
  if (err.code === "EEXIST") {
14608
14962
  res.status(409).json({
14609
- error: `${kind === "memory" ? "Memory" : "Resource"} with slug "${requestedSlug}" already exists in project "${basename3(projectDir)}".`
14963
+ error: `${kind === "memory" ? "Memory" : "Resource"} with slug "${requestedSlug}" already exists in project "${basename4(projectDir)}".`
14610
14964
  });
14611
14965
  return;
14612
14966
  }
14613
14967
  throw err;
14614
14968
  }
14615
- res.status(201).json({ slug: requestedSlug, projectSlug: basename3(projectDir), content });
14969
+ res.status(201).json({ slug: requestedSlug, projectSlug: basename4(projectDir), content });
14616
14970
  } catch (error) {
14617
14971
  console.error(`Error creating ${kind}:`, error);
14618
14972
  res.status(500).json({ error: `Failed to create ${kind}: ${error.message}` });
@@ -14647,7 +15001,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
14647
15001
  ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
14648
15002
  merged = setTopLevelField(merged, "updated", nowTimestamp());
14649
15003
  await writeFileForce(filePath, merged);
14650
- const detail = await getItemDetail(kind, basename3(projectDir), itemSlug);
15004
+ const detail = await getItemDetail(kind, basename4(projectDir), itemSlug);
14651
15005
  res.json({ [kind]: detail, content: merged });
14652
15006
  } catch (error) {
14653
15007
  console.error(`Error updating ${kind}:`, error);
@@ -17811,25 +18165,29 @@ async function resolveSessionPlan(input, terminal) {
17811
18165
  }
17812
18166
  let cwd = session.path;
17813
18167
  let fallbackWarning = null;
17814
- if (session.projectSlug && session.assignmentSlug) {
17815
- const detail = await getAssignmentDetail(
18168
+ if (!isExistingDir(session.path)) {
18169
+ const detail = session.projectSlug && session.assignmentSlug ? await getAssignmentDetail(
17816
18170
  input.projectsDir,
17817
18171
  session.projectSlug,
17818
18172
  session.assignmentSlug
17819
- );
18173
+ ) : session.assignmentSlug ? await getAssignmentDetailById(
18174
+ input.projectsDir,
18175
+ input.assignmentsDir,
18176
+ session.assignmentSlug
18177
+ ) : null;
17820
18178
  if (detail) {
17821
18179
  const picked = resolveWorkspaceCwd({
17822
18180
  worktreePath: detail.workspace.worktreePath,
17823
18181
  repository: detail.workspace.repository,
17824
18182
  branch: detail.workspace.branch,
17825
- assignmentSlug: session.assignmentSlug
18183
+ assignmentSlug: detail.slug
17826
18184
  });
17827
18185
  if (picked.cwd !== null) {
17828
18186
  cwd = picked.cwd;
17829
18187
  fallbackWarning = picked.fallbackWarning;
17830
18188
  } else {
17831
18189
  fallbackWarning = formatFallbackCwdWarning({
17832
- assignmentSlug: session.assignmentSlug,
18190
+ assignmentSlug: detail.slug,
17833
18191
  workspaceDir: session.path,
17834
18192
  worktreePath: detail.workspace.worktreePath,
17835
18193
  branch: detail.workspace.branch
@@ -17865,7 +18223,7 @@ async function resolveSessionPlan(input, terminal) {
17865
18223
  // src/launch/execute.ts
17866
18224
  import { spawn as spawn3 } from "child_process";
17867
18225
  import { homedir as homedir5 } from "os";
17868
- import { basename as basename4, join as join6, resolve as resolve26 } from "path";
18226
+ import { basename as basename5, join as join6, resolve as resolve26 } from "path";
17869
18227
  init_fs();
17870
18228
  init_config2();
17871
18229
  init_session_id();
@@ -17885,7 +18243,7 @@ var TerminalNotFoundError = class extends Error {
17885
18243
  var realSpawn = (command, args, options) => spawn3(command, args, options);
17886
18244
  var WRAPPER_COMMANDS = /* @__PURE__ */ new Set(["osascript", "open", "sh"]);
17887
18245
  function isWrapperCommand(command) {
17888
- return WRAPPER_COMMANDS.has(basename4(command));
18246
+ return WRAPPER_COMMANDS.has(basename5(command));
17889
18247
  }
17890
18248
  var WRAPPER_EXIT_TIMEOUT_MS = 1500;
17891
18249
  async function executeLaunchPlan(plan, spawnFn = realSpawn) {
@@ -19665,7 +20023,7 @@ async function readEvents(jobId) {
19665
20023
 
19666
20024
  // src/schedules/attempt.ts
19667
20025
  import { createHash as createHash3 } from "crypto";
19668
- import { open as open4, readFile as readFile22, stat as stat4, unlink as unlink6 } from "fs/promises";
20026
+ import { open as open4, readFile as readFile22, stat as stat5, unlink as unlink6 } from "fs/promises";
19669
20027
  import { resolve as resolve31 } from "path";
19670
20028
 
19671
20029
  // src/schedules/triggers.ts
@@ -19862,7 +20220,7 @@ async function acquireJobLock(id) {
19862
20220
  const code = err.code;
19863
20221
  if (code !== "EEXIST") throw err;
19864
20222
  try {
19865
- const info = await stat4(lockPath);
20223
+ const info = await stat5(lockPath);
19866
20224
  if (Date.now() - info.mtimeMs > LOCK_STALE_MS2) {
19867
20225
  await unlink6(lockPath).catch(() => {
19868
20226
  });
@@ -21864,8 +22222,8 @@ init_api();
21864
22222
  import { raw } from "express";
21865
22223
 
21866
22224
  // src/todos/attachments.ts
21867
- import { mkdir as mkdir4, readdir as readdir15, stat as stat5, rename as rename5, rm as rm4, unlink as unlink7, writeFile as writeFile6, cp } from "fs/promises";
21868
- import { resolve as resolve39, basename as basename5, dirname as dirname9, extname } from "path";
22225
+ import { mkdir as mkdir4, readdir as readdir15, stat as stat6, rename as rename5, rm as rm4, unlink as unlink7, writeFile as writeFile6, cp } from "fs/promises";
22226
+ import { resolve as resolve39, basename as basename6, dirname as dirname9, extname } from "path";
21869
22227
 
21870
22228
  // src/utils/proof-artifact-id.ts
21871
22229
  import { randomBytes as randomBytes2 } from "crypto";
@@ -21938,7 +22296,7 @@ function isSafeInlineMime(mime) {
21938
22296
  return SAFE_INLINE_MIME.has(mime);
21939
22297
  }
21940
22298
  function sanitizeAttachmentName(name) {
21941
- let n = basename5(name || "").replace(/["'\\/]/g, "_");
22299
+ let n = basename6(name || "").replace(/["'\\/]/g, "_");
21942
22300
  n = Array.from(n, (ch) => {
21943
22301
  const code = ch.charCodeAt(0);
21944
22302
  return code < 32 || code === 127 ? "_" : ch;
@@ -21961,7 +22319,7 @@ function attachmentDirFor(todosDir2, scopeId, todoId) {
21961
22319
  }
21962
22320
  async function dirExists(p) {
21963
22321
  try {
21964
- return (await stat5(p)).isDirectory();
22322
+ return (await stat6(p)).isDirectory();
21965
22323
  } catch {
21966
22324
  return false;
21967
22325
  }
@@ -21996,7 +22354,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
21996
22354
  if (!ATTACHMENT_ID_RE.test(id)) continue;
21997
22355
  const filename = stored.slice(sep2 + 2);
21998
22356
  try {
21999
- const st = await stat5(resolve39(dir, stored));
22357
+ const st = await stat6(resolve39(dir, stored));
22000
22358
  if (!st.isFile()) continue;
22001
22359
  out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
22002
22360
  } catch {
@@ -24162,7 +24520,7 @@ init_fs();
24162
24520
  init_config2();
24163
24521
  import { execFile as execFile2 } from "child_process";
24164
24522
  import { promisify as promisify2 } from "util";
24165
- import { cp as cp2, mkdtemp, rm as rm5, readFile as readFile29, writeFile as writeFile7, unlink as unlink8, stat as stat6, open as open5, rename as rename8 } from "fs/promises";
24523
+ import { cp as cp2, mkdtemp, rm as rm5, readFile as readFile29, writeFile as writeFile7, unlink as unlink8, stat as stat7, open as open5, rename as rename8 } from "fs/promises";
24166
24524
  import { resolve as resolve42, join as join9 } from "path";
24167
24525
  import { tmpdir } from "os";
24168
24526
  var exec2 = promisify2(execFile2);
@@ -24256,7 +24614,7 @@ async function cloneOrInit(repoUrl, destDir) {
24256
24614
  }
24257
24615
  async function copyRecursive(src, dest) {
24258
24616
  if (!await fileExists(src)) return;
24259
- const s = await stat6(src);
24617
+ const s = await stat7(src);
24260
24618
  if (s.isDirectory()) {
24261
24619
  await ensureDir(dest);
24262
24620
  await cp2(src, dest, { recursive: true, force: true });
@@ -25911,6 +26269,8 @@ function createDashboardServer(options) {
25911
26269
  });
25912
26270
  }
25913
26271
  let watcherHandle = null;
26272
+ const STALENESS_WATCHDOG_INTERVAL_MS = 5 * 60 * 1e3;
26273
+ let stalenessWatchdogTimer = null;
25914
26274
  return {
25915
26275
  async start() {
25916
26276
  const { recomputeAndWrite: recomputeAndWrite2, recomputeAll: recomputeAll2, resolveDeriveContext: resolveDeriveContext2, isDeriveMigrated: isDeriveMigrated2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
@@ -26007,6 +26367,38 @@ function createDashboardServer(options) {
26007
26367
  onAgentSessionsChanged: () => broadcast({ type: "agent-sessions-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() })
26008
26368
  });
26009
26369
  startUsageCollector();
26370
+ const startupConfig = await readConfig();
26371
+ if (startupConfig.stalenessWatchdog) {
26372
+ const { collectStaleCandidates: collectStaleCandidates2 } = await Promise.resolve().then(() => (init_api(), api_exports));
26373
+ const { runStalenessWatchdogTick: runStalenessWatchdogTick2 } = await Promise.resolve().then(() => (init_watchdog(), watchdog_exports));
26374
+ const { emitEvent: emitEvent2 } = await Promise.resolve().then(() => (init_event_emit(), event_emit_exports));
26375
+ const stalenessSeen = /* @__PURE__ */ new Set();
26376
+ const watchdogTick = async () => {
26377
+ if (!await migrationGate()) return;
26378
+ try {
26379
+ const candidates = await collectStaleCandidates2(projectsDir, assignmentsDir2);
26380
+ const summary = runStalenessWatchdogTick2(candidates, stalenessSeen, (e) => {
26381
+ emitEvent2({
26382
+ assignmentId: e.assignmentId,
26383
+ projectSlug: e.projectSlug,
26384
+ type: e.type,
26385
+ actor: "system",
26386
+ details: { reasons: e.reasons.map((r) => r.kind) }
26387
+ });
26388
+ });
26389
+ if (summary.newlyStale > 0 || summary.cleared > 0) {
26390
+ console.log(
26391
+ `staleness watchdog: ${summary.newlyStale} newly stale, ${summary.cleared} cleared (${summary.stale}/${summary.scanned} stale).`
26392
+ );
26393
+ }
26394
+ } catch (err) {
26395
+ console.error("staleness watchdog tick failed:", err);
26396
+ }
26397
+ };
26398
+ void watchdogTick();
26399
+ stalenessWatchdogTimer = setInterval(() => void watchdogTick(), STALENESS_WATCHDOG_INTERVAL_MS);
26400
+ stalenessWatchdogTimer.unref?.();
26401
+ }
26010
26402
  return new Promise((resolvePromise, reject) => {
26011
26403
  server.on("error", (err) => {
26012
26404
  if (err.code === "EADDRINUSE") {
@@ -26028,6 +26420,10 @@ function createDashboardServer(options) {
26028
26420
  });
26029
26421
  },
26030
26422
  async stop() {
26423
+ if (stalenessWatchdogTimer) {
26424
+ clearInterval(stalenessWatchdogTimer);
26425
+ stalenessWatchdogTimer = null;
26426
+ }
26031
26427
  await stopAutodiscovery();
26032
26428
  await stopUsageCollector();
26033
26429
  if (watcherHandle) {