pi-crew 0.10.1 → 0.10.3
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 +347 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/executor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/dist/index.mjs +2555 -932
- package/package.json +2 -1
- package/schema.json +503 -94
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +6 -2
- package/skills/real-test-pi-crew/SKILL.md +278 -79
- package/src/config/config-merge.ts +11 -1
- package/src/config/config-validation.ts +47 -2
- package/src/config/config.ts +28 -6
- package/src/config/defaults.ts +43 -11
- package/src/config/env-vars.ts +27 -2
- package/src/config/role-tools.ts +4 -2
- package/src/config/types.ts +55 -1
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/run.ts +15 -1
- package/src/extension/registration/commands/shared.ts +8 -0
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +92 -11
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/team-tool.ts +58 -6
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/team-tool/doctor.ts +364 -7
- package/src/extension/team-tool/handle-settings.ts +19 -0
- package/src/extension/team-tool/inspect.ts +10 -2
- package/src/extension/team-tool/run-deadline.ts +20 -3
- package/src/extension/team-tool/run.ts +26 -2
- package/src/extension/team-tool/status.ts +7 -0
- package/src/extension/team-tool.ts +35 -2
- package/src/hooks/registry.ts +59 -56
- package/src/prompt/inbox-poll.ts +90 -0
- package/src/prompt/message-tool.ts +166 -0
- package/src/prompt/prompt-runtime.ts +201 -18
- package/src/prompt/surface-worker.ts +720 -0
- package/src/prompt/worker-events-channel.ts +49 -3
- package/src/runtime/async-runner.ts +29 -1
- package/src/runtime/background-runner.ts +13 -7
- package/src/runtime/broker/broker-issuer.ts +27 -2
- package/src/runtime/broker/crew-broker-tokens.ts +56 -4
- package/src/runtime/broker/crew-broker.ts +261 -41
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +23 -9
- package/src/runtime/child-pi/child-pi-streams.ts +30 -2
- package/src/runtime/child-pi/child-pi.ts +353 -5
- package/src/runtime/crew-agent-records.ts +13 -1
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/dispatch-batch.ts +12 -1
- package/src/runtime/event-log-tail-source.ts +374 -0
- package/src/runtime/finalize-run.ts +4 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +30 -3
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +5 -1
- package/src/runtime/live-session/live-agent-manager.ts +34 -1
- package/src/runtime/live-session/live-control-realtime.ts +10 -0
- package/src/runtime/live-session/live-session-runtime.ts +47 -27
- package/src/runtime/manifest-cache.ts +128 -17
- package/src/runtime/merge-gate.ts +25 -9
- package/src/runtime/model/model-fallback.ts +7 -3
- package/src/runtime/model/pi-args.ts +54 -65
- package/src/runtime/output/sidechain-output.ts +61 -6
- package/src/runtime/process/proc-stat.ts +46 -0
- package/src/runtime/process/zombie-scanner.ts +32 -19
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/crash-recovery.ts +16 -0
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/spawn-policy.ts +27 -41
- package/src/runtime/surface/degrade.ts +776 -0
- package/src/runtime/surface/herdr-provider.ts +546 -0
- package/src/runtime/surface/launch-script.ts +172 -0
- package/src/runtime/surface/resolve-surface.ts +274 -0
- package/src/runtime/surface/surface-provider.ts +129 -0
- package/src/runtime/surface/surface-spawn.ts +475 -0
- package/src/runtime/surface/tmux-provider.ts +400 -0
- package/src/runtime/task-runner/child-executor.ts +47 -0
- package/src/runtime/task-runner/post-execution.ts +57 -2
- package/src/runtime/task-runner/prompt-builder.ts +1 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +191 -56
- package/src/runtime/task-runner/state-helpers.ts +54 -30
- package/src/runtime/task-runner.ts +4 -2
- package/src/runtime/team-runner.ts +104 -2
- package/src/schema/config-schema.ts +25 -1
- package/src/state/atomic-write.ts +219 -40
- package/src/state/coordination/locks.ts +7 -5
- package/src/state/coordination/mailbox.ts +56 -10
- package/src/state/event-log/cursor.ts +413 -23
- package/src/state/event-log/event-log.ts +120 -113
- package/src/state/event-log/sequence-cache.ts +21 -3
- package/src/state/stores/plan-store.ts +1 -1
- package/src/state/stores/state-store.ts +171 -9
- package/src/state/types.ts +53 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/inline-panel/agent-pane.ts +378 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/render-diff.ts +16 -8
- package/src/ui/run-dashboard.ts +87 -42
- package/src/ui/run-event-bus.ts +10 -1
- package/src/ui/run-snapshot-cache.ts +83 -35
- package/src/ui/transcript-cache.ts +101 -13
- package/src/ui/transcript-viewer.ts +92 -24
- package/src/ui/widget/index.ts +203 -25
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +234 -37
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/visual.ts +43 -0
- package/src/workflows/discover-workflows.ts +1 -0
- package/src/workflows/workflow-config.ts +7 -0
- package/src/worktree/worktree-manager.ts +65 -4
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
|
@@ -10,6 +10,7 @@ import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../../utils/paths.t
|
|
|
10
10
|
import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
11
11
|
import { toPiSessionId } from "../../utils/session-utils.ts";
|
|
12
12
|
import type { WorkflowConfig } from "../../workflows/workflow-config.ts";
|
|
13
|
+
import type { WriteDurability } from "../atomic-write.ts";
|
|
13
14
|
import {
|
|
14
15
|
atomicWriteJson,
|
|
15
16
|
atomicWriteJsonAsync,
|
|
@@ -207,6 +208,26 @@ function resolveRunStateRoot(cwd: string, runId: string): string | undefined {
|
|
|
207
208
|
return scopedPath;
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
// PERF (2026-08-24): the artifacts containment verdict (existsSync + lstat +
|
|
212
|
+
// resolveRealContainedPath ≈ 10-25 syscalls) cannot change for a run dir that
|
|
213
|
+
// is not replaced — mirror the P1-12 runStateRootCache tradeoff: positive
|
|
214
|
+
// verdicts cached 10s, negatives never cached (a newly created artifacts dir
|
|
215
|
+
// must be found promptly). A stale positive is safe the same way P1-12 is:
|
|
216
|
+
// downstream manifest stat catches deleted runs, and any write through
|
|
217
|
+
// atomic-write re-runs isSymlinkSafeDirCached independently.
|
|
218
|
+
const artifactsVerdictCache = new Map<string, { expiresAt: number }>();
|
|
219
|
+
const ARTIFACTS_VERDICT_TTL_MS = 10_000;
|
|
220
|
+
const ARTIFACTS_VERDICT_CACHE_MAX = 256;
|
|
221
|
+
|
|
222
|
+
/** @internal — artifacts verdict cache introspection for unit tests. */
|
|
223
|
+
export function __test__artifactsVerdictCacheSize(): number {
|
|
224
|
+
return artifactsVerdictCache.size;
|
|
225
|
+
}
|
|
226
|
+
/** @internal */
|
|
227
|
+
export function __test__clearArtifactsVerdictCache(): void {
|
|
228
|
+
artifactsVerdictCache.clear();
|
|
229
|
+
}
|
|
230
|
+
|
|
210
231
|
function validateRunManifestPaths(cwd: string, runId: string, manifest: TeamRunManifest, stateRoot: string, tasksPath: string): boolean {
|
|
211
232
|
// Issue 2 fix: Reject manifests missing status field to prevent undefined
|
|
212
233
|
// behavior in callers like canTransitionRunStatus(manifest.status, newStatus).
|
|
@@ -221,6 +242,12 @@ function validateRunManifestPaths(cwd: string, runId: string, manifest: TeamRunM
|
|
|
221
242
|
const artifactsParent = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.artifactsSubdir);
|
|
222
243
|
const expectedArtifactsRoot = resolveContainedRelativePath(artifactsParent, runId, "runId");
|
|
223
244
|
if (manifest.artifactsRoot !== expectedArtifactsRoot) return false;
|
|
245
|
+
// PERF (2026-08-24): memoized verdict — see artifactsVerdictCache above.
|
|
246
|
+
// Hit only after the cheap manifest-identity checks above, so a tampered
|
|
247
|
+
// manifest (wrong paths/status) is still rejected without touching the memo.
|
|
248
|
+
const verdictKey = `${cwd}\0${runId}`;
|
|
249
|
+
const cachedVerdict = artifactsVerdictCache.get(verdictKey);
|
|
250
|
+
if (cachedVerdict && cachedVerdict.expiresAt > Date.now()) return true;
|
|
224
251
|
// Always validate artifactsRoot is not a symlink, even when manifest has
|
|
225
252
|
// no artifacts entries. A symlinked artifactsRoot pointing outside the
|
|
226
253
|
// artifacts parent is a security violation (could write to attacker-
|
|
@@ -237,8 +264,18 @@ function validateRunManifestPaths(cwd: string, runId: string, manifest: TeamRunM
|
|
|
237
264
|
} else if (manifest.artifacts && manifest.artifacts.length > 0) {
|
|
238
265
|
// Has artifacts entries but directory doesn't exist - benign state for
|
|
239
266
|
// runs still in progress.
|
|
267
|
+
if (artifactsVerdictCache.size >= ARTIFACTS_VERDICT_CACHE_MAX) {
|
|
268
|
+
const oldest = artifactsVerdictCache.keys().next().value;
|
|
269
|
+
if (oldest !== undefined) artifactsVerdictCache.delete(oldest);
|
|
270
|
+
}
|
|
271
|
+
artifactsVerdictCache.set(verdictKey, { expiresAt: Date.now() + ARTIFACTS_VERDICT_TTL_MS });
|
|
240
272
|
return true;
|
|
241
273
|
}
|
|
274
|
+
if (artifactsVerdictCache.size >= ARTIFACTS_VERDICT_CACHE_MAX) {
|
|
275
|
+
const oldest = artifactsVerdictCache.keys().next().value;
|
|
276
|
+
if (oldest !== undefined) artifactsVerdictCache.delete(oldest);
|
|
277
|
+
}
|
|
278
|
+
artifactsVerdictCache.set(verdictKey, { expiresAt: Date.now() + ARTIFACTS_VERDICT_TTL_MS });
|
|
242
279
|
return true;
|
|
243
280
|
}
|
|
244
281
|
|
|
@@ -260,7 +297,42 @@ export function createRunPaths(cwd: string, runId = createRunId()): RunPaths {
|
|
|
260
297
|
};
|
|
261
298
|
}
|
|
262
299
|
|
|
263
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Trailing phrases a step template leaves in front of `{goal}` — "Explore
|
|
302
|
+
* the codebase for the goal: {goal}" should name the task ("Explore the
|
|
303
|
+
* codebase"), not quote the whole run goal after every verb. The run goal is
|
|
304
|
+
* run-level context (statusline shows it once); a task list that repeats it
|
|
305
|
+
* per row reads like a roster of workers, not a list of work.
|
|
306
|
+
*/
|
|
307
|
+
const GOAL_TAILS = ["for the goal", "for goal", "the goal", "goal", "cho mục tiêu", "của mục tiêu", "mục tiêu", "for", "of", "cho", "của"];
|
|
308
|
+
|
|
309
|
+
function stripGoalTail(prefix: string): string {
|
|
310
|
+
let name = prefix.replace(/[\s:—-]+$/, "").trim();
|
|
311
|
+
let changed = true;
|
|
312
|
+
while (changed) {
|
|
313
|
+
changed = false;
|
|
314
|
+
const lowered = name.toLowerCase();
|
|
315
|
+
for (const tail of GOAL_TAILS) {
|
|
316
|
+
if (lowered.endsWith(tail) && lowered.length > tail.length) {
|
|
317
|
+
name = name
|
|
318
|
+
.slice(0, name.length - tail.length)
|
|
319
|
+
.replace(/[\s:—-]+$/, "")
|
|
320
|
+
.trim();
|
|
321
|
+
changed = true;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return name;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function createTasksFromWorkflow(
|
|
330
|
+
runId: string,
|
|
331
|
+
workflow: WorkflowConfig,
|
|
332
|
+
team: TeamConfig,
|
|
333
|
+
cwd: string,
|
|
334
|
+
goal?: string,
|
|
335
|
+
): TeamTaskState[] {
|
|
264
336
|
const stepToTaskId = new Map(workflow.steps.map((step, index) => [step.id, createTaskId(step.id, index)]));
|
|
265
337
|
return workflow.steps.map((step, index) => {
|
|
266
338
|
const role = team.roles.find((candidate) => candidate.name === step.role);
|
|
@@ -270,13 +342,48 @@ export function createTasksFromWorkflow(runId: string, workflow: WorkflowConfig,
|
|
|
270
342
|
.filter((candidate) => candidate.dependsOn?.includes(step.id))
|
|
271
343
|
.map((candidate) => stepToTaskId.get(candidate.id))
|
|
272
344
|
.filter((childId): childId is string => childId !== undefined);
|
|
345
|
+
// The plan's own words become the task's display identity: a heading
|
|
346
|
+
// from the step body if present, else its first content line; the
|
|
347
|
+
// remaining body is kept as `description` for detail surfaces (task
|
|
348
|
+
// list detail line, view). Falls back to the step id when the body
|
|
349
|
+
// carries no text of its own — `task` is optional on literal
|
|
350
|
+
// WorkflowStep objects.
|
|
351
|
+
//
|
|
352
|
+
// `{goal}` in the title line is the RUN's goal, context for every
|
|
353
|
+
// task, not this task's name. The template prefix before the
|
|
354
|
+
// placeholder names the task; a placeholder-only body ("{goal}",
|
|
355
|
+
// direct-run) adopts the goal itself as the title. `description`
|
|
356
|
+
// lines substitute the placeholder (the prompt path substitutes
|
|
357
|
+
// separately — unchanged).
|
|
358
|
+
const bodyLines =
|
|
359
|
+
typeof step.task === "string" && step.task.trim()
|
|
360
|
+
? step.task
|
|
361
|
+
.split("\n")
|
|
362
|
+
.map((line) => line.trim())
|
|
363
|
+
.filter(Boolean)
|
|
364
|
+
: [];
|
|
365
|
+
const heading = bodyLines.find((line) => line.startsWith("#"));
|
|
366
|
+
const titleLine = heading ?? bodyLines[0];
|
|
367
|
+
const substitute = (text: string): string => (goal ? text.replaceAll("{goal}", goal) : text);
|
|
368
|
+
let title: string;
|
|
369
|
+
let restLines: string[];
|
|
370
|
+
const placeholderAt = titleLine?.indexOf("{goal}") ?? -1;
|
|
371
|
+
if (titleLine && placeholderAt >= 0) {
|
|
372
|
+
title = stripGoalTail(titleLine.slice(0, placeholderAt)) || goal || step.id;
|
|
373
|
+
restLines = bodyLines.filter((line) => line !== titleLine);
|
|
374
|
+
} else {
|
|
375
|
+
title = (titleLine ?? step.id).replace(/^#+\s*/, "");
|
|
376
|
+
restLines = heading ? bodyLines.filter((line) => line !== titleLine) : bodyLines.slice(1);
|
|
377
|
+
}
|
|
378
|
+
const description = restLines.map(substitute).join("\n");
|
|
273
379
|
return {
|
|
274
380
|
id,
|
|
275
381
|
runId,
|
|
276
382
|
stepId: step.id,
|
|
277
383
|
role: step.role,
|
|
278
384
|
agent: role?.agent ?? step.role,
|
|
279
|
-
title:
|
|
385
|
+
title: title.slice(0, 120),
|
|
386
|
+
description: description && description !== title ? description : undefined,
|
|
280
387
|
status: "queued",
|
|
281
388
|
dependsOn: dependencies,
|
|
282
389
|
cwd,
|
|
@@ -305,7 +412,7 @@ export function createRunManifest(params: {
|
|
|
305
412
|
}): { manifest: TeamRunManifest; tasks: TeamTaskState[]; paths: RunPaths } {
|
|
306
413
|
const paths = createRunPaths(params.cwd);
|
|
307
414
|
const now = new Date().toISOString();
|
|
308
|
-
const tasks = params.workflow ? createTasksFromWorkflow(paths.runId, params.workflow, params.team, params.cwd) : [];
|
|
415
|
+
const tasks = params.workflow ? createTasksFromWorkflow(paths.runId, params.workflow, params.team, params.cwd, params.goal) : [];
|
|
309
416
|
const manifest: TeamRunManifest = {
|
|
310
417
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
311
418
|
runId: paths.runId,
|
|
@@ -554,19 +661,52 @@ export function saveRunTasks(manifest: TeamRunManifest, tasks: TeamTaskState[]):
|
|
|
554
661
|
* tasks.json stale (showing "running") while events.jsonl already shows
|
|
555
662
|
* the terminal event, causing false zombie detection / double-execution
|
|
556
663
|
* on crash recovery.
|
|
664
|
+
*
|
|
665
|
+
* PERF round 2, Task 3: optional `durability` (default "full") overrides the
|
|
666
|
+
* coalesced entry's durability. When the caller wants a non-terminal checkpoint
|
|
667
|
+
* written without fsync (persistence.skipTasksFsync opt-in), pass
|
|
668
|
+
* `durability: "best-effort"` — the coalesced entry stores it and the flush
|
|
669
|
+
* forwards it to atomicWriteFile (see atomic-write.ts:997). Terminal
|
|
670
|
+
* transitions (skipCoalesce=true) always fall to atomicWriteJson with full
|
|
671
|
+
* durability regardless of this param.
|
|
557
672
|
*/
|
|
558
673
|
/** @internal */
|
|
559
|
-
export function saveRunTasksCoalesced(
|
|
674
|
+
export function saveRunTasksCoalesced(
|
|
675
|
+
manifest: TeamRunManifest,
|
|
676
|
+
tasks: TeamTaskState[],
|
|
677
|
+
skipCoalesce: boolean = false,
|
|
678
|
+
durability: WriteDurability = "full",
|
|
679
|
+
): void {
|
|
560
680
|
// ST-4: refuse to persist [] over a previously-non-empty tasks file.
|
|
561
681
|
if (!shouldPersistTasks(manifest, tasks)) return;
|
|
562
|
-
//
|
|
563
|
-
|
|
682
|
+
// PERF (2026-08-24, Task 12): invalidating the WHOLE entry made every
|
|
683
|
+
// loadRunManifestById after a coalesced save re-read + re-parse
|
|
684
|
+
// manifest.json (24KB+) even though the manifest file did not change —
|
|
685
|
+
// persistSingleTaskUpdate's next call (~500ms later) always paid it. Keep
|
|
686
|
+
// the manifest half of the entry and zero only the tasks stamps, which is
|
|
687
|
+
// the exact pre-existing signal for "tasks on disk may be stale" (coalesced
|
|
688
|
+
// write not landed yet). The load path cooperates: the zeroed tasks stamps
|
|
689
|
+
// force the slow path (tasks are always re-read from disk), but when the
|
|
690
|
+
// retained manifest stamps still match a fresh stat of manifest.json — the
|
|
691
|
+
// same mtime/size verification the fast path performs — the retry loop
|
|
692
|
+
// reuses the cached manifest object and skips the re-read + re-parse.
|
|
693
|
+
// Crash safety is unchanged: a zeroed tasks stamp can only cause a miss,
|
|
694
|
+
// never a stale hit, and a concurrent manifest rewrite changes mtime/size
|
|
695
|
+
// so the manifest reuse never serves stale content. Generation semantics
|
|
696
|
+
// unchanged — setManifestCache stamps the CURRENT generation, so a
|
|
697
|
+
// concurrent writer's bump still invalidates us.
|
|
698
|
+
const cached = manifestCache.get(manifest.stateRoot);
|
|
699
|
+
if (cached) {
|
|
700
|
+
setManifestCache(manifest.stateRoot, { ...cached, tasks, tasksMtimeMs: 0, tasksSize: 0 });
|
|
701
|
+
} else {
|
|
702
|
+
invalidateRunCache(manifest.stateRoot);
|
|
703
|
+
}
|
|
564
704
|
try {
|
|
565
705
|
fs.statSync(manifest.stateRoot);
|
|
566
706
|
} catch {
|
|
567
707
|
return;
|
|
568
708
|
}
|
|
569
|
-
atomicWriteJsonCoalesced(manifest.tasksPath, tasks, undefined, { compact: true }, skipCoalesce);
|
|
709
|
+
atomicWriteJsonCoalesced(manifest.tasksPath, tasks, undefined, { compact: true, durability }, skipCoalesce);
|
|
570
710
|
}
|
|
571
711
|
|
|
572
712
|
export async function saveRunTasksAsync(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<void> {
|
|
@@ -873,7 +1013,19 @@ export function loadRunManifestById(cwd: string, runId: string): { manifest: Tea
|
|
|
873
1013
|
let tasks: TeamTaskState[] | undefined;
|
|
874
1014
|
while (attempts < LOAD_MANIFEST_RETRY_LIMIT) {
|
|
875
1015
|
const freshStat = fs.statSync(manifestPath);
|
|
876
|
-
|
|
1016
|
+
// PERF (2026-08-24, Task 12 realized): after saveRunTasksCoalesced the
|
|
1017
|
+
// cache keeps the manifest half of the entry while zeroing only the
|
|
1018
|
+
// tasks stamps, so this slow path runs solely to refresh tasks. When the
|
|
1019
|
+
// retained manifest stamps still match the fresh stat — the exact
|
|
1020
|
+
// mtime/size verification the fast path performs — reuse the cached
|
|
1021
|
+
// manifest object instead of re-reading + re-parsing manifest.json.
|
|
1022
|
+
// Stamp verification is NOT bypassed: any manifest.json rewrite changes
|
|
1023
|
+
// mtime/size and falls back to the disk read. The tasks re-read below
|
|
1024
|
+
// still always happens — zeroed tasks stamps must force it.
|
|
1025
|
+
manifest =
|
|
1026
|
+
cached && cached.manifestMtimeMs === freshStat.mtimeMs && cached.manifestSize === freshStat.size
|
|
1027
|
+
? cached.manifest
|
|
1028
|
+
: readJsonFile<TeamRunManifest>(manifestPath);
|
|
877
1029
|
const freshTasksStat = fs.existsSync(tasksPath) ? fs.statSync(tasksPath) : undefined;
|
|
878
1030
|
tasks = loadTasksWithRecovery(tasksPath, manifest?.eventsPath ?? path.join(stateRoot, "events.jsonl"), manifest?.runId ?? runId);
|
|
879
1031
|
// If size/mtime didn't change between stat and read, we're consistent.
|
|
@@ -1015,7 +1167,17 @@ export async function loadRunManifestByIdAsync(
|
|
|
1015
1167
|
let attempts = 0;
|
|
1016
1168
|
while (attempts < LOAD_MANIFEST_RETRY_LIMIT) {
|
|
1017
1169
|
const freshStat = await fs.promises.stat(manifestPath);
|
|
1018
|
-
|
|
1170
|
+
// PERF (2026-08-24, Task 12 realized): async twin of the sync reuse —
|
|
1171
|
+
// after saveRunTasksCoalesced only the tasks stamps are zeroed, so when
|
|
1172
|
+
// the retained manifest stamps match the fresh stat (same verification
|
|
1173
|
+
// the fast path performs), reuse the cached manifest object instead of
|
|
1174
|
+
// re-reading + re-parsing manifest.json. A manifest.json rewrite changes
|
|
1175
|
+
// mtime/size and falls back to the disk read; the tasks re-read below
|
|
1176
|
+
// always happens.
|
|
1177
|
+
manifest =
|
|
1178
|
+
cached && cached.manifestMtimeMs === freshStat.mtimeMs && cached.manifestSize === freshStat.size
|
|
1179
|
+
? cached.manifest
|
|
1180
|
+
: await readJsonFileAsync<TeamRunManifest>(manifestPath);
|
|
1019
1181
|
const freshTasksStat = await fs.promises.stat(tasksPath).catch(() => undefined);
|
|
1020
1182
|
tasks = await loadTasksWithRecoveryAsync(
|
|
1021
1183
|
tasksPath,
|
package/src/state/types.ts
CHANGED
|
@@ -408,6 +408,9 @@ export interface TeamRunManifest {
|
|
|
408
408
|
* models.json happens to list first.
|
|
409
409
|
*/
|
|
410
410
|
modelContext?: RunModelContext;
|
|
411
|
+
/** MuxSurface A1 (spec §8.3): pane/pid/lockout state of this run. Absent on
|
|
412
|
+
* runs that never booted a surface worker and on older manifests. */
|
|
413
|
+
surface?: ManifestSurfaceState;
|
|
411
414
|
}
|
|
412
415
|
|
|
413
416
|
export interface RunModelContext {
|
|
@@ -421,6 +424,54 @@ export interface RunModelContext {
|
|
|
421
424
|
availableModels?: string[];
|
|
422
425
|
}
|
|
423
426
|
|
|
427
|
+
/**
|
|
428
|
+
* MuxSurface A1 run-scope surface state (spec §8.3 / §12.3). Written by the
|
|
429
|
+
* team-runner's surface-degrade controller (runtime/surface/degrade.ts) and
|
|
430
|
+
* read by doctor (T12). Plain JSON — manifests parse without a schema, so an
|
|
431
|
+
* absent field means "no surface worker ever booted" and older writers simply
|
|
432
|
+
* don't emit it (backward-compatible in both directions).
|
|
433
|
+
*
|
|
434
|
+
* Per-run scope: every new run starts without `surface`, which IS the
|
|
435
|
+
* "reset ở run sau" of the anti-flap policy (spec §7 step 3).
|
|
436
|
+
*/
|
|
437
|
+
export interface ManifestSurfaceState {
|
|
438
|
+
provider: "tmux" | "herdr" | null;
|
|
439
|
+
/** taskId → pane id of the LIVE pane (removed when the pane is released). */
|
|
440
|
+
panes: Record<string, string>;
|
|
441
|
+
/** taskId → pid, captured from the worker's own `worker.started` event. */
|
|
442
|
+
workerPids: Record<string, number>;
|
|
443
|
+
/**
|
|
444
|
+
* taskId → session file path when the worker self-reports one (optional
|
|
445
|
+
* field of §12.2 — never populated in A1 because pi does not expose its
|
|
446
|
+
* session path to extensions; kept for the degrade-resume seam).
|
|
447
|
+
*/
|
|
448
|
+
sessionPaths: Record<string, string>;
|
|
449
|
+
/**
|
|
450
|
+
* Tab-layout (spec 2026-08-27-surface-tab-layout §5): tabKey (runId) →
|
|
451
|
+
* tab/window ids của run — ghi khi worker spawn trong tab-flow (handle.tabId
|
|
452
|
+
* qua outcome), KHÔNG gỡ khi từng worker xong (tab sống tới run end). Entry
|
|
453
|
+
* được clear (giữ key rỗng) khi run end đã đóng tab qua closeTabForRun.
|
|
454
|
+
* Run dài vượt MAX_PANES_PER_TAB tích lũy nhiều id cùng key.
|
|
455
|
+
*
|
|
456
|
+
* Manifest TRÊN ĐĨA giữ nguyên tabIds sau run end (evidence cho doctor) —
|
|
457
|
+
* tabs non-empty KHÔNG đồng nghĩa orphan; doctor phải liveness-check từng
|
|
458
|
+
* tabId qua mux rồi close-by-ID idempotent (cleanupOrphanSurfacePanes,
|
|
459
|
+
* provider.closeTabById), không đóng mù theo số entry.
|
|
460
|
+
*/
|
|
461
|
+
tabs?: Record<string, string[]>;
|
|
462
|
+
/**
|
|
463
|
+
* Surface is OFF for the rest of this run since `since`:
|
|
464
|
+
* - cause "degrade": ≥1 degrade entry after the classify timeout
|
|
465
|
+
* (anti-flap; counts keep per-cause evidence for doctor).
|
|
466
|
+
* - cause "spawn-fail": 3 consecutive spawn failures.
|
|
467
|
+
*/
|
|
468
|
+
lockout?: {
|
|
469
|
+
since: string;
|
|
470
|
+
counts: { pane: number; mux: number };
|
|
471
|
+
cause: "degrade" | "spawn-fail";
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
424
475
|
export interface UsageState {
|
|
425
476
|
input?: number;
|
|
426
477
|
output?: number;
|
|
@@ -554,6 +605,8 @@ export interface TeamTaskState {
|
|
|
554
605
|
role: string;
|
|
555
606
|
agent: string;
|
|
556
607
|
title: string;
|
|
608
|
+
/** Full step body (the detailed plan text behind `title`). */
|
|
609
|
+
description?: string;
|
|
557
610
|
displayName?: string;
|
|
558
611
|
status: TeamTaskStatus;
|
|
559
612
|
dependsOn: string[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dock-footer.ts — dependency-free bridge between the crew dock and the
|
|
3
|
+
* crew-vibes footer.
|
|
4
|
+
*
|
|
5
|
+
* When `ui.widgetPlacement` is `"bottom"`, the dock's rendered lines are
|
|
6
|
+
* produced by the crew widget but PAINTED by the crew-vibes footer, below the
|
|
7
|
+
* quota/meter lines — the very bottom of the terminal. The two halves are
|
|
8
|
+
* mounted by different modules (widget/index.ts owns the widget lifecycle,
|
|
9
|
+
* crew-vibes/footer.ts owns the footer), so the connection lives here as a
|
|
10
|
+
* tiny observable registry with no imports:
|
|
11
|
+
* - the widget registers a per-render line provider;
|
|
12
|
+
* - crew-vibes flips the "sink" flag when its footer is (un)installed;
|
|
13
|
+
* - the footer pulls the provider's lines on every render.
|
|
14
|
+
*
|
|
15
|
+
* Safety: the widget falls back to pi's `belowEditor` widget slot whenever no
|
|
16
|
+
* footer sink is active (crew-vibes disabled), so `"bottom"` never silently
|
|
17
|
+
* hides the dock.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type FooterDockLinesProvider = (width: number) => string[];
|
|
21
|
+
|
|
22
|
+
let dockProvider: FooterDockLinesProvider | undefined;
|
|
23
|
+
let sinkActive = false;
|
|
24
|
+
|
|
25
|
+
/** Register the dock line provider (widget side). Pass `undefined` to detach. */
|
|
26
|
+
export function setFooterDockProvider(provider: FooterDockLinesProvider | undefined): void {
|
|
27
|
+
dockProvider = provider;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Current dock line provider, or undefined when the dock is not in the footer. */
|
|
31
|
+
export function getFooterDockProvider(): FooterDockLinesProvider | undefined {
|
|
32
|
+
return dockProvider;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Mark whether a footer sink (crew-vibes) is currently installed. */
|
|
36
|
+
export function setFooterDockSinkActive(active: boolean): void {
|
|
37
|
+
sinkActive = active;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True while crew-vibes' custom footer is installed and can host the dock. */
|
|
41
|
+
export function isFooterDockSinkActive(): boolean {
|
|
42
|
+
return sinkActive;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Test isolation. */
|
|
46
|
+
export function resetFooterDockRegistry(): void {
|
|
47
|
+
dockProvider = undefined;
|
|
48
|
+
sinkActive = false;
|
|
49
|
+
}
|