pi-crew 0.1.41 → 0.1.43

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 (176) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +51 -0
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-extension-examples.md +297 -297
  18. package/docs/research-extension-system.md +324 -324
  19. package/docs/research-optimization-plan.md +548 -548
  20. package/docs/research-pi-coding-agent.md +357 -357
  21. package/docs/research-source-pi-crew-reference.md +174 -174
  22. package/docs/runtime-flow.md +148 -148
  23. package/docs/source-runtime-refactor-map.md +83 -83
  24. package/index.ts +6 -6
  25. package/package.json +1 -1
  26. package/src/agents/agent-serializer.ts +34 -34
  27. package/src/agents/discover-agents.ts +4 -4
  28. package/src/config/config.ts +9 -4
  29. package/src/extension/cross-extension-rpc.ts +82 -82
  30. package/src/extension/management.ts +37 -8
  31. package/src/extension/notification-router.ts +2 -2
  32. package/src/extension/register.ts +3 -0
  33. package/src/extension/registration/commands.ts +11 -9
  34. package/src/extension/registration/compaction-guard.ts +125 -125
  35. package/src/extension/registration/subagent-tools.ts +28 -19
  36. package/src/extension/result-watcher.ts +4 -4
  37. package/src/extension/run-bundle-schema.ts +8 -4
  38. package/src/extension/run-import.ts +4 -0
  39. package/src/extension/run-index.ts +4 -1
  40. package/src/extension/run-maintenance.ts +43 -24
  41. package/src/extension/team-tool/api.ts +1 -1
  42. package/src/extension/team-tool/cancel.ts +31 -31
  43. package/src/extension/team-tool/doctor.ts +8 -1
  44. package/src/extension/team-tool/handle-settings.ts +188 -0
  45. package/src/extension/team-tool/inspect.ts +41 -41
  46. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  47. package/src/extension/team-tool/plan.ts +19 -19
  48. package/src/extension/team-tool/run.ts +4 -2
  49. package/src/extension/team-tool/status.ts +93 -93
  50. package/src/extension/team-tool.ts +3 -1
  51. package/src/i18n.ts +184 -0
  52. package/src/observability/correlation.ts +2 -2
  53. package/src/observability/event-to-metric.ts +4 -3
  54. package/src/observability/exporters/adapter.ts +7 -1
  55. package/src/observability/exporters/otlp-exporter.ts +14 -2
  56. package/src/observability/exporters/prometheus-exporter.ts +9 -2
  57. package/src/observability/metric-registry.ts +18 -3
  58. package/src/observability/metric-retention.ts +11 -3
  59. package/src/observability/metric-sink.ts +9 -4
  60. package/src/observability/metrics-primitives.ts +4 -3
  61. package/src/prompt/prompt-runtime.ts +72 -68
  62. package/src/runtime/agent-control.ts +63 -63
  63. package/src/runtime/agent-memory.ts +72 -72
  64. package/src/runtime/agent-observability.ts +114 -114
  65. package/src/runtime/async-marker.ts +26 -26
  66. package/src/runtime/attention-events.ts +28 -23
  67. package/src/runtime/background-runner.ts +53 -53
  68. package/src/runtime/child-pi.ts +4 -4
  69. package/src/runtime/completion-guard.ts +103 -99
  70. package/src/runtime/concurrency.ts +1 -1
  71. package/src/runtime/crash-recovery.ts +2 -1
  72. package/src/runtime/crew-agent-runtime.ts +58 -58
  73. package/src/runtime/deadletter.ts +14 -4
  74. package/src/runtime/direct-run.ts +35 -35
  75. package/src/runtime/foreground-control.ts +82 -82
  76. package/src/runtime/green-contract.ts +46 -46
  77. package/src/runtime/group-join.ts +106 -106
  78. package/src/runtime/heartbeat-gradient.ts +28 -28
  79. package/src/runtime/heartbeat-watcher.ts +48 -4
  80. package/src/runtime/live-agent-control.ts +87 -87
  81. package/src/runtime/live-agent-manager.ts +85 -85
  82. package/src/runtime/live-control-realtime.ts +36 -36
  83. package/src/runtime/live-session-runtime.ts +305 -305
  84. package/src/runtime/manifest-cache.ts +2 -2
  85. package/src/runtime/model-fallback.ts +269 -261
  86. package/src/runtime/parallel-research.ts +44 -44
  87. package/src/runtime/parallel-utils.ts +1 -1
  88. package/src/runtime/pi-json-output.ts +111 -111
  89. package/src/runtime/policy-engine.ts +79 -78
  90. package/src/runtime/post-exit-stdio-guard.ts +2 -2
  91. package/src/runtime/process-status.ts +56 -56
  92. package/src/runtime/progress-event-coalescer.ts +43 -43
  93. package/src/runtime/recovery-recipes.ts +74 -74
  94. package/src/runtime/retry-executor.ts +5 -0
  95. package/src/runtime/role-permission.ts +39 -39
  96. package/src/runtime/runtime-resolver.ts +1 -1
  97. package/src/runtime/session-usage.ts +79 -79
  98. package/src/runtime/sidechain-output.ts +29 -29
  99. package/src/runtime/subagent-manager.ts +3 -3
  100. package/src/runtime/task-display.ts +38 -38
  101. package/src/runtime/task-output-context.ts +127 -127
  102. package/src/runtime/task-runner/live-executor.ts +101 -101
  103. package/src/runtime/task-runner/progress.ts +119 -111
  104. package/src/runtime/task-runner/result-utils.ts +14 -14
  105. package/src/runtime/task-runner/state-helpers.ts +22 -22
  106. package/src/runtime/team-runner.ts +3 -6
  107. package/src/runtime/worker-heartbeat.ts +21 -21
  108. package/src/runtime/worker-startup.ts +57 -57
  109. package/src/schema/config-schema.ts +1 -1
  110. package/src/schema/team-tool-schema.ts +110 -109
  111. package/src/state/artifact-store.ts +4 -2
  112. package/src/state/atomic-write.ts +12 -4
  113. package/src/state/contracts.ts +105 -105
  114. package/src/state/event-log.ts +3 -4
  115. package/src/state/jsonl-writer.ts +4 -1
  116. package/src/state/locks.ts +9 -1
  117. package/src/state/task-claims.ts +44 -42
  118. package/src/state/usage.ts +29 -29
  119. package/src/subagents/async-entry.ts +1 -1
  120. package/src/subagents/index.ts +3 -3
  121. package/src/subagents/live/control.ts +1 -1
  122. package/src/subagents/live/manager.ts +1 -1
  123. package/src/subagents/live/realtime.ts +1 -1
  124. package/src/subagents/live/session-runtime.ts +1 -1
  125. package/src/subagents/manager.ts +1 -1
  126. package/src/subagents/spawn.ts +1 -1
  127. package/src/teams/discover-teams.ts +2 -2
  128. package/src/teams/team-serializer.ts +38 -38
  129. package/src/types/diff.d.ts +18 -18
  130. package/src/ui/crew-footer.ts +101 -101
  131. package/src/ui/crew-select-list.ts +111 -111
  132. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  133. package/src/ui/dynamic-border.ts +25 -25
  134. package/src/ui/layout-primitives.ts +106 -106
  135. package/src/ui/live-run-sidebar.ts +1 -1
  136. package/src/ui/loaders.ts +158 -158
  137. package/src/ui/mascot.ts +3 -2
  138. package/src/ui/powerbar-publisher.ts +4 -3
  139. package/src/ui/render-diff.ts +119 -119
  140. package/src/ui/render-scheduler.ts +54 -14
  141. package/src/ui/run-dashboard.ts +39 -11
  142. package/src/ui/run-snapshot-cache.ts +63 -37
  143. package/src/ui/spinner.ts +17 -17
  144. package/src/ui/status-colors.ts +54 -54
  145. package/src/ui/syntax-highlight.ts +116 -116
  146. package/src/ui/theme-adapter.ts +1 -1
  147. package/src/ui/transcript-viewer.ts +7 -2
  148. package/src/utils/completion-dedupe.ts +63 -63
  149. package/src/utils/file-coalescer.ts +5 -3
  150. package/src/utils/frontmatter.ts +68 -36
  151. package/src/utils/git.ts +262 -262
  152. package/src/utils/ids.ts +12 -12
  153. package/src/utils/internal-error.ts +1 -1
  154. package/src/utils/names.ts +27 -26
  155. package/src/utils/paths.ts +1 -1
  156. package/src/utils/redaction.ts +44 -41
  157. package/src/utils/safe-paths.ts +47 -34
  158. package/src/utils/sleep.ts +2 -2
  159. package/src/utils/timings.ts +2 -0
  160. package/src/utils/visual.ts +9 -1
  161. package/src/workflows/discover-workflows.ts +4 -1
  162. package/src/workflows/validate-workflow.ts +40 -40
  163. package/src/worktree/branch-freshness.ts +45 -45
  164. package/src/worktree/worktree-manager.ts +6 -1
  165. package/teams/default.team.md +12 -12
  166. package/teams/fast-fix.team.md +11 -11
  167. package/teams/implementation.team.md +18 -18
  168. package/teams/parallel-research.team.md +14 -14
  169. package/teams/research.team.md +11 -11
  170. package/teams/review.team.md +12 -12
  171. package/workflows/default.workflow.md +29 -29
  172. package/workflows/fast-fix.workflow.md +22 -22
  173. package/workflows/implementation.workflow.md +38 -38
  174. package/workflows/parallel-research.workflow.md +46 -46
  175. package/workflows/research.workflow.md +22 -22
  176. package/workflows/review.workflow.md +30 -30
