agent-orchestrator-kit 0.14.0 → 0.15.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/CHANGELOG.md +24 -0
- package/README.md +16 -4
- package/bin/agent-orchestrator.js +162 -23
- package/package.json +2 -1
- package/profiles/generic/orchestrator.yaml +3 -0
- package/profiles/mvp/orchestrator.yaml +3 -0
- package/profiles/node/orchestrator.yaml +3 -0
- package/profiles/vue3/orchestrator.yaml +3 -0
- package/templates/orchestrator.yaml +3 -0
- package/templates/scripts/cursor-spend-collect.cjs +24 -1
- package/templates/scripts/sync-local-agent-skills.sh +27 -0
- package/templates/.cursor/memory.json +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.15.0] - 2026-09-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`costUsdTotal` — one USD figure per change, phase, platform, model, and session.** Each session contributes its billed `costUsd` when present, otherwise its `costUsdEstimated`; sums are rounded to 4 decimals. A change that ran on Amp (billed `$14.48`) plus Claude (`~$5.16`) and Cursor (`~$1.43`) now carries `spend.costUsdTotal: 21.0779` instead of forcing a dashboard to pick `costUsd` and drop the estimated platforms. `costUsd` and `costUsdEstimated` stay separate fields and Amp credits stay out of every USD field. `metrics --summary-json` carries the field; the human `cost:` line prints `$21.08 ($14.48 billed + ~$6.60 est.)`.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Billed sums are rounded like estimates.** `spendByModel[].costUsd`, `spendByPlatform.*.costUsd`, `phases.*.costUsd`, and `spend.costUsd` stored raw float sums such as `4.4399999999999995` and `0.009000000000000001`; they are now `Math.round(x * 10000) / 10000`, the same rule `costUsdEstimated` already had.
|
|
14
|
+
- **Cache-split cost estimates survive the session recompute.** `applyCollectedSessionFields` recomputed `byModel[].costUsdEstimated` from `inputTokens` + `outputTokens` alone and overwrote the value the adapters had already produced. Since `inputTokens` includes `cache_read_*` / `cache_creation_*`, every cached token was billed at the full input rate: a real Claude change with 3.5M tokens estimated `$30.11` instead of `$5.16`, and Cursor rows lost their own rate table to the Claude `$3/$15` fallback. The recompute now only fills rows that have no adapter estimate, so ESTIMATE-ALL still covers Amp rows without `Cost:` while `claude-opus-5` with 100k input + 900k cache read + 10k output stays at `$1.20`.
|
|
15
|
+
|
|
16
|
+
## [0.14.1] - 2026-09-08
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Memory graph survives `handoff` persist.** `loadMemoryItems` classified a memory file by its first character, so every JSONL graph (the format `@modelcontextprotocol/server-memory` reads and writes) was treated as an aggregate `{entities, relations}` document, failed to parse, and returned `[]` — the next persist then overwrote the whole file with just the current change's two entities. Everything the Memory MCP server had accumulated — all `Decision:*`, every other change, and all relations — was destroyed on each persist, leaving cross-session memory permanently one change deep. Detection is now by shape, `handoff --restore` reads JSONL graphs again instead of reporting "Memory JSON empty or missing", and a line that cannot be parsed is preserved verbatim rather than dropped.
|
|
20
|
+
- **`/opsx:*` commands reach the IDEs.** `.agents/commands/` was installed and documented but synced nowhere. `sync` now writes `.cursor/commands/opsx-<phase>.md` (flat, `/opsx-<phase>`) and `.claude/commands/opsx/<phase>.md` (namespaced, so the documented `/opsx:<phase>` exists in Claude Code), with the same stale-file deletion as skills and subagents. `sync-local-agent-skills.sh` matches.
|
|
21
|
+
- **`status` readiness mirrors the archive gates.** "ready to archive" was computed from `tasks.md` checkboxes alone, so it appeared on a change `archive` would refuse for a missing or non-APPROVE verdict. It now applies `require_spec_review` / `require_design_brief` and names the blockers.
|
|
22
|
+
- **`gate-check` no longer passes silently.** When the git diff cannot be computed (shallow clone, missing base ref, no commits) the review gate is verified instead of skipped.
|
|
23
|
+
- The next-thread prompt renders `- Change: <name>` instead of `- Change: - name: <name>`.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
- `templates/.cursor/memory.json` — an unused snapshot of the kit's own development memory. It was referenced by nothing (`init` writes an empty `.cursor/memory.json`), gitignored so it never showed up in a diff, yet the `files` allowlist still published it to npm. `files` now excludes `templates/.cursor` so it cannot ship again.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `pipeline.src_glob` in `orchestrator.yaml` — the paths `gate-check` treats as product code. The hardcoded `src/` default silently disabled the review gate in repos whose code lives elsewhere; `--src-glob` still overrides.
|
|
30
|
+
|
|
7
31
|
## [0.14.0] - 2026-09-07
|
|
8
32
|
|
|
9
33
|
### Changed
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ npx agent-orchestrator-kit@latest init --profile generic --ci gitlab --spec-veri
|
|
|
74
74
|
|
|
75
75
|
See [Installation](#installation) for profile/CI options.
|
|
76
76
|
|
|
77
|
-
**🔄 Already have the kit installed? Upgrade to latest (
|
|
77
|
+
**🔄 Already have the kit installed? Upgrade to latest (v0.15.0 adds `costUsdTotal`, one USD figure per change / phase / platform / model that sums billed Amp with estimated Cursor and Claude, rounds billed sums like estimates, and keeps cache-split Claude estimates from being re-billed at the full input rate):**
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
80
|
npx agent-orchestrator-kit@latest update
|
|
@@ -189,7 +189,7 @@ your-project/
|
|
|
189
189
|
|----------|----------|
|
|
190
190
|
| Orchestration | 5-role pipeline, `AGENTS.md`, `orchestrator.yaml`, review command |
|
|
191
191
|
| OpenSpec skills | All 7 skills for `/opsx:*` workflow |
|
|
192
|
-
| IDE sync | Cursor + Claude Code sync script (`--delete` semantics — removes stale skills/subagents) |
|
|
192
|
+
| IDE sync | Cursor + Claude Code sync script (`--delete` semantics — removes stale skills/subagents/commands) |
|
|
193
193
|
| Subagents | 12 exclusive routes: guide/setup/session-handoff, explore/design/propose/review/archive stage agents, and apply implementation/test/code-review agents — native in Cursor + Claude Code, isolated Amp `subagent-*` wrappers |
|
|
194
194
|
| CLI gates | `npx agent-orchestrator-kit status` / `gate-check` / `archive` / `handoff` / `metrics` / `memory-setup` — deterministic review-gate, archive, session-handoff, and change metrics (always via `npx`; see `cli-via-npm.mdc`) |
|
|
195
195
|
| CI | `agent-verify.yml` — GitHub (default) or GitLab fragment + `prebuild` hook, both run `gate-check` |
|
|
@@ -816,7 +816,7 @@ Schema v2 stores compact `sourceIds`, `sourceTotals`, and `byModel` per session
|
|
|
816
816
|
- **Locked client** — `--restore` records `pending.platform` and Amp `pending.threadId` before phase work. Persist follows that client’s flow even if persist runs in another shell (no `AMP_*` / `CURSOR_*`). Amp: `amp threads export` plus `amp threads usage --details` (`AOK_AMP_BIN`) and local `threads/*.json`. Export supplies `model` / tokens / `agentMode`; usage supplies billed `$`. If Amp runs tools over a pipe (`/dev/null`), thread id comes from `amp threads list`, not stale `session.json` `lastThreadId`. When env and Amp parent do not win, restore locks a fresh `session.json` `lastThreadId` as `amp-session-last`. Cursor: spend hook file. Claude: `~/.claude/projects`. `--collect` still runs all three adapters.
|
|
817
817
|
- **Cursor spend hook (optional)** — Cursor never writes token usage to disk, so the kit can install `scripts/cursor-spend-hook.cjs` plus `.cursor/hooks.json` entries (`stop` / `subagentStop` / `afterAgentResponse`) in `init` / `update` / `sync` / `mcp-setup`: the hook appends each turn's tokens to gitignored `.agents/spend/cursor-usage.jsonl`. After a successful `stop` / `afterAgentResponse` append the hook runs leftover (fail-open, no stdout). Hook and collect resolve the consumer in a multi-root window (not the first cwd with `.agents` or `openspec`). Persist auto-reads that file when the locked client is Cursor. Persist and restore do not self-heal the hook. `sessionEnd` still runs `scripts/cursor-spend-collect.cjs`. Restart Cursor once after the first install. `status` shows a `Spend capture` section. Claude JSONL remains a fallback. Amp web/CLI spend is taken from `amp threads export` (tokens, model, `agentMode`) and `amp threads usage` (billed USD).
|
|
818
818
|
|
|
819
|
-
Aggregates are recomputed on every write: per-phase totals (`startedAt`, `endedAt`, `leadTimeMs` from that phase’s sessions, `durationMs` = sum of session work time — not `totals.leadTimeMs` and not `endedAt − startedAt`, tokens, `costUsd`, `costUsdEstimated` to 4 decimals, `sessions`, `roles`, `models`) plus overall `totals` (`sessions`, `cloudSessions`, `durationMs` = sum of session work time, `leadTimeMs` = wall clock from first session start to last session end), `spend` (USD only), and separate **by platform** / **by model** tables. Numbers are null-honest: a metric nobody reported stays `null`, never a fake `0`. No single total that adds Amp credits to USD.
|
|
819
|
+
Aggregates are recomputed on every write: per-phase totals (`startedAt`, `endedAt`, `leadTimeMs` from that phase’s sessions, `durationMs` = sum of session work time — not `totals.leadTimeMs` and not `endedAt − startedAt`, tokens, `costUsd`, `costUsdEstimated`, `costUsdTotal` to 4 decimals, `sessions`, `roles`, `models`) plus overall `totals` (`sessions`, `cloudSessions`, `durationMs` = sum of session work time, `leadTimeMs` = wall clock from first session start to last session end), `spend` (USD only), and separate **by platform** / **by model** tables. Numbers are null-honest: a metric nobody reported stays `null`, never a fake `0`. No single total that adds Amp credits to USD. `costUsdTotal` is the one USD figure per change, phase, platform, model, and session: each session contributes its billed `costUsd` when present, otherwise its `costUsdEstimated`, so a change that ran on Amp (billed) plus Cursor and Claude (estimated) sums all three platforms instead of showing only the billed part. Amp credits never enter it. The human `cost:` line prints it as `$21.08 ($14.48 billed + ~$6.60 est.)`.
|
|
820
820
|
|
|
821
821
|
Fill `## Metrics` in `handoff.md` **before** persist (unknown is fine; do not invent `0`):
|
|
822
822
|
|
|
@@ -843,7 +843,7 @@ npx agent-orchestrator-kit archive add-thing --sync # finalize + the same t
|
|
|
843
843
|
|
|
844
844
|
#### Для дашбордів
|
|
845
845
|
|
|
846
|
-
Use only `phases.<phase>.startedAt`, `endedAt`, and `durationMs` for phase boundaries and duration. Use `totals.leadTimeMs` only for the whole change. Git log MUST NOT be used for phase boundaries; the kit does not provide per-phase commit counts. `metrics <name> --summary-json` returns only aggregate `totals`, `phases`, and spend maps, without sessions or commits.
|
|
846
|
+
Use only `phases.<phase>.startedAt`, `endedAt`, and `durationMs` for phase boundaries and duration. Use `totals.leadTimeMs` only for the whole change. Git log MUST NOT be used for phase boundaries; the kit does not provide per-phase commit counts. `metrics <name> --summary-json` returns only aggregate `totals`, `phases`, and spend maps, without sessions or commits. For the headline cost use `spend.costUsdTotal` (and `phases.<phase>.costUsdTotal`, `spendByPlatform.<platform>.costUsdTotal`, `spendByModel[].costUsdTotal`): `costUsd` alone is only the billed part and `costUsdEstimated` alone is only the estimated part, so picking one of them drops every platform that reported the other.
|
|
847
847
|
|
|
848
848
|
Recording is on by default and never a persist/archive/`gate-check` gate; opt out per persist with `--no-metrics`. Persist and archive collect the locked client without `--collect`; `--collect` runs every adapter. Flags (`--model`, `--platform`, `--input-tokens`, …) override session totals in `metrics.json` and do not rewrite the `## Metrics` section.
|
|
849
849
|
|
|
@@ -986,11 +986,13 @@ npx agent-orchestrator-kit metrics [change-name] [--json] [--collect]
|
|
|
986
986
|
skills/ # Synced from .agents/skills/
|
|
987
987
|
rules/ # Synced from .agents/rules/
|
|
988
988
|
agents/ # Synced from .agents/subagents/
|
|
989
|
+
commands/ # Synced from .agents/commands/ (flat — /opsx-apply)
|
|
989
990
|
memory.json # Memory MCP data
|
|
990
991
|
|
|
991
992
|
.claude/ # Local only — Claude Code runtime
|
|
992
993
|
skills/ # Synced from .agents/skills/
|
|
993
994
|
agents/ # Synced from .agents/subagents/
|
|
995
|
+
commands/opsx/ # Synced from .agents/commands/ (namespaced — /opsx:apply)
|
|
994
996
|
CLAUDE.md # Synced from root CLAUDE.md
|
|
995
997
|
|
|
996
998
|
.amp/ # Local only — Amp config
|
|
@@ -1015,6 +1017,16 @@ Phase bounds and non-goals: [`openspec/specs/agentic-factory-roadmap/spec.md`](o
|
|
|
1015
1017
|
|
|
1016
1018
|
## Changelog
|
|
1017
1019
|
|
|
1020
|
+
### 0.15.0
|
|
1021
|
+
- **`costUsdTotal`** on `spend`, `phases.*`, `spendByPlatform.*`, `spendByModel[]`, and sessions — billed `costUsd` when present, otherwise `costUsdEstimated`, per session; a change that ran on Amp (billed) plus Cursor and Claude (estimated) now sums all three platforms instead of showing only the billed part; the human `cost:` line prints `$21.08 ($14.48 billed + ~$6.60 est.)` and `--summary-json` carries the field
|
|
1022
|
+
- Billed `costUsd` aggregates are rounded to 4 decimals like estimates (no more `4.4399999999999995` in `spendByModel`)
|
|
1023
|
+
- Cache-split Claude cost estimates survive the session recompute instead of being re-billed at the full input rate (`$30.11` → `$5.16` on a 3.5M-token change)
|
|
1024
|
+
|
|
1025
|
+
### 0.14.1
|
|
1026
|
+
- **Memory graph survives `handoff` persist** — JSONL graphs were misread as an aggregate document and returned empty, so each persist overwrote the file with only the current change; all `Decision:*`, other changes, and relations were lost
|
|
1027
|
+
- `/opsx:*` commands now sync to `.cursor/commands/` (flat) and `.claude/commands/opsx/` (namespaced), so the documented slash commands exist in both IDEs
|
|
1028
|
+
- `status` applies `require_spec_review` / `require_design_brief` before saying "ready to archive"; `gate-check` verifies the gate instead of exiting 0 when the diff is unknown, and reads `pipeline.src_glob` for code outside `src/`
|
|
1029
|
+
|
|
1018
1030
|
### 0.14.0
|
|
1019
1031
|
- **BREAKING: compact metrics schema v2** — sessions persist `sourceIds` / `sourceTotals` / `byModel` instead of `sources`; `metrics --migrate` is schema-only; `metrics --summary-json` for dashboards
|
|
1020
1032
|
- Bounded persist/leftover windows, thread-scoped Amp usage + fresh Cost, Claude dedup/subagent capture, static Claude/Amp estimates
|
|
@@ -220,6 +220,47 @@ function copyDir(src, dest, opts = {}) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// `.agents/commands/opsx-<phase>.md` is the source of truth for the /opsx:*
|
|
224
|
+
// role commands. Cursor reads `.cursor/commands/<file>.md` as `/<file>`, so it
|
|
225
|
+
// gets a flat copy. Claude Code namespaces by subdirectory —
|
|
226
|
+
// `.claude/commands/opsx/<phase>.md` is what makes the documented
|
|
227
|
+
// `/opsx:<phase>` actually exist there.
|
|
228
|
+
function syncCommands(projectDir, ideDir, { namespaced }) {
|
|
229
|
+
const src = join(projectDir, '.agents', 'commands');
|
|
230
|
+
const dest = join(projectDir, ideDir, 'commands');
|
|
231
|
+
if (!existsSync(src)) return;
|
|
232
|
+
|
|
233
|
+
const files = readdirSync(src).filter((f) => f.endsWith('.md'));
|
|
234
|
+
const written = new Set();
|
|
235
|
+
mkdirSync(dest, { recursive: true });
|
|
236
|
+
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
const match = namespaced && file.match(/^([a-z0-9]+)-(.+\.md)$/i);
|
|
239
|
+
const rel = match ? join(match[1], match[2]) : file;
|
|
240
|
+
const destPath = join(dest, rel);
|
|
241
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
242
|
+
copyFileSync(join(src, file), destPath);
|
|
243
|
+
written.add(rel);
|
|
244
|
+
log.ok(destPath.replace(process.cwd() + '/', ''));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Drop commands a kit `update` removed, mirroring the skills/subagents sync.
|
|
248
|
+
const walk = (dir, prefix = '') => {
|
|
249
|
+
for (const entry of readdirSync(dir)) {
|
|
250
|
+
const full = join(dir, entry);
|
|
251
|
+
const rel = prefix ? join(prefix, entry) : entry;
|
|
252
|
+
if (statSync(full).isDirectory()) {
|
|
253
|
+
walk(full, rel);
|
|
254
|
+
if (readdirSync(full).length === 0) rmSync(full, { recursive: true, force: true });
|
|
255
|
+
} else if (!written.has(rel)) {
|
|
256
|
+
rmSync(full, { force: true });
|
|
257
|
+
log.warn(`removed stale: ${join(dest, rel).replace(process.cwd() + '/', '')}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
walk(dest);
|
|
262
|
+
}
|
|
263
|
+
|
|
223
264
|
function gitignoreLines(content) {
|
|
224
265
|
return content.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
225
266
|
}
|
|
@@ -960,6 +1001,26 @@ function parseHandoffMarkdown(content) {
|
|
|
960
1001
|
return sections;
|
|
961
1002
|
}
|
|
962
1003
|
|
|
1004
|
+
// The `## Change` section is a bullet list (`- name: <x>`), but the next-thread
|
|
1005
|
+
// prompt inlines it after its own bullet. Flatten it so the prompt does not
|
|
1006
|
+
// read "- Change: - name: <x>".
|
|
1007
|
+
function inlineChangeLabel(value, fallbackName) {
|
|
1008
|
+
const text = String(value || '').trim();
|
|
1009
|
+
if (!text) return fallbackName;
|
|
1010
|
+
const parts = text
|
|
1011
|
+
.split('\n')
|
|
1012
|
+
.map((line) => line.replace(/^\s*[-*]\s*/, '').trim())
|
|
1013
|
+
.filter(Boolean);
|
|
1014
|
+
if (parts.length === 0) return fallbackName;
|
|
1015
|
+
const named = parts.find((p) => /^name:\s*/i.test(p));
|
|
1016
|
+
if (named) {
|
|
1017
|
+
const rest = parts.filter((p) => p !== named);
|
|
1018
|
+
const label = named.replace(/^name:\s*/i, '').trim() || fallbackName;
|
|
1019
|
+
return rest.length ? `${label} (${rest.join('; ')})` : label;
|
|
1020
|
+
}
|
|
1021
|
+
return parts.join('; ');
|
|
1022
|
+
}
|
|
1023
|
+
|
|
963
1024
|
function firstLineCommand(value) {
|
|
964
1025
|
const match = String(value || '').match(/\/opsx:[^\s`]+(?:\s+[^\s`]+)?/);
|
|
965
1026
|
if (match) return match[0].trim();
|
|
@@ -1397,7 +1458,7 @@ function buildNextSessionPrompt(fields, agentLanguage) {
|
|
|
1397
1458
|
|
|
1398
1459
|
## Повний контекст попередньої сесії (самодостатній — не покладайся лише на Memory)
|
|
1399
1460
|
- Закрита роль: ${fields.closedRole || 'не вказано'}
|
|
1400
|
-
- Зміна: ${fields.change
|
|
1461
|
+
- Зміна: ${inlineChangeLabel(fields.change, name)}
|
|
1401
1462
|
- Зроблено:
|
|
1402
1463
|
${fields.done || 'не вказано'}
|
|
1403
1464
|
- Рішення:
|
|
@@ -1449,7 +1510,7 @@ Do not mix phases. Do not start the following role in this chat until this phase
|
|
|
1449
1510
|
|
|
1450
1511
|
## Full previous-session context (self-contained — do not rely on Memory alone)
|
|
1451
1512
|
- Closed role: ${fields.closedRole || 'not set'}
|
|
1452
|
-
- Change: ${fields.change
|
|
1513
|
+
- Change: ${inlineChangeLabel(fields.change, name)}
|
|
1453
1514
|
- Done:
|
|
1454
1515
|
${fields.done || 'not set'}
|
|
1455
1516
|
- Decisions:
|
|
@@ -1481,26 +1542,48 @@ function loadMemoryItems(filePath) {
|
|
|
1481
1542
|
if (!existsSync(filePath)) return [];
|
|
1482
1543
|
const raw = readFileSync(filePath, 'utf-8').trim();
|
|
1483
1544
|
if (!raw) return [];
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1545
|
+
|
|
1546
|
+
// Aggregate form is a single JSON document: { entities: [...], relations: [...] }.
|
|
1547
|
+
// Every JSONL line is an object too, so the *shape* decides — not the first
|
|
1548
|
+
// character. Keying off `{` classified every JSONL graph as an aggregate,
|
|
1549
|
+
// parsed it as one document, failed, and returned [] — which made the next
|
|
1550
|
+
// persist overwrite the whole graph with just the current change.
|
|
1551
|
+
try {
|
|
1552
|
+
const parsed = JSON.parse(raw);
|
|
1553
|
+
if (parsed && !Array.isArray(parsed) && (Array.isArray(parsed.entities) || Array.isArray(parsed.relations))) {
|
|
1487
1554
|
const entities = (parsed.entities || []).map((entity) => ({ type: 'entity', ...entity }));
|
|
1488
1555
|
const relations = (parsed.relations || []).map((relation) => ({ type: 'relation', ...relation }));
|
|
1489
1556
|
return [...entities, ...relations];
|
|
1557
|
+
}
|
|
1558
|
+
} catch {
|
|
1559
|
+
// Not a single JSON document — fall through to JSONL.
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// JSONL: the format @modelcontextprotocol/server-memory reads and writes,
|
|
1563
|
+
// one entity or relation per line. A line we cannot parse is kept verbatim
|
|
1564
|
+
// so a persist never drops memory it failed to understand.
|
|
1565
|
+
const items = [];
|
|
1566
|
+
for (const line of raw.split('\n')) {
|
|
1567
|
+
const trimmed = line.trim();
|
|
1568
|
+
if (!trimmed) continue;
|
|
1569
|
+
let parsed;
|
|
1570
|
+
try {
|
|
1571
|
+
parsed = JSON.parse(trimmed);
|
|
1490
1572
|
} catch {
|
|
1491
|
-
|
|
1573
|
+
items.push({ __raw: trimmed });
|
|
1574
|
+
continue;
|
|
1492
1575
|
}
|
|
1576
|
+
if (parsed && typeof parsed === 'object' && parsed.type) items.push(parsed);
|
|
1577
|
+
else items.push({ __raw: trimmed });
|
|
1493
1578
|
}
|
|
1494
|
-
return
|
|
1495
|
-
.split('\n')
|
|
1496
|
-
.map((line) => line.trim())
|
|
1497
|
-
.filter(Boolean)
|
|
1498
|
-
.map((line) => JSON.parse(line));
|
|
1579
|
+
return items;
|
|
1499
1580
|
}
|
|
1500
1581
|
|
|
1501
1582
|
function saveMemoryItems(filePath, items) {
|
|
1502
1583
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
1503
|
-
const body = items
|
|
1584
|
+
const body = items
|
|
1585
|
+
.map((item) => (item && item.__raw !== undefined ? item.__raw : JSON.stringify(item)))
|
|
1586
|
+
.join('\n');
|
|
1504
1587
|
writeFileSync(filePath, body ? `${body}\n` : '');
|
|
1505
1588
|
}
|
|
1506
1589
|
|
|
@@ -1627,12 +1710,18 @@ function roundUsd4(x) {
|
|
|
1627
1710
|
return Math.round(Number(x) * 10000) / 10000;
|
|
1628
1711
|
}
|
|
1629
1712
|
|
|
1713
|
+
function costUsdTotalOf(obj) {
|
|
1714
|
+
const billed = numOrNull(obj && obj.costUsd);
|
|
1715
|
+
if (billed != null) return billed;
|
|
1716
|
+
return numOrNull(obj && obj.costUsdEstimated);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1630
1719
|
function metricsFilePath(projectDir, changeName) {
|
|
1631
1720
|
return join(projectDir, 'openspec', 'changes', changeName, 'metrics.json');
|
|
1632
1721
|
}
|
|
1633
1722
|
|
|
1634
1723
|
function emptySpendTotals() {
|
|
1635
|
-
return { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null };
|
|
1724
|
+
return { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null, costUsdTotal: null };
|
|
1636
1725
|
}
|
|
1637
1726
|
|
|
1638
1727
|
function emptyPlatformSpend(source = 'none') {
|
|
@@ -1643,6 +1732,7 @@ function emptyPlatformSpend(source = 'none') {
|
|
|
1643
1732
|
costUsd: null,
|
|
1644
1733
|
ampCredits: null,
|
|
1645
1734
|
costUsdEstimated: null,
|
|
1735
|
+
costUsdTotal: null,
|
|
1646
1736
|
source,
|
|
1647
1737
|
};
|
|
1648
1738
|
}
|
|
@@ -2383,6 +2473,9 @@ function applyCollectedSessionFields(session, sources, resolvedModel, opts, repo
|
|
|
2383
2473
|
if (session.costUsd == null) {
|
|
2384
2474
|
for (const row of session.byModel) {
|
|
2385
2475
|
if (row.costUsd != null) continue;
|
|
2476
|
+
// adapters already estimated with the cache-read / cache-write split; do not
|
|
2477
|
+
// overwrite it with the coarse input+output estimate below
|
|
2478
|
+
if (row.costUsdEstimated != null) continue;
|
|
2386
2479
|
const described = describeClaudeCostEstimate({
|
|
2387
2480
|
model: String(row.model || '').replace(/^Claude\s+/i, 'claude-').replaceAll(' ', '-').toLowerCase(),
|
|
2388
2481
|
inputTokens: row.inputTokens,
|
|
@@ -2463,6 +2556,7 @@ function spendTuple(obj) {
|
|
|
2463
2556
|
costUsd: numOrNull(obj && obj.costUsd),
|
|
2464
2557
|
ampCredits: numOrNull(obj && obj.ampCredits),
|
|
2465
2558
|
costUsdEstimated: numOrNull(obj && obj.costUsdEstimated),
|
|
2559
|
+
costUsdTotal: numOrNull(obj && obj.costUsdTotal) ?? costUsdTotalOf(obj),
|
|
2466
2560
|
};
|
|
2467
2561
|
}
|
|
2468
2562
|
|
|
@@ -2482,6 +2576,7 @@ function addSpendNums(target, nums) {
|
|
|
2482
2576
|
target.costUsd = addNullable(target.costUsd, nums.costUsd);
|
|
2483
2577
|
target.ampCredits = addNullable(target.ampCredits, nums.ampCredits);
|
|
2484
2578
|
target.costUsdEstimated = addNullable(target.costUsdEstimated, nums.costUsdEstimated);
|
|
2579
|
+
target.costUsdTotal = addNullable(target.costUsdTotal, nums.costUsdTotal);
|
|
2485
2580
|
}
|
|
2486
2581
|
|
|
2487
2582
|
function recomputeSpendMaps(metrics) {
|
|
@@ -2500,6 +2595,7 @@ function recomputeSpendMaps(metrics) {
|
|
|
2500
2595
|
costUsd: null,
|
|
2501
2596
|
ampCredits: null,
|
|
2502
2597
|
costUsdEstimated: null,
|
|
2598
|
+
costUsdTotal: null,
|
|
2503
2599
|
};
|
|
2504
2600
|
addSpendNums(row, nums);
|
|
2505
2601
|
byModel.set(key, row);
|
|
@@ -2520,10 +2616,14 @@ function recomputeSpendMaps(metrics) {
|
|
|
2520
2616
|
}
|
|
2521
2617
|
}
|
|
2522
2618
|
for (const key of Object.keys(byPlatform)) {
|
|
2619
|
+
byPlatform[key].costUsd = roundUsd4(byPlatform[key].costUsd);
|
|
2523
2620
|
byPlatform[key].costUsdEstimated = roundUsd4(byPlatform[key].costUsdEstimated);
|
|
2621
|
+
byPlatform[key].costUsdTotal = roundUsd4(byPlatform[key].costUsdTotal);
|
|
2524
2622
|
}
|
|
2525
2623
|
for (const row of byModel.values()) {
|
|
2624
|
+
row.costUsd = roundUsd4(row.costUsd);
|
|
2526
2625
|
row.costUsdEstimated = roundUsd4(row.costUsdEstimated);
|
|
2626
|
+
row.costUsdTotal = roundUsd4(row.costUsdTotal);
|
|
2527
2627
|
}
|
|
2528
2628
|
metrics.spendByPlatform = byPlatform;
|
|
2529
2629
|
metrics.spendByModel = [...byModel.values()];
|
|
@@ -2575,6 +2675,13 @@ function recomputeMetricsAggregates(metrics) {
|
|
|
2575
2675
|
phase[spendKey] = addNullable(phase[spendKey], value);
|
|
2576
2676
|
spend[spendKey] = addNullable(spend[spendKey], value);
|
|
2577
2677
|
}
|
|
2678
|
+
const sessionCostUsdTotal = costUsdTotalOf({
|
|
2679
|
+
costUsd: sessionFieldOrSources(session, 'costUsd'),
|
|
2680
|
+
costUsdEstimated: sessionFieldOrSources(session, 'costUsdEstimated'),
|
|
2681
|
+
});
|
|
2682
|
+
session.costUsdTotal = roundUsd4(sessionCostUsdTotal);
|
|
2683
|
+
phase.costUsdTotal = addNullable(phase.costUsdTotal, sessionCostUsdTotal);
|
|
2684
|
+
spend.costUsdTotal = addNullable(spend.costUsdTotal, sessionCostUsdTotal);
|
|
2578
2685
|
if (session.role && !phase.agents.includes(session.role)) phase.agents.push(session.role);
|
|
2579
2686
|
if (session.model && !phase.models.includes(session.model)) phase.models.push(session.model);
|
|
2580
2687
|
if (Array.isArray(session.models)) {
|
|
@@ -2593,9 +2700,13 @@ function recomputeMetricsAggregates(metrics) {
|
|
|
2593
2700
|
phase.leadTimeMs = Number.isFinite(startMs) && Number.isFinite(endMs)
|
|
2594
2701
|
? Math.max(0, endMs - startMs)
|
|
2595
2702
|
: null;
|
|
2703
|
+
phase.costUsd = roundUsd4(phase.costUsd);
|
|
2596
2704
|
phase.costUsdEstimated = roundUsd4(phase.costUsdEstimated);
|
|
2705
|
+
phase.costUsdTotal = roundUsd4(phase.costUsdTotal);
|
|
2597
2706
|
}
|
|
2707
|
+
spend.costUsd = roundUsd4(spend.costUsd);
|
|
2598
2708
|
spend.costUsdEstimated = roundUsd4(spend.costUsdEstimated);
|
|
2709
|
+
spend.costUsdTotal = roundUsd4(spend.costUsdTotal);
|
|
2599
2710
|
metrics.phases = phases;
|
|
2600
2711
|
metrics.totals = totals;
|
|
2601
2712
|
metrics.spend = spend;
|
|
@@ -2822,7 +2933,8 @@ function formatMetricsCostLine(spend) {
|
|
|
2822
2933
|
const estimated = spend && spend.costUsdEstimated;
|
|
2823
2934
|
if (billed == null && estimated == null) return '—';
|
|
2824
2935
|
if (billed != null && estimated != null) {
|
|
2825
|
-
|
|
2936
|
+
const total = spend.costUsdTotal != null ? spend.costUsdTotal : billed + estimated;
|
|
2937
|
+
return `${formatMetricsCost(total)} (${formatMetricsCost(billed)} billed + ~${formatMetricsCost(estimated)} est.)`;
|
|
2826
2938
|
}
|
|
2827
2939
|
if (billed != null) return formatMetricsCost(billed);
|
|
2828
2940
|
return `~${formatMetricsCost(estimated)} est.`;
|
|
@@ -3055,11 +3167,13 @@ function readPipelineConfig(projectDir) {
|
|
|
3055
3167
|
const requireBriefMatch = content.match(/require_design_brief:\s*(true|false)/);
|
|
3056
3168
|
const maxActiveMatch = content.match(/max_active_changes:\s*(\d+)/);
|
|
3057
3169
|
const taskContractMatch = content.match(/task_contract:\s*(warn|strict|off)/);
|
|
3170
|
+
const srcGlobMatch = content.match(/src_glob:\s*["']?([^"'\s#]+)["']?/);
|
|
3058
3171
|
return {
|
|
3059
3172
|
requireSpecReview: requireReviewMatch ? requireReviewMatch[1] === 'true' : true,
|
|
3060
3173
|
requireDesignBrief: requireBriefMatch ? requireBriefMatch[1] === 'true' : false,
|
|
3061
3174
|
maxActiveChanges: maxActiveMatch ? parseInt(maxActiveMatch[1], 10) : null,
|
|
3062
3175
|
taskContract: taskContractMatch ? taskContractMatch[1] : 'warn',
|
|
3176
|
+
srcGlob: srcGlobMatch ? srcGlobMatch[1] : null,
|
|
3063
3177
|
};
|
|
3064
3178
|
}
|
|
3065
3179
|
|
|
@@ -3891,6 +4005,7 @@ program
|
|
|
3891
4005
|
}
|
|
3892
4006
|
copyDir(join(projectDir, '.agents', 'rules'), join(projectDir, '.cursor', 'rules'), { overwrite: true, delete: true });
|
|
3893
4007
|
copyDir(join(projectDir, '.agents', 'subagents'), join(projectDir, '.cursor', 'agents'), { overwrite: true, delete: true });
|
|
4008
|
+
syncCommands(projectDir, '.cursor', { namespaced: false });
|
|
3894
4009
|
}
|
|
3895
4010
|
|
|
3896
4011
|
if (syncClaude) {
|
|
@@ -3900,6 +4015,7 @@ program
|
|
|
3900
4015
|
rmSync(join(projectDir, '.claude', 'skills', wrapper), { recursive: true, force: true });
|
|
3901
4016
|
}
|
|
3902
4017
|
copyDir(join(projectDir, '.agents', 'subagents'), join(projectDir, '.claude', 'agents'), { overwrite: true, delete: true });
|
|
4018
|
+
syncCommands(projectDir, '.claude', { namespaced: true });
|
|
3903
4019
|
|
|
3904
4020
|
const claudeMd = join(projectDir, 'CLAUDE.md');
|
|
3905
4021
|
const claudeDir = join(projectDir, '.claude');
|
|
@@ -3937,6 +4053,13 @@ program
|
|
|
3937
4053
|
if (changes.length === 0) {
|
|
3938
4054
|
log.info('No active changes');
|
|
3939
4055
|
} else {
|
|
4056
|
+
// Readiness must mirror the gates `archive` actually enforces — reporting
|
|
4057
|
+
// "ready" on task count alone told the conductor to archive a change the
|
|
4058
|
+
// CLI would then refuse.
|
|
4059
|
+
const config = readPipelineConfig(projectDir);
|
|
4060
|
+
const requireReview = config ? config.requireSpecReview : true;
|
|
4061
|
+
const requireBrief = config ? config.requireDesignBrief : false;
|
|
4062
|
+
|
|
3940
4063
|
for (const name of changes) {
|
|
3941
4064
|
const changeDir = join(projectDir, 'openspec', 'changes', name);
|
|
3942
4065
|
const progress = parseTasksProgress(changeDir);
|
|
@@ -3944,13 +4067,24 @@ program
|
|
|
3944
4067
|
const hasBrief = parseDesignBrief(changeDir);
|
|
3945
4068
|
const progressStr = progress ? `${progress.done}/${progress.total} tasks` : 'no tasks.md';
|
|
3946
4069
|
const verdictStr = verdict || 'none';
|
|
3947
|
-
|
|
4070
|
+
|
|
4071
|
+
const blockers = [];
|
|
4072
|
+
if (!(progress && progress.total > 0 && progress.done === progress.total)) {
|
|
4073
|
+
blockers.push('tasks incomplete');
|
|
4074
|
+
}
|
|
4075
|
+
if (requireReview && !(verdict && /^APPROVE/i.test(verdict))) {
|
|
4076
|
+
blockers.push(verdict ? `review verdict "${verdict}" (need APPROVE)` : 'no review.md');
|
|
4077
|
+
}
|
|
4078
|
+
if (requireBrief && !hasBrief && !hasDesignOptOut(changeDir)) {
|
|
4079
|
+
blockers.push('no design-brief.md');
|
|
4080
|
+
}
|
|
3948
4081
|
|
|
3949
4082
|
console.log(`\n${pc.bold(name)}`);
|
|
3950
4083
|
console.log(` tasks: ${progressStr}`);
|
|
3951
4084
|
console.log(` review: ${verdictStr}`);
|
|
3952
4085
|
console.log(` brief: ${hasBrief ? 'yes' : 'no'}`);
|
|
3953
|
-
if (
|
|
4086
|
+
if (blockers.length === 0) log.ok('ready to archive');
|
|
4087
|
+
else log.info(`not ready to archive — ${blockers.join('; ')}`);
|
|
3954
4088
|
}
|
|
3955
4089
|
console.log('');
|
|
3956
4090
|
}
|
|
@@ -3963,7 +4097,7 @@ program
|
|
|
3963
4097
|
program
|
|
3964
4098
|
.command('gate-check [change-name]')
|
|
3965
4099
|
.description('Deterministically check the review gate before apply/merge (exit non-zero if unmet)')
|
|
3966
|
-
.option('--src-glob <glob>', 'source path filter used to detect code changes
|
|
4100
|
+
.option('--src-glob <glob>', 'source path filter used to detect code changes (default: pipeline.src_glob, else src/)')
|
|
3967
4101
|
.option('--base <ref>', 'git ref to diff against', 'HEAD~1')
|
|
3968
4102
|
.option('--staged', 'check staged files (git diff --cached) instead of --base...HEAD', false)
|
|
3969
4103
|
.option('--tasks <name>', 'lint task contracts (Files/Do/Done-when) of a change')
|
|
@@ -4019,16 +4153,21 @@ program
|
|
|
4019
4153
|
return;
|
|
4020
4154
|
}
|
|
4021
4155
|
|
|
4156
|
+
// A repo whose code does not live in src/ used to fall through to
|
|
4157
|
+
// "nothing to gate" on every run, silently disabling the review gate.
|
|
4158
|
+
const srcGlob = opts.srcGlob || config.srcGlob || 'src/';
|
|
4159
|
+
|
|
4022
4160
|
const touchesSrc = opts.staged
|
|
4023
|
-
? gitStagedTouchesGlob(projectDir,
|
|
4024
|
-
: gitDiffTouchesGlob(projectDir, opts.base,
|
|
4161
|
+
? gitStagedTouchesGlob(projectDir, srcGlob)
|
|
4162
|
+
: gitDiffTouchesGlob(projectDir, opts.base, srcGlob);
|
|
4025
4163
|
if (touchesSrc === false) {
|
|
4026
|
-
log.ok(`no ${opts.staged ? 'staged ' : ''}changes under ${
|
|
4164
|
+
log.ok(`no ${opts.staged ? 'staged ' : ''}changes under ${srcGlob} — nothing to gate`);
|
|
4027
4165
|
return;
|
|
4028
4166
|
}
|
|
4029
4167
|
if (touchesSrc === null) {
|
|
4030
|
-
|
|
4031
|
-
|
|
4168
|
+
// Cannot prove nothing changed (shallow clone, missing base ref, no
|
|
4169
|
+
// commits yet) — verify the gate instead of passing a blocking check.
|
|
4170
|
+
log.warn(`could not compute git ${opts.staged ? 'staged ' : ''}diff — verifying the review gate anyway`);
|
|
4032
4171
|
}
|
|
4033
4172
|
|
|
4034
4173
|
const changes = listActiveChanges(projectDir);
|
|
@@ -4039,7 +4178,7 @@ program
|
|
|
4039
4178
|
let target = changeName;
|
|
4040
4179
|
if (!target) {
|
|
4041
4180
|
if (changes.length === 0) {
|
|
4042
|
-
log.warn(`${
|
|
4181
|
+
log.warn(`${srcGlob} changed but no active OpenSpec change found — cannot verify review gate`);
|
|
4043
4182
|
return;
|
|
4044
4183
|
}
|
|
4045
4184
|
target = changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Universal AI agent orchestration kit for Cursor, Claude Code, and Amp Code — spec-driven OpenSpec pipeline, conductor subagents, durable session handoff, factory gates and MCP setup, cloud-agent handoff, and optional local Figma PAT setup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"files": [
|
|
36
36
|
"bin/",
|
|
37
37
|
"templates/",
|
|
38
|
+
"!templates/.cursor",
|
|
38
39
|
"profiles/",
|
|
39
40
|
"README.md",
|
|
40
41
|
"CHANGELOG.md",
|
|
@@ -11,6 +11,9 @@ pipeline:
|
|
|
11
11
|
max_active_changes: 1
|
|
12
12
|
archive_after_merge: true
|
|
13
13
|
task_contract: warn
|
|
14
|
+
# Paths gate-check treats as product code. Widen it when code lives
|
|
15
|
+
# outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
|
|
16
|
+
src_glob: "src/"
|
|
14
17
|
|
|
15
18
|
roles:
|
|
16
19
|
explorer:
|
|
@@ -16,6 +16,9 @@ pipeline:
|
|
|
16
16
|
archive_after_merge: false
|
|
17
17
|
quick_mode_enabled: true
|
|
18
18
|
task_contract: off
|
|
19
|
+
# Paths gate-check treats as product code. Widen it when code lives
|
|
20
|
+
# outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
|
|
21
|
+
src_glob: "src/"
|
|
19
22
|
|
|
20
23
|
roles:
|
|
21
24
|
explorer:
|
|
@@ -14,6 +14,9 @@ pipeline:
|
|
|
14
14
|
max_active_changes: 1
|
|
15
15
|
archive_after_merge: true
|
|
16
16
|
task_contract: warn
|
|
17
|
+
# Paths gate-check treats as product code. Widen it when code lives
|
|
18
|
+
# outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
|
|
19
|
+
src_glob: "src/"
|
|
17
20
|
|
|
18
21
|
roles:
|
|
19
22
|
explorer:
|
|
@@ -14,6 +14,9 @@ pipeline:
|
|
|
14
14
|
max_active_changes: 1
|
|
15
15
|
archive_after_merge: true
|
|
16
16
|
task_contract: warn
|
|
17
|
+
# Paths gate-check treats as product code. Widen it when code lives
|
|
18
|
+
# outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
|
|
19
|
+
src_glob: "src/"
|
|
17
20
|
|
|
18
21
|
roles:
|
|
19
22
|
explorer:
|
|
@@ -13,6 +13,9 @@ pipeline:
|
|
|
13
13
|
max_active_changes: 1
|
|
14
14
|
archive_after_merge: true
|
|
15
15
|
task_contract: warn
|
|
16
|
+
# Paths gate-check treats as product code. Widen it when code lives
|
|
17
|
+
# outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
|
|
18
|
+
src_glob: "src/"
|
|
16
19
|
|
|
17
20
|
roles:
|
|
18
21
|
explorer:
|
|
@@ -23,6 +23,12 @@ function roundUsd4(x) {
|
|
|
23
23
|
return Math.round(Number(x) * 10000) / 10000;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function costUsdTotalOf(obj) {
|
|
27
|
+
const billed = numOrNull(obj && obj.costUsd);
|
|
28
|
+
if (billed != null) return billed;
|
|
29
|
+
return numOrNull(obj && obj.costUsdEstimated);
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
function timestampMs(value) {
|
|
27
33
|
if (value == null || value === '') return NaN;
|
|
28
34
|
const ms = Date.parse(value);
|
|
@@ -435,6 +441,7 @@ function emptyPlatform(source = 'none') {
|
|
|
435
441
|
costUsd: null,
|
|
436
442
|
ampCredits: null,
|
|
437
443
|
costUsdEstimated: null,
|
|
444
|
+
costUsdTotal: null,
|
|
438
445
|
source,
|
|
439
446
|
};
|
|
440
447
|
}
|
|
@@ -442,7 +449,7 @@ function emptyPlatform(source = 'none') {
|
|
|
442
449
|
function recompute(metrics) {
|
|
443
450
|
const phases = {};
|
|
444
451
|
const totals = { sessions: 0, durationMs: null, leadTimeMs: null, cloudSessions: 0 };
|
|
445
|
-
const spend = { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null };
|
|
452
|
+
const spend = { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null, costUsdTotal: null };
|
|
446
453
|
const byPlatform = {
|
|
447
454
|
cursor: emptyPlatform(),
|
|
448
455
|
claude: emptyPlatform(),
|
|
@@ -477,6 +484,7 @@ function recompute(metrics) {
|
|
|
477
484
|
totalTokens: null,
|
|
478
485
|
costUsd: null,
|
|
479
486
|
costUsdEstimated: null,
|
|
487
|
+
costUsdTotal: null,
|
|
480
488
|
agents: [],
|
|
481
489
|
models: [],
|
|
482
490
|
};
|
|
@@ -493,6 +501,10 @@ function recompute(metrics) {
|
|
|
493
501
|
phase[spendKey] = addNullable(phase[spendKey], value);
|
|
494
502
|
spend[spendKey] = addNullable(spend[spendKey], value);
|
|
495
503
|
}
|
|
504
|
+
const sessionCostUsdTotal = costUsdTotalOf(session);
|
|
505
|
+
session.costUsdTotal = roundUsd4(sessionCostUsdTotal);
|
|
506
|
+
phase.costUsdTotal = addNullable(phase.costUsdTotal, sessionCostUsdTotal);
|
|
507
|
+
spend.costUsdTotal = addNullable(spend.costUsdTotal, sessionCostUsdTotal);
|
|
496
508
|
if (session.role && !phase.agents.includes(session.role)) phase.agents.push(session.role);
|
|
497
509
|
if (session.model && !phase.models.includes(session.model)) phase.models.push(session.model);
|
|
498
510
|
if (Array.isArray(session.models)) {
|
|
@@ -508,6 +520,7 @@ function recompute(metrics) {
|
|
|
508
520
|
for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'ampCredits', 'costUsdEstimated']) {
|
|
509
521
|
bucket[key] = addNullable(bucket[key], numOrNull(session[key]));
|
|
510
522
|
}
|
|
523
|
+
bucket.costUsdTotal = addNullable(bucket.costUsdTotal, sessionCostUsdTotal);
|
|
511
524
|
if ((session.sourceIds || []).length) bucket.source = platform === 'cursor' ? 'cursor-hook' : `${platform}-jsonl`;
|
|
512
525
|
}
|
|
513
526
|
const modelRows = (session.byModel || []).length ? session.byModel : [session];
|
|
@@ -523,6 +536,7 @@ function recompute(metrics) {
|
|
|
523
536
|
costUsd: null,
|
|
524
537
|
ampCredits: null,
|
|
525
538
|
costUsdEstimated: null,
|
|
539
|
+
costUsdTotal: null,
|
|
526
540
|
};
|
|
527
541
|
row.inputTokens = addNullable(row.inputTokens, numOrNull(src.inputTokens));
|
|
528
542
|
row.outputTokens = addNullable(row.outputTokens, numOrNull(src.outputTokens));
|
|
@@ -530,6 +544,7 @@ function recompute(metrics) {
|
|
|
530
544
|
row.costUsd = addNullable(row.costUsd, numOrNull(src.costUsd));
|
|
531
545
|
row.ampCredits = addNullable(row.ampCredits, numOrNull(src.ampCredits));
|
|
532
546
|
row.costUsdEstimated = addNullable(row.costUsdEstimated, numOrNull(src.costUsdEstimated));
|
|
547
|
+
row.costUsdTotal = addNullable(row.costUsdTotal, costUsdTotalOf(src));
|
|
533
548
|
byModel.set(modelKey, row);
|
|
534
549
|
}
|
|
535
550
|
}
|
|
@@ -544,14 +559,22 @@ function recompute(metrics) {
|
|
|
544
559
|
phase.leadTimeMs = Number.isFinite(startMs) && Number.isFinite(endMs)
|
|
545
560
|
? Math.max(0, endMs - startMs)
|
|
546
561
|
: null;
|
|
562
|
+
phase.costUsd = roundUsd4(phase.costUsd);
|
|
547
563
|
phase.costUsdEstimated = roundUsd4(phase.costUsdEstimated);
|
|
564
|
+
phase.costUsdTotal = roundUsd4(phase.costUsdTotal);
|
|
548
565
|
}
|
|
566
|
+
spend.costUsd = roundUsd4(spend.costUsd);
|
|
549
567
|
spend.costUsdEstimated = roundUsd4(spend.costUsdEstimated);
|
|
568
|
+
spend.costUsdTotal = roundUsd4(spend.costUsdTotal);
|
|
550
569
|
for (const bucket of Object.values(byPlatform)) {
|
|
570
|
+
bucket.costUsd = roundUsd4(bucket.costUsd);
|
|
551
571
|
bucket.costUsdEstimated = roundUsd4(bucket.costUsdEstimated);
|
|
572
|
+
bucket.costUsdTotal = roundUsd4(bucket.costUsdTotal);
|
|
552
573
|
}
|
|
553
574
|
for (const row of byModel.values()) {
|
|
575
|
+
row.costUsd = roundUsd4(row.costUsd);
|
|
554
576
|
row.costUsdEstimated = roundUsd4(row.costUsdEstimated);
|
|
577
|
+
row.costUsdTotal = roundUsd4(row.costUsdTotal);
|
|
555
578
|
}
|
|
556
579
|
metrics.phases = phases;
|
|
557
580
|
metrics.totals = totals;
|
|
@@ -76,6 +76,13 @@ if [ -d .agents/subagents ]; then
|
|
|
76
76
|
ok ".cursor/agents/"
|
|
77
77
|
fi
|
|
78
78
|
|
|
79
|
+
# Cursor reads .cursor/commands/<file>.md as /<file> — flat copy.
|
|
80
|
+
if [ -d .agents/commands ]; then
|
|
81
|
+
mkdir -p .cursor/commands
|
|
82
|
+
rsync -a --delete .agents/commands/ .cursor/commands/
|
|
83
|
+
ok ".cursor/commands/"
|
|
84
|
+
fi
|
|
85
|
+
|
|
79
86
|
if [ ! -f .mcp.json ] && [ -f .agents/mcp.json.example ]; then
|
|
80
87
|
cp .agents/mcp.json.example .mcp.json
|
|
81
88
|
ok ".mcp.json created from example"
|
|
@@ -97,6 +104,26 @@ if [ -d .agents/subagents ]; then
|
|
|
97
104
|
ok ".claude/agents/"
|
|
98
105
|
fi
|
|
99
106
|
|
|
107
|
+
# Claude Code namespaces commands by subdirectory: .claude/commands/opsx/apply.md
|
|
108
|
+
# is what makes the documented /opsx:apply exist.
|
|
109
|
+
if [ -d .agents/commands ]; then
|
|
110
|
+
rm -rf .claude/commands
|
|
111
|
+
mkdir -p .claude/commands
|
|
112
|
+
for cmd in .agents/commands/*.md; do
|
|
113
|
+
[ -f "$cmd" ] || continue
|
|
114
|
+
BASE="$(basename "$cmd" .md)"
|
|
115
|
+
NS="${BASE%%-*}"
|
|
116
|
+
REST="${BASE#*-}"
|
|
117
|
+
if [ "$NS" != "$BASE" ] && [ -n "$REST" ]; then
|
|
118
|
+
mkdir -p ".claude/commands/$NS"
|
|
119
|
+
cp "$cmd" ".claude/commands/$NS/$REST.md"
|
|
120
|
+
else
|
|
121
|
+
cp "$cmd" ".claude/commands/$BASE.md"
|
|
122
|
+
fi
|
|
123
|
+
done
|
|
124
|
+
ok ".claude/commands/"
|
|
125
|
+
fi
|
|
126
|
+
|
|
100
127
|
if [ -f CLAUDE.md ]; then
|
|
101
128
|
cp CLAUDE.md .claude/CLAUDE.md
|
|
102
129
|
ok ".claude/CLAUDE.md"
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{"type":"entity","name":"Change:add-factory-memory-and-skills","entityType":"Change","observations":["status: archived","tasks: 9/9","last_role: Archiver","review: APPROVE","summary: archived to openspec/changes/archive/2026-08-27-add-factory-memory-and-skills"]}
|
|
2
|
-
{"type":"entity","name":"Handoff:add-factory-memory-and-skills","entityType":"Handoff","observations":["next_role: none","next_command: none","summary: archived to openspec/changes/archive/2026-08-27-add-factory-memory-and-skills","blocked: none"]}
|
|
3
|
-
{"type":"entity","name":"Decision:apply-complete","entityType":"Decision","observations":["all 9 tasks implemented and verified (npm test 101/101, openspec validate --strict, gate-check --tasks)","change: add-factory-memory-and-skills","date: 2026-08-27"]}
|
|
4
|
-
{"type":"entity","name":"Decision:m1-followed","entityType":"Decision","observations":["task 2.3 Skill health stale/missing verified in a temporary init+sync project (smoke test), not in the kit repo","change: add-factory-memory-and-skills","date: 2026-08-27"]}
|
|
5
|
-
{"type":"entity","name":"Decision:m2-followed","entityType":"Decision","observations":["skills.kit drift test iterates all five orchestrator.yaml files against templates/.agents/skills/","change: add-factory-memory-and-skills","date: 2026-08-27"]}
|
|
6
|
-
{"type":"entity","name":"Decision:i1-honored","entityType":"Decision","observations":["did not change templates/orchestrator.yaml handoff.spawn_handoff_subagent (still false)","change: add-factory-memory-and-skills","date: 2026-08-27"]}
|
|
7
|
-
{"type":"relation","from":"Change:add-factory-memory-and-skills","to":"Handoff:add-factory-memory-and-skills","relationType":"hasHandoff"}
|
|
8
|
-
{"type":"relation","from":"Change:add-factory-memory-and-skills","to":"Decision:apply-complete","relationType":"hasDecision"}
|
|
9
|
-
{"type":"relation","from":"Change:add-factory-memory-and-skills","to":"Decision:m1-followed","relationType":"hasDecision"}
|
|
10
|
-
{"type":"relation","from":"Change:add-factory-memory-and-skills","to":"Decision:m2-followed","relationType":"hasDecision"}
|
|
11
|
-
{"type":"relation","from":"Change:add-factory-memory-and-skills","to":"Decision:i1-honored","relationType":"hasDecision"}
|