pi-crew 0.11.4 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +1 -1
- package/dist/index.mjs +432 -431
- package/docs/actions-reference.md +22 -0
- package/index.ts +9 -5
- package/package.json +8 -8
- package/scripts/analyze-run.mjs +6 -3
- package/skills/real-test-pi-crew/SKILL.md +1 -1
- 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/runtime/recovery/crash-recovery.ts +6 -1
- package/src/runtime/stale-reconciler.ts +8 -0
- package/src/runtime/task-output-context.ts +11 -1
- package/src/runtime/task-runner/post-execution.ts +2 -0
- package/src/runtime/task-runner/pre-execution.ts +29 -21
- package/src/schema/team-tool-schema.ts +2 -1
|
@@ -48,6 +48,7 @@ Reads the versioned `PlanRecord` list at `<stateRoot>/plans/plans.json` (ADR-4).
|
|
|
48
48
|
| `search` | BM25-ranked agent/team discovery | Smart search |
|
|
49
49
|
| `events` | Read the event log | Debug/audit |
|
|
50
50
|
| `artifacts` | List run artifacts | View outputs |
|
|
51
|
+
| `breakdown` | Read per-task prompt-token breakdown artifacts | Debug prompt bloat (requires `PI_CREW_PROMPT_BREAKDOWN=1`) |
|
|
51
52
|
| `worktrees` | List run worktree metadata | Inspect worktrees |
|
|
52
53
|
| `graph` | Load/save/list run graphs | Visualization |
|
|
53
54
|
| `explain` | Explain a run or task structure | Understand a run |
|
|
@@ -77,6 +78,7 @@ Reads the versioned `PlanRecord` list at `<stateRoot>/plans/plans.json` (ADR-4).
|
|
|
77
78
|
| **`workflow-list`** | **v0.9.0** List static + dynamic workflows | Discover workflows |
|
|
78
79
|
| **`workflow-save`** | **v0.9.0** Overwrite `.dwf.ts` source (requires `confirm:true`) | Update dynamic workflow |
|
|
79
80
|
| **`workflow-delete`** | **v0.9.0** Delete a `.dwf.ts` (requires `confirm:true`) | Remove dynamic workflow |
|
|
81
|
+
| `compare` | Compare two runs side by side (US-021) | Diff before/after a fix, or two attempts at one goal |
|
|
80
82
|
| `help` | Display help text | Help |
|
|
81
83
|
|
|
82
84
|
---
|
|
@@ -420,6 +422,26 @@ Append-only JSONL events: task.started, task.completed, run.blocked, etc.
|
|
|
420
422
|
|
|
421
423
|
---
|
|
422
424
|
|
|
425
|
+
### `breakdown` — Per-task prompt-token breakdown
|
|
426
|
+
|
|
427
|
+
G18 (opt-in measurement surface): reads the per-task
|
|
428
|
+
`metadata/<taskId>.prompt-breakdown.json` artifacts and aggregates per-task
|
|
429
|
+
`estTokens` totals plus the top-5 largest prompt sections. Artifacts are
|
|
430
|
+
recorded only when the host Pi session is started with
|
|
431
|
+
`PI_CREW_PROMPT_BREAKDOWN=1` (default off — zero cost when off).
|
|
432
|
+
|
|
433
|
+
```json
|
|
434
|
+
{
|
|
435
|
+
"action": "breakdown",
|
|
436
|
+
"runId": "team_...",
|
|
437
|
+
"taskId": "01_plan"
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
`taskId` is optional — omit it to aggregate every task with a breakdown.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
423
445
|
### `worktrees` — Worktree metadata
|
|
424
446
|
|
|
425
447
|
```json
|
package/index.ts
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* Resolution order:
|
|
5
5
|
* 1. dist/index.mjs (bundle) when present AND not explicitly disabled.
|
|
6
6
|
* Default since v0.9.17 close-out — benchmarks show bundle is
|
|
7
|
-
* ~19% faster
|
|
7
|
+
* ~19% faster cold-start on a cold Node cache; with a warm
|
|
8
|
+
* NODE_COMPILE_CACHE the gap shrinks to ~2%. The bundle's primary
|
|
9
|
+
* value is correctness (vendored typebox) and deterministic startup,
|
|
10
|
+
* not raw speed.
|
|
8
11
|
* See `scripts/bench-cold-start.mjs` for reproducible numbers.
|
|
9
12
|
* 2. Inline strip-types loading — fallback when bundle is missing
|
|
10
13
|
* (e.g. dev clone without `npm run build:bundle`) OR when
|
|
@@ -52,9 +55,9 @@ import { fileURLToPath } from "node:url";
|
|
|
52
55
|
import { dirname, resolve } from "node:path";
|
|
53
56
|
|
|
54
57
|
// IMPORTANT: src/ imports here MUST be dynamic (lazy `import()`), never
|
|
55
|
-
// top-level static imports.
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
+
// top-level static imports. The npm tarball ships dist/index.mjs as the
|
|
59
|
+
// entry and ALSO includes src/ (see "files" in package.json) for source
|
|
60
|
+
// maps of future tooling. dist/index.mjs is ~1.6MB. A top-level static
|
|
58
61
|
// `import { x } from "./src/..."` is resolved by the module loader BEFORE
|
|
59
62
|
// this file's code runs, so it would break every fresh `npm install`
|
|
60
63
|
// (Cannot find module './src/...'). Dynamic import() defers src/ resolution
|
|
@@ -105,7 +108,8 @@ if (!envForceOff) {
|
|
|
105
108
|
// Lazy src/ fallback — only resolved when the bundle is unavailable
|
|
106
109
|
// (dev clones without a built dist/). NEVER imported in published
|
|
107
110
|
// installs, where the bundle always ships + loads above. This dynamic
|
|
108
|
-
// import()
|
|
111
|
+
// import() keeps startup lazy so the bundle loads only when the extension
|
|
112
|
+
// actually activates.
|
|
109
113
|
let srcRegister:
|
|
110
114
|
| typeof import("./src/extension/register.ts").registerPiTeams
|
|
111
115
|
| undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-crew",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
|
|
5
5
|
"author": "baphuongna",
|
|
6
6
|
"license": "MIT",
|
|
@@ -136,23 +136,23 @@
|
|
|
136
136
|
"@earendil-works/pi-tui": "*"
|
|
137
137
|
},
|
|
138
138
|
"dependencies": {
|
|
139
|
-
"@sinclair/typebox": "^0.34.
|
|
140
|
-
"acorn": "^8.
|
|
139
|
+
"@sinclair/typebox": "^0.34.52",
|
|
140
|
+
"acorn": "^8.18.0",
|
|
141
141
|
"ajv": "^8.20.0",
|
|
142
142
|
"cli-highlight": "^2.1.11",
|
|
143
143
|
"diff": "^5.2.0",
|
|
144
|
-
"esbuild": "^0.28.
|
|
144
|
+
"esbuild": "^0.28.2",
|
|
145
145
|
"jiti": "^2.7.0",
|
|
146
|
-
"yaml": "^2.9.
|
|
146
|
+
"yaml": "^2.9.1"
|
|
147
147
|
},
|
|
148
148
|
"devDependencies": {
|
|
149
|
-
"@biomejs/biome": "^2.5.
|
|
149
|
+
"@biomejs/biome": "^2.5.14",
|
|
150
150
|
"@earendil-works/pi-agent-core": "^0.84.0",
|
|
151
151
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
152
152
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
153
153
|
"@earendil-works/pi-tui": "^0.84.0",
|
|
154
|
-
"@types/node": "^25.9.
|
|
155
|
-
"tsx": "^4.23.
|
|
154
|
+
"@types/node": "^25.9.8",
|
|
155
|
+
"tsx": "^4.23.15",
|
|
156
156
|
"typescript": "^7.0.2"
|
|
157
157
|
},
|
|
158
158
|
"peerDependenciesMeta": {
|
package/scripts/analyze-run.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* pathological multi-GB runs, which pi-crew does not produce.
|
|
13
13
|
*
|
|
14
14
|
* Output:
|
|
15
|
-
* docs/perf-report-<runId>.md
|
|
15
|
+
* docs/perf/perf-report-<runId>.md — báo cáo tiếng Việt, bảng + 🔴 highlight
|
|
16
16
|
* bench/results/<runId>.json — structured JSON
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -706,7 +706,7 @@ async function main() {
|
|
|
706
706
|
|
|
707
707
|
// F1 (audit): validate runId to prevent path injection — runId flows into
|
|
708
708
|
// join(crewRoot, "state", "runs", runId) AND into output filenames
|
|
709
|
-
// (docs/perf-report-<runId>.md, bench/results/<runId>.json). Without this,
|
|
709
|
+
// (docs/perf/perf-report-<runId>.md, bench/results/<runId>.json). Without this,
|
|
710
710
|
// runId="../../tmp/x" would read/write outside the intended dirs.
|
|
711
711
|
if (!/^[A-Za-z0-9_.-]+$/.test(runId)) {
|
|
712
712
|
process.stderr.write(`Error: invalid runId (must be alphanumeric/_/./-): ${runId}\n`);
|
|
@@ -939,7 +939,10 @@ async function main() {
|
|
|
939
939
|
|
|
940
940
|
const resultsDir = join(process.cwd(), "bench", "results");
|
|
941
941
|
mkdirSync(resultsDir, { recursive: true });
|
|
942
|
-
|
|
942
|
+
// Perf reports write to docs/perf/ (runtime output, gitignored via the
|
|
943
|
+
// narrow `docs/perf/perf-report-team_*` pattern — curated perf docs in
|
|
944
|
+
// docs/perf/ stay tracked). Legacy location was docs/ top level.
|
|
945
|
+
const docsDir = join(process.cwd(), "docs", "perf");
|
|
943
946
|
mkdirSync(docsDir, { recursive: true });
|
|
944
947
|
const jsonPath = join(resultsDir, `${runId}.json`);
|
|
945
948
|
writeFileSync(jsonPath, JSON.stringify(report, null, 2));
|
|
@@ -1290,7 +1290,7 @@ Use this to answer "đủ full tính năng chưa?" without re-deriving. Every us
|
|
|
1290
1290
|
|
|
1291
1291
|
| Feature | Code entry | Verify via |
|
|
1292
1292
|
|---|---|---|
|
|
1293
|
-
| Team tool —
|
|
1293
|
+
| Team tool — 56 actions / 5 domains | `src/schema/team-tool-schema.ts:391-437`, dispatch in `src/extension/team-tool/` | 9a (read-only) + 9b/9c/9d/9e/9f theo domain |
|
|
1294
1294
|
| Runtime mode `child-process` (default) | `src/runtime/child-pi/` | 9b sync run + T7 |
|
|
1295
1295
|
| Runtime mode `scaffold` (dry-run) | `src/runtime/task-runner/pre-execution.ts:176` | 9b `action='plan'`/`'plans'` (preview không spawn) hoặc run với `runtime.mode='scaffold'` |
|
|
1296
1296
|
| Runtime mode `live-session` (experimental) | `src/runtime/live-session/` | Run với `runtime.mode='live-session'` + irc tool xuất hiện trong worker (`src/runtime/custom-tools/irc-tool.ts`) |
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 54→5 mapping:
|
|
9
9
|
* run (9): run, parallel, plan, orchestrate, resume, retry, wait, steer, goal
|
|
10
|
-
* status (
|
|
11
|
-
* health, worktrees, checkpoint, cache, explain, onboard, recommend, help
|
|
10
|
+
* status (17): status, list, get, events, artifacts, breakdown, summary, graph,
|
|
11
|
+
* search, health, worktrees, checkpoint, cache, explain, onboard, recommend, help
|
|
12
12
|
* control (7): cancel, invalidate, respond, cleanup, prune, forget, doctor
|
|
13
13
|
* manage (17): create, update, delete, init, config, validate, autonomy, settings,
|
|
14
14
|
* workflow-create/get/list/save/delete, import, imports, export, compare
|
|
@@ -36,12 +36,13 @@ const ACTION_TO_DOMAIN: Record<string, TeamDomain> = {
|
|
|
36
36
|
steer: "run",
|
|
37
37
|
goal: "run",
|
|
38
38
|
|
|
39
|
-
// status domain (
|
|
39
|
+
// status domain (17)
|
|
40
40
|
status: "status",
|
|
41
41
|
list: "status",
|
|
42
42
|
get: "status",
|
|
43
43
|
events: "status",
|
|
44
44
|
artifacts: "status",
|
|
45
|
+
breakdown: "status",
|
|
45
46
|
summary: "status",
|
|
46
47
|
graph: "status",
|
|
47
48
|
search: "status",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API-5 facade dispatch — Status domain router.
|
|
3
3
|
*
|
|
4
|
-
* Actions: status, list, get, events, artifacts, summary, graph,
|
|
5
|
-
* health, worktrees, checkpoint, cache, explain, onboard, recommend,
|
|
4
|
+
* Actions: status, list, get, events, artifacts, breakdown, summary, graph,
|
|
5
|
+
* search, health, worktrees, checkpoint, cache, explain, onboard, recommend,
|
|
6
|
+
* help.
|
|
6
7
|
*
|
|
7
8
|
* Inline cases (graph, search, onboard, explain, cache, checkpoint, recommend,
|
|
8
9
|
* help) are moved verbatim from the former handleTeamTool switch.
|
|
@@ -28,7 +29,7 @@ import type { PiTeamsToolResult } from "../../tool-result.ts";
|
|
|
28
29
|
import { result, type TeamContext } from "../context.ts";
|
|
29
30
|
import { handleExplain } from "../explain.ts";
|
|
30
31
|
import { handleHealthMonitor } from "../health-monitor.ts";
|
|
31
|
-
import { handleArtifacts, handleEvents, handleSummary } from "../inspect.ts";
|
|
32
|
+
import { handleArtifacts, handleBreakdown, handleEvents, handleSummary } from "../inspect.ts";
|
|
32
33
|
import { handleWorktrees } from "../lifecycle-actions.ts";
|
|
33
34
|
import { handleStatus } from "../status.ts";
|
|
34
35
|
|
|
@@ -45,6 +46,7 @@ export const STATUS_DOMAIN_ACTIONS = [
|
|
|
45
46
|
"get",
|
|
46
47
|
"events",
|
|
47
48
|
"artifacts",
|
|
49
|
+
"breakdown",
|
|
48
50
|
"summary",
|
|
49
51
|
"graph",
|
|
50
52
|
"search",
|
|
@@ -73,6 +75,8 @@ export async function handleStatusDomain(params: TeamToolParamsValue, ctx: TeamC
|
|
|
73
75
|
return handleEvents(params, ctx);
|
|
74
76
|
case "artifacts":
|
|
75
77
|
return handleArtifacts(params, ctx);
|
|
78
|
+
case "breakdown":
|
|
79
|
+
return handleBreakdown(params, ctx);
|
|
76
80
|
case "worktrees":
|
|
77
81
|
return handleWorktrees(params, ctx);
|
|
78
82
|
case "summary":
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
4
|
import { readEventsCursor } from "../../state/event-log/event-log.ts";
|
|
3
5
|
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
@@ -110,3 +112,91 @@ export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
110
112
|
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
111
113
|
});
|
|
112
114
|
}
|
|
115
|
+
|
|
116
|
+
// ─── G18 (SDD 2026-09-30 WI-3): prompt-token breakdown surface ──────────────
|
|
117
|
+
|
|
118
|
+
/** File suffix of the per-task breakdown artifact written by pre-execution
|
|
119
|
+
* (`metadata/<taskId>.prompt-breakdown.json`) when PI_CREW_PROMPT_BREAKDOWN=1. */
|
|
120
|
+
const BREAKDOWN_FILE_SUFFIX = ".prompt-breakdown.json";
|
|
121
|
+
|
|
122
|
+
/** Shape of one section entry in a prompt-breakdown JSON artifact
|
|
123
|
+
* (see writeArtifact call in src/runtime/task-runner/pre-execution.ts). */
|
|
124
|
+
interface PromptBreakdownSection {
|
|
125
|
+
chars: number;
|
|
126
|
+
estTokens: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* G18: read the opt-in per-task prompt-token breakdown artifacts of a run and
|
|
131
|
+
* surface per-task totals + the top-5 largest sections. Consumers: the leader
|
|
132
|
+
* prompt (future W-F token decisions, G15/G17) and humans debugging prompt
|
|
133
|
+
* bloat. The artifacts exist only when the host session was started with
|
|
134
|
+
* PI_CREW_PROMPT_BREAKDOWN=1 (default off — zero cost when off, SR-02).
|
|
135
|
+
* Mirrors the handleArtifacts pattern: resolve run → read the manifest
|
|
136
|
+
* artifacts index → filter kind=metadata breakdown entries → read files.
|
|
137
|
+
*/
|
|
138
|
+
export function handleBreakdown(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
139
|
+
if (!params.runId)
|
|
140
|
+
return result(
|
|
141
|
+
paramRequired("breakdown", "runId", "{ action: 'breakdown', runId: 'team_...' }"),
|
|
142
|
+
{ action: "breakdown", status: "error" },
|
|
143
|
+
true,
|
|
144
|
+
);
|
|
145
|
+
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
146
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "breakdown", status: "error" }, true);
|
|
147
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
148
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "breakdown", status: "error" }, true);
|
|
149
|
+
// Index-driven lookup (no disk scan): the manifest artifacts index carries
|
|
150
|
+
// one metadata entry per task with a prompt breakdown.
|
|
151
|
+
const breakdownArtifacts = loaded.manifest.artifacts.filter(
|
|
152
|
+
(artifact) => artifact.kind === "metadata" && path.basename(artifact.path).endsWith(BREAKDOWN_FILE_SUFFIX),
|
|
153
|
+
);
|
|
154
|
+
const perTask: Array<{
|
|
155
|
+
taskId: string;
|
|
156
|
+
totalEstTokens: number;
|
|
157
|
+
topSections: Array<{ section: string; estTokens: number; chars: number }>;
|
|
158
|
+
}> = [];
|
|
159
|
+
for (const artifact of breakdownArtifacts) {
|
|
160
|
+
const taskId = path.basename(artifact.path).slice(0, -BREAKDOWN_FILE_SUFFIX.length);
|
|
161
|
+
if (params.taskId && taskId !== params.taskId) continue;
|
|
162
|
+
let sections: Record<string, PromptBreakdownSection>;
|
|
163
|
+
try {
|
|
164
|
+
sections = JSON.parse(fs.readFileSync(artifact.path, "utf-8")) as Record<string, PromptBreakdownSection>;
|
|
165
|
+
} catch {
|
|
166
|
+
continue; // unreadable/corrupt breakdown JSON — skip rather than fail the whole action
|
|
167
|
+
}
|
|
168
|
+
const entries = Object.entries(sections).filter((entry): entry is [string, PromptBreakdownSection] => {
|
|
169
|
+
const value = entry[1];
|
|
170
|
+
return value !== null && typeof value === "object" && typeof value.estTokens === "number";
|
|
171
|
+
});
|
|
172
|
+
const totalEstTokens = entries.reduce((sum, [, value]) => sum + value.estTokens, 0);
|
|
173
|
+
const topSections = [...entries]
|
|
174
|
+
.sort((a, b) => b[1].estTokens - a[1].estTokens)
|
|
175
|
+
.slice(0, 5)
|
|
176
|
+
.map(([section, value]) => ({ section, estTokens: value.estTokens, chars: value.chars ?? 0 }));
|
|
177
|
+
perTask.push({ taskId, totalEstTokens, topSections });
|
|
178
|
+
}
|
|
179
|
+
if (perTask.length === 0) {
|
|
180
|
+
const scope = params.taskId ? ` for task '${params.taskId}'` : "";
|
|
181
|
+
return result(
|
|
182
|
+
`No prompt-breakdown artifacts${scope} for ${loaded.manifest.runId}. Breakdown capture is opt-in: start the host Pi session with PI_CREW_PROMPT_BREAKDOWN=1 and re-run to record per-section token estimates.`,
|
|
183
|
+
{ action: "breakdown", status: "ok", runId: loaded.manifest.runId, data: { runTotal: 0, perTask: [] } },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const runTotal = perTask.reduce((sum, task) => sum + task.totalEstTokens, 0);
|
|
187
|
+
const lines = [
|
|
188
|
+
`Prompt-token breakdown for ${loaded.manifest.runId}:`,
|
|
189
|
+
...perTask.flatMap((task) => [
|
|
190
|
+
`- ${task.taskId}: ~${task.totalEstTokens} tokens`,
|
|
191
|
+
...task.topSections.map((section) => ` - ${section.section}: ~${section.estTokens} tokens (${section.chars} chars)`),
|
|
192
|
+
]),
|
|
193
|
+
`Run total: ~${runTotal} tokens`,
|
|
194
|
+
];
|
|
195
|
+
return result(lines.join("\n"), {
|
|
196
|
+
action: "breakdown",
|
|
197
|
+
status: "ok",
|
|
198
|
+
runId: loaded.manifest.runId,
|
|
199
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
200
|
+
data: { runTotal, perTask },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
@@ -750,7 +750,12 @@ export async function reconcileAllStaleRuns(
|
|
|
750
750
|
const result = reconcileStaleRun(fresh.manifest, fresh.tasks, now);
|
|
751
751
|
if (result.repaired || result.verdict === "result_exists") {
|
|
752
752
|
if (result.repairedTasks) {
|
|
753
|
-
|
|
753
|
+
// NEW-1 (SDD 2026-09-30 WI-2): persist the FULL task array — the
|
|
754
|
+
// individual-stale branch filters `repairedTasks` down to the stale
|
|
755
|
+
// subset, and saveRunTasks full-overwrites tasks.json, which used to
|
|
756
|
+
// drop every healthy task from disk. `persistTasks` carries the full
|
|
757
|
+
// array; every other branch leaves it unset and falls back here.
|
|
758
|
+
saveRunTasks(fresh.manifest, result.persistTasks ?? result.repairedTasks);
|
|
754
759
|
for (const task of result.repairedTasks) {
|
|
755
760
|
try {
|
|
756
761
|
upsertCrewAgent(fresh.manifest, recordFromTask(fresh.manifest, task, "scaffold"));
|
|
@@ -44,6 +44,13 @@ export interface ReconcileResult {
|
|
|
44
44
|
detail: string;
|
|
45
45
|
/** Repaired task state, returned to a locked caller for persistence. */
|
|
46
46
|
repairedTasks?: TeamTaskState[];
|
|
47
|
+
/** NEW-1 (SDD 2026-09-30 WI-2): what the caller should PERSIST — the FULL
|
|
48
|
+
* task array, including tasks the repair did not touch. Only the
|
|
49
|
+
* individual-stale branch sets it (its `repairedTasks` is filtered down to
|
|
50
|
+
* the stale subset, which must NOT be full-overwrite-persisted or healthy
|
|
51
|
+
* tasks vanish from tasks.json). Callers fall back to `repairedTasks`
|
|
52
|
+
* when absent — every other branch already returns the full array there. */
|
|
53
|
+
persistTasks?: TeamTaskState[];
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
/**
|
|
@@ -490,6 +497,7 @@ export function reconcileStaleRun(manifest: TeamRunManifest, tasks: TeamTaskStat
|
|
|
490
497
|
repaired: true,
|
|
491
498
|
detail: `No PID; ${staleTaskIds.length} individually stale task(s) repaired: ${staleTaskIds.join(", ")}`,
|
|
492
499
|
repairedTasks: repaired.filter((t) => staleTaskIds.includes(t.id)),
|
|
500
|
+
persistTasks: repaired, // NEW-1: persist FULL array (data-loss fix — SDD WI-2)
|
|
493
501
|
};
|
|
494
502
|
}
|
|
495
503
|
// Fall through: no recent activity but not all tasks stale enough yet.
|
|
@@ -526,6 +526,16 @@ export function collectDependencyOutputContext(
|
|
|
526
526
|
return { dependencies: trimmedDependencies, sharedReads };
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
// ADR-5 §trust-fence: dependency output is DATA, never instructions. Mirror the
|
|
530
|
+
// ask/delegate seams in src/prompt/prompt-runtime.ts (:323, :426, :589): strip
|
|
531
|
+
// control chars and neutralize a smuggled closing fence tag so worker-controlled
|
|
532
|
+
// content can never close the <dependency-context> fence early (see SDD 2026-09-30 WI-1).
|
|
533
|
+
const DEPENDENCY_CONTROL_CHAR_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
534
|
+
|
|
535
|
+
function sanitizeFencedBody(body: string): string {
|
|
536
|
+
return body.replace(DEPENDENCY_CONTROL_CHAR_PATTERN, "").replace(/<\/dependency-context/g, "</dependency-context");
|
|
537
|
+
}
|
|
538
|
+
|
|
529
539
|
export function renderDependencyOutputContext(context: DependencyOutputContext): string {
|
|
530
540
|
const parts: string[] = [];
|
|
531
541
|
if (context.dependencies.length) {
|
|
@@ -565,7 +575,7 @@ export function renderDependencyOutputContext(context: DependencyOutputContext):
|
|
|
565
575
|
parts.push("", read.content.trim(), "");
|
|
566
576
|
}
|
|
567
577
|
}
|
|
568
|
-
return parts.join("\n").trim();
|
|
578
|
+
return sanitizeFencedBody(parts.join("\n").trim());
|
|
569
579
|
}
|
|
570
580
|
|
|
571
581
|
export function writeTaskSharedOutput(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState): ArtifactDescriptor | undefined {
|
|
@@ -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;
|
|
@@ -623,6 +624,7 @@ export async function finalizeTaskResult(ctx: TaskExecutionContext, execResult:
|
|
|
623
624
|
artifacts: [
|
|
624
625
|
...manifest.artifacts,
|
|
625
626
|
promptArtifact,
|
|
627
|
+
...(breakdownArtifact ? [breakdownArtifact] : []),
|
|
626
628
|
...(resultArtifact ? [resultArtifact] : []),
|
|
627
629
|
inputsArtifact,
|
|
628
630
|
coordinationArtifact,
|
|
@@ -71,6 +71,8 @@ export interface TaskExecutionContext {
|
|
|
71
71
|
skillPaths: string[] | undefined;
|
|
72
72
|
prompt: string;
|
|
73
73
|
promptArtifact: ArtifactDescriptor;
|
|
74
|
+
/** G18: prompt-breakdown artifact descriptor (undefined when PI_CREW_PROMPT_BREAKDOWN off). */
|
|
75
|
+
breakdownArtifact?: ArtifactDescriptor;
|
|
74
76
|
inputsArtifact: ArtifactDescriptor;
|
|
75
77
|
skillArtifact: ArtifactDescriptor | undefined;
|
|
76
78
|
coordinationArtifact: ArtifactDescriptor;
|
|
@@ -343,27 +345,32 @@ export async function prepareTaskExecutionContext(
|
|
|
343
345
|
// SR-02 phase 1: per-section breakdown artifact (opt-in via
|
|
344
346
|
// PI_CREW_PROMPT_BREAKDOWN=1). Adds the SYSTEM-side pieces the prompt
|
|
345
347
|
// 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
|
-
|
|
348
|
+
// G18 (SDD Buổi-1 WI-3 follow-up): capture the descriptor so it flows
|
|
349
|
+
// through ctx into the manifest artifacts index — otherwise the
|
|
350
|
+
// `breakdown` team-tool action cannot find it on real runs.
|
|
351
|
+
const breakdownArtifact: ArtifactDescriptor | undefined = promptBreakdownEnabled()
|
|
352
|
+
? (() => {
|
|
353
|
+
const sections: Record<string, number> = {
|
|
354
|
+
"system.agentDefinition": input.agent?.systemPrompt?.length ?? 0,
|
|
355
|
+
...(promptResult.sections ?? {}),
|
|
356
|
+
"dynamic.preStepOutput": preStepOutput?.length ?? 0,
|
|
357
|
+
};
|
|
358
|
+
return writeArtifact(manifest.artifactsRoot, {
|
|
359
|
+
kind: "metadata",
|
|
360
|
+
relativePath: `metadata/${task.id}.prompt-breakdown.json`,
|
|
361
|
+
content: `${JSON.stringify(
|
|
362
|
+
Object.fromEntries(
|
|
363
|
+
Object.entries(sections)
|
|
364
|
+
.filter(([, chars]) => chars > 0)
|
|
365
|
+
.map(([name, chars]) => [name, { chars, estTokens: estimateTokens(chars) }]),
|
|
366
|
+
),
|
|
367
|
+
null,
|
|
368
|
+
2,
|
|
369
|
+
)}\n`,
|
|
370
|
+
producer: "prompt-breakdown",
|
|
371
|
+
});
|
|
372
|
+
})()
|
|
373
|
+
: undefined;
|
|
367
374
|
|
|
368
375
|
const collectedJsonEvents: Record<string, unknown>[] | undefined = collectYieldEvents ? [] : undefined;
|
|
369
376
|
|
|
@@ -415,6 +422,7 @@ export async function prepareTaskExecutionContext(
|
|
|
415
422
|
skillPaths,
|
|
416
423
|
prompt,
|
|
417
424
|
promptArtifact,
|
|
425
|
+
breakdownArtifact,
|
|
418
426
|
inputsArtifact,
|
|
419
427
|
skillArtifact,
|
|
420
428
|
coordinationArtifact,
|
|
@@ -398,7 +398,7 @@ const sharedFields = {
|
|
|
398
398
|
),
|
|
399
399
|
};
|
|
400
400
|
|
|
401
|
-
// ─── Domain action unions (10+
|
|
401
|
+
// ─── Domain action unions (10+17+7+17+6 = 57 actions) ──────────────────────
|
|
402
402
|
|
|
403
403
|
const ACTION_DESCRIPTION = "Team action. Defaults to 'list' when omitted.";
|
|
404
404
|
|
|
@@ -411,6 +411,7 @@ const STATUS_ACTIONS = [
|
|
|
411
411
|
"get",
|
|
412
412
|
"events",
|
|
413
413
|
"artifacts",
|
|
414
|
+
"breakdown",
|
|
414
415
|
"summary",
|
|
415
416
|
"graph",
|
|
416
417
|
"search",
|