pi-crew 0.2.3 → 0.2.5
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/AGENTS.md +57 -32
- package/CHANGELOG.md +466 -448
- package/LICENSE +21 -21
- package/NOTICE.md +16 -16
- package/README.md +323 -323
- package/docs/FEATURE_INTAKE.md +126 -0
- package/docs/HARNESS.md +86 -0
- package/docs/HARNESS_BACKLOG.md +41 -0
- package/docs/TEST_MATRIX.md +49 -0
- package/docs/actions-reference.md +595 -595
- package/docs/architecture.md +180 -180
- package/docs/code-review-2026-05-11.md +592 -592
- package/docs/commands-reference.md +347 -347
- package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
- package/docs/decisions/0001-durable-state.md +41 -0
- package/docs/decisions/0002-child-process-for-async.md +42 -0
- package/docs/decisions/0003-depth-guard.md +36 -0
- package/docs/decisions/0004-execfile-over-exec.md +34 -0
- package/docs/decisions/0005-no-parameter-properties.md +49 -0
- package/docs/decisions/0006-publish-bundled-esm.md +63 -0
- package/docs/decisions/0007-active-run-binary-index.md +54 -0
- package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
- package/docs/decisions/README.md +23 -0
- package/docs/followup-review-round4-2026-05-13.md +107 -0
- package/docs/implementation-plan-top3.md +333 -0
- package/docs/live-mailbox-runtime.md +36 -36
- package/docs/next-upgrade-roadmap.md +808 -808
- package/docs/oh-my-pi-research.md +509 -0
- package/docs/perf/baseline-2026-05.md +113 -0
- package/docs/perf/final-report-2026-05.md +206 -0
- package/docs/perf/sprint-1-report.md +71 -0
- package/docs/perf/sprint-2-report.md +81 -0
- package/docs/perf/sprint-2.5-report.md +53 -0
- package/docs/perf/sprint-3-report.md +36 -0
- package/docs/perf/sprint-4-report.md +47 -0
- package/docs/perf/sprint-5-report.md +51 -0
- package/docs/perf/sprint-6-report.md +94 -0
- package/docs/perf/sprint-7-report.md +74 -0
- package/docs/perf/upgrade-plan-2026-05.md +147 -0
- package/docs/pi-subagents3-deep-analysis.md +508 -0
- package/docs/product/README.md +31 -0
- package/docs/product/platform.md +27 -0
- package/docs/product/runtime-safety.md +37 -0
- package/docs/product/team-run.md +39 -0
- package/docs/product/team-tool.md +37 -0
- package/docs/publishing.md +65 -65
- package/docs/resource-formats.md +134 -134
- package/docs/runtime-analysis-child-vs-live.md +171 -0
- package/docs/runtime-flow.md +148 -148
- package/docs/runtime-migration-in-process-analysis.md +250 -0
- package/docs/stories/README.md +30 -0
- package/docs/stories/backlog.md +36 -0
- package/docs/templates/decision.md +27 -0
- package/docs/templates/story.md +44 -0
- package/docs/templates/validation-report.md +32 -0
- package/docs/usage.md +238 -238
- package/index.ts +7 -6
- package/install.mjs +65 -65
- package/package.json +107 -100
- package/schema.json +222 -222
- package/skills/child-pi-spawning/SKILL.md +213 -0
- package/skills/context-artifact-hygiene/SKILL.md +32 -0
- package/skills/event-log-tracing/SKILL.md +299 -0
- package/skills/git-master/SKILL.md +225 -24
- package/skills/live-agent-lifecycle/SKILL.md +192 -0
- package/skills/mailbox-interactive/SKILL.md +300 -19
- package/skills/model-routing-context/SKILL.md +94 -0
- package/skills/multi-perspective-review/SKILL.md +88 -0
- package/skills/read-only-explorer/SKILL.md +250 -26
- package/skills/safe-bash/SKILL.md +307 -21
- package/skills/verification-before-done/SKILL.md +11 -2
- package/skills/widget-rendering/SKILL.md +258 -0
- package/skills/workspace-isolation/SKILL.md +202 -0
- package/skills/worktree-isolation/SKILL.md +202 -18
- package/src/adapters/claude-adapter.ts +25 -25
- package/src/adapters/codex-adapter.ts +21 -21
- package/src/adapters/cursor-adapter.ts +17 -17
- package/src/adapters/export-util.ts +137 -137
- package/src/adapters/index.ts +15 -15
- package/src/adapters/registry.ts +18 -18
- package/src/adapters/types.ts +23 -23
- package/src/agents/agent-config.ts +38 -38
- package/src/agents/agent-serializer.ts +38 -38
- package/src/agents/discover-agents.ts +121 -118
- package/src/config/config.ts +740 -858
- package/src/config/defaults.ts +96 -96
- package/src/config/drift-detector.ts +211 -211
- package/src/config/markers.ts +327 -327
- package/src/config/resilient-parser.ts +109 -108
- package/src/config/suggestions.ts +74 -74
- package/src/config/types.ts +199 -0
- package/src/extension/async-notifier.ts +123 -89
- package/src/extension/autonomous-policy.ts +169 -169
- package/src/extension/cross-extension-rpc.ts +104 -104
- package/src/extension/help.ts +47 -47
- package/src/extension/import-index.ts +69 -69
- package/src/extension/management.ts +395 -382
- package/src/extension/notification-router.ts +116 -116
- package/src/extension/notification-sink.ts +51 -51
- package/src/extension/project-init.ts +168 -168
- package/src/extension/register.ts +859 -668
- package/src/extension/registration/artifact-cleanup.ts +15 -15
- package/src/extension/registration/command-utils.ts +54 -54
- package/src/extension/registration/commands.ts +559 -452
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +102 -102
- package/src/extension/registration/subagent-tools.ts +220 -159
- package/src/extension/registration/team-tool.ts +159 -99
- package/src/extension/registration/viewers.ts +29 -0
- package/src/extension/result-watcher.ts +128 -128
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +73 -73
- package/src/extension/run-import.ts +84 -84
- package/src/extension/run-index.ts +94 -94
- package/src/extension/run-maintenance.ts +142 -142
- package/src/extension/session-summary.ts +8 -8
- package/src/extension/team-manager-command.ts +96 -96
- package/src/extension/team-recommendation.ts +188 -188
- package/src/extension/team-tool/api.ts +5 -2
- package/src/extension/team-tool/cancel.ts +224 -209
- package/src/extension/team-tool/config-patch.ts +36 -36
- package/src/extension/team-tool/context.ts +60 -60
- package/src/extension/team-tool/doctor.ts +242 -242
- package/src/extension/team-tool/handle-settings.ts +421 -195
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +139 -139
- package/src/extension/team-tool/parallel-dispatch.ts +156 -156
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +112 -111
- package/src/extension/team-tool/run.ts +246 -229
- package/src/extension/team-tool/status.ts +110 -110
- package/src/extension/team-tool-types.ts +13 -13
- package/src/extension/team-tool.ts +344 -344
- package/src/extension/tool-result.ts +16 -16
- package/src/extension/validate-resources.ts +77 -77
- package/src/hooks/registry.ts +61 -61
- package/src/hooks/types.ts +40 -40
- package/src/i18n.ts +184 -184
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +68 -68
- package/src/observability/exporters/adapter.ts +30 -30
- package/src/observability/exporters/otlp-exporter.ts +106 -92
- package/src/observability/exporters/prometheus-exporter.ts +54 -54
- package/src/observability/metric-registry.ts +87 -87
- package/src/observability/metric-retention.ts +54 -54
- package/src/observability/metric-sink.ts +81 -56
- package/src/observability/metrics-primitives.ts +167 -167
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/adaptive-plan.ts +338 -0
- package/src/runtime/agent-control.ts +169 -169
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/async-runner.ts +153 -153
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -100
- package/src/runtime/background-runner.ts +122 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi.ts +541 -461
- package/src/runtime/code-summary.ts +247 -247
- package/src/runtime/compaction-summary.ts +271 -271
- package/src/runtime/concurrency.ts +58 -58
- package/src/runtime/crash-recovery.ts +317 -301
- package/src/runtime/crew-agent-records.ts +379 -281
- package/src/runtime/crew-agent-runtime.ts +60 -60
- package/src/runtime/cross-extension-rpc.ts +72 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -201
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
- package/src/runtime/deadletter.ts +47 -47
- package/src/runtime/delivery-coordinator.ts +176 -176
- package/src/runtime/delta-conflict.ts +360 -360
- package/src/runtime/diagnostic-export.ts +102 -102
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +82 -81
- package/src/runtime/errors/crew-errors.ts +166 -0
- package/src/runtime/event-stream-bridge.ts +92 -92
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +234 -106
- package/src/runtime/heartbeat-watcher.ts +145 -124
- package/src/runtime/iteration-hooks.ts +267 -267
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +377 -179
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +676 -600
- package/src/runtime/loop-gates.ts +129 -129
- package/src/runtime/manifest-cache.ts +263 -263
- package/src/runtime/mcp-proxy.ts +113 -113
- package/src/runtime/metric-parser.ts +40 -40
- package/src/runtime/model-fallback.ts +282 -274
- package/src/runtime/model-resolver.ts +118 -0
- package/src/runtime/output-validator.ts +187 -187
- package/src/runtime/overflow-recovery.ts +175 -175
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +156 -156
- package/src/runtime/parent-guard.ts +80 -80
- package/src/runtime/phase-progress.ts +217 -217
- package/src/runtime/pi-args.ts +165 -165
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +167 -167
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +125 -125
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +97 -73
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -81
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/run-tracker.ts +99 -0
- package/src/runtime/runtime-policy.ts +21 -0
- package/src/runtime/runtime-resolver.ts +94 -91
- package/src/runtime/scheduler.ts +294 -0
- package/src/runtime/semaphore.ts +131 -131
- package/src/runtime/sensitive-paths.ts +92 -92
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/settings-store.ts +103 -0
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -222
- package/src/runtime/stale-reconciler.ts +198 -189
- package/src/runtime/streaming-output.ts +47 -0
- package/src/runtime/subagent-manager.ts +404 -400
- package/src/runtime/subprocess-tool-registry.ts +67 -67
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph-scheduler.ts +122 -122
- package/src/runtime/task-graph.ts +207 -207
- package/src/runtime/task-output-context.ts +177 -177
- package/src/runtime/task-packet.ts +93 -93
- package/src/runtime/task-quality.ts +207 -207
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +131 -113
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +139 -139
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +469 -459
- package/src/runtime/team-runner.ts +693 -945
- package/src/runtime/usage-tracker.ts +71 -0
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -187
- package/src/runtime/yield-handler.ts +190 -190
- package/src/schema/config-schema.ts +172 -168
- package/src/schema/team-tool-schema.ts +126 -126
- package/src/schema/validation-types.ts +151 -148
- package/src/skills/discover-skills.ts +67 -67
- package/src/skills/skill-templates.ts +374 -374
- package/src/state/active-run-registry.ts +227 -191
- package/src/state/artifact-store.ts +130 -129
- package/src/state/atomic-write.ts +262 -195
- package/src/state/blob-store.ts +116 -116
- package/src/state/contracts.ts +111 -111
- package/src/state/event-log-rotation.ts +161 -158
- package/src/state/event-log.ts +383 -303
- package/src/state/event-reconstructor.ts +217 -217
- package/src/state/jsonl-writer.ts +82 -82
- package/src/state/locks.ts +146 -146
- package/src/state/mailbox.ts +446 -405
- package/src/state/state-store.ts +364 -351
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +285 -285
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +116 -116
- package/src/teams/team-config.ts +27 -27
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -144
- package/src/ui/crew-widget.ts +487 -370
- package/src/ui/dashboard-panes/agents-pane.ts +109 -28
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/health-pane.ts +30 -30
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/progress-pane.ts +30 -30
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
- package/src/ui/heartbeat-aggregator.ts +63 -63
- package/src/ui/keybinding-map.ts +97 -94
- package/src/ui/live-conversation-overlay.ts +152 -0
- package/src/ui/live-run-sidebar.ts +180 -180
- package/src/ui/mascot.ts +442 -442
- package/src/ui/overlays/agent-picker-overlay.ts +57 -57
- package/src/ui/overlays/confirm-overlay.ts +58 -58
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
- package/src/ui/pi-ui-compat.ts +57 -57
- package/src/ui/powerbar-publisher.ts +221 -197
- package/src/ui/render-scheduler.ts +216 -143
- package/src/ui/run-action-dispatcher.ts +118 -118
- package/src/ui/run-dashboard.ts +526 -464
- package/src/ui/run-event-bus.ts +208 -208
- package/src/ui/run-snapshot-cache.ts +826 -777
- package/src/ui/settings-overlay.ts +721 -0
- package/src/ui/snapshot-types.ts +86 -70
- package/src/ui/theme-adapter.ts +190 -190
- package/src/ui/tool-progress-formatter.ts +89 -0
- package/src/ui/transcript-cache.ts +94 -94
- package/src/ui/transcript-viewer.ts +335 -335
- package/src/utils/conflict-detect.ts +662 -0
- package/src/utils/file-coalescer.ts +86 -86
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/fs-watch.ts +88 -31
- package/src/utils/gh-protocol.ts +479 -0
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/internal-error.ts +6 -6
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +102 -63
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sse-parser.ts +134 -134
- package/src/utils/task-name-generator.ts +337 -337
- package/src/utils/timings.ts +33 -33
- package/src/utils/visual.ts +243 -198
- package/src/workflows/discover-workflows.ts +139 -139
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +26 -26
- package/src/workflows/workflow-serializer.ts +32 -32
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +75 -75
- package/src/worktree/worktree-manager.ts +188 -188
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/tsconfig.json +19 -19
- package/workflows/default.workflow.md +30 -30
- package/workflows/fast-fix.workflow.md +23 -23
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/skills/task-packet/SKILL.md +0 -28
- package/skills/verify-evidence/SKILL.md +0 -27
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
|
|
3
|
-
interface TimerApi {
|
|
4
|
-
setTimeout(handler: () => void, delayMs: number): unknown;
|
|
5
|
-
clearTimeout(handle: unknown): void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const defaultTimerApi: TimerApi = {
|
|
9
|
-
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
10
|
-
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export interface FileCoalescer {
|
|
14
|
-
schedule(file: string, delayMs?: number): boolean;
|
|
15
|
-
clear(): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function createFileCoalescer(handler: (file: string) => void, defaultDelayMs: number, timerApi: TimerApi = defaultTimerApi): FileCoalescer {
|
|
19
|
-
const pending = new Map<string, unknown>();
|
|
20
|
-
return {
|
|
21
|
-
schedule(file, delayMs = defaultDelayMs) {
|
|
22
|
-
if (pending.has(file)) return false;
|
|
23
|
-
const timer = timerApi.setTimeout(() => {
|
|
24
|
-
pending.delete(file);
|
|
25
|
-
handler(file);
|
|
26
|
-
}, delayMs);
|
|
27
|
-
pending.set(file, timer);
|
|
28
|
-
return true;
|
|
29
|
-
},
|
|
30
|
-
clear() {
|
|
31
|
-
for (const timer of pending.values()) timerApi.clearTimeout(timer);
|
|
32
|
-
pending.clear();
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ReadCacheEntry<T> {
|
|
38
|
-
value: T;
|
|
39
|
-
mtimeMs: number;
|
|
40
|
-
size: number;
|
|
41
|
-
expiresAt: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const readCache = new Map<string, ReadCacheEntry<unknown>>();
|
|
45
|
-
const readCacheSizeLimit = 128;
|
|
46
|
-
|
|
47
|
-
function evictOldestCacheEntry(): void {
|
|
48
|
-
if (readCache.size < readCacheSizeLimit) return;
|
|
49
|
-
// Map iteration order is insertion order; first key is LRU.
|
|
50
|
-
const oldestKey = readCache.keys().next().value;
|
|
51
|
-
if (oldestKey !== undefined) readCache.delete(oldestKey);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function clearReadCache(): void {
|
|
55
|
-
readCache.clear();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function readJsonFileCoalesced<T>(filePath: string, ttlMs: number, read: () => T): T {
|
|
59
|
-
const now = Date.now();
|
|
60
|
-
const stat = (() => {
|
|
61
|
-
try {
|
|
62
|
-
const fileStat = fs.statSync(filePath);
|
|
63
|
-
return { mtimeMs: fileStat.mtimeMs, size: fileStat.size };
|
|
64
|
-
} catch {
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
})();
|
|
68
|
-
const cached = readCache.get(filePath);
|
|
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);
|
|
73
|
-
return cached.value as T;
|
|
74
|
-
}
|
|
75
|
-
const value = read();
|
|
76
|
-
if (stat !== undefined) {
|
|
77
|
-
readCache.set(filePath, {
|
|
78
|
-
value,
|
|
79
|
-
mtimeMs: stat.mtimeMs,
|
|
80
|
-
size: stat.size,
|
|
81
|
-
expiresAt: now + ttlMs,
|
|
82
|
-
});
|
|
83
|
-
evictOldestCacheEntry();
|
|
84
|
-
}
|
|
85
|
-
return value;
|
|
86
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
interface TimerApi {
|
|
4
|
+
setTimeout(handler: () => void, delayMs: number): unknown;
|
|
5
|
+
clearTimeout(handle: unknown): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const defaultTimerApi: TimerApi = {
|
|
9
|
+
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
10
|
+
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface FileCoalescer {
|
|
14
|
+
schedule(file: string, delayMs?: number): boolean;
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createFileCoalescer(handler: (file: string) => void, defaultDelayMs: number, timerApi: TimerApi = defaultTimerApi): FileCoalescer {
|
|
19
|
+
const pending = new Map<string, unknown>();
|
|
20
|
+
return {
|
|
21
|
+
schedule(file, delayMs = defaultDelayMs) {
|
|
22
|
+
if (pending.has(file)) return false;
|
|
23
|
+
const timer = timerApi.setTimeout(() => {
|
|
24
|
+
pending.delete(file);
|
|
25
|
+
handler(file);
|
|
26
|
+
}, delayMs);
|
|
27
|
+
pending.set(file, timer);
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
clear() {
|
|
31
|
+
for (const timer of pending.values()) timerApi.clearTimeout(timer);
|
|
32
|
+
pending.clear();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ReadCacheEntry<T> {
|
|
38
|
+
value: T;
|
|
39
|
+
mtimeMs: number;
|
|
40
|
+
size: number;
|
|
41
|
+
expiresAt: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const readCache = new Map<string, ReadCacheEntry<unknown>>();
|
|
45
|
+
const readCacheSizeLimit = 128;
|
|
46
|
+
|
|
47
|
+
function evictOldestCacheEntry(): void {
|
|
48
|
+
if (readCache.size < readCacheSizeLimit) return;
|
|
49
|
+
// Map iteration order is insertion order; first key is LRU.
|
|
50
|
+
const oldestKey = readCache.keys().next().value;
|
|
51
|
+
if (oldestKey !== undefined) readCache.delete(oldestKey);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function clearReadCache(): void {
|
|
55
|
+
readCache.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function readJsonFileCoalesced<T>(filePath: string, ttlMs: number, read: () => T): T {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const stat = (() => {
|
|
61
|
+
try {
|
|
62
|
+
const fileStat = fs.statSync(filePath);
|
|
63
|
+
return { mtimeMs: fileStat.mtimeMs, size: fileStat.size };
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
const cached = readCache.get(filePath);
|
|
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);
|
|
73
|
+
return cached.value as T;
|
|
74
|
+
}
|
|
75
|
+
const value = read();
|
|
76
|
+
if (stat !== undefined) {
|
|
77
|
+
readCache.set(filePath, {
|
|
78
|
+
value,
|
|
79
|
+
mtimeMs: stat.mtimeMs,
|
|
80
|
+
size: stat.size,
|
|
81
|
+
expiresAt: now + ttlMs,
|
|
82
|
+
});
|
|
83
|
+
evictOldestCacheEntry();
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
package/src/utils/frontmatter.ts
CHANGED
|
@@ -1,68 +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) {
|
|
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
|
-
}
|
|
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
|
+
}
|
package/src/utils/fs-watch.ts
CHANGED
|
@@ -1,31 +1,88 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
watcher.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { FSWatcher, WatchListener } from "node:fs";
|
|
4
|
+
|
|
5
|
+
export const FS_WATCH_RETRY_DELAY_MS = 5000;
|
|
6
|
+
|
|
7
|
+
export function closeWatcher(watcher: FSWatcher | null | undefined): void {
|
|
8
|
+
if (!watcher) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
watcher.close();
|
|
14
|
+
} catch {
|
|
15
|
+
// Ignore watcher close errors
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function watchWithErrorHandler(
|
|
20
|
+
path: string,
|
|
21
|
+
listener: WatchListener<string>,
|
|
22
|
+
onError: (error?: unknown) => void,
|
|
23
|
+
): FSWatcher | null {
|
|
24
|
+
try {
|
|
25
|
+
const watcher = fs.watch(path, listener);
|
|
26
|
+
watcher.on("error", onError);
|
|
27
|
+
return watcher;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
onError(error);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 1.3 — Watch a directory recursively and invoke `onChange` when any file
|
|
36
|
+
* inside changes. Falls back to `null` on systems where `fs.watch` rejects
|
|
37
|
+
* recursive mode (e.g., Linux when running on older kernels via FUSE/network FS).
|
|
38
|
+
*
|
|
39
|
+
* Callers MUST handle null and fall back to polling. The watcher emits the
|
|
40
|
+
* filename relative to `rootDir` (forward-slash normalised on Windows).
|
|
41
|
+
*/
|
|
42
|
+
export function createRecursiveWatcher(
|
|
43
|
+
rootDir: string,
|
|
44
|
+
onChange: (relativePath: string) => void,
|
|
45
|
+
onError: (error: unknown) => void,
|
|
46
|
+
): FSWatcher | null {
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(rootDir)) fs.mkdirSync(rootDir, { recursive: true });
|
|
49
|
+
const watcher = fs.watch(rootDir, { recursive: true }, (_eventType, filename) => {
|
|
50
|
+
if (typeof filename !== "string" || filename.length === 0) return;
|
|
51
|
+
onChange(filename.replace(/\\/g, "/"));
|
|
52
|
+
});
|
|
53
|
+
watcher.on("error", (error) => {
|
|
54
|
+
try { watcher.close(); } catch { /* ignore */ }
|
|
55
|
+
onError(error);
|
|
56
|
+
});
|
|
57
|
+
return watcher;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
onError(error);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Given a path relative to `<crewRoot>/state`, return the runId that owns
|
|
66
|
+
* the change, or undefined if the path doesn't match any tracked run layout.
|
|
67
|
+
*/
|
|
68
|
+
export function runIdFromStateRelativePath(relativePath: string): string | undefined {
|
|
69
|
+
const parts = relativePath.split("/");
|
|
70
|
+
// Layout is `runs/{runId}/...` — see docs/architecture.md state layer.
|
|
71
|
+
if (parts.length >= 2 && parts[0] === "runs" && parts[1]) return parts[1];
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Convenience: combine the two helpers for `<crewRoot>/state` watching. */
|
|
76
|
+
export function watchCrewState(
|
|
77
|
+
stateDir: string,
|
|
78
|
+
onRunChange: (runId: string) => void,
|
|
79
|
+
onError: (error: unknown) => void,
|
|
80
|
+
): FSWatcher | null {
|
|
81
|
+
return createRecursiveWatcher(stateDir, (relativePath) => {
|
|
82
|
+
const runId = runIdFromStateRelativePath(relativePath);
|
|
83
|
+
if (runId) onRunChange(runId);
|
|
84
|
+
}, onError);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Re-export path helper so callers don't pull node:path just for join.
|
|
88
|
+
export const joinPath = path.join;
|