pi-crew 0.11.4 → 0.11.6
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 +109 -0
- package/README.md +2 -2
- package/dist/index.mjs +498 -492
- package/docs/actions-reference.md +22 -0
- package/docs/dynamic-workflows.md +37 -0
- package/index.ts +9 -5
- package/package.json +18 -14
- package/scripts/analyze-run.mjs +101 -14
- package/skills/model-routing-context/SKILL.md +4 -3
- package/skills/real-test-pi-crew/SKILL.md +10 -7
- package/src/config/env-vars.ts +6 -0
- package/src/extension/crew-vibes/index.ts +29 -10
- package/src/extension/pi-api.ts +1 -0
- package/src/extension/registration/lifecycle-handlers.ts +65 -3
- package/src/extension/team-tool/dispatch/index.ts +4 -3
- package/src/extension/team-tool/dispatch/status.ts +7 -3
- package/src/extension/team-tool/inspect.ts +90 -0
- package/src/extension/team-tool/respond.ts +54 -0
- package/src/extension/team-tool/status.ts +10 -0
- package/src/extension/team-tool.ts +393 -3
- package/src/extension/webhook-notify.ts +23 -1
- package/src/observability/exporters/otlp-exporter.ts +93 -11
- package/src/prompt/prompt-runtime.ts +13 -4
- package/src/runtime/agent-control.ts +1 -15
- package/src/runtime/background-runner.ts +3 -0
- package/src/runtime/broker/crew-broker.ts +12 -103
- package/src/runtime/broker/protocol/request-parsers.ts +9 -1
- package/src/runtime/broker/protocol/wait-resolve.ts +168 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +5 -4
- package/src/runtime/dispatch-batch.ts +3 -1
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +111 -12
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +19 -0
- package/src/runtime/live-session/live-agent-manager.ts +35 -0
- package/src/runtime/live-session/live-session-runtime.ts +34 -5
- package/src/runtime/mcp-proxy.ts +110 -7
- package/src/runtime/model/pi-args.ts +14 -11
- package/src/runtime/model/provider-extensions.ts +67 -1
- package/src/runtime/recovery/crash-recovery.ts +6 -1
- package/src/runtime/scheduling/run-coalesced-task-group.ts +89 -6
- package/src/runtime/stale-reconciler.ts +55 -0
- package/src/runtime/task-output-context.ts +11 -1
- package/src/runtime/task-runner/child-executor.ts +130 -4
- package/src/runtime/task-runner/output-splitter.ts +48 -5
- package/src/runtime/task-runner/post-execution.ts +2 -2
- package/src/runtime/task-runner/pre-execution.ts +37 -24
- package/src/runtime/task-runner/retrieval-orchestrator.ts +28 -2
- package/src/schema/team-tool-schema.ts +27 -1
- package/src/state/contracts.ts +16 -0
- package/src/state/crew-init.ts +114 -0
- package/src/state/stores/active-run-registry.ts +13 -6
- package/src/state/types.ts +0 -8
- package/src/ui/goal-flag.ts +44 -0
- package/src/ui/live-run-sidebar.ts +5 -1
- package/src/ui/pi-ui-compat.ts +20 -3
- package/src/ui/run-dashboard.ts +2 -1
- package/src/ui/widget/widget-model.ts +6 -2
- package/src/utils/ip-literal.ts +145 -0
- package/src/workflows/workflow-config.ts +6 -0
|
@@ -131,6 +131,7 @@ export async function finalizeTaskResult(ctx: TaskExecutionContext, execResult:
|
|
|
131
131
|
const skillNames = ctx.skillNames;
|
|
132
132
|
const skillPaths = ctx.skillPaths;
|
|
133
133
|
const promptArtifact = ctx.promptArtifact;
|
|
134
|
+
const breakdownArtifact = ctx.breakdownArtifact;
|
|
134
135
|
const inputsArtifact = ctx.inputsArtifact;
|
|
135
136
|
const skillArtifact = ctx.skillArtifact;
|
|
136
137
|
const coordinationArtifact = ctx.coordinationArtifact;
|
|
@@ -166,7 +167,6 @@ export async function finalizeTaskResult(ctx: TaskExecutionContext, execResult:
|
|
|
166
167
|
error: undefined,
|
|
167
168
|
claim: undefined,
|
|
168
169
|
heartbeat: touchWorkerHeartbeat(task.heartbeat ?? createWorkerHeartbeat(task.id), { alive: false }),
|
|
169
|
-
workerExitStatus: terminalEvidence.at(-1)?.exitStatus,
|
|
170
170
|
terminalEvidence: terminalEvidence.length ? [...(task.terminalEvidence ?? []), ...terminalEvidence] : task.terminalEvidence,
|
|
171
171
|
diagnostics: {
|
|
172
172
|
...(task.diagnostics ?? {}),
|
|
@@ -544,7 +544,6 @@ export async function finalizeTaskResult(ctx: TaskExecutionContext, execResult:
|
|
|
544
544
|
resultArtifact,
|
|
545
545
|
claim: undefined,
|
|
546
546
|
heartbeat: touchWorkerHeartbeat(task.heartbeat ?? createWorkerHeartbeat(task.id), { alive: false }),
|
|
547
|
-
workerExitStatus: terminalEvidence.at(-1)?.exitStatus,
|
|
548
547
|
terminalEvidence: terminalEvidence.length ? [...(task.terminalEvidence ?? []), ...terminalEvidence] : task.terminalEvidence,
|
|
549
548
|
...(logArtifact ? { logArtifact } : {}),
|
|
550
549
|
...(transcriptArtifact ? { transcriptArtifact } : {}),
|
|
@@ -623,6 +622,7 @@ export async function finalizeTaskResult(ctx: TaskExecutionContext, execResult:
|
|
|
623
622
|
artifacts: [
|
|
624
623
|
...manifest.artifacts,
|
|
625
624
|
promptArtifact,
|
|
625
|
+
...(breakdownArtifact ? [breakdownArtifact] : []),
|
|
626
626
|
...(resultArtifact ? [resultArtifact] : []),
|
|
627
627
|
inputsArtifact,
|
|
628
628
|
coordinationArtifact,
|
|
@@ -23,7 +23,6 @@ import { logInternalError } from "../../utils/internal-error.ts";
|
|
|
23
23
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
24
24
|
import type { PreparedTaskWorkspace } from "../../worktree/worktree-manager.ts";
|
|
25
25
|
import { prepareTaskWorkspaceAsync } from "../../worktree/worktree-manager.ts";
|
|
26
|
-
import { reserveControlChannel } from "../agent-control.ts";
|
|
27
26
|
import { emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "../crew-agent-records.ts";
|
|
28
27
|
import type { CrewRuntimeKind } from "../crew-agent-runtime.ts";
|
|
29
28
|
import type { registerStreamBridge } from "../event-stream-bridge.ts";
|
|
@@ -71,6 +70,8 @@ export interface TaskExecutionContext {
|
|
|
71
70
|
skillPaths: string[] | undefined;
|
|
72
71
|
prompt: string;
|
|
73
72
|
promptArtifact: ArtifactDescriptor;
|
|
73
|
+
/** G18: prompt-breakdown artifact descriptor (undefined when PI_CREW_PROMPT_BREAKDOWN off). */
|
|
74
|
+
breakdownArtifact?: ArtifactDescriptor;
|
|
74
75
|
inputsArtifact: ArtifactDescriptor;
|
|
75
76
|
skillArtifact: ArtifactDescriptor | undefined;
|
|
76
77
|
coordinationArtifact: ArtifactDescriptor;
|
|
@@ -150,12 +151,18 @@ export async function prepareTaskExecutionContext(
|
|
|
150
151
|
startedAt: new Date().toISOString(),
|
|
151
152
|
claim: createTaskClaim(`task-runner:${input.task.id}`),
|
|
152
153
|
heartbeat: createWorkerHeartbeat(input.task.id),
|
|
154
|
+
// NEW-3 (SDD-4 W-D) note: deliberately NO pid here — at prepare time the
|
|
155
|
+
// worker does not exist yet (spawn happens later in child-executor.ts).
|
|
156
|
+
// The singleton path's real pid arrives via checkpoint.childPid
|
|
157
|
+
// (child-executor "child-spawned" checkpoint), which the
|
|
158
|
+
// `heartbeat?.pid ?? checkpoint?.childPid` consumers already read as a
|
|
159
|
+
// fallback. Do NOT stamp process.pid here — that is the orchestrator, and
|
|
160
|
+
// it outlives the worker, which would blind the stale-reconciler's
|
|
161
|
+
// PID-liveness gate forever.
|
|
153
162
|
agentProgress: input.task.agentProgress ?? emptyCrewAgentProgress(),
|
|
154
163
|
// Lifetime usage accumulator — survives compaction unlike session.stats
|
|
155
164
|
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
156
165
|
...(dependencyContextText ? { dependencyContextText } : {}),
|
|
157
|
-
// Reserve control channel before spawn so cancel/steer can target this task immediately
|
|
158
|
-
controlReservation: reserveControlChannel(input.task.id, manifest.runId),
|
|
159
166
|
} as TeamTaskState;
|
|
160
167
|
let tasks = updateTask(input.tasks, task);
|
|
161
168
|
// T2/R4 (ADR-4 §3 single-writer linkage): record on the CURRENT plan
|
|
@@ -343,27 +350,32 @@ export async function prepareTaskExecutionContext(
|
|
|
343
350
|
// SR-02 phase 1: per-section breakdown artifact (opt-in via
|
|
344
351
|
// PI_CREW_PROMPT_BREAKDOWN=1). Adds the SYSTEM-side pieces the prompt
|
|
345
352
|
// builder cannot see (agent definition, skills, pre-step output).
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
353
|
+
// G18 (SDD Buổi-1 WI-3 follow-up): capture the descriptor so it flows
|
|
354
|
+
// through ctx into the manifest artifacts index — otherwise the
|
|
355
|
+
// `breakdown` team-tool action cannot find it on real runs.
|
|
356
|
+
const breakdownArtifact: ArtifactDescriptor | undefined = promptBreakdownEnabled()
|
|
357
|
+
? (() => {
|
|
358
|
+
const sections: Record<string, number> = {
|
|
359
|
+
"system.agentDefinition": input.agent?.systemPrompt?.length ?? 0,
|
|
360
|
+
...(promptResult.sections ?? {}),
|
|
361
|
+
"dynamic.preStepOutput": preStepOutput?.length ?? 0,
|
|
362
|
+
};
|
|
363
|
+
return writeArtifact(manifest.artifactsRoot, {
|
|
364
|
+
kind: "metadata",
|
|
365
|
+
relativePath: `metadata/${task.id}.prompt-breakdown.json`,
|
|
366
|
+
content: `${JSON.stringify(
|
|
367
|
+
Object.fromEntries(
|
|
368
|
+
Object.entries(sections)
|
|
369
|
+
.filter(([, chars]) => chars > 0)
|
|
370
|
+
.map(([name, chars]) => [name, { chars, estTokens: estimateTokens(chars) }]),
|
|
371
|
+
),
|
|
372
|
+
null,
|
|
373
|
+
2,
|
|
374
|
+
)}\n`,
|
|
375
|
+
producer: "prompt-breakdown",
|
|
376
|
+
});
|
|
377
|
+
})()
|
|
378
|
+
: undefined;
|
|
367
379
|
|
|
368
380
|
const collectedJsonEvents: Record<string, unknown>[] | undefined = collectYieldEvents ? [] : undefined;
|
|
369
381
|
|
|
@@ -415,6 +427,7 @@ export async function prepareTaskExecutionContext(
|
|
|
415
427
|
skillPaths,
|
|
416
428
|
prompt,
|
|
417
429
|
promptArtifact,
|
|
430
|
+
breakdownArtifact,
|
|
418
431
|
inputsArtifact,
|
|
419
432
|
skillArtifact,
|
|
420
433
|
coordinationArtifact,
|
|
@@ -31,6 +31,27 @@ export const MAX_SUGGESTED_FILES = 10;
|
|
|
31
31
|
/** Minimum files suggested when retrieval returns anything. */
|
|
32
32
|
export const MIN_SUGGESTED_FILES = 5;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* G23b (2026-10-02): minimum retrieval score for a file to be suggested.
|
|
36
|
+
*
|
|
37
|
+
* Rationale — the legacy `score > 0` floor in the byPath pass only keeps
|
|
38
|
+
* out zero-score files, so generic path-keyword grazers sail through and
|
|
39
|
+
* fill the top-10. Live evidence (run team_20261002065548, the small-wins
|
|
40
|
+
* batch this fix ships in): 10 junk suggestions scoring 0.45–0.46 (paths
|
|
41
|
+
* grazing generic tokens like "template", "security", "skill", "md",
|
|
42
|
+
* "ts", "source") were injected into worker context ahead of any useful
|
|
43
|
+
* file. 0.5 sits above that observed junk band while still admitting
|
|
44
|
+
* genuinely relevant path-only matches (useful path-only scores reach
|
|
45
|
+
* ~0.64; HIGH_RELEVANCE_THRESHOLD=0.7 stays convergence-only). Applied
|
|
46
|
+
* BEFORE the top-N cap/slice so junk can never occupy a slot even when
|
|
47
|
+
* the qualified set is small.
|
|
48
|
+
*
|
|
49
|
+
* Tuning: recalibrate against real-run evidence if the junk band shifts
|
|
50
|
+
* above 0.5 or gold files start scoring below — and update the G23b
|
|
51
|
+
* pinning tests in the same change (they import this constant).
|
|
52
|
+
*/
|
|
53
|
+
export const MIN_SUGGEST_SCORE = 0.5;
|
|
54
|
+
|
|
34
55
|
/** Stopwords dropped during keyword tokenization (lowercase comparison). */
|
|
35
56
|
// PERF round 3: expanded from 14 function words to the common verb/pronoun/
|
|
36
57
|
// filler set. These multiply the scoring cost (keywords × files × passes)
|
|
@@ -492,8 +513,13 @@ export async function runRetrievalCycle(task: string, goal: string, cwd: string)
|
|
|
492
513
|
const converged = hasConverged(evaluations);
|
|
493
514
|
// Sort by score desc, take top N (5..10).
|
|
494
515
|
evaluations.sort((a, b) => b.relevance - a.relevance);
|
|
495
|
-
|
|
496
|
-
|
|
516
|
+
// G23b: drop sub-floor keyword-grazers BEFORE the cap/slice so junk can
|
|
517
|
+
// never occupy a top-N slot. Sub-floor evaluations stay in `evaluations`
|
|
518
|
+
// above — convergence only counts ≥0.7 hits over always-empty
|
|
519
|
+
// missingContext, so filtering here cannot change `converged`.
|
|
520
|
+
const qualified = evaluations.filter((e) => e.relevance >= MIN_SUGGEST_SCORE);
|
|
521
|
+
const cap = Math.min(MAX_SUGGESTED_FILES, Math.max(MIN_SUGGESTED_FILES, qualified.length));
|
|
522
|
+
const top = qualified.slice(0, cap).map((e) => ({
|
|
497
523
|
path: path.isAbsolute(e.path) ? path.relative(cwd, e.path) : e.path,
|
|
498
524
|
score: e.relevance,
|
|
499
525
|
reason: e.reason,
|
|
@@ -376,6 +376,27 @@ const sharedFields = {
|
|
|
376
376
|
},
|
|
377
377
|
),
|
|
378
378
|
),
|
|
379
|
+
// SDD-3 W-C G13: agent-call cap for dynamic-workflow runs. A run is always
|
|
380
|
+
// bounded — unset falls back to DEFAULT_MAX_AGENT_CALLS (200) in the ctx;
|
|
381
|
+
// tripping the cap terminates the run with a structured reason instead of
|
|
382
|
+
// the blind 30-min script timeout. Same loose-numeric shape as tokenBudget.
|
|
383
|
+
maxAgentCalls: Type.Optional(
|
|
384
|
+
Type.Union(
|
|
385
|
+
[
|
|
386
|
+
Type.Literal(""),
|
|
387
|
+
Type.Number({
|
|
388
|
+
description:
|
|
389
|
+
"Cap on ctx.agent() invocations per dynamic-workflow run (steps ≡ agent calls). Default 200 when unset. Tripping the cap terminates the run with a structured agent-call-cap reason. Overrides workflow.maxAgentCalls.",
|
|
390
|
+
minimum: 1,
|
|
391
|
+
}),
|
|
392
|
+
Type.String({ pattern: NUMERIC_STRING_RE }),
|
|
393
|
+
],
|
|
394
|
+
{
|
|
395
|
+
description:
|
|
396
|
+
"Cap on ctx.agent() invocations per dynamic-workflow run (steps ≡ agent calls). Default 200 when unset. Tripping the cap terminates the run with a structured agent-call-cap reason. Overrides workflow.maxAgentCalls.",
|
|
397
|
+
},
|
|
398
|
+
),
|
|
399
|
+
),
|
|
379
400
|
args: Type.Optional(Type.Any()),
|
|
380
401
|
analysis: Type.Optional(
|
|
381
402
|
Type.String({
|
|
@@ -398,7 +419,7 @@ const sharedFields = {
|
|
|
398
419
|
),
|
|
399
420
|
};
|
|
400
421
|
|
|
401
|
-
// ─── Domain action unions (10+
|
|
422
|
+
// ─── Domain action unions (10+17+7+17+6 = 57 actions) ──────────────────────
|
|
402
423
|
|
|
403
424
|
const ACTION_DESCRIPTION = "Team action. Defaults to 'list' when omitted.";
|
|
404
425
|
|
|
@@ -411,6 +432,7 @@ const STATUS_ACTIONS = [
|
|
|
411
432
|
"get",
|
|
412
433
|
"events",
|
|
413
434
|
"artifacts",
|
|
435
|
+
"breakdown",
|
|
414
436
|
"summary",
|
|
415
437
|
"graph",
|
|
416
438
|
"search",
|
|
@@ -586,6 +608,10 @@ export interface TeamToolParamsValue {
|
|
|
586
608
|
runKind?: "team-run" | "goal-loop" | "dynamic-workflow";
|
|
587
609
|
/** Per-workflow token budget for dynamic-workflow runs (round-14 P1-2). */
|
|
588
610
|
tokenBudget?: number;
|
|
611
|
+
/** Cap on ctx.agent() invocations per dynamic-workflow run (SDD-3 W-C G13).
|
|
612
|
+
* Unset → DEFAULT_MAX_AGENT_CALLS (200); tripping the cap terminates the
|
|
613
|
+
* run with a structured agent-call-cap reason. */
|
|
614
|
+
maxAgentCalls?: number;
|
|
589
615
|
/** Typed workflow arguments for .dwf.ts scripts, accessible via ctx.args<T>() (round-14 P1-5). */
|
|
590
616
|
args?: unknown;
|
|
591
617
|
/** Inline analysis/context notes from the calling session. Persisted to
|
package/src/state/contracts.ts
CHANGED
|
@@ -94,6 +94,17 @@ export const TEAM_EVENT_TYPES = [
|
|
|
94
94
|
"async.stale",
|
|
95
95
|
"task.waiting",
|
|
96
96
|
"task.resumed",
|
|
97
|
+
// G5 (deep-review 2026-10-01): respond-delivery observability. The leader
|
|
98
|
+
// previously had NO confirmation that a mailbox respond reached the worker:
|
|
99
|
+
// `respond_delivered` fires at the broker's wait.resolve ack when the parked
|
|
100
|
+
// worker reports it PICKED UP the questionId-tagged response (terminal
|
|
101
|
+
// outcome "answered" — timed-out/aborted resolves are NOT deliveries);
|
|
102
|
+
// `respond_missed` fires root-side when the respond discriminator finds the
|
|
103
|
+
// waiting worker dead (delivery rides the requeue+inject path instead).
|
|
104
|
+
// Neither transitions run state — sibling ask.answered/task.resumed carry
|
|
105
|
+
// the lifecycle; these two carry only the delivery verdict.
|
|
106
|
+
"task.respond_delivered",
|
|
107
|
+
"task.respond_missed",
|
|
97
108
|
"task.retried",
|
|
98
109
|
// WP-2/R2 waiting-producer (ADR-0 2026-08-17-waiting-producer-ask item 10):
|
|
99
110
|
// `ask` tool lifecycle — requested on park acceptance, answered on delivery
|
|
@@ -231,6 +242,10 @@ export const TEAM_EVENT_TYPES = [
|
|
|
231
242
|
"run.forget_requested",
|
|
232
243
|
"run.goal_achievement",
|
|
233
244
|
"run.resume_requested",
|
|
245
|
+
// SDD-3 W-C G4 (team-tool.ts handleResume): forced resume of a FOREIGN-owned
|
|
246
|
+
// run — ownership was bypassed with force:true (liveness was NOT bypassable;
|
|
247
|
+
// live runs are refused before this event can fire). Security-audit trace.
|
|
248
|
+
"run.resume_forced_foreign",
|
|
234
249
|
// Runtime/surface resolution
|
|
235
250
|
"runtime.resolved",
|
|
236
251
|
"surface.degraded",
|
|
@@ -249,6 +264,7 @@ export const TEAM_EVENT_TYPES = [
|
|
|
249
264
|
"task.retry_attempt",
|
|
250
265
|
"task.budget_fair_share",
|
|
251
266
|
"task.model_dropped",
|
|
267
|
+
"task.mcp_enforcement_degraded",
|
|
252
268
|
"task.output_validation",
|
|
253
269
|
"task.steer_queued",
|
|
254
270
|
"task.steer_dropped",
|
package/src/state/crew-init.ts
CHANGED
|
@@ -276,6 +276,116 @@ function computeCrewRoot(cwd: string): string {
|
|
|
276
276
|
return fs.existsSync(piDir) ? safeJoin(piDir, "teams") : crewDir;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* G23c (2026-10-02): sweep an empty legacy `.pi/teams` residue once the
|
|
281
|
+
* project has migrated to the `.crew/` layout.
|
|
282
|
+
*
|
|
283
|
+
* When `.crew/` exists, `computeCrewRoot` resolves to `.crew/` and the
|
|
284
|
+
* legacy `.pi/teams/` tree from an older pi-crew version is never revisited
|
|
285
|
+
* — dead residue stays on disk forever (plan §G23c: "0 code dọn"). This
|
|
286
|
+
* sweep is deliberately CONSERVATIVE because it deletes from the user's
|
|
287
|
+
* workspace:
|
|
288
|
+
*
|
|
289
|
+
* 1. Fires ONLY when the active crew root is the `.crew` layout AND
|
|
290
|
+
* `<repoRoot>/.pi/teams` exists (a live legacy root is never touched).
|
|
291
|
+
* 2. Removes the legacy tree ONLY when it is a bare pi-crew skeleton:
|
|
292
|
+
* known pi-crew directory names at every depth, and only placeholder
|
|
293
|
+
* files (`.gitkeep` anywhere; `README.md`/`.gitignore` at the root).
|
|
294
|
+
* Every entry is content-checked before anything is deleted.
|
|
295
|
+
* 3. Any real data — a run manifest under `state/`, artifact content, a
|
|
296
|
+
* user-authored agent/workflow file, an unknown entry — keeps the
|
|
297
|
+
* residue in place with ONE info log line: auto-migrating run state
|
|
298
|
+
* would change the run-lookup path, which is too risky for an
|
|
299
|
+
* automatic sweep (leader/user decision).
|
|
300
|
+
* 4. Never touches anything under `.pi/` other than the `teams/` path
|
|
301
|
+
* itself — the `.pi` parent and all siblings (`.pi/agent`, ...) stay.
|
|
302
|
+
*
|
|
303
|
+
* Self-contained by design: inline helpers only, no new module imports.
|
|
304
|
+
* Any failure inside the sweep is logged and swallowed — init must never
|
|
305
|
+
* break because the residue could not be inspected.
|
|
306
|
+
*/
|
|
307
|
+
|
|
308
|
+
/** Directory names pi-crew itself creates/uses under a crew root (any depth).
|
|
309
|
+
* Sources: ensureCrewDirectory (state/subagents/runs/artifacts/cache/graphs/
|
|
310
|
+
* audit), project-init.ts (agents/teams/workflows/imports), and the legacy
|
|
311
|
+
* .gitignore entries in gitignore-manager.ts (worktrees/imports). */
|
|
312
|
+
const LEGACY_TEAMS_SKELETON_DIRS = new Set([
|
|
313
|
+
"state",
|
|
314
|
+
"subagents",
|
|
315
|
+
"runs",
|
|
316
|
+
"artifacts",
|
|
317
|
+
"cache",
|
|
318
|
+
"graphs",
|
|
319
|
+
"audit",
|
|
320
|
+
"worktrees",
|
|
321
|
+
"agents",
|
|
322
|
+
"teams",
|
|
323
|
+
"workflows",
|
|
324
|
+
"imports",
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
/** Placeholder files pi-crew itself writes at the crew-root level. */
|
|
328
|
+
const LEGACY_TEAMS_SKELETON_ROOT_FILES = new Set(["README.md", ".gitignore"]);
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* True when `root` holds no data: every directory has a pi-crew skeleton
|
|
332
|
+
* name and every file is a pi-crew placeholder. Anything else — run
|
|
333
|
+
* manifests, artifacts, user agent/workflow content, unknown entries —
|
|
334
|
+
* returns false (the caller then keeps the residue).
|
|
335
|
+
*/
|
|
336
|
+
function isBareLegacyTeamsSkeleton(root: string): boolean {
|
|
337
|
+
const walk = (dir: string, depth: number): boolean => {
|
|
338
|
+
let entries: fs.Dirent[];
|
|
339
|
+
try {
|
|
340
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
341
|
+
} catch {
|
|
342
|
+
// Unreadable => assume data; never delete what we cannot inspect.
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
for (const entry of entries) {
|
|
346
|
+
if (entry.isDirectory()) {
|
|
347
|
+
if (!LEGACY_TEAMS_SKELETON_DIRS.has(entry.name)) return false;
|
|
348
|
+
if (!walk(safeJoin(dir, entry.name), depth + 1)) return false;
|
|
349
|
+
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
350
|
+
// `.gitkeep` placeholders are written at any depth; README.md and
|
|
351
|
+
// .gitignore only at the crew-root level. Any other file (run
|
|
352
|
+
// manifest, artifact, user content) counts as data.
|
|
353
|
+
if (entry.name === ".gitkeep") continue;
|
|
354
|
+
if (depth === 0 && LEGACY_TEAMS_SKELETON_ROOT_FILES.has(entry.name)) continue;
|
|
355
|
+
return false;
|
|
356
|
+
} else {
|
|
357
|
+
// Sockets/FIFOs/devices — unknown => keep.
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
};
|
|
363
|
+
return walk(root, 0);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** See the G23c block comment above. Returns silently when there is nothing
|
|
367
|
+
* safe to sweep. */
|
|
368
|
+
function sweepLegacyTeamsResidue(cwd: string, activeCrewRoot: string): void {
|
|
369
|
+
try {
|
|
370
|
+
const repoRoot = findProjectRoot(cwd) ?? cwd;
|
|
371
|
+
const legacyTeamsDir = safeJoin(repoRoot, ".pi", "teams");
|
|
372
|
+
// Sweep only when the ACTIVE layout is `.crew` (computeCrewRoot did NOT
|
|
373
|
+
// resolve to the legacy root) and a legacy tree actually exists.
|
|
374
|
+
if (activeCrewRoot === legacyTeamsDir || !fs.existsSync(legacyTeamsDir)) return;
|
|
375
|
+
if (isBareLegacyTeamsSkeleton(legacyTeamsDir)) {
|
|
376
|
+
// Exact-path removal of `.pi/teams` only — the `.pi` parent and
|
|
377
|
+
// every sibling under it stay untouched.
|
|
378
|
+
fs.rmSync(legacyTeamsDir, { recursive: true, force: true });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
console.info(
|
|
382
|
+
`[pi-crew] legacy ${legacyTeamsDir} still contains data — kept in place (auto-migration would change the run-lookup path); remove manually once you no longer need it`,
|
|
383
|
+
);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.info(`[pi-crew] legacy .pi/teams residue sweep skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
279
389
|
/**
|
|
280
390
|
* Ensure the .crew directory structure exists with all required subdirectories,
|
|
281
391
|
* placeholder files, README, and .gitignore entries.
|
|
@@ -287,6 +397,10 @@ function computeCrewRoot(cwd: string): string {
|
|
|
287
397
|
export async function ensureCrewDirectory(cwd: string): Promise<void> {
|
|
288
398
|
const crewRoot = computeCrewRoot(cwd);
|
|
289
399
|
|
|
400
|
+
// G23c: once `.crew/` is the active layout, sweep an empty legacy
|
|
401
|
+
// `.pi/teams` skeleton left behind by an older pi-crew version.
|
|
402
|
+
sweepLegacyTeamsResidue(cwd, crewRoot);
|
|
403
|
+
|
|
290
404
|
// 1. Create directory structure
|
|
291
405
|
const dirs = [
|
|
292
406
|
crewRoot,
|
|
@@ -16,6 +16,12 @@ const BINARY_MAGIC = Buffer.from("PICREW2BIN", "utf-8");
|
|
|
16
16
|
/** Binary format version for forward compatibility. */
|
|
17
17
|
const BINARY_VERSION = 1;
|
|
18
18
|
|
|
19
|
+
/** Staleness horizon (2.19 / FIX Issue 2): entries — non-async ones, and async
|
|
20
|
+
* ones guarding against PID reuse — whose manifest last write is older than
|
|
21
|
+
* this are filtered as not alive. Exported so resume-liveness consumers
|
|
22
|
+
* (team-tool.ts) share the exact horizon instead of mirroring a copy. */
|
|
23
|
+
export const ACTIVE_RUN_STALE_MS = 30 * 60_000;
|
|
24
|
+
|
|
19
25
|
export interface ActiveRunRegistryEntry {
|
|
20
26
|
runId: string;
|
|
21
27
|
cwd: string;
|
|
@@ -334,16 +340,17 @@ function filterAliveEntries(entries: ActiveRunRegistryEntry[]): {
|
|
|
334
340
|
if (code !== "ESRCH" && code !== "ENOENT") return false;
|
|
335
341
|
return false;
|
|
336
342
|
}
|
|
337
|
-
// FIX Issue 2: Async runs older than
|
|
338
|
-
// This guards against PID reuse after the original process exits.
|
|
343
|
+
// FIX Issue 2: Async runs older than the staleness horizon are stale even if
|
|
344
|
+
// PID is alive. This guards against PID reuse after the original process exits.
|
|
339
345
|
const updatedAt = typeof raw.updatedAt === "string" ? Date.parse(raw.updatedAt) : NaN;
|
|
340
|
-
if (Number.isFinite(updatedAt) && Date.now() - updatedAt >
|
|
346
|
+
if (Number.isFinite(updatedAt) && Date.now() - updatedAt > ACTIVE_RUN_STALE_MS) return false;
|
|
341
347
|
}
|
|
342
|
-
// 2.19 — Stale non-async run: live-session/scaffold runs older than
|
|
343
|
-
// Without this, test runs that crash/leak would stay in
|
|
348
|
+
// 2.19 — Stale non-async run: live-session/scaffold runs older than the
|
|
349
|
+
// staleness horizon. Without this, test runs that crash/leak would stay in
|
|
350
|
+
// the registry forever.
|
|
344
351
|
if (!raw.async) {
|
|
345
352
|
const updatedAt = typeof raw.updatedAt === "string" ? Date.parse(raw.updatedAt) : NaN;
|
|
346
|
-
if (Number.isFinite(updatedAt) && Date.now() - updatedAt >
|
|
353
|
+
if (Number.isFinite(updatedAt) && Date.now() - updatedAt > ACTIVE_RUN_STALE_MS) return false;
|
|
347
354
|
}
|
|
348
355
|
} catch {
|
|
349
356
|
return false;
|
package/src/state/types.ts
CHANGED
|
@@ -638,7 +638,6 @@ export interface TeamTaskState {
|
|
|
638
638
|
heartbeat?: WorkerHeartbeatState;
|
|
639
639
|
checkpoint?: TaskCheckpointState;
|
|
640
640
|
attempts?: TaskAttemptState[];
|
|
641
|
-
workerExitStatus?: WorkerExitStatus;
|
|
642
641
|
terminalEvidence?: OperationTerminalEvidence[];
|
|
643
642
|
taskPacket?: TaskPacket;
|
|
644
643
|
verification?: VerificationEvidence;
|
|
@@ -675,7 +674,6 @@ export interface TeamTaskState {
|
|
|
675
674
|
retryCount?: number;
|
|
676
675
|
lastDecision?: PolicyDecision;
|
|
677
676
|
};
|
|
678
|
-
controlReservation?: ControlReservation;
|
|
679
677
|
|
|
680
678
|
/** Structured diagnostics per task (ASI pattern from pi-autoresearch). */
|
|
681
679
|
diagnostics?: Record<string, unknown>;
|
|
@@ -713,12 +711,6 @@ export interface TeamTaskState {
|
|
|
713
711
|
};
|
|
714
712
|
}
|
|
715
713
|
|
|
716
|
-
export interface ControlReservation {
|
|
717
|
-
reservedAt: string;
|
|
718
|
-
controllerId: string;
|
|
719
|
-
acceptsControlEvents: boolean;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
714
|
/**
|
|
723
715
|
* A task scheduled to fire on a cron expression, interval, or one-shot.
|
|
724
716
|
* Persisted at `<cwd>/.crew/state/schedules/<sessionId>.json`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G19 (W-E Phase 1): shared goal-achievement surfacing helpers.
|
|
3
|
+
*
|
|
4
|
+
* `manifest.goalAchieved` (set once by `applyGoalAchievement` at run
|
|
5
|
+
* completion) had ZERO readers — the false-green verdict was persisted but
|
|
6
|
+
* never shown. These helpers are the single render rule so every surface
|
|
7
|
+
* (team-tool status, widget label, dashboard rows, live sidebar) agrees:
|
|
8
|
+
*
|
|
9
|
+
* - NEVER render while the run is still active (verdict is not final).
|
|
10
|
+
* - `true` → silent (a green run needs no extra noise).
|
|
11
|
+
* - `false` / `"unknown"` → ⚠ warning (false-green / not verified).
|
|
12
|
+
* - `undefined` → silent (legacy runs created before the assessment existed,
|
|
13
|
+
* or goal-loop assessment not enabled).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { isTerminalRunStatus, type TeamRunStatus } from "../state/contracts.ts";
|
|
17
|
+
|
|
18
|
+
/** True when the run is terminal AND the goal was NOT confirmed achieved. */
|
|
19
|
+
export function isGoalNotAchieved(run: { status?: string; goalAchieved?: boolean | "unknown" }): boolean {
|
|
20
|
+
if (run.status === undefined) return false;
|
|
21
|
+
if (!isTerminalRunStatus(run.status as TeamRunStatus)) return false;
|
|
22
|
+
return run.goalAchieved === false || run.goalAchieved === "unknown";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Short suffix for one-line labels (widget title, dashboard row, sidebar). */
|
|
26
|
+
export function goalFlagSuffix(run: { status?: string; goalAchieved?: boolean | "unknown" }): string {
|
|
27
|
+
return isGoalNotAchieved(run) ? " ⚠" : "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Full status line for team-tool status output. Empty string when silent
|
|
32
|
+
* (non-terminal, achieved, or never assessed). Includes the false-green note
|
|
33
|
+
* (the reason string recorded by `applyGoalAchievement`) when present.
|
|
34
|
+
*/
|
|
35
|
+
export function goalAchievedStatusLabel(run: {
|
|
36
|
+
status?: string;
|
|
37
|
+
goalAchieved?: boolean | "unknown";
|
|
38
|
+
goalAchievementNote?: string;
|
|
39
|
+
}): string {
|
|
40
|
+
if (!isGoalNotAchieved(run)) return "";
|
|
41
|
+
const verdict = run.goalAchieved === "unknown" ? "unknown (not verified)" : "not achieved";
|
|
42
|
+
const note = run.goalAchievementNote ? ` — ${run.goalAchievementNote}` : "";
|
|
43
|
+
return `Goal achieved: ⚠ ${verdict}${note}`;
|
|
44
|
+
}
|
|
@@ -9,6 +9,7 @@ import { aggregateUsage, formatTokens } from "../state/usage.ts";
|
|
|
9
9
|
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
10
10
|
import { truncate } from "../utils/visual.ts";
|
|
11
11
|
import { formatCount, teamWorkflowLabel } from "./format-helpers.ts";
|
|
12
|
+
import { goalFlagSuffix } from "./goal-flag.ts";
|
|
12
13
|
import { DASHBOARD_KEYS } from "./keybinding-map.ts";
|
|
13
14
|
import { ACTIVE, canopyLine, formatHint, RAIL, type RailSlot, railLine, sectionLine, shortId, statusSlot } from "./rail.ts";
|
|
14
15
|
import type { OverlaySchedulerHandle } from "./shared-overlay-scheduler.ts";
|
|
@@ -230,6 +231,9 @@ export class LiveRunSidebar {
|
|
|
230
231
|
// user sees *why* a run ended without having to switch panes. The reason
|
|
231
232
|
// is already computed on the consumed snapshot (cancellationReason).
|
|
232
233
|
const TERMINAL_WITH_REASON = ["failed", "cancelled", "stopped"];
|
|
234
|
+
// G19 (W-E Phase 1): goal false-green flag on terminal runs — same rule
|
|
235
|
+
// as the team-tool status output; never rendered while still running.
|
|
236
|
+
const goalSuffix = goalFlagSuffix(run) ? " · ⚠ goal" : "";
|
|
233
237
|
const reasonSuffix =
|
|
234
238
|
TERMINAL_WITH_REASON.includes(run.status) && snapshot?.cancellationReason
|
|
235
239
|
? ` · ${truncate(snapshot.cancellationReason, 40)}`
|
|
@@ -241,7 +245,7 @@ export class LiveRunSidebar {
|
|
|
241
245
|
"border",
|
|
242
246
|
this.theme.fg(
|
|
243
247
|
"muted",
|
|
244
|
-
`${shortId(run.runId)} · ${run.status ?? "?"}${reasonSuffix} · ${run.workspaceMode ?? "single"}`,
|
|
248
|
+
`${shortId(run.runId)} · ${run.status ?? "?"}${reasonSuffix}${goalSuffix} · ${run.workspaceMode ?? "single"}`,
|
|
245
249
|
),
|
|
246
250
|
this.theme,
|
|
247
251
|
budget,
|
package/src/ui/pi-ui-compat.ts
CHANGED
|
@@ -36,6 +36,8 @@ export function setWorkingIndicator(ctx: UiContext, options?: WorkingIndicatorOp
|
|
|
36
36
|
if (typeof fn === "function") fn.call(ctx.ui, options);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** Install or update an extension widget slot.
|
|
40
|
+
* No-op when the host UI predates the `setWidget` API. */
|
|
39
41
|
export function setExtensionWidget(
|
|
40
42
|
ctx: UiContext,
|
|
41
43
|
key: string,
|
|
@@ -43,7 +45,9 @@ export function setExtensionWidget(
|
|
|
43
45
|
options?: WidgetOptionsWithPersist,
|
|
44
46
|
): void {
|
|
45
47
|
const { persist: _persist, ...widgetOptions } = options ?? {};
|
|
46
|
-
ctx.ui
|
|
48
|
+
const record = maybeRecord(ctx.ui);
|
|
49
|
+
const fn = record?.setWidget;
|
|
50
|
+
if (typeof fn === "function") fn.call(ctx.ui, key, content as never, widgetOptions as WidgetOptions);
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/**
|
|
@@ -66,12 +70,25 @@ export function setFooter(ctx: UiContext | undefined, factory: FooterFactory | u
|
|
|
66
70
|
if (typeof fn === "function") fn.call(ctx.ui, factory as never);
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/** Show a custom focused component.
|
|
74
|
+
* When the host UI predates (or mangles) the `custom` API, resolves `undefined`
|
|
75
|
+
* instead of rejecting or hanging: the sole caller today
|
|
76
|
+
* (src/extension/registration/ui.ts) already fire-and-forgets via
|
|
77
|
+
* `void showCustom(...)`, so a rejected or never-settling promise would only
|
|
78
|
+
* surface as an unhandled rejection. */
|
|
69
79
|
export function showCustom<T>(ctx: UiContext, factory: CustomFactory<T>, options?: CustomOptions): Promise<T> {
|
|
70
|
-
const
|
|
80
|
+
const record = maybeRecord(ctx.ui);
|
|
81
|
+
const fn = record?.custom;
|
|
82
|
+
if (typeof fn !== "function") return Promise.resolve(undefined as T);
|
|
83
|
+
const custom = fn as unknown as GenericCustom;
|
|
71
84
|
return custom<T>(factory, options);
|
|
72
85
|
}
|
|
73
86
|
|
|
87
|
+
/** Set a status fallback line.
|
|
88
|
+
* No-op when the host UI predates the `setStatus` API. */
|
|
74
89
|
export function setStatusFallback(ctx: UiContext, key: string, lines: string | readonly string[] | undefined, segment?: string): void {
|
|
75
90
|
const text = typeof lines === "string" ? lines : lines ? [...lines].join("\n") : undefined;
|
|
76
|
-
ctx.ui
|
|
91
|
+
const record = maybeRecord(ctx.ui);
|
|
92
|
+
const fn = record?.setStatus;
|
|
93
|
+
if (typeof fn === "function") fn.call(ctx.ui, segment ? `${key}:${segment}` : key, text);
|
|
77
94
|
}
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { renderProgressPane } from "./dashboard-panes/progress-pane.ts";
|
|
|
29
29
|
import { renderScheduleDetails, renderSchedulesPane } from "./dashboard-panes/schedules-pane.ts";
|
|
30
30
|
import { renderTranscriptPane } from "./dashboard-panes/transcript-pane.ts";
|
|
31
31
|
import { formatCount, teamWorkflowLabel } from "./format-helpers.ts";
|
|
32
|
+
import { goalFlagSuffix } from "./goal-flag.ts";
|
|
32
33
|
import { type DashboardKeyAction, dashboardActionForKey } from "./keybinding-map.ts";
|
|
33
34
|
import { HelpOverlay } from "./overlays/help-overlay.ts";
|
|
34
35
|
import { ACTIVE, CURSOR, canopyLine, formatHint, gaugeBar, overflowHint, RAIL, railLine, sectionLine } from "./rail.ts";
|
|
@@ -448,7 +449,7 @@ function runLabel(
|
|
|
448
449
|
// survive; team/workflow/step clip) rather than the goal. Optional `maxW`
|
|
449
450
|
// enables the goal-aware truncation; legacy 3-arg callers (dev patch
|
|
450
451
|
// scripts) get the untruncated full label.
|
|
451
|
-
const head = `${marker} ${icon} ${run.runId.slice(-8)} ${status}`;
|
|
452
|
+
const head = `${marker} ${icon} ${run.runId.slice(-8)} ${status}${goalFlagSuffix(run)}`;
|
|
452
453
|
const meta = `${teamWorkflowLabel(run.team, run.workflow)} · ${step}`;
|
|
453
454
|
const goal = sanitizeLine(run.goal ?? "");
|
|
454
455
|
if (maxW === undefined) return sanitizeLine(`${head} · ${meta} · ${goal}`);
|
|
@@ -12,6 +12,7 @@ import type { ManifestCache } from "../../runtime/manifest-cache.ts";
|
|
|
12
12
|
import { isDisplayActiveRun } from "../../runtime/process-status.ts";
|
|
13
13
|
import { reconcileAllStaleRuns } from "../../runtime/recovery/crash-recovery.ts";
|
|
14
14
|
import type { TeamRunManifest } from "../../state/types.ts";
|
|
15
|
+
import { goalFlagSuffix } from "../goal-flag.ts";
|
|
15
16
|
import { ACTIVE, RAIL, shortId } from "../rail.ts";
|
|
16
17
|
import type { RunSnapshotCache } from "../snapshot-types.ts";
|
|
17
18
|
import type { WidgetRun } from "./widget-types.ts";
|
|
@@ -128,6 +129,9 @@ export function statusSummary(runs: WidgetRun[]): string {
|
|
|
128
129
|
export function shortRunLabel(run: TeamRunManifest): string {
|
|
129
130
|
const team = (run.team ?? "").trim();
|
|
130
131
|
const workflow = (run.workflow ?? "").trim();
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
// G19 (W-E Phase 1): append the ⚠ goal flag when a TERMINAL run did not
|
|
133
|
+
// achieve its goal (false-green surfacing) — silent otherwise, so active
|
|
134
|
+
// runs and green runs never flicker.
|
|
135
|
+
const label = team && workflow && team !== workflow ? `${team}/${workflow}` : team || workflow || shortId(run.runId);
|
|
136
|
+
return `${label}${goalFlagSuffix(run)}`;
|
|
133
137
|
}
|