package/src/ui/spinner.ts CHANGED
@@ -1,17 +1,17 @@
1
- export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
2
- export const SUBAGENT_SPINNER_FRAME_MS = 160;
3
-
4
- export function spinnerBucket(now = Date.now(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
5
- return Math.floor(now / Math.max(1, frameMs));
6
- }
7
-
8
- function hashKey(key: string): number {
9
- let hash = 0;
10
- for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
11
- return hash;
12
- }
13
-
14
- export function spinnerFrame(key = "", now = Date.now()): string {
15
- const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
16
- return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
17
- }
1
+ export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
2
+ export const SUBAGENT_SPINNER_FRAME_MS = 160;
3
+
4
+ export function spinnerBucket(now = Date.now(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
5
+ return Math.floor(now / Math.max(1, frameMs));
6
+ }
7
+
8
+ function hashKey(key: string): number {
9
+ let hash = 0;
10
+ for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
11
+ return hash;
12
+ }
13
+
14
+ export function spinnerFrame(key = "", now = Date.now()): string {
15
+ const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
16
+ return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
17
+ }
@@ -1,54 +1,54 @@
1
- import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
2
-
3
- export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
4
-
5
- export function colorForStatus(status: RunStatus): CrewThemeColor {
6
- switch (status) {
7
- case "running":
8
- return "accent";
9
- case "completed":
10
- return "success";
11
- case "failed":
12
- case "stale":
13
- return "error";
14
- case "cancelled":
15
- case "blocked":
16
- case "stopped":
17
- return "warning";
18
- case "queued":
19
- default:
20
- return "dim";
21
- }
22
- }
23
-
24
- export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string {
25
- const glyph = options?.runningGlyph ?? "▶";
26
- switch (status) {
27
- case "completed":
28
- return "✓";
29
- case "failed":
30
- case "stale":
31
- return "✗";
32
- case "cancelled":
33
- case "stopped":
34
- return "■";
35
- case "running":
36
- return glyph;
37
- case "queued":
38
- return "◦";
39
- case "blocked":
40
- return "⏸";
41
- default:
42
- return "·";
43
- }
44
- }
45
-
46
- export function colorForActivity(activityState: string | undefined): CrewThemeColor {
47
- if (activityState === "needs_attention") return "warning";
48
- if (activityState === "stale") return "error";
49
- return "dim";
50
- }
51
-
52
- export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string {
53
- return theme.fg(colorForStatus(status), text);
54
- }
1
+ import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
2
+
3
+ export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
4
+
5
+ export function colorForStatus(status: RunStatus): CrewThemeColor {
6
+ switch (status) {
7
+ case "running":
8
+ return "accent";
9
+ case "completed":
10
+ return "success";
11
+ case "failed":
12
+ case "stale":
13
+ return "error";
14
+ case "cancelled":
15
+ case "blocked":
16
+ case "stopped":
17
+ return "warning";
18
+ case "queued":
19
+ default:
20
+ return "dim";
21
+ }
22
+ }
23
+
24
+ export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string {
25
+ const glyph = options?.runningGlyph ?? "▶";
26
+ switch (status) {
27
+ case "completed":
28
+ return "✓";
29
+ case "failed":
30
+ case "stale":
31
+ return "✗";
32
+ case "cancelled":
33
+ case "stopped":
34
+ return "■";
35
+ case "running":
36
+ return glyph;
37
+ case "queued":
38
+ return "◦";
39
+ case "blocked":
40
+ return "⏸";
41
+ default:
42
+ return "·";
43
+ }
44
+ }
45
+
46
+ export function colorForActivity(activityState: string | undefined): CrewThemeColor {
47
+ if (activityState === "needs_attention") return "warning";
48
+ if (activityState === "stale") return "error";
49
+ return "dim";
50
+ }
51
+
52
+ export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string {
53
+ return theme.fg(colorForStatus(status), text);
54
+ }
@@ -1,116 +1,116 @@
1
- import { supportsLanguage, highlight } from "cli-highlight";
2
- import type { CrewTheme } from "./theme-adapter.ts";
3
- import { asCrewTheme } from "./theme-adapter.ts";
4
-
5
- function buildCliTheme(theme: CrewTheme): Record<string, (text: string) => string> {
6
- return {
7
- keyword: (text) => theme.fg("syntaxKeyword", text),
8
- built_in: (text) => theme.fg("syntaxType", text),
9
- literal: (text) => theme.fg("syntaxNumber", text),
10
- number: (text) => theme.fg("syntaxNumber", text),
11
- string: (text) => theme.fg("syntaxString", text),
12
- comment: (text) => theme.fg("syntaxComment", text),
13
- function: (text) => theme.fg("syntaxFunction", text),
14
- title: (text) => theme.fg("syntaxFunction", text),
15
- class: (text) => theme.fg("syntaxType", text),
16
- type: (text) => theme.fg("syntaxType", text),
17
- attr: (text) => theme.fg("syntaxVariable", text),
18
- variable: (text) => theme.fg("syntaxVariable", text),
19
- params: (text) => theme.fg("syntaxVariable", text),
20
- operator: (text) => theme.fg("syntaxOperator", text),
21
- punctuation: (text) => theme.fg("syntaxPunctuation", text),
22
- };
23
- }
24
-
25
- export function detectLanguageFromPath(filePath: string): string | undefined {
26
- const ext = filePath.split(".").pop()?.toLowerCase();
27
- if (!ext) return undefined;
28
- return languageMap[ext];
29
- }
30
-
31
- export const languageMap: Record<string, string> = {
32
- ts: "typescript",
33
- tsx: "typescript",
34
- js: "javascript",
35
- jsx: "javascript",
36
- mjs: "javascript",
37
- cjs: "javascript",
38
- py: "python",
39
- md: "markdown",
40
- markdown: "markdown",
41
- json: "json",
42
- yml: "yaml",
43
- yaml: "yaml",
44
- toml: "yaml",
45
- html: "html",
46
- htm: "html",
47
- css: "css",
48
- scss: "scss",
49
- sass: "sass",
50
- bash: "bash",
51
- sh: "bash",
52
- zsh: "bash",
53
- fish: "bash",
54
- ps1: "powershell",
55
- sql: "sql",
56
- rust: "rust",
57
- rb: "ruby",
58
- go: "go",
59
- java: "java",
60
- kt: "kotlin",
61
- cpp: "cpp",
62
- cc: "cpp",
63
- cxx: "cpp",
64
- hpp: "cpp",
65
- c: "c",
66
- h: "c",
67
- cs: "csharp",
68
- php: "php",
69
- };
70
-
71
- export function highlightCode(code: string, language: string | undefined, themeLike: unknown = undefined): string {
72
- const theme = asCrewTheme(themeLike);
73
- const validLanguage = language && supportsLanguage(language) ? language : undefined;
74
- if (!validLanguage) {
75
- return code
76
- .split("\n")
77
- .map((line) => theme.fg("mdCodeBlock", line))
78
- .join("\n");
79
- }
80
- try {
81
- return highlight(code, {
82
- language: validLanguage,
83
- ignoreIllegals: true,
84
- theme: buildCliTheme(theme),
85
- }).trimEnd();
86
- } catch {
87
- return code
88
- .split("\n")
89
- .map((line) => theme.fg("mdCodeBlock", line))
90
- .join("\n");
91
- }
92
- }
93
-
94
- export function highlightJson(payload: string, themeLike: unknown = undefined): string {
95
- const theme = asCrewTheme(themeLike);
96
- try {
97
- return highlight(payload, {
98
- language: "json",
99
- ignoreIllegals: true,
100
- theme: buildCliTheme(theme),
101
- }).trimEnd();
102
- } catch {
103
- try {
104
- const parsed = JSON.parse(payload);
105
- return JSON.stringify(parsed, null, 2)
106
- .split("\n")
107
- .map((line) => theme.fg("mdCodeBlock", line))
108
- .join("\n");
109
- } catch {
110
- return payload
111
- .split("\n")
112
- .map((line) => theme.fg("mdCodeBlock", line))
113
- .join("\n");
114
- }
115
- }
116
- }
1
+ import { supportsLanguage, highlight } from "cli-highlight";
2
+ import type { CrewTheme } from "./theme-adapter.ts";
3
+ import { asCrewTheme } from "./theme-adapter.ts";
4
+
5
+ function buildCliTheme(theme: CrewTheme): Record<string, (text: string) => string> {
6
+ return {
7
+ keyword: (text) => theme.fg("syntaxKeyword", text),
8
+ built_in: (text) => theme.fg("syntaxType", text),
9
+ literal: (text) => theme.fg("syntaxNumber", text),
10
+ number: (text) => theme.fg("syntaxNumber", text),
11
+ string: (text) => theme.fg("syntaxString", text),
12
+ comment: (text) => theme.fg("syntaxComment", text),
13
+ function: (text) => theme.fg("syntaxFunction", text),
14
+ title: (text) => theme.fg("syntaxFunction", text),
15
+ class: (text) => theme.fg("syntaxType", text),
16
+ type: (text) => theme.fg("syntaxType", text),
17
+ attr: (text) => theme.fg("syntaxVariable", text),
18
+ variable: (text) => theme.fg("syntaxVariable", text),
19
+ params: (text) => theme.fg("syntaxVariable", text),
20
+ operator: (text) => theme.fg("syntaxOperator", text),
21
+ punctuation: (text) => theme.fg("syntaxPunctuation", text),
22
+ };
23
+ }
24
+
25
+ export function detectLanguageFromPath(filePath: string): string | undefined {
26
+ const ext = filePath.split(".").pop()?.toLowerCase();
27
+ if (!ext) return undefined;
28
+ return languageMap[ext];
29
+ }
30
+
31
+ export const languageMap: Record<string, string> = {
32
+ ts: "typescript",
33
+ tsx: "typescript",
34
+ js: "javascript",
35
+ jsx: "javascript",
36
+ mjs: "javascript",
37
+ cjs: "javascript",
38
+ py: "python",
39
+ md: "markdown",
40
+ markdown: "markdown",
41
+ json: "json",
42
+ yml: "yaml",
43
+ yaml: "yaml",
44
+ toml: "yaml",
45
+ html: "html",
46
+ htm: "html",
47
+ css: "css",
48
+ scss: "scss",
49
+ sass: "sass",
50
+ bash: "bash",
51
+ sh: "bash",
52
+ zsh: "bash",
53
+ fish: "bash",
54
+ ps1: "powershell",
55
+ sql: "sql",
56
+ rust: "rust",
57
+ rb: "ruby",
58
+ go: "go",
59
+ java: "java",
60
+ kt: "kotlin",
61
+ cpp: "cpp",
62
+ cc: "cpp",
63
+ cxx: "cpp",
64
+ hpp: "cpp",
65
+ c: "c",
66
+ h: "c",
67
+ cs: "csharp",
68
+ php: "php",
69
+ };
70
+
71
+ export function highlightCode(code: string, language: string | undefined, themeLike: unknown = undefined): string {
72
+ const theme = asCrewTheme(themeLike);
73
+ const validLanguage = language && supportsLanguage(language) ? language : undefined;
74
+ if (!validLanguage) {
75
+ return code
76
+ .split("\n")
77
+ .map((line) => theme.fg("mdCodeBlock", line))
78
+ .join("\n");
79
+ }
80
+ try {
81
+ return highlight(code, {
82
+ language: validLanguage,
83
+ ignoreIllegals: true,
84
+ theme: buildCliTheme(theme),
85
+ }).trimEnd();
86
+ } catch {
87
+ return code
88
+ .split("\n")
89
+ .map((line) => theme.fg("mdCodeBlock", line))
90
+ .join("\n");
91
+ }
92
+ }
93
+
94
+ export function highlightJson(payload: string, themeLike: unknown = undefined): string {
95
+ const theme = asCrewTheme(themeLike);
96
+ try {
97
+ return highlight(payload, {
98
+ language: "json",
99
+ ignoreIllegals: true,
100
+ theme: buildCliTheme(theme),
101
+ }).trimEnd();
102
+ } catch {
103
+ try {
104
+ const parsed = JSON.parse(payload);
105
+ return JSON.stringify(parsed, null, 2)
106
+ .split("\n")
107
+ .map((line) => theme.fg("mdCodeBlock", line))
108
+ .join("\n");
109
+ } catch {
110
+ return payload
111
+ .split("\n")
112
+ .map((line) => theme.fg("mdCodeBlock", line))
113
+ .join("\n");
114
+ }
115
+ }
116
+ }
@@ -148,7 +148,7 @@ function startThemeSourceSubscription(theme: object, subscription: ThemeSourceSu
148
148
  subscription.lastSignature = nextSignature;
149
149
  emit();
150
150
  }, 1000);
151
- subscription.pollTimer.unref?.();
151
+ subscription.pollTimer.unref();
152
152
  }
