pi-crew 0.9.41 → 0.9.44
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/CHANGELOG.md +253 -0
- package/README.md +14 -0
- package/dist/build-meta.json +6450 -12419
- package/dist/index.mjs +57998 -68417
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +2 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
- package/package.json +4 -2
- package/scripts/README.md +6 -0
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/check-all-skills.ts +3 -3
- package/scripts/dev-runner.mjs +106 -0
- package/scripts/postinstall.mjs +6 -2
- package/scripts/verify-skill.ts +2 -2
- package/src/agents/agent-config.ts +1 -1
- package/src/config/config.ts +20 -1
- package/src/config/drift-detector.ts +97 -15
- package/src/config/resilient-parser.ts +4 -4
- package/src/config/types.ts +0 -2
- package/src/extension/crew-cleanup.ts +17 -3
- package/src/extension/crew-vibes/config.ts +1 -1
- package/src/extension/crew-vibes/font-detect.ts +16 -3
- package/src/extension/crew-vibes/index.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +26 -9
- package/src/extension/management.ts +6 -5
- package/src/extension/register.ts +58 -1573
- package/src/extension/registration/command-registration.ts +57 -0
- package/src/extension/registration/commands.ts +7 -2
- package/src/extension/registration/context-builder.ts +131 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +731 -0
- package/src/extension/registration/observability.ts +33 -4
- package/src/extension/registration/registration-types.ts +149 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-manager-setup.ts +211 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/rpc-hmac.ts +21 -1
- package/src/extension/run-import.ts +3 -3
- package/src/extension/team-tool/auto-summarize.ts +4 -4
- package/src/extension/team-tool/cancel.ts +5 -5
- package/src/extension/team-tool/chain-executor.ts +5 -4
- package/src/extension/team-tool/context.ts +11 -1
- package/src/extension/team-tool/doctor.ts +2 -1
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +25 -36
- package/src/extension/team-tool/orchestrate.ts +0 -1
- package/src/extension/team-tool/parallel-dispatch.ts +5 -0
- package/src/extension/team-tool/run.ts +220 -214
- package/src/extension/team-tool/workflow-manage.ts +1 -1
- package/src/extension/team-tool.ts +69 -16
- package/src/hooks/registry.ts +7 -2
- package/src/observability/correlation.ts +4 -3
- package/src/observability/event-bus.ts +10 -1
- package/src/observability/event-to-metric.ts +8 -7
- package/src/observability/exporters/adapter.ts +6 -9
- package/src/observability/exporters/otlp-exporter.ts +81 -0
- package/src/observability/metric-retention.ts +4 -0
- package/src/observability/metric-sink.ts +14 -4
- package/src/prompt/prompt-runtime.ts +15 -0
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/async-runner.ts +13 -4
- package/src/runtime/background-runner.ts +32 -23
- package/src/runtime/capability-inventory.ts +1 -1
- package/src/runtime/chain-runner.ts +5 -5
- package/src/runtime/checkpoint.ts +6 -16
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-spawn.ts +234 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +80 -822
- package/src/runtime/coalesce-tasks.ts +1 -1
- package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
- package/src/runtime/crash-recovery.ts +63 -47
- package/src/runtime/delivery-coordinator.ts +1 -1
- package/src/runtime/deterministic-ast.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -1
- package/src/runtime/dwf-state-store.ts +5 -0
- package/src/runtime/dynamic-workflow-context.ts +78 -18
- package/src/runtime/dynamic-workflow-runner.ts +18 -2
- package/src/runtime/foreground-control.ts +3 -2
- package/src/runtime/goal-evaluator.ts +59 -27
- package/src/runtime/goal-loop-runner.ts +46 -13
- package/src/runtime/goal-state-store.ts +1 -1
- package/src/runtime/handoff-manager.ts +0 -2
- package/src/runtime/intercom-bridge.ts +0 -2
- package/src/runtime/live-agent-manager.ts +30 -5
- package/src/runtime/live-extension-bridge.ts +0 -2
- package/src/runtime/live-session-runtime.ts +65 -57
- package/src/runtime/manifest-cache.ts +59 -0
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/orphan-worker-registry.ts +3 -3
- package/src/runtime/output-validator.ts +1 -1
- package/src/runtime/path-overlap.ts +1 -1
- package/src/runtime/pi-args.ts +3 -3
- package/src/runtime/pi-json-output.ts +4 -3
- package/src/runtime/pi-spawn.ts +7 -1
- package/src/runtime/pipeline-runner.ts +1 -4
- package/src/runtime/post-checks.ts +0 -1
- package/src/runtime/retry-runner.ts +1 -1
- package/src/runtime/run-coalesced-task-group.ts +18 -23
- package/src/runtime/scheduler.ts +2 -0
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/settings-store.ts +31 -1
- package/src/runtime/skill-effectiveness.ts +2 -2
- package/src/runtime/stale-reconciler.ts +23 -53
- package/src/runtime/subagent-manager.ts +19 -3
- package/src/runtime/task-output-context.ts +2 -1
- package/src/runtime/task-runner/prompt-builder.ts +3 -3
- package/src/runtime/task-runner/run-projection.ts +3 -1
- package/src/runtime/task-runner/state-helpers.ts +13 -1
- package/src/runtime/task-runner.ts +31 -6
- package/src/runtime/team-runner.ts +8 -2
- package/src/runtime/verification-gates.ts +54 -5
- package/src/schema/config-schema.ts +19 -1
- package/src/schema/team-tool-schema.ts +2 -0
- package/src/skills/discover-skills.ts +25 -15
- package/src/state/active-run-registry.ts +0 -1
- package/src/state/atomic-write.ts +37 -11
- package/src/state/blob-store.ts +3 -9
- package/src/state/contracts.ts +5 -1
- package/src/state/crew-init.ts +3 -2
- package/src/state/decision-ledger.ts +2 -2
- package/src/state/event-log-rotation.ts +1 -1
- package/src/state/event-log.ts +4 -3
- package/src/state/gitignore-manager.ts +2 -1
- package/src/state/health-store.ts +5 -2
- package/src/state/instinct-store.ts +11 -2
- package/src/state/locks.ts +132 -49
- package/src/state/mailbox.ts +3 -4
- package/src/state/observation-store.ts +1 -13
- package/src/state/run-cache.ts +12 -12
- package/src/state/run-graph.ts +2 -1
- package/src/state/run-metrics.ts +1 -1
- package/src/state/schedule.ts +7 -1
- package/src/state/state-store.ts +9 -3
- package/src/state/tiered-eval.ts +1 -1
- package/src/state/worker-atomic-writer.ts +1 -0
- package/src/ui/dynamic-border.ts +13 -3
- package/src/ui/live-conversation-overlay.ts +7 -27
- package/src/ui/live-duration.ts +2 -2
- package/src/ui/live-run-sidebar.ts +56 -32
- package/src/ui/mascot.ts +19 -13
- package/src/ui/powerbar-publisher.ts +244 -178
- package/src/ui/render-coalescer.ts +44 -3
- package/src/ui/render-diff.ts +2 -2
- package/src/ui/run-dashboard.ts +122 -21
- package/src/ui/run-event-bus.ts +66 -0
- package/src/ui/run-snapshot-cache.ts +84 -81
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/status-colors.ts +24 -0
- package/src/ui/tool-progress-formatter.ts +1 -1
- package/src/ui/tool-render.ts +3 -3
- package/src/ui/transcript-cache.ts +5 -10
- package/src/ui/transcript-entries.ts +68 -72
- package/src/ui/transcript-viewer.ts +54 -14
- package/src/ui/widget/index.ts +26 -15
- package/src/ui/widget/widget-model.ts +18 -9
- package/src/ui/widget/widget-renderer.ts +2 -5
- package/src/utils/fingerprint.ts +1 -1
- package/src/utils/paths.ts +1 -1
- package/src/utils/project-detector.ts +1 -1
- package/src/worktree/worktree-manager.ts +47 -19
- package/src/state/atomic-write-v2.ts +0 -108
|
@@ -37,6 +37,7 @@ export const PiTeamsLimitsConfigSchema = Type.Object(
|
|
|
37
37
|
maxRetriesPerTask: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
38
38
|
maxTasksPerRun: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
39
39
|
heartbeatStaleMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
40
|
+
serializeOnPathOverlap: Type.Optional(Type.Boolean()),
|
|
40
41
|
},
|
|
41
42
|
{ additionalProperties: false },
|
|
42
43
|
);
|
|
@@ -59,6 +60,17 @@ export const PiTeamsRuntimeConfigSchema = Type.Object(
|
|
|
59
60
|
effectivenessGuard: Type.Optional(
|
|
60
61
|
Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("block"), Type.Literal("fail")]),
|
|
61
62
|
),
|
|
63
|
+
yield: Type.Optional(
|
|
64
|
+
Type.Object(
|
|
65
|
+
{
|
|
66
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
67
|
+
maxReminders: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
68
|
+
reminderPrompt: Type.Optional(Type.String({ maxLength: 1000 })),
|
|
69
|
+
},
|
|
70
|
+
{ additionalProperties: false },
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
excludeContextBash: Type.Optional(Type.Boolean()),
|
|
62
74
|
isolationPolicy: Type.Optional(
|
|
63
75
|
Type.Object(
|
|
64
76
|
{
|
|
@@ -182,7 +194,7 @@ export const PiTeamsObservabilityConfigSchema = Type.Object(
|
|
|
182
194
|
{
|
|
183
195
|
enabled: Type.Optional(Type.Boolean()),
|
|
184
196
|
pollIntervalMs: Type.Optional(Type.Integer({ minimum: 1000, maximum: 60000 })),
|
|
185
|
-
metricRetentionDays: Type.Optional(Type.Integer({ minimum: 1, maximum:
|
|
197
|
+
metricRetentionDays: Type.Optional(Type.Integer({ minimum: 1, maximum: 90 })),
|
|
186
198
|
},
|
|
187
199
|
{ additionalProperties: false },
|
|
188
200
|
);
|
|
@@ -205,6 +217,11 @@ export const PiTeamsReliabilityConfigSchema = Type.Object(
|
|
|
205
217
|
autoRecover: Type.Optional(Type.Boolean()),
|
|
206
218
|
deadletterThreshold: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
207
219
|
cleanupOrphanedTempDirs: Type.Optional(Type.Boolean()),
|
|
220
|
+
autoRepairIntervalMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
221
|
+
forcePreflight: Type.Optional(Type.Boolean()),
|
|
222
|
+
ambientStatusInjection: Type.Optional(Type.Boolean()),
|
|
223
|
+
perWriteValidation: Type.Optional(Type.Boolean()),
|
|
224
|
+
scopeModels: Type.Optional(Type.Boolean()),
|
|
208
225
|
},
|
|
209
226
|
{ additionalProperties: false },
|
|
210
227
|
);
|
|
@@ -235,6 +252,7 @@ export const PiTeamsUiConfigSchema = Type.Object(
|
|
|
235
252
|
dashboardLiveRefreshMs: Type.Optional(Type.Integer({ minimum: 250, maximum: 60000 })),
|
|
236
253
|
autoOpenDashboard: Type.Optional(Type.Boolean()),
|
|
237
254
|
autoOpenDashboardForForegroundRuns: Type.Optional(Type.Boolean()),
|
|
255
|
+
autoCloseDashboardMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
238
256
|
showModel: Type.Optional(Type.Boolean()),
|
|
239
257
|
showTokens: Type.Optional(Type.Boolean()),
|
|
240
258
|
showTools: Type.Optional(Type.Boolean()),
|
|
@@ -194,6 +194,8 @@ export const TeamToolParams = Type.Object({
|
|
|
194
194
|
description: "Path to a markdown plan document for orchestration.",
|
|
195
195
|
}),
|
|
196
196
|
),
|
|
197
|
+
subAction: Type.Optional(Type.String({ description: "Sub-action for schedule management (remove, disable, enable, update)." })),
|
|
198
|
+
jobId: Type.Optional(Type.String({ description: "Job ID for schedule management actions." })),
|
|
197
199
|
cron: Type.Optional(
|
|
198
200
|
Type.String({
|
|
199
201
|
description: "Cron expression for recurring scheduled runs (e.g., '0 9 * * MON').",
|
|
@@ -28,23 +28,34 @@ export interface SkillDescriptor {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* F6 (v0.7.9): discover skills from all
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
31
|
+
* F6 (v0.7.9): discover skills from all roots (matching pi-subagents' skill-loader
|
|
32
|
+
* so users authoring skills under either convention find them).
|
|
33
|
+
*
|
|
34
|
+
* IMPORTANT — inventory vs injection (SEC-003): this function returns EVERY
|
|
35
|
+
* skill found across roots (NO first-wins dedup) because it feeds the capability
|
|
36
|
+
* INVENTORY, which intentionally surfaces all skills (incl. project shadows) for
|
|
37
|
+
* transparency; `capability-inventory.ts` then marks project/user items that
|
|
38
|
+
* shadow a package/builtin one and re-sorts by id.
|
|
39
|
+
*
|
|
40
|
+
* The actual skill-content INJECTION path is separate: `skill-instructions.ts`
|
|
41
|
+
* `candidateSkillDirs` + `readSkillMarkdown` use first-wins with PACKAGE FIRST
|
|
42
|
+
* (SEC-003 fix, regression-tested in test/unit/skill-instructions.test.ts) so a
|
|
43
|
+
* project skill can NEVER override a trusted package skill's content in worker
|
|
44
|
+
* prompts. The package-first order below mirrors that injection path for
|
|
45
|
+
* consistency/defense-in-depth.
|
|
46
|
+
*
|
|
47
|
+
* Roots:
|
|
48
|
+
* 1. PACKAGE_SKILLS_DIR (bundled, trusted)
|
|
49
|
+
* 2. <cwd>/.pi/skills (project, Pi standard)
|
|
50
|
+
* 3. <cwd>/.agents/skills (project, Agent Skills spec — agentskills.io)
|
|
51
|
+
* 4. <cwd>/skills (project, legacy pi-crew convention)
|
|
52
|
+
* 5. <getAgentDir>/skills (user, Pi standard)
|
|
53
|
+
* 6. <homedir>/.agents/skills (user, Agent Skills spec)
|
|
54
|
+
* 7. <homedir>/.pi/skills (user, legacy Pi — pre-standard)
|
|
45
55
|
*/
|
|
46
56
|
function listSkillDirs(cwd: string): Array<{ root: string; source: SkillDescriptor["source"] }> {
|
|
47
57
|
return [
|
|
58
|
+
{ root: PACKAGE_SKILLS_DIR, source: "package" },
|
|
48
59
|
{ root: path.resolve(cwd, ".pi", "skills"), source: "project-pi" },
|
|
49
60
|
{
|
|
50
61
|
root: path.resolve(cwd, ".agents", "skills"),
|
|
@@ -57,7 +68,6 @@ function listSkillDirs(cwd: string): Array<{ root: string; source: SkillDescript
|
|
|
57
68
|
source: "user-agents",
|
|
58
69
|
},
|
|
59
70
|
{ root: path.join(os.homedir(), ".pi", "skills"), source: "user-pi" },
|
|
60
|
-
{ root: PACKAGE_SKILLS_DIR, source: "package" },
|
|
61
71
|
];
|
|
62
72
|
}
|
|
63
73
|
|
|
@@ -2,7 +2,6 @@ import * as crypto from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
|
|
5
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
6
5
|
import { userCrewRoot } from "../utils/paths.ts";
|
|
7
6
|
import { isSafePathId } from "../utils/safe-paths.ts";
|
|
8
7
|
import { sharedScanCache } from "../utils/scan-cache.ts";
|
|
@@ -315,8 +315,13 @@ function isRetryableLinkError(error: unknown): boolean {
|
|
|
315
315
|
* It atomically creates a hard link to the source. We then unlink the source.
|
|
316
316
|
* This prevents TOCTOU attacks where an attacker plants a symlink at the
|
|
317
317
|
* destination between the check and the rename operation.
|
|
318
|
+
*
|
|
319
|
+
* ST-8: this is the canonical symlink-safe rename helper used by both
|
|
320
|
+
* `atomicWriteFile` (sync) and `atomicWriteFileAsync` (async) paths. The
|
|
321
|
+
* helper is exported so any future caller can share the same symlink-safe
|
|
322
|
+
* rename semantics; keep this signature stable.
|
|
318
323
|
*/
|
|
319
|
-
function renameWithLinkSync(tempPath: string, filePath: string, retries = 8): void {
|
|
324
|
+
export function renameWithLinkSync(tempPath: string, filePath: string, retries = 8): void {
|
|
320
325
|
let lastError: unknown;
|
|
321
326
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
322
327
|
try {
|
|
@@ -371,7 +376,12 @@ function renameWithLinkSync(tempPath: string, filePath: string, retries = 8): vo
|
|
|
371
376
|
throw lastError;
|
|
372
377
|
}
|
|
373
378
|
|
|
374
|
-
|
|
379
|
+
/**
|
|
380
|
+
* ST-8: async twin of `renameWithLinkSync`. Exported so any future caller
|
|
381
|
+
* can share the same symlink-safe rename semantics as the primary
|
|
382
|
+
* `atomicWriteFileAsync` path.
|
|
383
|
+
*/
|
|
384
|
+
export async function renameWithLinkAsync(tempPath: string, filePath: string, retries = 8): Promise<void> {
|
|
375
385
|
let lastError: unknown;
|
|
376
386
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
377
387
|
try {
|
|
@@ -496,21 +506,21 @@ export type WriteDurability = "full" | "best-effort";
|
|
|
496
506
|
/** Options accepted by atomicWriteFile (forward-compatible string | object form). */
|
|
497
507
|
export type AtomicWriteOptions =
|
|
498
508
|
| string // legacy: expectedHash
|
|
499
|
-
| { expectedHash?: string; durability?: WriteDurability };
|
|
509
|
+
| { expectedHash?: string; durability?: WriteDurability; mode?: number };
|
|
500
510
|
|
|
501
|
-
function normalizeOptions(arg: unknown): { expectedHash?: string; durability: WriteDurability } {
|
|
502
|
-
if (typeof arg === "string") return { expectedHash: arg, durability: "full" };
|
|
511
|
+
function normalizeOptions(arg: unknown): { expectedHash?: string; durability: WriteDurability; mode?: number } {
|
|
512
|
+
if (typeof arg === "string") return { expectedHash: arg, durability: "full", mode: undefined };
|
|
503
513
|
if (arg && typeof arg === "object") {
|
|
504
|
-
const o = arg as { expectedHash?: string; durability?: WriteDurability };
|
|
514
|
+
const o = arg as { expectedHash?: string; durability?: WriteDurability; mode?: number };
|
|
505
515
|
const durability: WriteDurability = o.durability === "best-effort" ? "best-effort" : "full";
|
|
506
|
-
return { expectedHash: o.expectedHash, durability };
|
|
516
|
+
return { expectedHash: o.expectedHash, durability, mode: o.mode };
|
|
507
517
|
}
|
|
508
|
-
return { durability: "full" };
|
|
518
|
+
return { durability: "full", mode: undefined };
|
|
509
519
|
}
|
|
510
520
|
|
|
511
521
|
export function atomicWriteFile(filePath: string, content: string, options?: AtomicWriteOptions): void {
|
|
512
522
|
cancelPendingCoalescedWrite(filePath);
|
|
513
|
-
const { durability } = normalizeOptions(options);
|
|
523
|
+
const { durability, mode } = normalizeOptions(options);
|
|
514
524
|
if (!isSymlinkSafeDirCached(filePath))
|
|
515
525
|
throw new Error(`Refusing to write: target is a symlink or inside untrusted directory: ${filePath}`);
|
|
516
526
|
// On Windows the parent directory may be referenced via a short-name alias
|
|
@@ -556,7 +566,7 @@ export function atomicWriteFile(filePath: string, content: string, options?: Ato
|
|
|
556
566
|
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
557
567
|
let fd: number | undefined;
|
|
558
568
|
try {
|
|
559
|
-
fd = fs.openSync(tempPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
|
|
569
|
+
fd = fs.openSync(tempPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, mode ?? 0o600);
|
|
560
570
|
// Post-open verification: on Windows O_NOFOLLOW is 0, so verify FD is a regular file
|
|
561
571
|
const openedStat = fs.fstatSync(fd);
|
|
562
572
|
if (!openedStat.isFile()) {
|
|
@@ -747,6 +757,14 @@ let flushInProgress = 0;
|
|
|
747
757
|
* Buffer a JSON write and flush it after `coalesceMs` ms (default 50).
|
|
748
758
|
* Multiple writes to the same path within the window collapse to one disk write.
|
|
749
759
|
*
|
|
760
|
+
* ST-7 escape hatch: pass `skipCoalesce: true` to bypass the buffer entirely
|
|
761
|
+
* and write synchronously via `atomicWriteJson`. Use for terminal task status
|
|
762
|
+
* transitions where a SIGKILL during the 50ms coalesce window must NOT lose
|
|
763
|
+
* the terminal status update — otherwise crash recovery would see "running"
|
|
764
|
+
* in tasks.json while events.jsonl already shows "completed", causing
|
|
765
|
+
* double-execution / zombie detection. Defaults to `false` (buffered write)
|
|
766
|
+
* so all existing callers preserve their behavior.
|
|
767
|
+
*
|
|
750
768
|
* @see The coalescing caveat: a `readJsonFile` call between buffer and flush
|
|
751
769
|
* sees the previous on-disk content. Callers needing read-after-write
|
|
752
770
|
* must call `flushPendingAtomicWrites()` first or use `atomicWriteJson`.
|
|
@@ -754,9 +772,17 @@ let flushInProgress = 0;
|
|
|
754
772
|
export function atomicWriteJsonCoalesced<T>(
|
|
755
773
|
filePath: string,
|
|
756
774
|
value: T,
|
|
757
|
-
coalesceMs = DEFAULT_ATOMIC_COALESCE_MS,
|
|
775
|
+
coalesceMs: number = DEFAULT_ATOMIC_COALESCE_MS,
|
|
758
776
|
options?: AtomicWriteOptions,
|
|
777
|
+
skipCoalesce: boolean = false,
|
|
759
778
|
): void {
|
|
779
|
+
// ST-7: bypass coalescing for callers that need durable synchronous
|
|
780
|
+
// semantics. atomicWriteJson goes through atomicWriteFile which uses
|
|
781
|
+
// renameWithLinkSync + fsync — fully durable, no SIGKILL window.
|
|
782
|
+
if (skipCoalesce) {
|
|
783
|
+
atomicWriteJson(filePath, value, options);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
760
786
|
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
761
787
|
const previous = pendingAtomicWrites.get(filePath);
|
|
762
788
|
if (previous) clearTimeout(previous.timer);
|
package/src/state/blob-store.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
5
|
-
import { sleepSync } from "../utils/sleep.ts";
|
|
6
5
|
import { atomicWriteFile, renameWithRetry } from "./atomic-write.ts";
|
|
7
6
|
import { withFileLockSync } from "./locks.ts";
|
|
8
7
|
|
|
@@ -152,14 +151,9 @@ export function writeBlob(
|
|
|
152
151
|
// FIX: Write blob content FIRST, then metadata. If blob write fails, no orphan metadata.
|
|
153
152
|
// Previous order was metadata first, then blob - causing orphan metadata on blob failure.
|
|
154
153
|
let blobContentWritten = false;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
blobContentWritten = true;
|
|
159
|
-
} catch (error) {
|
|
160
|
-
// Blob write failed - no metadata to clean up since we haven't written it yet
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
154
|
+
// Write blob content first (immutable, content-addressed)
|
|
155
|
+
atomicWriteBuffer(blobPath, Buffer.isBuffer(content) ? content : Buffer.from(content, "utf-8"));
|
|
156
|
+
blobContentWritten = true;
|
|
163
157
|
|
|
164
158
|
// Metadata only after blob content is successfully written
|
|
165
159
|
// Issue 3 fix: Use two-phase commit for metadata - write to temp file first,
|
package/src/state/contracts.ts
CHANGED
|
@@ -44,7 +44,7 @@ export const TEAM_TASK_STATUS_TRANSITIONS: Readonly<Record<TeamTaskStatus, reado
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
/** @internal */
|
|
47
|
-
const TEAM_EVENT_TYPES = [
|
|
47
|
+
export const TEAM_EVENT_TYPES = [
|
|
48
48
|
"run.created",
|
|
49
49
|
"run.queued",
|
|
50
50
|
"run.planning",
|
|
@@ -56,6 +56,10 @@ const TEAM_EVENT_TYPES = [
|
|
|
56
56
|
"task.created",
|
|
57
57
|
"task.started",
|
|
58
58
|
"task.progress",
|
|
59
|
+
"hook.pre_step_started",
|
|
60
|
+
"hook.pre_step_completed",
|
|
61
|
+
"hook.pre_step_failed",
|
|
62
|
+
"hook.pre_step_optional_failed",
|
|
59
63
|
"task.blocked",
|
|
60
64
|
"task.green",
|
|
61
65
|
"task.red",
|
package/src/state/crew-init.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
19
|
import * as path from "node:path";
|
|
20
|
+
import { atomicWriteFile } from "./atomic-write.ts";
|
|
20
21
|
import { updateGitignore } from "./gitignore-manager.ts";
|
|
21
22
|
|
|
22
23
|
// Re-export updateGitignore for backwards compatibility with tests.
|
|
@@ -238,12 +239,12 @@ export async function ensureCrewDirectory(cwd: string): Promise<void> {
|
|
|
238
239
|
|
|
239
240
|
for (const placeholder of placeholders) {
|
|
240
241
|
if (!fs.existsSync(placeholder)) {
|
|
241
|
-
|
|
242
|
+
atomicWriteFile(placeholder, "");
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
// 3. Write README.md (always overwrite to keep it current)
|
|
246
|
-
|
|
247
|
+
atomicWriteFile(safeJoin(crewRoot, "README.md"), buildCrewReadme());
|
|
247
248
|
|
|
248
249
|
// 4. Update .gitignore at project root
|
|
249
250
|
const repoRoot = findProjectRoot(cwd);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
-
import { dirname, join } from "path";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { projectCrewRoot } from "../utils/paths.ts";
|
|
4
4
|
import { assertSafePathId } from "../utils/safe-paths.ts";
|
|
5
5
|
import { atomicWriteFile } from "./atomic-write.ts";
|
|
@@ -220,7 +220,7 @@ export function rotateEventLogUnlocked(eventsPath: string): boolean {
|
|
|
220
220
|
// copyFileSync + writeFileSync("") ensures eventsPath ALWAYS exists
|
|
221
221
|
// (no missing-file window for concurrent readers).
|
|
222
222
|
fs.copyFileSync(eventsPath, archivePath);
|
|
223
|
-
|
|
223
|
+
atomicWriteFile(eventsPath, "");
|
|
224
224
|
return true;
|
|
225
225
|
} catch (error) {
|
|
226
226
|
logInternalError("event-log.rotate", error, `eventsPath=${eventsPath}`);
|
package/src/state/event-log.ts
CHANGED
|
@@ -93,7 +93,7 @@ export function withEventLogLockSync<T>(eventsPath: string, fn: () => T, options
|
|
|
93
93
|
const pidFile = path.join(lockDir, "pid");
|
|
94
94
|
const start = Date.now();
|
|
95
95
|
// SECURITY (HIGH #2 fix): Reduced from 120s to 5s to prevent blocking the
|
|
96
|
-
// event loop indefinitely.
|
|
96
|
+
// event loop indefinitely. ~100 retries × 50ms ≈ 5s max. After timeout, we
|
|
97
97
|
// throw a clear error instead of blocking forever. This ensures AbortSignal
|
|
98
98
|
// handlers, SIGTERM, and graceful shutdown can fire within seconds.
|
|
99
99
|
const timeout = options?.timeoutMs ?? 5000;
|
|
@@ -109,7 +109,7 @@ export function withEventLogLockSync<T>(eventsPath: string, fn: () => T, options
|
|
|
109
109
|
// (reduced from 120s) is appropriate.
|
|
110
110
|
fs.mkdirSync(lockDir);
|
|
111
111
|
try {
|
|
112
|
-
|
|
112
|
+
atomicWriteFile(pidFile, String(process.pid));
|
|
113
113
|
} catch {
|
|
114
114
|
/* best-effort */
|
|
115
115
|
}
|
|
@@ -162,7 +162,7 @@ export function withEventLogLockSync<T>(eventsPath: string, fn: () => T, options
|
|
|
162
162
|
} catch {
|
|
163
163
|
/* no pid file — mtime check above already handles it */
|
|
164
164
|
}
|
|
165
|
-
sleepSync(
|
|
165
|
+
sleepSync(50);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
try {
|
|
@@ -998,6 +998,7 @@ export function appendEventBuffered(eventsPath: string, event: AppendTeamEvent,
|
|
|
998
998
|
});
|
|
999
999
|
}, bufferMs);
|
|
1000
1000
|
bufferedTimers.set(eventsPath, timer);
|
|
1001
|
+
timer.unref();
|
|
1001
1002
|
}
|
|
1002
1003
|
});
|
|
1003
1004
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Only adds entries if not already present; preserves existing content.
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
|
+
import { atomicWriteFile } from "./atomic-write.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Entries to add to .gitignore for .crew directory management.
|
|
@@ -25,7 +26,7 @@ const CREW_GITIGNORE_ENTRIES = [
|
|
|
25
26
|
*/
|
|
26
27
|
export async function updateGitignore(gitignorePath: string): Promise<void> {
|
|
27
28
|
if (!fs.existsSync(gitignorePath)) {
|
|
28
|
-
|
|
29
|
+
atomicWriteFile(gitignorePath, CREW_GITIGNORE_ENTRIES.join("\n") + "\n");
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { ManifestSummary
|
|
3
|
+
import type { ManifestSummary } from "../runtime/task-health.ts";
|
|
4
4
|
import { computeRunHealth } from "../runtime/task-health.ts";
|
|
5
|
+
import { atomicWriteFile } from "./atomic-write.ts";
|
|
5
6
|
|
|
6
7
|
// Relative to the crew root (`<cwd>/.crew`). BUG A fix (pts/2 hang
|
|
7
8
|
// investigation 2026-06-16): this was `.crew/state/health`, which double-joined
|
|
@@ -41,7 +42,9 @@ export class HealthStore {
|
|
|
41
42
|
const dir = this.healthDir();
|
|
42
43
|
fs.mkdirSync(dir, { recursive: true });
|
|
43
44
|
const file = path.join(dir, `${manifest.runId}.json`);
|
|
44
|
-
|
|
45
|
+
// ST-2: atomic (temp + rename + fsync) — a crash mid-writeFileSync would truncate
|
|
46
|
+
// the snapshot and loadAllSnapshots() silently skips unparseable files.
|
|
47
|
+
atomicWriteFile(file, JSON.stringify(snapshot, null, 2) + "\n");
|
|
45
48
|
return snapshot;
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { assertSafePathId } from "../utils/safe-paths.ts";
|
|
5
|
+
import { atomicWriteFile } from "./atomic-write.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Represents a learned instinct that guides agent behavior.
|
|
@@ -87,7 +88,13 @@ export class InstinctStore {
|
|
|
87
88
|
private appendInstinctToFile(filePath: string, instinct: Instinct): void {
|
|
88
89
|
const dir = path.dirname(filePath);
|
|
89
90
|
this.ensureDir(dir);
|
|
90
|
-
|
|
91
|
+
// ST-3: read existing + append + atomic rewrite. appendFileSync has no fsync and
|
|
92
|
+
// can interleave bytes on concurrent writes. Callers should wrap this in
|
|
93
|
+
// withFileLockSync(filePath) to serialize concurrent RMW on the same file.
|
|
94
|
+
const existing = fs.existsSync(filePath) ? this.readInstinctsFromFile(filePath) : [];
|
|
95
|
+
existing.push(instinct);
|
|
96
|
+
const content = existing.map((i) => JSON.stringify(i)).join("\n") + "\n";
|
|
97
|
+
atomicWriteFile(filePath, content);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
/**
|
|
@@ -97,7 +104,9 @@ export class InstinctStore {
|
|
|
97
104
|
const dir = path.dirname(filePath);
|
|
98
105
|
this.ensureDir(dir);
|
|
99
106
|
const content = instincts.map((i) => JSON.stringify(i)).join("\n") + "\n";
|
|
100
|
-
|
|
107
|
+
// ST-3: atomic write (temp + rename + fsync) — crash mid-writeFileSync truncates
|
|
108
|
+
// the JSONL and readInstinctsFromFile silently returns [] (all instincts lost).
|
|
109
|
+
atomicWriteFile(filePath, content);
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
/**
|