codecartographer-pi 0.9.1 → 0.11.0
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/.codecarto/GUIDE.md +10 -3
- package/.codecarto/findings/porting/SKILL.md +7 -0
- package/.codecarto/findings/reimplementation-spec/SKILL.md +10 -0
- package/.codecarto/templates/architecture-map.md +9 -0
- package/.codecarto/templates/behavioral-contracts.md +9 -0
- package/.codecarto/templates/defect-report.md +9 -0
- package/.codecarto/templates/mechanical-defects.md +9 -0
- package/.codecarto/templates/phase-checkpoint.md +41 -0
- package/.codecarto/templates/protocols-and-state.md +9 -0
- package/.codecarto/templates/reimplementation-spec-opinionated.md +10 -0
- package/.codecarto/templates/reimplementation-spec.md +10 -0
- package/.codecarto/templates/reverse-engineering-bundle.md +29 -3
- package/.codecarto/templates/semantic-defects.md +9 -0
- package/.codecarto/workflow/pipeline-architecture-only.yaml +1 -0
- package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
- package/.codecarto/workflow/pipeline-full-with-audit.yaml +8 -3
- package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +9 -5
- package/.codecarto/workflow/pipeline-lite.yaml +3 -0
- package/.codecarto/workflow/pipeline.yaml +7 -3
- package/README.md +38 -2
- package/dist/core/dashboard.js +178 -15
- package/dist/core/prompts.js +6 -0
- package/dist/core/usage.d.ts +11 -0
- package/dist/core/usage.js +64 -46
- package/dist/extensions/codecarto/agent-rewriter.js +0 -1
- package/dist/extensions/codecarto/agent-runner.d.ts +19 -0
- package/dist/extensions/codecarto/agent-runner.js +68 -6
- package/dist/extensions/codecarto/agent-state.d.ts +3 -0
- package/dist/extensions/codecarto/agent-state.js +2 -0
- package/dist/extensions/codecarto/agent-summary.d.ts +5 -0
- package/dist/extensions/codecarto/agent-summary.js +9 -0
- package/dist/extensions/codecarto/agent-widget.js +6 -0
- package/dist/extensions/codecarto/auto-runner.js +13 -1
- package/dist/extensions/codecarto/dashboard-narrator.js +0 -1
- package/dist/extensions/codecarto/index.d.ts +1 -1
- package/dist/extensions/codecarto/index.js +28 -1
- package/dist/extensions/codecarto/phase-compaction.d.ts +11 -0
- package/dist/extensions/codecarto/phase-compaction.js +115 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -114,6 +114,39 @@ For multi-session work, every new session reads `.codecarto/GUIDE.md` (or the li
|
|
|
114
114
|
|
|
115
115
|
---
|
|
116
116
|
|
|
117
|
+
## Progressive distillation and context resilience
|
|
118
|
+
|
|
119
|
+
CodeCartographer is a progressive, evidence-tagged distillation of a codebase. It does not ask one context window to retain the entire investigation. Instead, each phase turns a large body of source evidence into a smaller, more task-specific artifact that the next phase can read:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
source code
|
|
123
|
+
→ architecture map
|
|
124
|
+
→ behavioral contracts + protocols + defect findings
|
|
125
|
+
→ porting bundle
|
|
126
|
+
→ reimplementation spec
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This is deliberate distillation, not incidental chat summarization. Each artifact follows a template, preserves evidence levels and known unknowns, and must pass validation before it becomes an input to downstream phases.
|
|
130
|
+
|
|
131
|
+
### What happens when conversation context is compacted?
|
|
132
|
+
|
|
133
|
+
The filesystem, not the conversation, is the durable memory of a run:
|
|
134
|
+
|
|
135
|
+
- Each phase gets a fresh context window. In the Pi extension it runs as an isolated phase sub-agent; MCP and drop-in hosts should use the same one-session-per-phase pattern.
|
|
136
|
+
- Completed findings live under `.codecarto/findings/`. Later phases re-read the specific upstream artifacts declared by the active pipeline instead of relying on conversational recall.
|
|
137
|
+
- `workflow/status.yaml` records progress, `open_questions`, and `carry_forward` items routed to later phases. `CONVENTIONS.md`, `DECISIONS.md`, closeouts, and `THREAD_LOG.md` preserve cross-session knowledge and handoffs.
|
|
138
|
+
- Pi phase transcripts are file-backed and remain available through `/resume`, `/tree`, and `/export`, even when the active model context has been compacted.
|
|
139
|
+
- For isolated Pi phase sessions, compaction uses a phase-aware continuation summary that explicitly preserves evidence, files inspected, output progress, open questions, and validation gaps. The resulting summary is also checkpointed atomically at `.codecarto/scratch/checkpoints/<phase>.md`.
|
|
140
|
+
- Pi records successful, failed, and aborted compactions plus their trigger (`threshold`, `overflow`, or `manual`) in local usage data and exposes the totals in the widget, `/codecarto-usage`, completion summaries, and dashboard.
|
|
141
|
+
|
|
142
|
+
As a result, compaction—or even replacement—of the orchestrator session does not erase pipeline progress. A new session can reconstruct the relevant state from disk and continue.
|
|
143
|
+
|
|
144
|
+
The remaining limit is **within a single oversized phase**. Even Pi's phase-aware summary is still a lossy distillation, and MCP/drop-in compaction remains entirely host-controlled. Phase instructions therefore prioritize targeted reads, durable checkpoints, and explicit coverage accounting; if full coverage will not fit, the phase records `PARTIAL` validation and places unresolved work in `open_questions` or `carry_forward`. Cross-phase context loss is largely designed out; intra-phase context pressure is observed and bounded rather than hidden.
|
|
145
|
+
|
|
146
|
+
The porting bundle is the final intentional compression boundary. It carries a source index, load-bearing invariants, defect dispositions, and deep-read triggers. `reimplementation-spec` reads that bundle by default and opens lower-level reports only for a named gap, conflict, missing acceptance detail, or defect rationale.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
117
150
|
## Phases produce these artifacts
|
|
118
151
|
|
|
119
152
|
| Artifact | Description |
|
|
@@ -164,7 +197,7 @@ Every state change re-renders `.codecarto/dashboard.html` — a self-contained s
|
|
|
164
197
|
|
|
165
198
|
- Pipeline progress strip with per-phase status badges
|
|
166
199
|
- Per-phase cards with output links, open questions, carry-forward routing, owner notes, last-run usage
|
|
167
|
-
- Aggregate token
|
|
200
|
+
- Aggregate token and compaction telemetry + per-phase breakdown
|
|
168
201
|
- Activity timeline with session-file links
|
|
169
202
|
- Open questions roll-up grouped by source phase
|
|
170
203
|
- Closeouts list (reverse-chronological) with relative-path links
|
|
@@ -191,11 +224,13 @@ Beyond the slash commands, the Pi extension layers on:
|
|
|
191
224
|
|
|
192
225
|
**File-backed phase sessions.** Phase transcripts persist to the same Pi session directory the orchestrator uses, so `/resume`, `/tree`, and `/export` browse them as first-class sessions. Each appears as `CodeCartographer phase: <id>` with lineage back to the orchestrator's session.
|
|
193
226
|
|
|
227
|
+
**Phase-aware compaction and checkpoints.** Only isolated sessions named `CodeCartographer phase: <id>` receive the specialized compaction prompt. It preserves the phase goal, evidence, inspected files, output progress, open questions, and validation gaps, then writes the resulting summary to `.codecarto/scratch/checkpoints/<phase>.md`. Orchestrator and unrelated Pi sessions retain normal host compaction.
|
|
228
|
+
|
|
194
229
|
**Phase-completion summary in the orchestrator transcript.** When a phase finishes, a Markdown closeout block is appended to the orchestrator's session via `pi.sendMessage(...)`. Visible in the TUI scrollback; available to the orchestrator's LLM as context on your next message. No auto-trigger — you stay in control.
|
|
195
230
|
|
|
196
231
|
**Opt-in LLM-steered seed prompts.** Set `orchestrator.llm_steer_next_phase: true` in `.codecarto/workflow/config.yaml` (or pass `--llm-steer` per invocation), and the orchestrator's LLM rewrites the next phase's seed prompt to highlight relevant prior findings. Off by default — extra orchestrator-side tokens, opt-in. The rewritten prompt is injected into the orchestrator transcript so you can audit what the rewriter chose to emphasize.
|
|
197
232
|
|
|
198
|
-
**Per-phase usage tracking.** Each phase run is appended to `.codecarto/workflow/.usage.local.yaml`. `/codecarto-usage` reports cumulative + per-phase totals.
|
|
233
|
+
**Per-phase usage tracking.** Each phase run is appended to `.codecarto/workflow/.usage.local.yaml`. `/codecarto-usage` reports cumulative + per-phase token, runtime, tool-use, and compaction totals, including threshold/overflow/manual triggers and successful/failed/aborted outcomes.
|
|
199
234
|
|
|
200
235
|
**Tool interception.** `bash` is blocked outright; `edit` and `write` are confined to `.codecarto/`. Same rules apply to phase sub-agents.
|
|
201
236
|
|
|
@@ -204,6 +239,7 @@ Beyond the slash commands, the Pi extension layers on:
|
|
|
204
239
|
| Command | Purpose |
|
|
205
240
|
|---|---|
|
|
206
241
|
| `/codecarto-init [variant]` | Copy `.codecarto/` into the current repository, select pipeline variant |
|
|
242
|
+
| `/codecarto-open` | Activate an existing `.codecarto/` workspace in a new Pi session without resetting durable state |
|
|
207
243
|
| `/codecarto-status` | Current phase, progress, open questions |
|
|
208
244
|
| `/codecarto-next [--auto [--strict]] [--llm-steer \| --no-llm-steer]` | Spawn the next eligible phase as a sub-agent. `--auto` walks the full pipeline end-to-end (auto-validate + auto-complete + advance); `--strict` flips the `PASS WITH GAPS` rule from "advance" to "pause". |
|
|
209
245
|
| `/codecarto-phase <id>` | Force a specific phase, even out of pipeline order |
|
package/dist/core/dashboard.js
CHANGED
|
@@ -14,6 +14,7 @@ export function renderDashboard(inputs) {
|
|
|
14
14
|
`<div class="cc-main">`,
|
|
15
15
|
renderHeader(inputs),
|
|
16
16
|
renderStalenessWarning(inputs),
|
|
17
|
+
renderHealthPanel(inputs),
|
|
17
18
|
inputs.narration ? renderNarration(inputs.narration, completedPhaseCount(inputs.status)) : "",
|
|
18
19
|
renderKeyResults(inputs),
|
|
19
20
|
renderProgressBar(inputs.pipeline, inputs.status),
|
|
@@ -84,7 +85,7 @@ function renderHeader(inputs) {
|
|
|
84
85
|
const { status, pipeline, packageVersion, generatedAt } = inputs;
|
|
85
86
|
const projectName = status.project_name || "(unnamed project)";
|
|
86
87
|
const pipelineLabel = pipeline.workflow_name || status.pipeline;
|
|
87
|
-
const currentPhase = status
|
|
88
|
+
const currentPhase = displayCurrentPhase(status);
|
|
88
89
|
const totals = computeTotals(inputs.usage);
|
|
89
90
|
const completed = completedPhaseCount(status);
|
|
90
91
|
const total = pipeline.phase_order.length;
|
|
@@ -100,7 +101,7 @@ function renderHeader(inputs) {
|
|
|
100
101
|
renderStat("Pipeline", pipelineLabel),
|
|
101
102
|
renderStat("Current phase", currentPhase),
|
|
102
103
|
renderStat("Progress", `${completed}/${total} phases`),
|
|
103
|
-
renderStat("Recorded tokens",
|
|
104
|
+
renderStat("Recorded tokens", formatTokensForDashboard(inputs.usage)),
|
|
104
105
|
renderStat("Tool uses", String(totals.tool_uses)),
|
|
105
106
|
renderStat("Package", `v${packageVersion}`),
|
|
106
107
|
`</div>`,
|
|
@@ -125,6 +126,66 @@ function renderStalenessWarning(inputs) {
|
|
|
125
126
|
return "";
|
|
126
127
|
return `<section class="cc-warning" data-section><strong>Dashboard may be stale.</strong> Status was updated at ${escapeHtml(inputs.status.last_updated)} after this dashboard was generated at ${escapeHtml(inputs.generatedAt)}. Regenerate with <code>/codecarto-dashboard</code>.</section>`;
|
|
127
128
|
}
|
|
129
|
+
function renderHealthPanel(inputs) {
|
|
130
|
+
const { status, pipeline, usage } = inputs;
|
|
131
|
+
const completed = completedPhaseCount(status);
|
|
132
|
+
const total = pipeline.phase_order.length;
|
|
133
|
+
const totals = computeTotals(usage);
|
|
134
|
+
const issues = collectDashboardIssues(inputs);
|
|
135
|
+
const openQuestionCount = countOpenQuestions(status);
|
|
136
|
+
const carryForwardCount = countCarryForward(status);
|
|
137
|
+
const health = issues.some((i) => i.severity === "blocker") ? "attention required" : issues.length ? "review recommended" : completed === total ? "complete" : "on track";
|
|
138
|
+
const healthClass = issues.some((i) => i.severity === "blocker") ? "bad" : issues.length ? "warn" : "ok";
|
|
139
|
+
const tokenText = usageHasTokenAccounting(usage) ? formatTokenCount(totals.tokens.input + totals.tokens.output) : usage.runs.length ? "unavailable" : "0";
|
|
140
|
+
const issueMarkup = issues.length
|
|
141
|
+
? `<div class="cc-health-issues">${issues.map(renderDashboardIssue).join("\n")}</div>`
|
|
142
|
+
: `<p class="cc-health-ok">No blocking artifact gaps detected.</p>`;
|
|
143
|
+
return [
|
|
144
|
+
`<section class="cc-card cc-health cc-health-${healthClass}" aria-label="Dashboard health" data-section data-search-text="health status blockers missing artifacts open questions">`,
|
|
145
|
+
`<div class="cc-health-hero">`,
|
|
146
|
+
`<div><div class="cc-eyebrow">Pipeline health</div><h2>${escapeHtml(health)}</h2><p>${escapeHtml(status.current_phase && status.current_phase !== "complete" ? `Current phase: ${status.current_phase}` : "Pipeline complete — all phases have finished.")}</p></div>`,
|
|
147
|
+
`<div class="cc-health-ring" aria-label="${completed} of ${total} phases complete"><strong>${completed}/${total}</strong><span>phases</span></div>`,
|
|
148
|
+
`</div>`,
|
|
149
|
+
`<div class="cc-health-grid">`,
|
|
150
|
+
renderHealthMetric("Artifacts needing attention", String(issues.length), issues.length ? "bad" : "ok"),
|
|
151
|
+
renderHealthMetric("Open questions", String(openQuestionCount), openQuestionCount ? "warn" : "ok"),
|
|
152
|
+
renderHealthMetric("Carry-forward items", String(carryForwardCount), carryForwardCount ? "warn" : "ok"),
|
|
153
|
+
renderHealthMetric("Tool uses", String(totals.tool_uses), "neutral"),
|
|
154
|
+
renderHealthMetric("Runtime", formatMillis(totals.duration_ms), "neutral"),
|
|
155
|
+
renderHealthMetric("Tokens", tokenText, tokenText === "unavailable" ? "warn" : "neutral"),
|
|
156
|
+
`</div>`,
|
|
157
|
+
issueMarkup,
|
|
158
|
+
`</section>`,
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
function renderHealthMetric(label, value, tone) {
|
|
162
|
+
return `<div class="cc-health-metric cc-tone-${tone}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
|
163
|
+
}
|
|
164
|
+
function renderDashboardIssue(issue) {
|
|
165
|
+
const path = issue.path ? `<code>${escapeHtml(issue.path)}</code>` : "";
|
|
166
|
+
return `<article class="cc-health-issue cc-issue-${issue.severity}"><div><span class="cc-pill ${issue.severity === "blocker" ? "cc-pill-bad" : ""}">${escapeHtml(issue.severity)}</span><a href="#${phaseAnchor(issue.phaseId)}">${escapeHtml(issue.phaseId)}</a></div><strong>${escapeHtml(issue.title)}</strong><p>${escapeHtml(issue.detail)} ${path}</p></article>`;
|
|
167
|
+
}
|
|
168
|
+
function collectDashboardIssues(inputs) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const phaseId of inputs.pipeline.phase_order) {
|
|
171
|
+
const phase = getPhase(inputs.pipeline, phaseId);
|
|
172
|
+
if (!phase?.primary_output)
|
|
173
|
+
continue;
|
|
174
|
+
const state = phaseRenderState(inputs.status, phaseId);
|
|
175
|
+
const primary = inputs.outputsPresent.get(phaseId)?.primary;
|
|
176
|
+
const shouldExist = state === "complete" || state === "current" || state === "running";
|
|
177
|
+
if (shouldExist && primary?.exists === false) {
|
|
178
|
+
out.push({
|
|
179
|
+
severity: state === "complete" || state === "current" ? "blocker" : "warning",
|
|
180
|
+
phaseId,
|
|
181
|
+
title: "Required primary output is missing",
|
|
182
|
+
detail: state === "complete" ? "Phase is marked complete but the dashboard cannot find its primary artifact at" : "This phase is active or ready, but its required artifact is not present at",
|
|
183
|
+
path: phase.primary_output,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
128
189
|
function renderNarration(narration, currentCompletedCount) {
|
|
129
190
|
const runsSince = Math.max(0, currentCompletedCount - narration.phaseCountAtGeneration);
|
|
130
191
|
const staleness = runsSince === 0 ? "current" : `${runsSince} run${runsSince === 1 ? "" : "s"} since`;
|
|
@@ -305,38 +366,84 @@ function renderPhaseOwnerNotes(phaseState) {
|
|
|
305
366
|
function renderPhaseLastRun(run) {
|
|
306
367
|
if (!run)
|
|
307
368
|
return `<div class="cc-phase-section"><h3>Last run</h3><p class="cc-muted">No usage record for this phase.</p></div>`;
|
|
308
|
-
const tokensTotal = (run
|
|
369
|
+
const tokensTotal = formatRunTokens(run);
|
|
309
370
|
const sessionLink = run.session_file ? renderSafeLink(run.session_file, "transcript") : undefined;
|
|
310
371
|
const session = sessionLink ? `<dt>Session</dt><dd>${sessionLink}</dd>` : "";
|
|
311
|
-
|
|
372
|
+
const compactions = run.compactions ? `<dt>Compactions</dt><dd>${formatCompactionCounts(run.compactions)}</dd>` : "";
|
|
373
|
+
return [`<div class="cc-phase-section">`, `<h3>Last run</h3>`, `<dl class="cc-run-meta">`, `<dt>Timestamp</dt><dd>${escapeHtml(run.timestamp)}</dd>`, `<dt>Status</dt><dd>${escapeHtml(run.status)}</dd>`, `<dt>Turns</dt><dd>${run.turn_count}</dd>`, `<dt>Tool uses</dt><dd>${run.tool_uses}</dd>`, `<dt>Tokens</dt><dd>${escapeHtml(tokensTotal)}</dd>`, `<dt>Duration</dt><dd>${escapeHtml(formatMillis(run.duration_ms))}</dd>`, compactions, session, `</dl>`, `</div>`].join("");
|
|
312
374
|
}
|
|
313
375
|
function renderUsagePanel(inputs) {
|
|
314
376
|
const { usage, pipeline, status } = inputs;
|
|
315
377
|
const totals = computeTotals(usage);
|
|
316
378
|
const perPhase = computePerPhaseTotals(usage);
|
|
379
|
+
const tokenAccounting = usageHasTokenAccounting(usage);
|
|
380
|
+
const maxTools = Math.max(1, ...[...perPhase.values()].map((t) => t.tool_uses));
|
|
381
|
+
const maxDuration = Math.max(1, ...[...perPhase.values()].map((t) => t.duration_ms));
|
|
317
382
|
const rows = pipeline.phase_order.map((phaseId) => {
|
|
318
383
|
const t = perPhase.get(phaseId);
|
|
319
384
|
if (!t) {
|
|
320
385
|
const complete = status.phases[phaseId]?.status === "complete";
|
|
321
|
-
return `<tr class="${complete ? "cc-usage-missing" : ""}"><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>0</td><td>—</td><td>—</td><td>${complete ? "usage not recorded" : "not run"}</td></tr>`;
|
|
386
|
+
return `<tr class="${complete ? "cc-usage-missing" : ""}"><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>0</td><td>—</td><td>—</td><td>—</td><td>—</td><td>${complete ? "usage not recorded" : "not run"}</td></tr>`;
|
|
322
387
|
}
|
|
323
388
|
const tokensTotal = t.tokens.input + t.tokens.output;
|
|
324
|
-
|
|
389
|
+
const compactions = t.compaction_runs > 0 ? formatCompactionTriplet(t.compactions) : "unavailable";
|
|
390
|
+
return `<tr><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>${t.runs}</td><td>${escapeHtml(tokenAccounting ? formatTokenCount(tokensTotal) : "unavailable")}</td><td>${renderUsageBar(t.tool_uses, maxTools, String(t.tool_uses))}</td><td>${renderUsageBar(t.duration_ms, maxDuration, formatMillis(t.duration_ms))}</td><td>${compactions}</td><td>${usagePhaseNote(phaseId, status)}</td></tr>`;
|
|
325
391
|
}).join("");
|
|
326
392
|
return [
|
|
327
393
|
`<section class="cc-card cc-usage" id="usage" aria-label="Token usage" data-section data-search-text="usage tokens tool duration">`,
|
|
328
394
|
`<div class="cc-section-head"><h2>Usage</h2><span>${totals.runs} runs</span></div>`,
|
|
329
|
-
usage.runs.length === 0 ? `<p class="cc-empty">No phase runs recorded yet.</p>` : `<dl class="cc-usage-totals">${renderUsageTotalsList(totals)}</dl>`,
|
|
395
|
+
usage.runs.length === 0 ? `<p class="cc-empty">No phase runs recorded yet.</p>` : `<dl class="cc-usage-totals">${renderUsageTotalsList(totals, tokenAccounting)}</dl>`,
|
|
396
|
+
renderUsageInsights(perPhase, status),
|
|
330
397
|
`<table class="cc-usage-table">`,
|
|
331
|
-
`<thead><tr><th>Phase</th><th>Runs</th><th>Tokens</th><th>Tools</th><th>Duration</th></tr></thead>`,
|
|
398
|
+
`<thead><tr><th>Phase</th><th>Runs</th><th>Tokens</th><th>Tools</th><th>Duration</th><th>Compactions</th><th>State</th></tr></thead>`,
|
|
332
399
|
`<tbody>${rows}</tbody>`,
|
|
333
400
|
`</table>`,
|
|
334
401
|
`</section>`,
|
|
335
402
|
].join("\n");
|
|
336
403
|
}
|
|
337
|
-
function renderUsageTotalsList(totals) {
|
|
404
|
+
function renderUsageTotalsList(totals, tokenAccounting) {
|
|
338
405
|
const tokensTotal = totals.tokens.input + totals.tokens.output;
|
|
339
|
-
|
|
406
|
+
const tokenDetail = tokenAccounting
|
|
407
|
+
? `${escapeHtml(formatTokenCount(totals.tokens.input))} / ${escapeHtml(formatTokenCount(totals.tokens.output))} / ${escapeHtml(formatTokenCount(totals.tokens.cache_write))}`
|
|
408
|
+
: `<span class="cc-muted">unavailable — host did not report token counts</span>`;
|
|
409
|
+
const tokenTotal = tokenAccounting ? escapeHtml(formatTokenCount(tokensTotal)) : `<span class="cc-muted">unavailable</span>`;
|
|
410
|
+
const compactionCounts = totals.compaction_runs > 0 ? formatCompactionCounts(totals.compactions) : `<span class="cc-muted">unavailable — host did not report compaction events</span>`;
|
|
411
|
+
const compactionReasons = totals.compaction_runs > 0 ? `<dt>Reasons</dt><dd>${formatCompactionReasons(totals.compactions)}</dd>` : "";
|
|
412
|
+
return [`<dt>Total runs</dt><dd>${totals.runs}</dd>`, `<dt>Tokens (in / out / cache)</dt><dd>${tokenDetail}</dd>`, `<dt>Total tokens</dt><dd>${tokenTotal}</dd>`, `<dt>Tool uses</dt><dd>${totals.tool_uses}</dd>`, `<dt>Total duration</dt><dd>${escapeHtml(formatMillis(totals.duration_ms))}</dd>`, `<dt>Compactions</dt><dd>${compactionCounts}</dd>`, compactionReasons].join("");
|
|
413
|
+
}
|
|
414
|
+
function formatCompactionCounts(value) {
|
|
415
|
+
return `${value.successful} successful · ${value.failed} failed · ${value.aborted} aborted`;
|
|
416
|
+
}
|
|
417
|
+
function formatCompactionReasons(value) {
|
|
418
|
+
return `threshold ${value.reasons.threshold} · overflow ${value.reasons.overflow} · manual ${value.reasons.manual}`;
|
|
419
|
+
}
|
|
420
|
+
function formatCompactionTriplet(value) {
|
|
421
|
+
return `${value.successful} / ${value.failed} / ${value.aborted}`;
|
|
422
|
+
}
|
|
423
|
+
function renderUsageInsights(perPhase, status) {
|
|
424
|
+
if (perPhase.size === 0)
|
|
425
|
+
return "";
|
|
426
|
+
const entries = [...perPhase.entries()];
|
|
427
|
+
const longest = entries.reduce((best, entry) => entry[1].duration_ms > best[1].duration_ms ? entry : best, entries[0]);
|
|
428
|
+
const mostTools = entries.reduce((best, entry) => entry[1].tool_uses > best[1].tool_uses ? entry : best, entries[0]);
|
|
429
|
+
const current = status.current_phase ? perPhase.get(status.current_phase) : undefined;
|
|
430
|
+
return [
|
|
431
|
+
`<div class="cc-usage-insights">`,
|
|
432
|
+
renderInsight("Longest phase", longest[0], formatMillis(longest[1].duration_ms)),
|
|
433
|
+
renderInsight("Most tool-heavy", mostTools[0], `${mostTools[1].tool_uses} tools`),
|
|
434
|
+
current && status.current_phase ? renderInsight("Current phase usage", status.current_phase, `${current.runs} run${current.runs === 1 ? "" : "s"} · ${formatMillis(current.duration_ms)}`) : "",
|
|
435
|
+
`</div>`,
|
|
436
|
+
].filter(Boolean).join("\n");
|
|
437
|
+
}
|
|
438
|
+
function renderInsight(label, phaseId, value) {
|
|
439
|
+
return `<article class="cc-insight"><span>${escapeHtml(label)}</span><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a><strong>${escapeHtml(value)}</strong></article>`;
|
|
440
|
+
}
|
|
441
|
+
function renderUsageBar(value, max, label) {
|
|
442
|
+
const pct = Math.max(3, Math.min(100, Math.round((value / max) * 100)));
|
|
443
|
+
return `<span class="cc-bar-cell"><span class="cc-bar" style="--cc-bar:${pct}%"></span><span>${escapeHtml(label)}</span></span>`;
|
|
444
|
+
}
|
|
445
|
+
function usagePhaseNote(phaseId, status) {
|
|
446
|
+
return escapeHtml(phaseRenderState(status, phaseId));
|
|
340
447
|
}
|
|
341
448
|
function renderActivityTimeline(runs) {
|
|
342
449
|
if (runs.length === 0)
|
|
@@ -344,17 +451,19 @@ function renderActivityTimeline(runs) {
|
|
|
344
451
|
const sorted = [...runs].sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1));
|
|
345
452
|
const visible = sorted.slice(0, TIMELINE_VISIBLE_COUNT);
|
|
346
453
|
const overflow = sorted.slice(TIMELINE_VISIBLE_COUNT);
|
|
454
|
+
const hasSessionLinks = sorted.some((run) => Boolean(run.session_file && safeRelativeHref(run.session_file)));
|
|
347
455
|
const rowOf = (run) => {
|
|
348
|
-
const tokensTotal = (run
|
|
456
|
+
const tokensTotal = formatRunTokens(run);
|
|
349
457
|
const sessionCell = run.session_file ? (renderSafeLink(run.session_file, "session") ?? "—") : "—";
|
|
350
|
-
return `<tr><td>${escapeHtml(run.timestamp)}</td><td><a href="#${phaseAnchor(run.phase)}">${escapeHtml(run.phase)}</a></td><td>${escapeHtml(run.status)}</td><td>${run.turn_count}</td><td>${run.tool_uses}</td><td>${escapeHtml(
|
|
458
|
+
return `<tr><td>${escapeHtml(run.timestamp)}</td><td><a href="#${phaseAnchor(run.phase)}">${escapeHtml(run.phase)}</a></td><td>${escapeHtml(run.status)}</td><td>${run.turn_count}</td><td>${run.tool_uses}</td><td>${escapeHtml(tokensTotal)}</td><td>${escapeHtml(formatMillis(run.duration_ms))}</td>${hasSessionLinks ? `<td>${sessionCell}</td>` : ""}</tr>`;
|
|
351
459
|
};
|
|
352
460
|
const overflowBlock = overflow.length === 0 ? "" : [`<details class="cc-timeline-older">`, `<summary>Older runs (${overflow.length})</summary>`, `<table class="cc-timeline-table">`, `<tbody>${overflow.map(rowOf).join("")}</tbody>`, `</table>`, `</details>`].join("\n");
|
|
353
|
-
return [`<section class="cc-card cc-timeline" aria-label="Activity timeline" data-section data-search-text="activity timeline sessions">`, `<div class="cc-section-head"><h2>Activity timeline</h2><span>newest first</span></div>`, `<table class="cc-timeline-table">`, `<thead><tr><th>When</th><th>Phase</th><th>Status</th><th
|
|
461
|
+
return [`<section class="cc-card cc-timeline" aria-label="Activity timeline" data-section data-search-text="activity timeline sessions">`, `<div class="cc-section-head"><h2>Activity timeline</h2><span>newest first</span></div>`, `<table class="cc-timeline-table">`, `<thead><tr><th>When</th><th>Phase</th><th>Status</th><th>Turns</th><th>Tools</th><th>Tokens</th><th>Duration</th>${hasSessionLinks ? "<th>Session</th>" : ""}</tr></thead>`, `<tbody>${visible.map(rowOf).join("")}</tbody>`, `</table>`, overflowBlock, `</section>`].join("\n");
|
|
354
462
|
}
|
|
355
463
|
function renderOpenQuestionsRollup(status) {
|
|
356
464
|
const buckets = [];
|
|
357
465
|
const seen = new Set();
|
|
466
|
+
const byKind = new Map();
|
|
358
467
|
let total = 0;
|
|
359
468
|
for (const [phaseId, phaseState] of Object.entries(status.phases)) {
|
|
360
469
|
const questions = [];
|
|
@@ -363,6 +472,8 @@ function renderOpenQuestionsRollup(status) {
|
|
|
363
472
|
if (seen.has(key))
|
|
364
473
|
continue;
|
|
365
474
|
seen.add(key);
|
|
475
|
+
const kind = String(q.kind ?? "unspecified");
|
|
476
|
+
byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
|
|
366
477
|
questions.push(renderOpenQuestion(q));
|
|
367
478
|
}
|
|
368
479
|
if (questions.length === 0)
|
|
@@ -372,7 +483,8 @@ function renderOpenQuestionsRollup(status) {
|
|
|
372
483
|
}
|
|
373
484
|
if (total === 0)
|
|
374
485
|
return "";
|
|
375
|
-
|
|
486
|
+
const kindSummary = [...byKind.entries()].sort((a, b) => b[1] - a[1]).map(([kind, count]) => `<span class="cc-kind-chip"><strong>${count}</strong>${escapeHtml(kind)}</span>`).join("");
|
|
487
|
+
return [`<section class="cc-card cc-rollup" aria-label="Open questions roll-up" data-section data-search-text="open questions">`, `<div class="cc-section-head"><h2>Open questions</h2><span>${total} unique</span></div>`, `<div class="cc-kind-summary">${kindSummary}</div>`, buckets.join("\n"), `</section>`].join("\n");
|
|
376
488
|
}
|
|
377
489
|
function renderCloseoutsList(inputs) {
|
|
378
490
|
const closeouts = inputs.closeouts;
|
|
@@ -478,9 +590,32 @@ function phaseRenderState(status, phaseId) {
|
|
|
478
590
|
return "current";
|
|
479
591
|
return "pending";
|
|
480
592
|
}
|
|
593
|
+
function displayCurrentPhase(status) {
|
|
594
|
+
return status.current_phase === "complete" ? "Pipeline complete" : status.current_phase || "—";
|
|
595
|
+
}
|
|
481
596
|
function completedPhaseCount(status) {
|
|
482
597
|
return Object.values(status.phases).filter((p) => p.status === "complete").length;
|
|
483
598
|
}
|
|
599
|
+
function countOpenQuestions(status) {
|
|
600
|
+
return Object.values(status.phases).reduce((sum, p) => sum + (p.open_questions?.length ?? 0), 0);
|
|
601
|
+
}
|
|
602
|
+
function countCarryForward(status) {
|
|
603
|
+
return Object.values(status.phases).reduce((sum, p) => sum + (p.carry_forward?.length ?? 0), 0);
|
|
604
|
+
}
|
|
605
|
+
function usageHasTokenAccounting(usage) {
|
|
606
|
+
return usage.runs.some((run) => (run.tokens?.input ?? 0) > 0 || (run.tokens?.output ?? 0) > 0 || (run.tokens?.cache_write ?? 0) > 0);
|
|
607
|
+
}
|
|
608
|
+
function formatTokensForDashboard(usage) {
|
|
609
|
+
const totals = computeTotals(usage);
|
|
610
|
+
if (usage.runs.length > 0 && !usageHasTokenAccounting(usage))
|
|
611
|
+
return "unavailable";
|
|
612
|
+
return formatTokenCount(totals.tokens.input + totals.tokens.output);
|
|
613
|
+
}
|
|
614
|
+
function formatRunTokens(run) {
|
|
615
|
+
const total = (run.tokens?.input ?? 0) + (run.tokens?.output ?? 0);
|
|
616
|
+
const hasAccounting = total > 0 || (run.tokens?.cache_write ?? 0) > 0;
|
|
617
|
+
return hasAccounting ? formatTokenCount(total) : "unavailable";
|
|
618
|
+
}
|
|
484
619
|
function lastRunPerPhase(runs) {
|
|
485
620
|
const out = new Map();
|
|
486
621
|
for (const r of runs) {
|
|
@@ -586,6 +721,24 @@ h2 { margin: 0; font-size: 18px; } h3 { margin: var(--s-2) 0 var(--s-1); color:
|
|
|
586
721
|
.cc-meta-details { margin-top: var(--s-3); } .cc-header-meta, .cc-run-meta, .cc-usage-totals { display: grid; grid-template-columns: max-content 1fr; gap: var(--s-1) var(--s-3); margin: var(--s-2) 0; } dt { color: var(--fg-dim); } dd { margin: 0; }
|
|
587
722
|
.cc-card, .cc-warning { padding: var(--s-3); margin-bottom: var(--s-4); }
|
|
588
723
|
.cc-warning { border-color: var(--status-running); background: color-mix(in srgb, var(--status-running) 9%, var(--bg-card)); }
|
|
724
|
+
.cc-health { border-width: 1px; position: relative; overflow: hidden; }
|
|
725
|
+
.cc-health::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 4px; background: var(--accent-2); }
|
|
726
|
+
.cc-health-bad::before { background: var(--status-error); } .cc-health-warn::before { background: var(--status-running); } .cc-health-ok::before { background: var(--status-complete); }
|
|
727
|
+
.cc-health-hero { display: flex; justify-content: space-between; align-items: center; gap: var(--s-3); margin-bottom: var(--s-3); }
|
|
728
|
+
.cc-health-hero h2 { font-size: clamp(24px, 3vw, 38px); text-transform: capitalize; margin: 2px 0; }
|
|
729
|
+
.cc-health-hero p { color: var(--fg-dim); margin: 0; }
|
|
730
|
+
.cc-health-ring { width: 112px; height: 112px; border-radius: 999px; display: grid; place-items: center; align-content: center; background: radial-gradient(circle at center, var(--bg-card) 58%, transparent 59%), conic-gradient(var(--accent-3), var(--accent-2)); border: 1px solid var(--border); flex: 0 0 auto; }
|
|
731
|
+
.cc-health-ring strong { font-size: 24px; line-height: 1; } .cc-health-ring span { color: var(--fg-dim); font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }
|
|
732
|
+
.cc-health-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--s-2); margin-bottom: var(--s-3); }
|
|
733
|
+
.cc-health-metric { padding: var(--s-2); border: 1px solid var(--border); border-radius: 14px; background: var(--bg); min-width: 0; }
|
|
734
|
+
.cc-health-metric span { display: block; color: var(--fg-dim); font-size: 12px; } .cc-health-metric strong { display: block; margin-top: 2px; font-size: 18px; overflow-wrap: anywhere; }
|
|
735
|
+
.cc-tone-ok strong { color: var(--status-complete); } .cc-tone-warn strong { color: var(--status-running); } .cc-tone-bad strong { color: var(--status-error); }
|
|
736
|
+
.cc-health-issues { display: grid; gap: var(--s-2); }
|
|
737
|
+
.cc-health-issue { padding: var(--s-2); border: 1px solid var(--border); border-radius: 14px; background: var(--bg); }
|
|
738
|
+
.cc-health-issue div { display: flex; flex-wrap: wrap; gap: var(--s-1); align-items: center; margin-bottom: 2px; }
|
|
739
|
+
.cc-health-issue strong { display: block; font-size: 15px; } .cc-health-issue p { color: var(--fg-dim); margin: 2px 0 0; }
|
|
740
|
+
.cc-issue-blocker { border-color: color-mix(in srgb, var(--status-error) 55%, var(--border)); background: color-mix(in srgb, var(--status-error) 8%, var(--bg-card)); }
|
|
741
|
+
.cc-health-ok { color: var(--status-complete); margin: 0; }
|
|
589
742
|
.cc-section-head { display: flex; justify-content: space-between; gap: var(--s-2); align-items: baseline; padding-bottom: var(--s-2); margin-bottom: var(--s-2); border-bottom: 1px solid var(--border); }
|
|
590
743
|
.cc-section-head span, .cc-muted, .cc-empty { color: var(--fg-dim); }
|
|
591
744
|
.cc-narration pre { white-space: pre-wrap; word-wrap: break-word; margin: 0; font-family: inherit; }
|
|
@@ -616,6 +769,15 @@ h2 { margin: 0; font-size: 18px; } h3 { margin: var(--s-2) 0 var(--s-1); color:
|
|
|
616
769
|
.cc-usage-table th, .cc-usage-table td, .cc-timeline-table th, .cc-timeline-table td { padding: var(--s-1) var(--s-2); text-align: left; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
|
|
617
770
|
.cc-usage-table th, .cc-timeline-table th { color: var(--fg-dim); font-weight: 700; text-transform: uppercase; font-size: 11px; letter-spacing: .04em; }
|
|
618
771
|
.cc-usage-missing td:last-child { color: var(--status-running); }
|
|
772
|
+
.cc-usage-insights { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-2); margin: var(--s-3) 0; }
|
|
773
|
+
.cc-insight { padding: var(--s-2); border: 1px solid var(--border); border-radius: 12px; background: var(--bg); display: grid; gap: 2px; }
|
|
774
|
+
.cc-insight span { color: var(--fg-dim); font-size: 12px; } .cc-insight a { font-weight: 800; } .cc-insight strong { font-size: 15px; }
|
|
775
|
+
.cc-bar-cell { min-width: 120px; display: grid; grid-template-columns: minmax(42px, 1fr) auto; align-items: center; gap: var(--s-2); }
|
|
776
|
+
.cc-bar-cell > span:last-child { min-width: 48px; text-align: right; }
|
|
777
|
+
.cc-bar { height: 8px; border-radius: 999px; background: linear-gradient(90deg, var(--accent-2) var(--cc-bar), var(--bg-soft) var(--cc-bar)); border: 1px solid var(--border); }
|
|
778
|
+
.cc-kind-summary { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0 var(--s-3); }
|
|
779
|
+
.cc-kind-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 9px; border: 1px solid var(--border); border-radius: 999px; background: var(--bg); color: var(--fg-dim); font-size: 12px; }
|
|
780
|
+
.cc-kind-chip strong { color: var(--fg); }
|
|
619
781
|
.cc-timeline-older { margin-top: var(--s-2); }
|
|
620
782
|
.cc-footer { margin-top: var(--s-5); padding-top: var(--s-3); border-top: 1px solid var(--border); color: var(--fg-dim); font-size: 13px; }
|
|
621
783
|
@media (max-width: 900px) {
|
|
@@ -624,7 +786,8 @@ h2 { margin: 0; font-size: 18px; } h3 { margin: var(--s-2) 0 var(--s-1); color:
|
|
|
624
786
|
body.cc-sidebar-open .cc-sidebar { transform: translateX(0); }
|
|
625
787
|
.cc-menu-button { display: block; }
|
|
626
788
|
.cc-main { padding: var(--s-3); padding-top: var(--s-5); }
|
|
627
|
-
.cc-stat-grid, .cc-artifact-row, .cc-closeout-row { grid-template-columns: 1fr; }
|
|
789
|
+
.cc-stat-grid, .cc-artifact-row, .cc-closeout-row, .cc-health-grid, .cc-usage-insights { grid-template-columns: 1fr; }
|
|
790
|
+
.cc-health-hero { align-items: flex-start; } .cc-health-ring { width: 88px; height: 88px; }
|
|
628
791
|
.cc-header-meta, .cc-run-meta, .cc-usage-totals { grid-template-columns: 1fr; }
|
|
629
792
|
table { display: block; overflow-x: auto; white-space: nowrap; }
|
|
630
793
|
}
|
package/dist/core/prompts.js
CHANGED
|
@@ -48,6 +48,10 @@ export async function buildPhasePrompt(state, phase, forced, options = {}) {
|
|
|
48
48
|
for (const path of phaseReads) {
|
|
49
49
|
lines.push(`- .codecarto/${path}`);
|
|
50
50
|
}
|
|
51
|
+
const checkpointRelativePath = `scratch/checkpoints/${phase.id}.md`;
|
|
52
|
+
if (await pathExists(join(state.workspaceDir, checkpointRelativePath))) {
|
|
53
|
+
lines.push(`- .codecarto/${checkpointRelativePath} (resume from durable in-phase progress after compaction or interruption)`);
|
|
54
|
+
}
|
|
51
55
|
const conventionsPath = join(state.workspaceDir, "CONVENTIONS.md");
|
|
52
56
|
if (await pathExists(conventionsPath)) {
|
|
53
57
|
lines.push("- .codecarto/CONVENTIONS.md (cross-cutting patterns the orchestrator has promoted)");
|
|
@@ -85,6 +89,8 @@ export async function buildPhasePrompt(state, phase, forced, options = {}) {
|
|
|
85
89
|
lines.push("- Do not modify source files outside .codecarto/.");
|
|
86
90
|
lines.push("- Follow the active pipeline and validation protocol.");
|
|
87
91
|
lines.push("- Update findings under .codecarto/findings/ for this phase.");
|
|
92
|
+
lines.push(`- For long phases, checkpoint resumable progress at .codecarto/scratch/checkpoints/${phase.id}.md; Pi writes this automatically after phase compaction.`);
|
|
93
|
+
lines.push("- Include a Coverage and limits section that names inspected scope, skipped scope, evidence basis, and blind spots; route material gaps through PARTIAL validation and open_questions/carry_forward.");
|
|
88
94
|
lines.push("- Distinguish open_questions (genuinely unknown) from carry_forward (routed to a specific later phase) when updating workflow/status.yaml — see GUIDE.md \"Open Questions vs Carry-Forward\".");
|
|
89
95
|
if (forced) {
|
|
90
96
|
lines.push("- The user explicitly requested this phase even if it is not the next eligible phase.");
|
package/dist/core/usage.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
export declare const USAGE_RELATIVE_PATH = "workflow/.usage.local.yaml";
|
|
2
2
|
export type UsageRunStatus = "completed" | "aborted" | "error";
|
|
3
|
+
export type CompactionReason = "threshold" | "overflow" | "manual";
|
|
3
4
|
export interface UsageTokens {
|
|
4
5
|
input: number;
|
|
5
6
|
output: number;
|
|
6
7
|
cache_write: number;
|
|
7
8
|
}
|
|
9
|
+
export interface CompactionTelemetry {
|
|
10
|
+
successful: number;
|
|
11
|
+
failed: number;
|
|
12
|
+
aborted: number;
|
|
13
|
+
reasons: Record<CompactionReason, number>;
|
|
14
|
+
}
|
|
15
|
+
export declare function emptyCompactionTelemetry(): CompactionTelemetry;
|
|
8
16
|
export interface UsageRun {
|
|
9
17
|
timestamp: string;
|
|
10
18
|
phase: string;
|
|
@@ -14,6 +22,7 @@ export interface UsageRun {
|
|
|
14
22
|
duration_ms: number;
|
|
15
23
|
tokens: UsageTokens;
|
|
16
24
|
session_file?: string;
|
|
25
|
+
compactions?: CompactionTelemetry;
|
|
17
26
|
}
|
|
18
27
|
export interface UsageFile {
|
|
19
28
|
version: number;
|
|
@@ -21,9 +30,11 @@ export interface UsageFile {
|
|
|
21
30
|
}
|
|
22
31
|
export interface UsageTotals {
|
|
23
32
|
runs: number;
|
|
33
|
+
compaction_runs: number;
|
|
24
34
|
tokens: UsageTokens;
|
|
25
35
|
tool_uses: number;
|
|
26
36
|
duration_ms: number;
|
|
37
|
+
compactions: CompactionTelemetry;
|
|
27
38
|
}
|
|
28
39
|
export declare function loadUsage(workspaceDir: string): Promise<UsageFile>;
|
|
29
40
|
export declare function appendUsageRun(workspaceDir: string, run: UsageRun): Promise<void>;
|
package/dist/core/usage.js
CHANGED
|
@@ -13,6 +13,9 @@ import { pathExists } from "./utils.js";
|
|
|
13
13
|
import { parseSimpleYaml, stringifySimpleYaml } from "./yaml.js";
|
|
14
14
|
export const USAGE_RELATIVE_PATH = "workflow/.usage.local.yaml";
|
|
15
15
|
const SCHEMA_VERSION = 1;
|
|
16
|
+
export function emptyCompactionTelemetry() {
|
|
17
|
+
return { successful: 0, failed: 0, aborted: 0, reasons: { threshold: 0, overflow: 0, manual: 0 } };
|
|
18
|
+
}
|
|
16
19
|
export async function loadUsage(workspaceDir) {
|
|
17
20
|
const path = join(workspaceDir, USAGE_RELATIVE_PATH);
|
|
18
21
|
if (!(await pathExists(path)))
|
|
@@ -23,8 +26,6 @@ export async function loadUsage(workspaceDir) {
|
|
|
23
26
|
return normalize(parsed);
|
|
24
27
|
}
|
|
25
28
|
catch {
|
|
26
|
-
// Malformed file: treat as empty rather than blocking the user. They
|
|
27
|
-
// can fix or delete the file; corrupt local state shouldn't stop work.
|
|
28
29
|
return emptyUsage();
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -33,45 +34,56 @@ export async function appendUsageRun(workspaceDir, run) {
|
|
|
33
34
|
current.runs.push(run);
|
|
34
35
|
const path = join(workspaceDir, USAGE_RELATIVE_PATH);
|
|
35
36
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
36
|
-
|
|
37
|
-
await writeFile(tempPath, serialized, "utf8");
|
|
37
|
+
await writeFile(tempPath, `${stringifySimpleYaml(current)}\n`, "utf8");
|
|
38
38
|
await rename(tempPath, path);
|
|
39
39
|
}
|
|
40
40
|
export function computeTotals(file) {
|
|
41
|
-
const totals =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
duration_ms: 0,
|
|
46
|
-
};
|
|
47
|
-
for (const r of file.runs) {
|
|
48
|
-
totals.tokens.input += r.tokens?.input ?? 0;
|
|
49
|
-
totals.tokens.output += r.tokens?.output ?? 0;
|
|
50
|
-
totals.tokens.cache_write += r.tokens?.cache_write ?? 0;
|
|
51
|
-
totals.tool_uses += r.tool_uses ?? 0;
|
|
52
|
-
totals.duration_ms += r.duration_ms ?? 0;
|
|
53
|
-
}
|
|
41
|
+
const totals = emptyTotals();
|
|
42
|
+
totals.runs = file.runs.length;
|
|
43
|
+
for (const run of file.runs)
|
|
44
|
+
addRun(totals, run);
|
|
54
45
|
return totals;
|
|
55
46
|
}
|
|
56
47
|
export function computePerPhaseTotals(file) {
|
|
57
48
|
const byPhase = new Map();
|
|
58
|
-
for (const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
duration_ms: 0,
|
|
64
|
-
};
|
|
65
|
-
t.runs += 1;
|
|
66
|
-
t.tokens.input += r.tokens?.input ?? 0;
|
|
67
|
-
t.tokens.output += r.tokens?.output ?? 0;
|
|
68
|
-
t.tokens.cache_write += r.tokens?.cache_write ?? 0;
|
|
69
|
-
t.tool_uses += r.tool_uses ?? 0;
|
|
70
|
-
t.duration_ms += r.duration_ms ?? 0;
|
|
71
|
-
byPhase.set(r.phase, t);
|
|
49
|
+
for (const run of file.runs) {
|
|
50
|
+
const totals = byPhase.get(run.phase) ?? emptyTotals();
|
|
51
|
+
totals.runs += 1;
|
|
52
|
+
addRun(totals, run);
|
|
53
|
+
byPhase.set(run.phase, totals);
|
|
72
54
|
}
|
|
73
55
|
return byPhase;
|
|
74
56
|
}
|
|
57
|
+
function emptyTotals() {
|
|
58
|
+
return {
|
|
59
|
+
runs: 0,
|
|
60
|
+
compaction_runs: 0,
|
|
61
|
+
tokens: { input: 0, output: 0, cache_write: 0 },
|
|
62
|
+
tool_uses: 0,
|
|
63
|
+
duration_ms: 0,
|
|
64
|
+
compactions: emptyCompactionTelemetry(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function addRun(totals, run) {
|
|
68
|
+
totals.tokens.input += run.tokens?.input ?? 0;
|
|
69
|
+
totals.tokens.output += run.tokens?.output ?? 0;
|
|
70
|
+
totals.tokens.cache_write += run.tokens?.cache_write ?? 0;
|
|
71
|
+
totals.tool_uses += run.tool_uses ?? 0;
|
|
72
|
+
totals.duration_ms += run.duration_ms ?? 0;
|
|
73
|
+
if (run.compactions)
|
|
74
|
+
totals.compaction_runs += 1;
|
|
75
|
+
addCompactions(totals.compactions, run.compactions);
|
|
76
|
+
}
|
|
77
|
+
function addCompactions(target, source) {
|
|
78
|
+
if (!source)
|
|
79
|
+
return;
|
|
80
|
+
target.successful += source.successful;
|
|
81
|
+
target.failed += source.failed;
|
|
82
|
+
target.aborted += source.aborted;
|
|
83
|
+
target.reasons.threshold += source.reasons.threshold;
|
|
84
|
+
target.reasons.overflow += source.reasons.overflow;
|
|
85
|
+
target.reasons.manual += source.reasons.manual;
|
|
86
|
+
}
|
|
75
87
|
function emptyUsage() {
|
|
76
88
|
return { version: SCHEMA_VERSION, runs: [] };
|
|
77
89
|
}
|
|
@@ -79,29 +91,35 @@ function normalize(raw) {
|
|
|
79
91
|
if (!raw || typeof raw !== "object")
|
|
80
92
|
return emptyUsage();
|
|
81
93
|
const runs = Array.isArray(raw.runs) ? raw.runs.filter(isUsageRun) : [];
|
|
82
|
-
return {
|
|
83
|
-
version: typeof raw.version === "number" ? raw.version : SCHEMA_VERSION,
|
|
84
|
-
runs,
|
|
85
|
-
};
|
|
94
|
+
return { version: typeof raw.version === "number" ? raw.version : SCHEMA_VERSION, runs };
|
|
86
95
|
}
|
|
87
96
|
function isUsageRun(x) {
|
|
88
97
|
if (!x || typeof x !== "object")
|
|
89
98
|
return false;
|
|
90
|
-
const
|
|
91
|
-
return (typeof
|
|
92
|
-
typeof
|
|
93
|
-
(
|
|
94
|
-
isFiniteNumber(
|
|
95
|
-
isFiniteNumber(
|
|
96
|
-
isFiniteNumber(
|
|
97
|
-
isUsageTokens(
|
|
98
|
-
(
|
|
99
|
+
const run = x;
|
|
100
|
+
return (typeof run.timestamp === "string" &&
|
|
101
|
+
typeof run.phase === "string" &&
|
|
102
|
+
(run.status === "completed" || run.status === "aborted" || run.status === "error") &&
|
|
103
|
+
isFiniteNumber(run.turn_count) &&
|
|
104
|
+
isFiniteNumber(run.tool_uses) &&
|
|
105
|
+
isFiniteNumber(run.duration_ms) &&
|
|
106
|
+
isUsageTokens(run.tokens) &&
|
|
107
|
+
(run.session_file === undefined || typeof run.session_file === "string") &&
|
|
108
|
+
(run.compactions === undefined || isCompactionTelemetry(run.compactions)));
|
|
99
109
|
}
|
|
100
110
|
function isUsageTokens(x) {
|
|
101
111
|
if (!x || typeof x !== "object")
|
|
102
112
|
return false;
|
|
103
|
-
const
|
|
104
|
-
return isFiniteNumber(
|
|
113
|
+
const tokens = x;
|
|
114
|
+
return isFiniteNumber(tokens.input) && isFiniteNumber(tokens.output) && isFiniteNumber(tokens.cache_write);
|
|
115
|
+
}
|
|
116
|
+
function isCompactionTelemetry(x) {
|
|
117
|
+
if (!x || typeof x !== "object")
|
|
118
|
+
return false;
|
|
119
|
+
const telemetry = x;
|
|
120
|
+
const reasons = telemetry.reasons;
|
|
121
|
+
return isFiniteNumber(telemetry.successful) && isFiniteNumber(telemetry.failed) && isFiniteNumber(telemetry.aborted) &&
|
|
122
|
+
Boolean(reasons) && isFiniteNumber(reasons?.threshold) && isFiniteNumber(reasons?.overflow) && isFiniteNumber(reasons?.manual);
|
|
105
123
|
}
|
|
106
124
|
function isFiniteNumber(x) {
|
|
107
125
|
return typeof x === "number" && Number.isFinite(x);
|
|
@@ -145,7 +145,6 @@ async function runRewriterOnce(ctx, prompt) {
|
|
|
145
145
|
agentDir,
|
|
146
146
|
sessionManager: SessionManager.inMemory(cwd),
|
|
147
147
|
settingsManager: SettingsManager.create(cwd, agentDir),
|
|
148
|
-
modelRegistry: ctx.modelRegistry,
|
|
149
148
|
model: ctx.model,
|
|
150
149
|
tools: [],
|
|
151
150
|
resourceLoader: loader,
|