153
153
 
154
154
  export function subscribeThemeChange(theme: unknown, callback: () => void): () => void {
@@ -129,11 +129,16 @@ export function readRunTranscript(manifest: TeamRunManifest, taskId?: string, op
129
129
  const agents = readCrewAgents(manifest);
130
130
  const agent = taskId ? agents.find((item) => item.taskId === taskId || item.id === taskId) : agents.find((item) => item.transcriptPath) ?? agents[0];
131
131
  const selectedTaskId = agent?.taskId ?? taskId ?? "unknown";
132
- let transcriptPath: string;
132
+ let transcriptPath = "";
133
133
  try {
134
134
  transcriptPath = agentOutputPath(manifest, selectedTaskId);
135
135
  } catch {
136
- transcriptPath = agentOutputPath(manifest, "unknown");
136
+ try {
137
+ transcriptPath = agentOutputPath(manifest, "unknown");
138
+ } catch {
139
+ // Both fallbacks failed — transcript will be empty.
140
+ transcriptPath = "";
141
+ }
137
142
  }
138
143
  if (agent?.transcriptPath) {
139
144
  try {
@@ -1,63 +1,63 @@
1
- interface CompletionDataLike {
2
- id?: unknown;
3
- agent?: unknown;
4
- timestamp?: unknown;
5
- sessionId?: unknown;
6
- taskIndex?: unknown;
7
- totalTasks?: unknown;
8
- success?: unknown;
9
- }
10
-
11
- function asNonEmptyString(value: unknown): string | undefined {
12
- if (typeof value !== "string") return undefined;
13
- const trimmed = value.trim();
14
- return trimmed.length > 0 ? trimmed : undefined;
15
- }
16
-
17
- function asFiniteNumber(value: unknown): number | undefined {
18
- if (typeof value !== "number") return undefined;
19
- return Number.isFinite(value) ? value : undefined;
20
- }
21
-
22
- export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
23
- const id = asNonEmptyString(data.id);
24
- if (id) return `id:${id}`;
25
- const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
26
- const agent = asNonEmptyString(data.agent) ?? "unknown";
27
- const timestamp = asFiniteNumber(data.timestamp);
28
- const taskIndex = asFiniteNumber(data.taskIndex);
29
- const totalTasks = asFiniteNumber(data.totalTasks);
30
- const success = typeof data.success === "boolean" ? (data.success ? "1" : "0") : "?";
31
- return [
32
- "meta",
33
- sessionId,
34
- agent,
35
- timestamp !== undefined ? String(timestamp) : "no-ts",
36
- taskIndex !== undefined ? String(taskIndex) : "-",
37
- totalTasks !== undefined ? String(totalTasks) : "-",
38
- success,
39
- fallback,
40
- ].join(":");
41
- }
42
-
43
- export function pruneSeenMap(seen: Map<string, number>, now: number, ttlMs: number): void {
44
- for (const [key, ts] of seen.entries()) {
45
- if (now - ts > ttlMs) seen.delete(key);
46
- }
47
- }
48
-
49
- export function markSeenWithTtl(seen: Map<string, number>, key: string, now: number, ttlMs: number): boolean {
50
- pruneSeenMap(seen, now, ttlMs);
51
- if (seen.has(key)) return true;
52
- seen.set(key, now);
53
- return false;
54
- }
55
-
56
- export function getGlobalSeenMap(storeKey: string): Map<string, number> {
57
- const globalStore = globalThis as Record<string, unknown>;
58
- const existing = globalStore[storeKey];
59
- if (existing instanceof Map) return existing as Map<string, number>;
60
- const map = new Map<string, number>();
61
- globalStore[storeKey] = map;
62
- return map;
63
- }
1
+ interface CompletionDataLike {
2
+ id?: unknown;
3
+ agent?: unknown;
4
+ timestamp?: unknown;
5
+ sessionId?: unknown;
6
+ taskIndex?: unknown;
7
+ totalTasks?: unknown;
8
+ success?: unknown;
9
+ }
10
+
11
+ function asNonEmptyString(value: unknown): string | undefined {
12
+ if (typeof value !== "string") return undefined;
13
+ const trimmed = value.trim();
14
+ return trimmed.length > 0 ? trimmed : undefined;
15
+ }
16
+
17
+ function asFiniteNumber(value: unknown): number | undefined {
18
+ if (typeof value !== "number") return undefined;
19
+ return Number.isFinite(value) ? value : undefined;
20
+ }
21
+
22
+ export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
23
+ const id = asNonEmptyString(data.id);
24
+ if (id) return `id:${id}`;
25
+ const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
26
+ const agent = asNonEmptyString(data.agent) ?? "unknown";
27
+ const timestamp = asFiniteNumber(data.timestamp);
28
+ const taskIndex = asFiniteNumber(data.taskIndex);
29
+ const totalTasks = asFiniteNumber(data.totalTasks);
30
+ const success = typeof data.success === "boolean" ? (data.success ? "1" : "0") : "?";
31
+ return [
32
+ "meta",
33
+ sessionId,
34
+ agent,
35
+ timestamp !== undefined ? String(timestamp) : "no-ts",
36
+ taskIndex !== undefined ? String(taskIndex) : "-",
37
+ totalTasks !== undefined ? String(totalTasks) : "-",
38
+ success,
39
+ fallback,
40
+ ].join(":");
41
+ }
42
+
43
+ export function pruneSeenMap(seen: Map<string, number>, now: number, ttlMs: number): void {
44
+ for (const [key, ts] of seen.entries()) {
45
+ if (now - ts > ttlMs) seen.delete(key);
46
+ }
47
+ }
48
+
49
+ export function markSeenWithTtl(seen: Map<string, number>, key: string, now: number, ttlMs: number): boolean {
50
+ pruneSeenMap(seen, now, ttlMs);
51
+ if (seen.has(key)) return true;
52
+ seen.set(key, now);
53
+ return false;
54
+ }
55
+
56
+ export function getGlobalSeenMap(storeKey: string): Map<string, number> {
57
+ const globalStore = globalThis as Record<string, unknown>;
58
+ const existing = globalStore[storeKey];
59
+ if (existing instanceof Map) return existing as Map<string, number>;
60
+ const map = new Map<string, number>();
61
+ globalStore[storeKey] = map;
62
+ return map;
63
+ }
@@ -46,10 +46,9 @@ const readCacheSizeLimit = 128;
46
46
 
47
47
  function evictOldestCacheEntry(): void {
48
48
  if (readCache.size < readCacheSizeLimit) return;
49
+ // Map iteration order is insertion order; first key is LRU.
49
50
  const oldestKey = readCache.keys().next().value;
50
- if (oldestKey !== undefined) {
51
- readCache.delete(oldestKey);
52
- }
51
+ if (oldestKey !== undefined) readCache.delete(oldestKey);
53
52
  }
54
53
 
55
54
  export function clearReadCache(): void {
@@ -68,6 +67,9 @@ export function readJsonFileCoalesced<T>(filePath: string, ttlMs: number, read:
68
67
  })();
69
68
  const cached = readCache.get(filePath);
70
69
  if (cached && stat && cached.expiresAt > now && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
70
+ // Re-insert to implement LRU: move to end of Map.
71
+ readCache.delete(filePath);
72
+ readCache.set(filePath, cached);
71
73
  return cached.value as T;
72
74
  }
73
75
  const value = read();
@@ -1,36 +1,68 @@
1
- export interface ParsedFrontmatter {
2
- frontmatter: Record<string, string>;
3
- body: string;
4
- }
5
-
6
- export function parseFrontmatter(content: string): ParsedFrontmatter {
7
- if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
8
- return { frontmatter: {}, body: content };
9
- }
10
-
11
- const normalized = content.replaceAll("\r\n", "\n");
12
- const end = normalized.indexOf("\n---\n", 4);
13
- if (end === -1) return { frontmatter: {}, body: content };
14
-
15
- const raw = normalized.slice(4, end);
16
- const body = normalized.slice(end + "\n---\n".length);
17
- const frontmatter: Record<string, string> = {};
18
-
19
- for (const line of raw.split("\n")) {
20
- const trimmed = line.trim();
21
- if (!trimmed || trimmed.startsWith("#")) continue;
22
- const separator = trimmed.indexOf(":");
23
- if (separator === -1) continue;
24
- const key = trimmed.slice(0, separator).trim();
25
- const value = trimmed.slice(separator + 1).trim();
26
- if (key) frontmatter[key] = value;
27
- }
28
-
29
- return { frontmatter, body };
30
- }
31
-
32
- export function parseCsv(value: string | undefined): string[] | undefined {
33
- if (value === undefined) return undefined;
34
- const values = value.split(",").map((item) => item.trim()).filter(Boolean);
35
- return values.length > 0 ? [...new Set(values)] : undefined;
36
- }
1
+ export interface ParsedFrontmatter {
2
+ frontmatter: Record<string, string>;
3
+ body: string;
4
+ }
5
+
6
+ export function parseFrontmatter(content: string): ParsedFrontmatter {
7
+ if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
8
+ return { frontmatter: {}, body: content };
9
+ }
10
+
11
+ const normalized = content.replaceAll("\r\n", "\n");
12
+ const end = normalized.indexOf("\n---\n", 4);
13
+ if (end === -1) {
14
+ // Support frontmatter that ends at EOF without trailing newline after ---.
15
+ const altEnd = normalized.indexOf("\n---", 4);
16
+ if (altEnd !== -1 && altEnd + 4 === normalized.length) {
17
+ const raw = normalized.slice(4, altEnd);
18
+ const frontmatter = parseLines(raw);
19
+ return { frontmatter, body: "" };
20
+ }
21
+ return { frontmatter: {}, body: content };
22
+ }
23
+
24
+ const raw = normalized.slice(4, end);
25
+ const body = normalized.slice(end + "\n---\n".length);
26
+ const frontmatter = parseLines(raw);
27
+ return { frontmatter, body };
28
+ }
29
+
30
+ function parseLines(raw: string): Record<string, string> {
31
+ const frontmatter: Record<string, string> = {};
32
+ for (const line of raw.split("\n")) {
33
+ const trimmed = line.trim();
34
+ if (!trimmed || trimmed.startsWith("#")) continue;
35
+ const separator = trimmed.indexOf(":");
36
+ if (separator === -1) continue;
37
+ const key = trimmed.slice(0, separator).trim();
38
+ const value = trimmed.slice(separator + 1).trim();
39
+ if (key) frontmatter[key] = value;
40
+ }
41
+ return frontmatter;
42
+ }
43
+
44
+ export function parseCsv(value: string | undefined): string[] | undefined {
45
+ if (value === undefined) return undefined;
46
+ // Handle quoted values with commas inside.
47
+ const values = splitCsv(value).map((item) => item.trim()).filter(Boolean);
48
+ return values.length > 0 ? [...new Set(values)] : undefined;
49
+ }
50
+
51
+ function splitCsv(input: string): string[] {
52
+ const result: string[] = [];
53
+ let current = "";
54
+ let inQuotes = false;
55
+ for (let i = 0; i < input.length; i++) {
56
+ const char = input[i];
57
+ if (char === '"') {
58
+ inQuotes = !inQuotes;
59
+ } else if (char === "," && !inQuotes) {
60
+ result.push(current);
61
+ current = "";
62
+ } else {
63
+ current += char;
64
+ }
65
+ }
66
+ result.push(current);
67
+ return result;
68
+ }