agent-orchestrator-kit 0.4.0 → 0.5.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 +7 -0
- package/README.md +37 -3
- package/bin/agent-orchestrator.js +312 -0
- package/package.json +1 -1
- package/templates/.agents/rules/session-handoff.mdc +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.0] - 2026-08-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Change metrics** — git-tracked `openspec/changes/<name>/metrics.json`: `handoff --restore` records the session start (`pending` marker), `handoff <name>` closes the session (duration, closed role → phase `explore|design|spec|review|apply|archive`, runtime, tasks snapshot, optional `--model` / `--input-tokens` / `--output-tokens` / `--total-tokens` / `--cost-usd`, `--started-at` when restore was skipped, `--no-metrics` to opt out), `archive <name>` sets `archivedAt`; per-phase and total aggregates (durations, tokens, cost, agents, models, `leadTimeMs`) are recomputed on every write and unreported values stay null-honest
|
|
11
|
+
- **`npx agent-orchestrator-kit metrics [change-name] [--json]`** — human summary (phases table, spend, recent sessions) or raw JSON; resolves archived changes under `openspec/changes/archive/*-<name>` too
|
|
12
|
+
|
|
7
13
|
## [0.4.0] - 2026-08-28
|
|
8
14
|
|
|
9
15
|
### Added
|
|
@@ -193,6 +199,7 @@ All notable changes to this project will be documented in this file.
|
|
|
193
199
|
### Added
|
|
194
200
|
- Initial release: 5-role orchestration pipeline, `/opsx:*` commands, IDE sync
|
|
195
201
|
|
|
202
|
+
[0.5.0]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.4.0...v0.5.0
|
|
196
203
|
[0.4.0]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.3.0...v0.4.0
|
|
197
204
|
[0.3.0]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.2.0...v0.3.0
|
|
198
205
|
[0.2.0]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.14...v0.2.0
|
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 (factory phases 1–3 in v0.4.0+, lean pipeline / archive CLI in v0.3.0+, handoff CLI in v0.1.14+, Figma PAT in v0.1.11+):**
|
|
77
|
+
**🔄 Already have the kit installed? Upgrade to latest (change metrics in v0.5.0+, factory phases 1–3 in v0.4.0+, lean pipeline / archive CLI in v0.3.0+, handoff CLI in v0.1.14+, Figma PAT in v0.1.11+):**
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
80
|
npx agent-orchestrator-kit@latest update
|
|
@@ -191,7 +191,7 @@ your-project/
|
|
|
191
191
|
| OpenSpec skills | All 7 skills for `/opsx:*` workflow |
|
|
192
192
|
| IDE sync | Cursor + Claude Code sync script (`--delete` semantics — removes stale skills/subagents) |
|
|
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
|
-
| CLI gates | `npx agent-orchestrator-kit status` / `gate-check` / `archive` / `handoff` / `memory-setup` — deterministic review-gate, archive,
|
|
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` |
|
|
196
196
|
| AI Spec Verifier | `spec-verify.yml` + verifier scripts — GitLab or GitHub, opt-in (`--spec-verify`) |
|
|
197
197
|
| MCP templates | Memory MCP for Cursor and Amp |
|
|
@@ -797,6 +797,24 @@ npx agent-orchestrator-kit handoff <name> --cloud-check # require exit 0
|
|
|
797
797
|
|
|
798
798
|
Persist with `runtime: cloud` prints those four steps on stderr; stdout stays the pure `/opsx:` next-thread prompt. Local persist is unchanged.
|
|
799
799
|
|
|
800
|
+
### Change metrics
|
|
801
|
+
|
|
802
|
+
Every change accumulates git-tracked `openspec/changes/<name>/metrics.json` — the data source for planning the next feature: how long each phase took, how many sessions it needed, what it cost.
|
|
803
|
+
|
|
804
|
+
- **Session start** — `handoff --restore` writes a `pending` marker (`startedAt`, expected role).
|
|
805
|
+
- **Session end** — `handoff <name>` closes the pending session: duration, closed role, mapped phase (`explore` / `design` / `spec` / `review` / `apply` / `archive`), runtime (local/cloud), tasks snapshot (`n/m`), and — when the agent passes them — `--model`, `--input-tokens`, `--output-tokens`, `--total-tokens`, `--cost-usd`. No restore marker? Pass `--started-at <iso>` or the duration stays honestly `null`.
|
|
806
|
+
- **Archive** — `archive <name>` sets `archivedAt` and clears any pending marker; the file moves to the archive folder with the change.
|
|
807
|
+
|
|
808
|
+
Aggregates are recomputed on every write: per-phase totals (`durationMs`, tokens, `costUsd`, `sessions`, agents, models) plus overall `totals` (`sessions`, `cloudSessions`, `durationMs` = sum of session work time, `leadTimeMs` = wall clock from first session start to last session end) and `spend` (token/cost sums). Numbers are null-honest: a metric nobody reported stays `null`, never a fake `0`.
|
|
809
|
+
|
|
810
|
+
```bash
|
|
811
|
+
npx agent-orchestrator-kit handoff add-thing --input-tokens 12000 --output-tokens 3000 --cost-usd 0.42 --model claude-sonnet
|
|
812
|
+
npx agent-orchestrator-kit metrics add-thing # human summary: phases, tokens, cost, agents
|
|
813
|
+
npx agent-orchestrator-kit metrics add-thing --json # raw metrics.json (works for archived changes too)
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Recording is on by default and never blocks persist; opt out per session with `--no-metrics`.
|
|
817
|
+
|
|
800
818
|
### Skill inventory
|
|
801
819
|
|
|
802
820
|
`.agents/orchestrator.yaml` carries a machine-readable `skills:` section (`kit` / `stack` / `external`) instead of hardcoded skill names in the CLI:
|
|
@@ -900,10 +918,21 @@ npx agent-orchestrator-kit archive <name> [--sync | --no-sync --force]
|
|
|
900
918
|
|
|
901
919
|
npx agent-orchestrator-kit handoff [change-name] [options]
|
|
902
920
|
--restore Print the restore briefing instead of persisting
|
|
921
|
+
(also records the session start into metrics.json)
|
|
903
922
|
--runtime <value> local | cloud (invalid values exit non-zero)
|
|
904
923
|
--agent-id <id> Cloud agent identifier (default: none)
|
|
905
924
|
--cloud-check Verify change artifacts are committed and pushed
|
|
906
925
|
(cloud: non-zero on failure; local: warning, exit 0)
|
|
926
|
+
--started-at <iso> Session start override when --restore was not run
|
|
927
|
+
--model <name> Model used in this session (metrics.json)
|
|
928
|
+
--input-tokens <n> / --output-tokens <n> / --total-tokens <n>
|
|
929
|
+
Token spend for this session (total defaults to in+out)
|
|
930
|
+
--cost-usd <usd> Session cost in USD
|
|
931
|
+
--no-metrics Skip recording this session into metrics.json
|
|
932
|
+
|
|
933
|
+
npx agent-orchestrator-kit metrics [change-name] [--json]
|
|
934
|
+
Show recorded session metrics for a change (active or archived):
|
|
935
|
+
time per phase, sessions, tokens, cost, agents, models, lead time
|
|
907
936
|
```
|
|
908
937
|
|
|
909
938
|
## Directory Reference
|
|
@@ -936,7 +965,7 @@ CLAUDE.md # Committed — synced to .claude/CLAUDE.md
|
|
|
936
965
|
openspec/ # Committed — spec-driven workflow
|
|
937
966
|
config.yaml # Project context for AI
|
|
938
967
|
specs/ # Source of truth after archive
|
|
939
|
-
changes/ # Active work; <name>/handoff.md
|
|
968
|
+
changes/ # Active work; <name>/handoff.md + metrics.json index session state
|
|
940
969
|
```
|
|
941
970
|
|
|
942
971
|
## Roadmap
|
|
@@ -950,6 +979,11 @@ Phase bounds and non-goals: [`openspec/specs/agentic-factory-roadmap/spec.md`](o
|
|
|
950
979
|
|
|
951
980
|
## Changelog
|
|
952
981
|
|
|
982
|
+
### 0.5.0
|
|
983
|
+
- **Change metrics** — git-tracked `openspec/changes/<name>/metrics.json` (session start on `handoff --restore`, close on persist)
|
|
984
|
+
- **`metrics` CLI** — human summary or `--json`; phases, tokens, cost, agents, models, lead time
|
|
985
|
+
- Optional persist spend flags: `--model`, `--input-tokens`, `--output-tokens`, `--total-tokens`, `--cost-usd`, `--started-at`, `--no-metrics`
|
|
986
|
+
|
|
953
987
|
### 0.4.0
|
|
954
988
|
- Factory phases 1–3: `hooks-setup` / `gate-check --staged`, `mcp-setup` (GitHub / GitLab / browser), MCP and Skill health in `status`
|
|
955
989
|
- Git-canonical append-only `decisions.md`; Memory `Decision:*` is a file→Memory mirror
|
|
@@ -1290,6 +1290,215 @@ function readHandoffFields(projectDir, changeName) {
|
|
|
1290
1290
|
return { filePath, fields: fieldsFromSections(changeName, sections) };
|
|
1291
1291
|
}
|
|
1292
1292
|
|
|
1293
|
+
const METRICS_VERSION = 1;
|
|
1294
|
+
const METRICS_SPEND_KEYS = ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd'];
|
|
1295
|
+
|
|
1296
|
+
function metricsFilePath(projectDir, changeName) {
|
|
1297
|
+
return join(projectDir, 'openspec', 'changes', changeName, 'metrics.json');
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function emptySpendTotals() {
|
|
1301
|
+
return { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null };
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function defaultMetrics(changeName, nowIso) {
|
|
1305
|
+
return {
|
|
1306
|
+
version: METRICS_VERSION,
|
|
1307
|
+
change: changeName,
|
|
1308
|
+
createdAt: nowIso,
|
|
1309
|
+
updatedAt: nowIso,
|
|
1310
|
+
archivedAt: null,
|
|
1311
|
+
spend: emptySpendTotals(),
|
|
1312
|
+
totals: { sessions: 0, durationMs: null, leadTimeMs: null, cloudSessions: 0 },
|
|
1313
|
+
phases: {},
|
|
1314
|
+
sessions: [],
|
|
1315
|
+
pending: null,
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function loadMetricsFile(filePath, changeName, nowIso) {
|
|
1320
|
+
if (!existsSync(filePath)) return defaultMetrics(changeName, nowIso);
|
|
1321
|
+
let parsed;
|
|
1322
|
+
try {
|
|
1323
|
+
parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
1324
|
+
} catch {
|
|
1325
|
+
return defaultMetrics(changeName, nowIso);
|
|
1326
|
+
}
|
|
1327
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1328
|
+
return defaultMetrics(changeName, nowIso);
|
|
1329
|
+
}
|
|
1330
|
+
const base = defaultMetrics(changeName, parsed.createdAt || nowIso);
|
|
1331
|
+
return {
|
|
1332
|
+
...base,
|
|
1333
|
+
...parsed,
|
|
1334
|
+
version: METRICS_VERSION,
|
|
1335
|
+
change: changeName,
|
|
1336
|
+
spend: { ...base.spend, ...(parsed.spend && typeof parsed.spend === 'object' ? parsed.spend : {}) },
|
|
1337
|
+
totals: { ...base.totals, ...(parsed.totals && typeof parsed.totals === 'object' ? parsed.totals : {}) },
|
|
1338
|
+
phases: parsed.phases && typeof parsed.phases === 'object' && !Array.isArray(parsed.phases) ? parsed.phases : {},
|
|
1339
|
+
sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [],
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function saveMetricsFile(filePath, metrics) {
|
|
1344
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
1345
|
+
writeFileSync(filePath, `${JSON.stringify(metrics, null, 2)}\n`);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function numOrNull(value) {
|
|
1349
|
+
if (value == null || value === '') return null;
|
|
1350
|
+
const n = Number(value);
|
|
1351
|
+
return Number.isFinite(n) ? n : null;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function addNullable(a, b) {
|
|
1355
|
+
if (a == null && b == null) return null;
|
|
1356
|
+
return (a ?? 0) + (b ?? 0);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function phaseForRole(role) {
|
|
1360
|
+
const value = String(role || '').toLowerCase();
|
|
1361
|
+
if (/explor/.test(value)) return 'explore';
|
|
1362
|
+
if (/review/.test(value)) return 'review';
|
|
1363
|
+
if (/implement|apply|code-writer|test-writer/.test(value)) return 'apply';
|
|
1364
|
+
if (/architect|propose/.test(value)) return 'spec';
|
|
1365
|
+
if (/design/.test(value)) return 'design';
|
|
1366
|
+
if (/archiv/.test(value)) return 'archive';
|
|
1367
|
+
return 'other';
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function isoOrNull(value) {
|
|
1371
|
+
if (!value) return null;
|
|
1372
|
+
const ms = Date.parse(String(value));
|
|
1373
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function recomputeMetricsAggregates(metrics) {
|
|
1377
|
+
const phases = {};
|
|
1378
|
+
const totals = { sessions: 0, durationMs: null, leadTimeMs: null, cloudSessions: 0 };
|
|
1379
|
+
const spend = emptySpendTotals();
|
|
1380
|
+
let firstStart = null;
|
|
1381
|
+
let lastEnd = null;
|
|
1382
|
+
for (const session of metrics.sessions) {
|
|
1383
|
+
totals.sessions += 1;
|
|
1384
|
+
if (session.runtime === 'cloud') totals.cloudSessions += 1;
|
|
1385
|
+
totals.durationMs = addNullable(totals.durationMs, numOrNull(session.durationMs));
|
|
1386
|
+
if (session.startedAt && (firstStart == null || session.startedAt < firstStart)) firstStart = session.startedAt;
|
|
1387
|
+
if (session.endedAt && (lastEnd == null || session.endedAt > lastEnd)) lastEnd = session.endedAt;
|
|
1388
|
+
const key = session.phase || 'other';
|
|
1389
|
+
const phase = phases[key] || { sessions: 0, durationMs: null, ...emptySpendTotals(), agents: [], models: [] };
|
|
1390
|
+
phase.sessions += 1;
|
|
1391
|
+
phase.durationMs = addNullable(phase.durationMs, numOrNull(session.durationMs));
|
|
1392
|
+
for (const spendKey of METRICS_SPEND_KEYS) {
|
|
1393
|
+
const value = numOrNull(session[spendKey]);
|
|
1394
|
+
phase[spendKey] = addNullable(phase[spendKey], value);
|
|
1395
|
+
spend[spendKey] = addNullable(spend[spendKey], value);
|
|
1396
|
+
}
|
|
1397
|
+
if (session.role && !phase.agents.includes(session.role)) phase.agents.push(session.role);
|
|
1398
|
+
if (session.model && !phase.models.includes(session.model)) phase.models.push(session.model);
|
|
1399
|
+
phases[key] = phase;
|
|
1400
|
+
}
|
|
1401
|
+
if (firstStart && lastEnd) {
|
|
1402
|
+
totals.leadTimeMs = Math.max(0, Date.parse(lastEnd) - Date.parse(firstStart));
|
|
1403
|
+
}
|
|
1404
|
+
metrics.phases = phases;
|
|
1405
|
+
metrics.totals = totals;
|
|
1406
|
+
metrics.spend = spend;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function metricsRecordSessionStart(projectDir, changeName, role) {
|
|
1410
|
+
const filePath = metricsFilePath(projectDir, changeName);
|
|
1411
|
+
const nowIso = new Date().toISOString();
|
|
1412
|
+
const metrics = loadMetricsFile(filePath, changeName, nowIso);
|
|
1413
|
+
metrics.pending = { startedAt: nowIso, role: role || '' };
|
|
1414
|
+
metrics.updatedAt = nowIso;
|
|
1415
|
+
saveMetricsFile(filePath, metrics);
|
|
1416
|
+
return filePath;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
1420
|
+
const filePath = metricsFilePath(projectDir, fields.changeName);
|
|
1421
|
+
const nowIso = new Date().toISOString();
|
|
1422
|
+
const metrics = loadMetricsFile(filePath, fields.changeName, nowIso);
|
|
1423
|
+
const startedAt = isoOrNull(opts.startedAt) || (metrics.pending && metrics.pending.startedAt) || null;
|
|
1424
|
+
const durationMs = startedAt ? Math.max(0, Date.parse(nowIso) - Date.parse(startedAt)) : null;
|
|
1425
|
+
const inputTokens = numOrNull(opts.inputTokens);
|
|
1426
|
+
const outputTokens = numOrNull(opts.outputTokens);
|
|
1427
|
+
let totalTokens = numOrNull(opts.totalTokens);
|
|
1428
|
+
if (totalTokens == null && (inputTokens != null || outputTokens != null)) {
|
|
1429
|
+
totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
1430
|
+
}
|
|
1431
|
+
metrics.sessions.push({
|
|
1432
|
+
startedAt,
|
|
1433
|
+
endedAt: nowIso,
|
|
1434
|
+
durationMs,
|
|
1435
|
+
role: fields.closedRole || '',
|
|
1436
|
+
phase: phaseForRole(fields.closedRole),
|
|
1437
|
+
runtime: fields.runtime || 'local',
|
|
1438
|
+
agentId: fields.agentId || 'none',
|
|
1439
|
+
model: opts.model || null,
|
|
1440
|
+
tasks: fields.tasks || null,
|
|
1441
|
+
inputTokens,
|
|
1442
|
+
outputTokens,
|
|
1443
|
+
totalTokens,
|
|
1444
|
+
costUsd: numOrNull(opts.costUsd),
|
|
1445
|
+
});
|
|
1446
|
+
metrics.pending = null;
|
|
1447
|
+
metrics.updatedAt = nowIso;
|
|
1448
|
+
recomputeMetricsAggregates(metrics);
|
|
1449
|
+
saveMetricsFile(filePath, metrics);
|
|
1450
|
+
return filePath;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function metricsFinalizeArchive(targetDir, changeName) {
|
|
1454
|
+
const filePath = join(targetDir, 'metrics.json');
|
|
1455
|
+
if (!existsSync(filePath)) return null;
|
|
1456
|
+
const nowIso = new Date().toISOString();
|
|
1457
|
+
const metrics = loadMetricsFile(filePath, changeName, nowIso);
|
|
1458
|
+
metrics.archivedAt = nowIso;
|
|
1459
|
+
metrics.pending = null;
|
|
1460
|
+
metrics.updatedAt = nowIso;
|
|
1461
|
+
recomputeMetricsAggregates(metrics);
|
|
1462
|
+
saveMetricsFile(filePath, metrics);
|
|
1463
|
+
return filePath;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function formatMetricsDuration(durationMs) {
|
|
1467
|
+
if (durationMs == null || !Number.isFinite(durationMs)) return '—';
|
|
1468
|
+
const totalSeconds = Math.max(0, Math.round(durationMs / 1000));
|
|
1469
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1470
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
1471
|
+
const seconds = totalSeconds % 60;
|
|
1472
|
+
if (hours > 0) return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
1473
|
+
if (minutes > 0) return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
1474
|
+
return `${seconds}s`;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function formatMetricsNumber(value) {
|
|
1478
|
+
return value == null ? '—' : String(value);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function formatMetricsCost(value) {
|
|
1482
|
+
return value == null ? '—' : `$${Number(value).toFixed(2)}`;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function resolveMetricsFile(projectDir, changeName) {
|
|
1486
|
+
const activePath = metricsFilePath(projectDir, changeName);
|
|
1487
|
+
if (existsSync(activePath)) return { filePath: activePath, archived: false };
|
|
1488
|
+
const archiveDir = join(projectDir, 'openspec', 'changes', 'archive');
|
|
1489
|
+
if (existsSync(archiveDir)) {
|
|
1490
|
+
const folders = readdirSync(archiveDir)
|
|
1491
|
+
.filter((name) => name === changeName || name.endsWith(`-${changeName}`))
|
|
1492
|
+
.sort()
|
|
1493
|
+
.reverse();
|
|
1494
|
+
for (const folder of folders) {
|
|
1495
|
+
const archivedPath = join(archiveDir, folder, 'metrics.json');
|
|
1496
|
+
if (existsSync(archivedPath)) return { filePath: archivedPath, archived: true };
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
return { filePath: activePath, archived: false, missing: true };
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1293
1502
|
function parseFigmaUrl(url) {
|
|
1294
1503
|
try {
|
|
1295
1504
|
const parsed = new URL(url);
|
|
@@ -2564,6 +2773,7 @@ program
|
|
|
2564
2773
|
};
|
|
2565
2774
|
writeFileSync(join(targetDir, 'handoff.md'), `${buildHandoffMarkdown(fields).trim()}\n`);
|
|
2566
2775
|
const memoryPath = persistMemoryFromHandoff(projectDir, fields);
|
|
2776
|
+
const metricsPath = metricsFinalizeArchive(targetDir, name);
|
|
2567
2777
|
|
|
2568
2778
|
console.log(`change: ${name}`);
|
|
2569
2779
|
console.log(`schema: ${status.schemaName || 'unknown'}`);
|
|
@@ -2571,6 +2781,7 @@ program
|
|
|
2571
2781
|
console.log(`sync: ${syncStatus}`);
|
|
2572
2782
|
console.log(`handoff: ${join(targetRel, 'handoff.md')} (next_command: none)`);
|
|
2573
2783
|
console.log(`memory: ${memoryPath.replace(`${projectDir}/`, '')}`);
|
|
2784
|
+
if (metricsPath) console.log(`metrics: ${metricsPath.replace(`${projectDir}/`, '')} (archived_at set)`);
|
|
2574
2785
|
log.ok(`archived ${name}`);
|
|
2575
2786
|
});
|
|
2576
2787
|
|
|
@@ -2770,6 +2981,13 @@ program
|
|
|
2770
2981
|
.option('--runtime <runtime>', 'Session runtime: local | cloud')
|
|
2771
2982
|
.option('--agent-id <id>', 'Cloud agent identifier')
|
|
2772
2983
|
.option('--cloud-check', 'Verify change artifacts are committed and pushed', false)
|
|
2984
|
+
.option('--started-at <iso>', 'Session start timestamp (overrides the pending marker from --restore)')
|
|
2985
|
+
.option('--model <name>', 'Model used in this session (recorded in metrics.json)')
|
|
2986
|
+
.option('--input-tokens <n>', 'Input tokens spent in this session')
|
|
2987
|
+
.option('--output-tokens <n>', 'Output tokens spent in this session')
|
|
2988
|
+
.option('--total-tokens <n>', 'Total tokens spent in this session (default: input + output)')
|
|
2989
|
+
.option('--cost-usd <usd>', 'Cost of this session in USD')
|
|
2990
|
+
.option('--no-metrics', 'Skip recording this session into metrics.json')
|
|
2773
2991
|
.action((changeName, opts) => {
|
|
2774
2992
|
const projectDir = process.cwd();
|
|
2775
2993
|
const resolved = resolveHandoffChange(projectDir, changeName);
|
|
@@ -2826,6 +3044,10 @@ program
|
|
|
2826
3044
|
} else {
|
|
2827
3045
|
log.warn(`Memory JSON empty or missing at ${memoryPath}`);
|
|
2828
3046
|
}
|
|
3047
|
+
if (opts.metrics !== false && existsSync(changeDir)) {
|
|
3048
|
+
const metricsPath = metricsRecordSessionStart(projectDir, name, fields ? fields.nextRole : '');
|
|
3049
|
+
log.ok(`metrics: session start recorded (${metricsPath.replace(`${projectDir}/`, '')})`);
|
|
3050
|
+
}
|
|
2829
3051
|
return;
|
|
2830
3052
|
}
|
|
2831
3053
|
|
|
@@ -2902,10 +3124,100 @@ program
|
|
|
2902
3124
|
const memoryPath = persistMemoryFromHandoff(projectDir, fields);
|
|
2903
3125
|
console.error(pc.green(' ✓'), `Memory JSON upserted: ${memoryPath}`);
|
|
2904
3126
|
|
|
3127
|
+
if (opts.metrics !== false) {
|
|
3128
|
+
const metricsPath = metricsRecordSessionEnd(projectDir, fields, {
|
|
3129
|
+
startedAt: opts.startedAt,
|
|
3130
|
+
model: opts.model,
|
|
3131
|
+
inputTokens: opts.inputTokens,
|
|
3132
|
+
outputTokens: opts.outputTokens,
|
|
3133
|
+
totalTokens: opts.totalTokens,
|
|
3134
|
+
costUsd: opts.costUsd,
|
|
3135
|
+
});
|
|
3136
|
+
console.error(pc.green(' ✓'), `metrics.json updated: ${metricsPath.replace(`${projectDir}/`, '')}`);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
2905
3139
|
if (fields.runtime === 'cloud') printCloudPersistNextSteps(name);
|
|
2906
3140
|
|
|
2907
3141
|
console.error(pc.dim('Copy the prompt below into the next chat as one fenced block. Do not include this line.'));
|
|
2908
3142
|
process.stdout.write(`${prompt}\n`);
|
|
2909
3143
|
});
|
|
2910
3144
|
|
|
3145
|
+
program
|
|
3146
|
+
.command('metrics [change-name]')
|
|
3147
|
+
.description('Show recorded session metrics for a change: time per phase, tokens, cost, agents, models')
|
|
3148
|
+
.option('--json', 'Print raw metrics.json', false)
|
|
3149
|
+
.action((changeName, opts) => {
|
|
3150
|
+
const projectDir = process.cwd();
|
|
3151
|
+
let name = changeName;
|
|
3152
|
+
if (!name) {
|
|
3153
|
+
const resolved = resolveHandoffChange(projectDir, changeName);
|
|
3154
|
+
if (!resolved) {
|
|
3155
|
+
log.err('No active change found. Pass a name: npx agent-orchestrator-kit metrics <name>');
|
|
3156
|
+
process.exitCode = 1;
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
if (resolved.ambiguous) {
|
|
3160
|
+
log.err(`Multiple active changes: ${resolved.ambiguous.join(', ')}. Pass the change name argument.`);
|
|
3161
|
+
process.exitCode = 1;
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
name = resolved;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
const { filePath, archived, missing } = resolveMetricsFile(projectDir, name);
|
|
3168
|
+
if (missing) {
|
|
3169
|
+
log.err(`No metrics.json for ${name}`);
|
|
3170
|
+
log.info(`Expected: ${filePath.replace(`${projectDir}/`, '')}`);
|
|
3171
|
+
log.info('Metrics are recorded by: handoff --restore (session start) and handoff <name> (session end)');
|
|
3172
|
+
process.exitCode = 1;
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
const metrics = loadMetricsFile(filePath, name, new Date().toISOString());
|
|
3177
|
+
if (opts.json) {
|
|
3178
|
+
process.stdout.write(`${JSON.stringify(metrics, null, 2)}\n`);
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
log.title(`metrics ${name}${archived ? ' (archived)' : ''}`);
|
|
3183
|
+
console.log(`file: ${filePath.replace(`${projectDir}/`, '')}`);
|
|
3184
|
+
console.log(`sessions: ${metrics.totals.sessions}${metrics.totals.cloudSessions ? ` (cloud: ${metrics.totals.cloudSessions})` : ''}`);
|
|
3185
|
+
console.log(`work time: ${formatMetricsDuration(metrics.totals.durationMs)}`);
|
|
3186
|
+
console.log(`lead time: ${formatMetricsDuration(metrics.totals.leadTimeMs)}`);
|
|
3187
|
+
console.log(`tokens: ${formatMetricsNumber(metrics.spend.totalTokens)} (in: ${formatMetricsNumber(metrics.spend.inputTokens)}, out: ${formatMetricsNumber(metrics.spend.outputTokens)})`);
|
|
3188
|
+
console.log(`cost: ${formatMetricsCost(metrics.spend.costUsd)}`);
|
|
3189
|
+
if (metrics.archivedAt) console.log(`archived: ${metrics.archivedAt}`);
|
|
3190
|
+
if (metrics.pending) log.warn(`open session since ${metrics.pending.startedAt} (${metrics.pending.role || 'unknown role'})`);
|
|
3191
|
+
|
|
3192
|
+
const phaseOrder = ['explore', 'design', 'spec', 'review', 'apply', 'archive', 'other'];
|
|
3193
|
+
const phaseKeys = phaseOrder.filter((key) => metrics.phases[key]);
|
|
3194
|
+
if (phaseKeys.length) {
|
|
3195
|
+
console.log('');
|
|
3196
|
+
console.log('phase sessions time tokens cost agents');
|
|
3197
|
+
for (const key of phaseKeys) {
|
|
3198
|
+
const phase = metrics.phases[key];
|
|
3199
|
+
const cols = [
|
|
3200
|
+
key.padEnd(10),
|
|
3201
|
+
String(phase.sessions).padEnd(9),
|
|
3202
|
+
formatMetricsDuration(phase.durationMs).padEnd(9),
|
|
3203
|
+
formatMetricsNumber(phase.totalTokens).padEnd(9),
|
|
3204
|
+
formatMetricsCost(phase.costUsd).padEnd(9),
|
|
3205
|
+
phase.agents.join(', ') || '—',
|
|
3206
|
+
];
|
|
3207
|
+
console.log(cols.join(' '));
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
if (metrics.sessions.length) {
|
|
3212
|
+
console.log('');
|
|
3213
|
+
console.log('recent sessions:');
|
|
3214
|
+
for (const session of metrics.sessions.slice(-5)) {
|
|
3215
|
+
const spendLabel = session.totalTokens != null || session.costUsd != null
|
|
3216
|
+
? ` — ${formatMetricsNumber(session.totalTokens)} tok, ${formatMetricsCost(session.costUsd)}`
|
|
3217
|
+
: '';
|
|
3218
|
+
console.log(`- ${session.endedAt} ${session.phase.padEnd(7)} ${formatMetricsDuration(session.durationMs).padEnd(9)} ${session.role || '(no role)'}${session.model ? ` [${session.model}]` : ''}${spendLabel}`);
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
});
|
|
3222
|
+
|
|
2911
3223
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -20,7 +20,7 @@ Agents (local or cloud) write session artifacts only to git-tracked paths — ne
|
|
|
20
20
|
|
|
21
21
|
## Session Exit (order)
|
|
22
22
|
1. The parent writes `openspec/changes/<name>/handoff.md` itself: Closed role, Change, Done, Decisions, Blocked, Next command, Next role, Attach, Subagents to spawn, Constraints, Runtime.
|
|
23
|
-
2. `npx agent-orchestrator-kit handoff <name>` — require exit 0 (appends non-empty Decisions into append-only `openspec/changes/<name>/decisions.md`, upserts absolute-path Memory JSON, prints the expanded prompt on stdout). `decisions.md` is the git canon of change decisions; Memory `Decision:*` is a file→Memory mirror only. Cloud sessions pass `--runtime cloud` (or set `AOK_RUNTIME=cloud` / `AOK_AGENT_ID` in the cloud-agent environment).
|
|
23
|
+
2. `npx agent-orchestrator-kit handoff <name>` — require exit 0 (appends non-empty Decisions into append-only `openspec/changes/<name>/decisions.md`, upserts absolute-path Memory JSON, records the session into `openspec/changes/<name>/metrics.json`, prints the expanded prompt on stdout). `decisions.md` is the git canon of change decisions; Memory `Decision:*` is a file→Memory mirror only. Cloud sessions pass `--runtime cloud` (or set `AOK_RUNTIME=cloud` / `AOK_AGENT_ID` in the cloud-agent environment). When session spend is known, pass it to persist: `--model <name> --input-tokens <n> --output-tokens <n> --cost-usd <usd>` — metrics power planning; never guess values you do not know.
|
|
24
24
|
3. Spawn `session-handoff` in persist mode ONLY if step 2 failed (Amp: isolated `subagent-session-handoff`). Fallback, never routine.
|
|
25
25
|
4. Memory MCP is an optional mirror: if tools are available, update `Change:<name>`, `Handoff:<name>`, `Decision:*` in one call; unavailability never blocks closing.
|
|
26
26
|
5. Paste CLI stdout as one fenced block. First line `/opsx:…`. Body uses `project.agent_language`. Self-contained (Done/Decisions/Blocked/spawn/HARD STOP). No banner.
|