@aperant/framework 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +64 -10
- package/agents/apt-improver.md +99 -0
- package/agents/apt-planner.md +115 -10
- package/dist/__test-helpers/run-cmd.d.mts +4 -2
- package/dist/__test-helpers/run-cmd.d.mts.map +1 -1
- package/dist/__test-helpers/run-cmd.mjs +56 -15
- package/dist/__test-helpers/run-cmd.mjs.map +1 -1
- package/dist/cli/ci-watch/lock.d.mts +53 -0
- package/dist/cli/ci-watch/lock.d.mts.map +1 -0
- package/dist/cli/ci-watch/lock.mjs +172 -0
- package/dist/cli/ci-watch/lock.mjs.map +1 -0
- package/dist/cli/ci-watch/state.d.mts +36 -0
- package/dist/cli/ci-watch/state.d.mts.map +1 -0
- package/dist/cli/ci-watch/state.mjs +103 -0
- package/dist/cli/ci-watch/state.mjs.map +1 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts +58 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -0
- package/dist/cli/ci-watch/stop-matrix.mjs +164 -0
- package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -0
- package/dist/cli/cli-wrappers/ci-watch.d.mts +2 -0
- package/dist/cli/cli-wrappers/ci-watch.d.mts.map +1 -0
- package/dist/cli/cli-wrappers/ci-watch.mjs +9 -0
- package/dist/cli/cli-wrappers/ci-watch.mjs.map +1 -0
- package/dist/cli/commands/adr.d.mts +5 -0
- package/dist/cli/commands/adr.d.mts.map +1 -0
- package/dist/cli/commands/adr.mjs +228 -0
- package/dist/cli/commands/adr.mjs.map +1 -0
- package/dist/cli/commands/ci-watch.d.mts +7 -0
- package/dist/cli/commands/ci-watch.d.mts.map +1 -0
- package/dist/cli/commands/ci-watch.mjs +465 -0
- package/dist/cli/commands/ci-watch.mjs.map +1 -0
- package/dist/cli/commands/context.d.mts +7 -0
- package/dist/cli/commands/context.d.mts.map +1 -0
- package/dist/cli/commands/context.mjs +224 -0
- package/dist/cli/commands/context.mjs.map +1 -0
- package/dist/cli/commands/event.d.mts.map +1 -1
- package/dist/cli/commands/event.mjs +59 -24
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/host-detect.d.mts +1 -1
- package/dist/cli/commands/host-detect.d.mts.map +1 -1
- package/dist/cli/commands/host-detect.mjs +30 -3
- package/dist/cli/commands/host-detect.mjs.map +1 -1
- package/dist/cli/commands/modes.d.mts +13 -0
- package/dist/cli/commands/modes.d.mts.map +1 -0
- package/dist/cli/commands/modes.mjs +220 -0
- package/dist/cli/commands/modes.mjs.map +1 -0
- package/dist/cli/commands/route.mjs +1 -1
- package/dist/cli/commands/route.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +108 -6
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/tokens.d.mts.map +1 -1
- package/dist/cli/commands/tokens.mjs +150 -6
- package/dist/cli/commands/tokens.mjs.map +1 -1
- package/dist/cli/commands/triage.d.mts +8 -0
- package/dist/cli/commands/triage.d.mts.map +1 -0
- package/dist/cli/commands/triage.mjs +259 -0
- package/dist/cli/commands/triage.mjs.map +1 -0
- package/dist/cli/config/post-merge-sweep.d.mts +18 -0
- package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
- package/dist/cli/config/post-merge-sweep.mjs +48 -3
- package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
- package/dist/cli/consistency/registry.d.mts +11 -0
- package/dist/cli/consistency/registry.d.mts.map +1 -1
- package/dist/cli/consistency/registry.mjs +13 -0
- package/dist/cli/consistency/registry.mjs.map +1 -1
- package/dist/cli/coordination/agent-identity.d.mts +9 -0
- package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
- package/dist/cli/coordination/agent-identity.mjs +11 -0
- package/dist/cli/coordination/agent-identity.mjs.map +1 -1
- package/dist/cli/coordination/event-log.d.mts +27 -0
- package/dist/cli/coordination/event-log.d.mts.map +1 -1
- package/dist/cli/coordination/event-log.mjs +72 -2
- package/dist/cli/coordination/event-log.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +14 -1
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/registry.d.mts +11 -0
- package/dist/cli/gate/registry.d.mts.map +1 -1
- package/dist/cli/gate/registry.mjs +13 -0
- package/dist/cli/gate/registry.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +1 -0
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/host/detect.d.mts +1 -0
- package/dist/cli/host/detect.d.mts.map +1 -1
- package/dist/cli/host/detect.mjs +5 -0
- package/dist/cli/host/detect.mjs.map +1 -1
- package/dist/cli/route/envelope.d.mts +68 -4
- package/dist/cli/route/envelope.d.mts.map +1 -1
- package/dist/cli/route/envelope.mjs +140 -103
- package/dist/cli/route/envelope.mjs.map +1 -1
- package/dist/cli/route/skill-discover.d.mts +9 -0
- package/dist/cli/route/skill-discover.d.mts.map +1 -1
- package/dist/cli/route/skill-discover.mjs +11 -0
- package/dist/cli/route/skill-discover.mjs.map +1 -1
- package/dist/cli/skill-author/contract.d.mts +1 -0
- package/dist/cli/skill-author/contract.d.mts.map +1 -1
- package/dist/cli/skill-author/contract.mjs +7 -0
- package/dist/cli/skill-author/contract.mjs.map +1 -1
- package/dist/cli/task/worktree-cleanup.d.mts +9 -1
- package/dist/cli/task/worktree-cleanup.d.mts.map +1 -1
- package/dist/cli/task/worktree-cleanup.mjs +190 -9
- package/dist/cli/task/worktree-cleanup.mjs.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/apt-planner.md +1 -1
- package/dist/plugin/skills/apt/SKILL.md +111 -5
- package/dist/plugin/skills/apt-author-skill/SKILL.md +11 -0
- package/dist/plugin/skills/apt-bootstrap/SKILL.md +1 -0
- package/dist/plugin/skills/apt-classify/SKILL.md +1 -0
- package/dist/plugin/skills/apt-close-task/SKILL.md +1 -0
- package/dist/plugin/skills/apt-create-docs/SKILL.md +1 -0
- package/dist/plugin/skills/apt-debug/SKILL.md +2 -0
- package/dist/plugin/skills/apt-design/SKILL.md +2 -0
- package/dist/plugin/skills/apt-discuss/SKILL.md +2 -0
- package/dist/plugin/skills/apt-docs/SKILL.md +2 -0
- package/dist/plugin/skills/apt-execute/SKILL.md +1 -0
- package/dist/plugin/skills/apt-mockup/SKILL.md +2 -0
- package/dist/plugin/skills/apt-pause/SKILL.md +1 -0
- package/dist/plugin/skills/apt-personas/SKILL.md +1 -0
- package/dist/plugin/skills/apt-plan/SKILL.md +2 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -0
- package/dist/plugin/skills/apt-quick/SKILL.md +2 -0
- package/dist/plugin/skills/apt-resume/SKILL.md +1 -0
- package/dist/plugin/skills/apt-review/SKILL.md +1 -0
- package/dist/plugin/skills/apt-roadmap/SKILL.md +1 -0
- package/dist/plugin/skills/apt-roundtable/SKILL.md +2 -0
- package/dist/plugin/skills/apt-run/SKILL.md +1 -0
- package/dist/plugin/skills/apt-scan/SKILL.md +1 -0
- package/dist/plugin/skills/apt-setup/SKILL.md +1 -0
- package/dist/plugin/skills/apt-ship/SKILL.md +6 -5
- package/dist/plugin/skills/apt-stress-test/SKILL.md +1 -0
- package/dist/plugin/skills/apt-terminal/SKILL.md +1 -0
- package/dist/plugin/skills/apt-update/SKILL.md +5 -0
- package/dist/plugin/skills/apt-verify/SKILL.md +1 -0
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +1 -0
- package/dist/types/config.d.ts +85 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +125 -125
- package/prompts/coder.md +2 -0
- package/prompts/planner.md +12 -0
- package/prompts/spec_writer.md +9 -0
- package/skills/apt/SKILL.md +1 -0
- package/skills/apt-close-task/SKILL.md +32 -1
- package/skills/apt-debug/SKILL.md +39 -6
- package/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
- package/skills/apt-diagram/SKILL.md +342 -0
- package/skills/apt-diagram/appendices/design-discipline.md +97 -0
- package/skills/apt-discuss/SKILL.md +25 -0
- package/skills/apt-discuss/appendices/grill-discipline.md +104 -0
- package/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
- package/skills/apt-execute/SKILL.md +48 -5
- package/skills/apt-execute/appendices/tdd-mode.md +107 -0
- package/skills/apt-improve/DEEPENING.md +84 -0
- package/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
- package/skills/apt-improve/LANGUAGE.md +104 -0
- package/skills/apt-improve/SKILL.md +141 -0
- package/skills/apt-plan/SKILL.md +147 -4
- package/skills/apt-planner.md +42 -1
- package/skills/apt-pr-review/SKILL.md +46 -16
- package/skills/apt-prototype/LOGIC.md +109 -0
- package/skills/apt-prototype/SKILL.md +143 -0
- package/skills/apt-prototype/UI.md +90 -0
- package/skills/apt-quick/SKILL.md +30 -0
- package/skills/apt-review/SKILL.md +2 -0
- package/skills/apt-run/SKILL.md +32 -4
- package/skills/apt-setup/SKILL.md +128 -2
- package/skills/apt-ship/SKILL.md +47 -0
- package/skills/apt-triage/AGENT-BRIEF.md +84 -0
- package/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
- package/skills/apt-triage/SKILL.md +169 -0
- package/skills/apt-verify/SKILL.md +3 -0
- package/skills/apt-verify-proof/SKILL.md +3 -0
- package/skills/apt-watch-ci/SKILL.md +163 -0
- package/skills/apt-zoom-out/SKILL.md +130 -0
- package/src/cli/ci-watch/lock.mjs +178 -0
- package/src/cli/ci-watch/state.mjs +103 -0
- package/src/cli/ci-watch/stop-matrix.mjs +181 -0
- package/src/cli/cli-wrappers/ci-watch.mjs +9 -0
- package/src/cli/commands/adr.mjs +243 -0
- package/src/cli/commands/ci-watch.mjs +503 -0
- package/src/cli/commands/context.mjs +244 -0
- package/src/cli/commands/event.mjs +63 -24
- package/src/cli/commands/host-detect.mjs +33 -7
- package/src/cli/commands/modes.mjs +215 -0
- package/src/cli/commands/route.mjs +1 -1
- package/src/cli/commands/task.mjs +125 -18
- package/src/cli/commands/tokens.mjs +157 -6
- package/src/cli/commands/triage.mjs +277 -0
- package/src/cli/config/post-merge-sweep.mjs +49 -3
- package/src/cli/consistency/registry.mjs +14 -0
- package/src/cli/coordination/agent-identity.mjs +12 -0
- package/src/cli/coordination/event-log.mjs +73 -2
- package/src/cli/dispatch.mjs +15 -1
- package/src/cli/gate/registry.mjs +14 -0
- package/src/cli/help.mjs +1 -0
- package/src/cli/host/detect.mjs +5 -0
- package/src/cli/route/envelope.mjs +140 -106
- package/src/cli/route/skill-discover.mjs +12 -0
- package/src/cli/skill-author/contract.mjs +7 -0
- package/src/cli/task/worktree-cleanup.mjs +191 -9
- package/templates/adr-format.md +56 -0
- package/templates/config.json +4 -0
- package/templates/context-format.md +34 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* commands/task.mjs — per-task isolated directories for multi-agent concurrency (C29).
|
|
3
3
|
*
|
|
4
|
-
* task subcommand: create | list | get | close | update | move | complete-subtask | prune
|
|
4
|
+
* task subcommand: create | list | get | close | update | move | complete-subtask | narration-drain | prune
|
|
5
5
|
*
|
|
6
6
|
* Layer 1 (post-CLI-layering refactor, subtask 9). cmdTask returns
|
|
7
7
|
* { envelope, exitCode } — it MUST NOT call process.exit, output(), or fail()
|
|
@@ -15,6 +15,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
|
16
16
|
import { join, resolve } from 'node:path'
|
|
17
17
|
import { getTaskIsolationConfig } from '../config/load.mjs'
|
|
18
|
+
import { findMostRecentEventForTask } from '../coordination/event-log.mjs'
|
|
18
19
|
import { createCoordinationStore } from '../coordination/store.mjs'
|
|
19
20
|
import { getGitIdentity } from '../git/identity.mjs'
|
|
20
21
|
import { detectWorktree } from '../git/worktree-detect.mjs'
|
|
@@ -43,7 +44,7 @@ import { err, exitWith, ok } from '../util/result.mjs'
|
|
|
43
44
|
export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
44
45
|
if (!subcommand)
|
|
45
46
|
return err(
|
|
46
|
-
'Usage: apt-tools task <create|list|get|close|update|move|complete-subtask|prune> <project-dir> [flags]',
|
|
47
|
+
'Usage: apt-tools task <create|list|get|close|update|move|complete-subtask|narration-drain|prune> <project-dir> [flags]',
|
|
47
48
|
)
|
|
48
49
|
if (!projectDir) return err('Usage: apt-tools task ' + subcommand + ' <project-dir> [flags]')
|
|
49
50
|
|
|
@@ -457,6 +458,11 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
457
458
|
const taskId = flags.get('id')
|
|
458
459
|
if (!taskId) return err('Required: --id <task-id>')
|
|
459
460
|
const verdict = flags.get('verdict') || 'completed'
|
|
461
|
+
// Internal flag set by `task close-merged` when
|
|
462
|
+
// GitHub confirmed state === 'MERGED'. Threads through to
|
|
463
|
+
// computeWorktreeCleanup so cleanup skips local merge-back and
|
|
464
|
+
// runs fetch + ff-only instead.
|
|
465
|
+
const prMerged = flags.has('pr-merged-remotely')
|
|
460
466
|
|
|
461
467
|
if (!existsSync(statePath)) return err('No state.json found')
|
|
462
468
|
|
|
@@ -521,11 +527,14 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
521
527
|
|
|
522
528
|
// Worktree cleanup hook — honors task_isolation.cleanup_on_close
|
|
523
529
|
// (never | ask | auto). See src/cli/task/worktree-cleanup.mjs.
|
|
530
|
+
// prMerged threads from `task close-merged`
|
|
531
|
+
// via --pr-merged-remotely; when true, cleanup runs fetch +
|
|
532
|
+
// ff-only instead of local merge-back.
|
|
524
533
|
let cleanup = null
|
|
525
534
|
if (task.worktree_path) {
|
|
526
535
|
const isolation = getTaskIsolationConfig(targetDir)
|
|
527
536
|
const mode = isolation.cleanup_on_close || 'ask'
|
|
528
|
-
cleanup = computeWorktreeCleanup(task, mode, targetDir)
|
|
537
|
+
cleanup = computeWorktreeCleanup(task, mode, targetDir, { prMerged })
|
|
529
538
|
}
|
|
530
539
|
|
|
531
540
|
// Best-effort team sync: commit shared bookkeeping so teammates
|
|
@@ -721,6 +730,21 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
721
730
|
let lifecycleTransition = null
|
|
722
731
|
if (flags.has('lifecycle-phase')) {
|
|
723
732
|
task.lifecycle_phase = flags.get('lifecycle-phase')
|
|
733
|
+
// R6 (roundtable 2026-05-14) — compute elapsed_ms since the
|
|
734
|
+
// previous LIFECYCLE event (or task.created) for phase-boundary
|
|
735
|
+
// telemetry. The 'task.lifecycle.' opPrefix is critical: without
|
|
736
|
+
// it, the most-recent event lookup picks up high-frequency events
|
|
737
|
+
// like task.tokens.recorded, which would silently corrupt
|
|
738
|
+
// elapsed_ms attribution (PR #115 review finding LOG-001).
|
|
739
|
+
const prev = findMostRecentEventForTask(targetDir, taskId, 'task.lifecycle.')
|
|
740
|
+
let prevTime = prev ? new Date(prev.t).getTime() : null
|
|
741
|
+
// LOG-003: when no prior lifecycle event exists (first transition),
|
|
742
|
+
// fall back to task.created_at so planning phase duration is captured.
|
|
743
|
+
if (!prevTime && task.created_at) {
|
|
744
|
+
const createdMs = new Date(task.created_at).getTime()
|
|
745
|
+
if (Number.isFinite(createdMs)) prevTime = createdMs
|
|
746
|
+
}
|
|
747
|
+
const elapsed_ms = prevTime ? Date.now() - prevTime : null
|
|
724
748
|
// R10 — emit the lifecycle transition event even when the
|
|
725
749
|
// side-effect helper decides the phase spec is a no-op; the
|
|
726
750
|
// caller still changed task.lifecycle_phase.
|
|
@@ -732,6 +756,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
732
756
|
scope: task.scope || 'project',
|
|
733
757
|
milestone_id: task.milestone_id ?? null,
|
|
734
758
|
phase_id: task.phase_id ?? null,
|
|
759
|
+
elapsed_ms,
|
|
735
760
|
},
|
|
736
761
|
})
|
|
737
762
|
// C56 B5 — exhaustive lifecycle→roadmap-phase map. `null`
|
|
@@ -1253,20 +1278,27 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1253
1278
|
// Drive the full close path via an in-process CLI call so all of
|
|
1254
1279
|
// task close's side effects (team-sync, worktree cleanup, phase flip
|
|
1255
1280
|
// when auto_close_phase is on, event emission, INDEX regen) run.
|
|
1256
|
-
const
|
|
1257
|
-
'
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
)
|
|
1281
|
+
const closeArgs = [
|
|
1282
|
+
resolve(import.meta.dirname ?? '.', '..', '..', '..', 'bin', 'apt-tools.mjs'),
|
|
1283
|
+
'task',
|
|
1284
|
+
'close',
|
|
1285
|
+
targetDir,
|
|
1286
|
+
'--id',
|
|
1287
|
+
id,
|
|
1288
|
+
'--verdict',
|
|
1289
|
+
closeVerdict,
|
|
1290
|
+
]
|
|
1291
|
+
// Only signal pr_merged_remotely when GitHub
|
|
1292
|
+
// confirmed MERGED. CLOSED-not-merged is a rejection path; leaving
|
|
1293
|
+
// cleanup behavior unchanged there is intentional (spec §11 R3).
|
|
1294
|
+
if (parsed.state === 'MERGED') {
|
|
1295
|
+
closeArgs.push('--pr-merged-remotely')
|
|
1296
|
+
}
|
|
1297
|
+
const closeRes = spawnSync('node', closeArgs, {
|
|
1298
|
+
cwd: targetDir,
|
|
1299
|
+
encoding: 'utf-8',
|
|
1300
|
+
env: { ...process.env },
|
|
1301
|
+
})
|
|
1270
1302
|
if (closeRes.status !== 0) {
|
|
1271
1303
|
errors.push({
|
|
1272
1304
|
id,
|
|
@@ -1317,6 +1349,36 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1317
1349
|
verdict: closeVerdict,
|
|
1318
1350
|
mergedAt: parsed.mergedAt ?? null,
|
|
1319
1351
|
})
|
|
1352
|
+
|
|
1353
|
+
// Append a pending_narration[] ledger entry on successful merged close
|
|
1354
|
+
// so /apt:close-task --narrate-only can drain it later. The ledger is
|
|
1355
|
+
// additive-only; failure to write must never undo the close above
|
|
1356
|
+
// (R4 ledger leak — caller can manually invoke the narrator).
|
|
1357
|
+
if (closeVerdict === 'approved') {
|
|
1358
|
+
try {
|
|
1359
|
+
withFileLock(
|
|
1360
|
+
statePath,
|
|
1361
|
+
() => {
|
|
1362
|
+
const fresh = JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
1363
|
+
if (!Array.isArray(fresh.pending_narration)) fresh.pending_narration = []
|
|
1364
|
+
fresh.pending_narration.push({
|
|
1365
|
+
task_id: id,
|
|
1366
|
+
scope: task.scope || 'project',
|
|
1367
|
+
phase_id: task.phase_id ?? null,
|
|
1368
|
+
closed_at: new Date().toISOString(),
|
|
1369
|
+
pr_number: parsed.number ?? null,
|
|
1370
|
+
})
|
|
1371
|
+
atomicWriteJson(statePath, fresh)
|
|
1372
|
+
},
|
|
1373
|
+
{ projectRoot: targetDir },
|
|
1374
|
+
)
|
|
1375
|
+
} catch (e) {
|
|
1376
|
+
errors.push({
|
|
1377
|
+
id,
|
|
1378
|
+
reason: `ledger-write-failed: ${e.message ?? String(e)}`,
|
|
1379
|
+
})
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1320
1382
|
}
|
|
1321
1383
|
|
|
1322
1384
|
return ok({
|
|
@@ -1329,6 +1391,51 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1329
1391
|
})
|
|
1330
1392
|
}
|
|
1331
1393
|
|
|
1394
|
+
case 'narration-drain': {
|
|
1395
|
+
// Remove a single pending_narration[] row under the same withFileLock
|
|
1396
|
+
// guard task close uses. Caller (apt:close-task --narrate-only) invokes
|
|
1397
|
+
// this once per drained entry AFTER its background narrator spawn
|
|
1398
|
+
// returns — so a spawn failure leaves the row in place for retry.
|
|
1399
|
+
const taskId = flags.get('task')
|
|
1400
|
+
if (!taskId) return err('Required: --task <task-id>')
|
|
1401
|
+
|
|
1402
|
+
if (!existsSync(statePath)) {
|
|
1403
|
+
return ok({
|
|
1404
|
+
status: 'ok',
|
|
1405
|
+
command: 'task-narration-drain',
|
|
1406
|
+
removed: null,
|
|
1407
|
+
remaining: 0,
|
|
1408
|
+
reason: 'no-state',
|
|
1409
|
+
})
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
let removed = null
|
|
1413
|
+
let remaining = 0
|
|
1414
|
+
withFileLock(
|
|
1415
|
+
statePath,
|
|
1416
|
+
() => {
|
|
1417
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
1418
|
+
const ledger = Array.isArray(state.pending_narration) ? state.pending_narration : []
|
|
1419
|
+
const idx = ledger.findIndex((row) => row && row.task_id === taskId)
|
|
1420
|
+
if (idx >= 0) {
|
|
1421
|
+
removed = ledger[idx]
|
|
1422
|
+
ledger.splice(idx, 1)
|
|
1423
|
+
state.pending_narration = ledger
|
|
1424
|
+
atomicWriteJson(statePath, state)
|
|
1425
|
+
}
|
|
1426
|
+
remaining = ledger.length
|
|
1427
|
+
},
|
|
1428
|
+
{ projectRoot: targetDir },
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
return ok({
|
|
1432
|
+
status: 'ok',
|
|
1433
|
+
command: 'task-narration-drain',
|
|
1434
|
+
removed,
|
|
1435
|
+
remaining,
|
|
1436
|
+
})
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1332
1439
|
case 'prune': {
|
|
1333
1440
|
// C56 A4 — retention verb. Removes `.aperant/tasks/{id}/` directories
|
|
1334
1441
|
// whose task row is in `state.completed_tasks[]` AND whose completion
|
|
@@ -1426,7 +1533,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1426
1533
|
|
|
1427
1534
|
default:
|
|
1428
1535
|
return err(
|
|
1429
|
-
`Unknown task subcommand: ${subcommand}. Expected: create, list, get, close, update, move, complete-subtask, close-merged, prune`,
|
|
1536
|
+
`Unknown task subcommand: ${subcommand}. Expected: create, list, get, close, update, move, complete-subtask, close-merged, narration-drain, prune`,
|
|
1430
1537
|
)
|
|
1431
1538
|
}
|
|
1432
1539
|
}
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* - state.active_tasks[id].tokens — flat rolled-up summary (downstream).
|
|
11
11
|
*
|
|
12
12
|
* Subcommands:
|
|
13
|
-
* record
|
|
14
|
-
* tally
|
|
15
|
-
* compare
|
|
16
|
-
* list
|
|
13
|
+
* record — append one entry from a Claude Code transcript or manual --input/--output.
|
|
14
|
+
* tally — emit a TaskCostSummary computed via CostTracker.
|
|
15
|
+
* compare — emit { aperant, baseline, ratio } (baseline JSON optional; C23 hook).
|
|
16
|
+
* list — scan all .aperant/tasks/{*}/tokens.json and emit a flat sorted list.
|
|
17
|
+
* phase-summary — join tokens.json × task.lifecycle.* events by phase
|
|
18
|
+
* (roundtable 2026-05-14 R6 — phase-boundary telemetry).
|
|
17
19
|
*
|
|
18
20
|
* All writes go through atomicWriteJson + withFileLock (R3), state mutations
|
|
19
21
|
* through createCoordinationStore.updateState (R8), and a lifecycle event is
|
|
@@ -29,6 +31,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
29
31
|
import { calculateCost } from '../../cost/pricing.mjs'
|
|
30
32
|
import { CostTracker } from '../../cost/tracker.mjs'
|
|
31
33
|
import { parseClaudeCodeTranscript } from '../../cost/transcript.mjs'
|
|
34
|
+
import { readTaskEventsOverDays } from '../coordination/event-log.mjs'
|
|
32
35
|
import { createCoordinationStore } from '../coordination/store.mjs'
|
|
33
36
|
import { withFileLock } from '../util/fs-lock.mjs'
|
|
34
37
|
import { atomicWriteJson } from '../util/io.mjs'
|
|
@@ -67,6 +70,12 @@ function nowIso() {
|
|
|
67
70
|
/**
|
|
68
71
|
* Convert the on-disk JSON entry shape (snake_case + cost_usd) into the
|
|
69
72
|
* canonical CostEntry shape used by CostTracker (camelCase + costUsd).
|
|
73
|
+
*
|
|
74
|
+
* R6 (roundtable 2026-05-14): `phase` is an optional per-entry tag indicating
|
|
75
|
+
* which pipeline phase (planning | executing | verifying | reviewing) the
|
|
76
|
+
* session belonged to. Carried through to enable `tokens phase-summary` to
|
|
77
|
+
* group by phase. Older entries lacking `phase` keep working — the field is
|
|
78
|
+
* additive and downstream consumers tolerate `undefined`.
|
|
70
79
|
*/
|
|
71
80
|
function entryToCostEntry(taskId, entry) {
|
|
72
81
|
return {
|
|
@@ -82,6 +91,7 @@ function entryToCostEntry(taskId, entry) {
|
|
|
82
91
|
costUsd: typeof entry.cost_usd === 'number' ? entry.cost_usd : 0,
|
|
83
92
|
timestamp: entry.timestamp,
|
|
84
93
|
durationMs: entry.duration_ms || 0,
|
|
94
|
+
phase: entry.phase || null,
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
97
|
|
|
@@ -162,6 +172,20 @@ function cmdRecord(targetDir, args) {
|
|
|
162
172
|
// DEFAULT_MODEL because there's no transcript to source it from.
|
|
163
173
|
const modelOverride = flagOf(args, '--model')
|
|
164
174
|
const providerId = flagOf(args, '--provider') || DEFAULT_PROVIDER
|
|
175
|
+
// R6 (roundtable 2026-05-14): optional --phase tags this batch of entries
|
|
176
|
+
// with the lifecycle phase they belong to. When omitted, phase-summary
|
|
177
|
+
// falls back to inferring phase from lifecycle event timestamps.
|
|
178
|
+
//
|
|
179
|
+
// SEC-001 (PR #115 review): whitelist accepted values to keep parity with
|
|
180
|
+
// --track (which also enforces a four-value allowlist). Unknown values
|
|
181
|
+
// fail closed rather than persisting arbitrary strings into tokens.json.
|
|
182
|
+
const phaseFlag = flagOf(args, '--phase')
|
|
183
|
+
const VALID_PHASES = new Set(['planning', 'executing', 'verifying', 'reviewing'])
|
|
184
|
+
if (phaseFlag !== null && !VALID_PHASES.has(phaseFlag)) {
|
|
185
|
+
throw new TokensUsageError(
|
|
186
|
+
`--phase must be one of: planning, executing, verifying, reviewing (got: ${phaseFlag})`,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
165
189
|
|
|
166
190
|
if (!transcript && (inputStr === null || outputStr === null)) {
|
|
167
191
|
throw new TokensUsageError('Provide --transcript <path> OR both --input N --output N')
|
|
@@ -195,6 +219,7 @@ function cmdRecord(targetDir, args) {
|
|
|
195
219
|
timestamp: p.timestamp || ts,
|
|
196
220
|
source: 'transcript',
|
|
197
221
|
transcript_path: transcript,
|
|
222
|
+
...(phaseFlag ? { phase: phaseFlag } : {}),
|
|
198
223
|
})
|
|
199
224
|
}
|
|
200
225
|
} else {
|
|
@@ -222,6 +247,7 @@ function cmdRecord(targetDir, args) {
|
|
|
222
247
|
cost_usd: costUsd,
|
|
223
248
|
timestamp: nowIso(),
|
|
224
249
|
source: 'manual',
|
|
250
|
+
...(phaseFlag ? { phase: phaseFlag } : {}),
|
|
225
251
|
})
|
|
226
252
|
}
|
|
227
253
|
|
|
@@ -419,12 +445,135 @@ function cmdList(targetDir, args) {
|
|
|
419
445
|
})
|
|
420
446
|
}
|
|
421
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Read lifecycle events for a task across today + yesterday (UTC).
|
|
450
|
+
* Returns events sorted ascending by `t`. Best-effort: missing event log
|
|
451
|
+
* directories return an empty array — phase-summary degrades gracefully
|
|
452
|
+
* when telemetry isn't enabled.
|
|
453
|
+
*/
|
|
454
|
+
function readTaskLifecycleEvents(targetDir, taskId) {
|
|
455
|
+
// Delegates to the shared two-day window helper in event-log.mjs so
|
|
456
|
+
// tokens.mjs and the lifecycle-event helpers cannot drift on the window
|
|
457
|
+
// strategy (PR #115 review findings QUA-001 + PRF-001).
|
|
458
|
+
const events = readTaskEventsOverDays(targetDir, taskId, 'task.lifecycle.')
|
|
459
|
+
events.sort((a, b) => Date.parse(a.t) - Date.parse(b.t))
|
|
460
|
+
return events
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Infer phase for a token entry by finding the most recent lifecycle event
|
|
465
|
+
* whose timestamp <= entry timestamp. Returns null when no lifecycle event
|
|
466
|
+
* precedes the entry (e.g. entries recorded before any lifecycle transition).
|
|
467
|
+
*/
|
|
468
|
+
function inferPhaseForEntry(entryTime, lifecycleEvents) {
|
|
469
|
+
const entryMs = Date.parse(entryTime)
|
|
470
|
+
if (!Number.isFinite(entryMs)) return null
|
|
471
|
+
let match = null
|
|
472
|
+
for (const evt of lifecycleEvents) {
|
|
473
|
+
const evtMs = Date.parse(evt.t)
|
|
474
|
+
// LOG-002 (PR #115 review): a malformed evt.t parses to NaN. `NaN <=
|
|
475
|
+
// entryMs` is false, which would trip an `else break` and drop all
|
|
476
|
+
// subsequent valid events. Skip NaN entries instead so a single bad
|
|
477
|
+
// row can't poison phase inference.
|
|
478
|
+
if (!Number.isFinite(evtMs)) continue
|
|
479
|
+
if (evtMs <= entryMs) match = evt
|
|
480
|
+
else break
|
|
481
|
+
}
|
|
482
|
+
return match?.data?.lifecycle_phase ?? null
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* phase-summary — join tokens.json × task.lifecycle.* events by task_id +
|
|
487
|
+
* phase. Roundtable 2026-05-14 R6 — provides the A/B baseline needed to
|
|
488
|
+
* evaluate sequential vs sub-agents mode without per-call profiling.
|
|
489
|
+
*
|
|
490
|
+
* Each row: { phase, duration_ms, input_tokens, output_tokens, cost_usd, session_count }.
|
|
491
|
+
* Duration is summed from lifecycle event `elapsed_ms`. Tokens are summed
|
|
492
|
+
* from entries whose `phase` is set (or inferred from event timestamps when
|
|
493
|
+
* the entry has no `phase` tag).
|
|
494
|
+
*/
|
|
495
|
+
function cmdPhaseSummary(targetDir, args) {
|
|
496
|
+
const taskId = flagOf(args, '--task-id')
|
|
497
|
+
if (!taskId) throw new TokensUsageError('--task-id is required for tokens phase-summary')
|
|
498
|
+
|
|
499
|
+
const tokensPath = tokensFilePath(targetDir, taskId)
|
|
500
|
+
const doc = readTokensFile(tokensPath)
|
|
501
|
+
const entries = doc?.entries || []
|
|
502
|
+
|
|
503
|
+
const lifecycleEvents = readTaskLifecycleEvents(targetDir, taskId)
|
|
504
|
+
|
|
505
|
+
// Per-phase accumulator.
|
|
506
|
+
const phases = new Map()
|
|
507
|
+
function bump(phase, patch) {
|
|
508
|
+
if (!phase) return
|
|
509
|
+
if (!phases.has(phase)) {
|
|
510
|
+
phases.set(phase, {
|
|
511
|
+
phase,
|
|
512
|
+
duration_ms: 0,
|
|
513
|
+
input_tokens: 0,
|
|
514
|
+
output_tokens: 0,
|
|
515
|
+
cost_usd: 0,
|
|
516
|
+
session_count: 0,
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
const row = phases.get(phase)
|
|
520
|
+
row.duration_ms += patch.duration_ms || 0
|
|
521
|
+
row.input_tokens += patch.input_tokens || 0
|
|
522
|
+
row.output_tokens += patch.output_tokens || 0
|
|
523
|
+
row.cost_usd += patch.cost_usd || 0
|
|
524
|
+
row.session_count += patch.session_count || 0
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Sum durations from lifecycle events (elapsed_ms attributed to the phase
|
|
528
|
+
// THIS event represents — i.e. the phase that was just entered. The
|
|
529
|
+
// elapsed_ms is "time spent in the prior phase before transitioning into
|
|
530
|
+
// this one." Attribute to the prior phase to keep the model intuitive.
|
|
531
|
+
let priorPhase = null
|
|
532
|
+
for (const evt of lifecycleEvents) {
|
|
533
|
+
const elapsed = evt.data?.elapsed_ms
|
|
534
|
+
if (priorPhase && typeof elapsed === 'number') {
|
|
535
|
+
bump(priorPhase, { duration_ms: elapsed })
|
|
536
|
+
}
|
|
537
|
+
priorPhase = evt.data?.lifecycle_phase || null
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Sum tokens per phase. Prefer entry.phase when set; otherwise infer from
|
|
541
|
+
// lifecycle event timestamps.
|
|
542
|
+
for (const entry of entries) {
|
|
543
|
+
const phase = entry.phase || inferPhaseForEntry(entry.timestamp, lifecycleEvents)
|
|
544
|
+
if (!phase) continue
|
|
545
|
+
bump(phase, {
|
|
546
|
+
input_tokens: entry.input_tokens || 0,
|
|
547
|
+
output_tokens: entry.output_tokens || 0,
|
|
548
|
+
cost_usd: typeof entry.cost_usd === 'number' ? entry.cost_usd : 0,
|
|
549
|
+
session_count: 1,
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const rows = Array.from(phases.values()).map((r) => ({
|
|
554
|
+
...r,
|
|
555
|
+
cost_usd: Number(r.cost_usd.toFixed(6)),
|
|
556
|
+
}))
|
|
557
|
+
|
|
558
|
+
return ok({
|
|
559
|
+
status: 'ok',
|
|
560
|
+
command: 'tokens',
|
|
561
|
+
action: 'phase-summary',
|
|
562
|
+
task_id: taskId,
|
|
563
|
+
entry_count: entries.length,
|
|
564
|
+
lifecycle_event_count: lifecycleEvents.length,
|
|
565
|
+
phases: rows,
|
|
566
|
+
})
|
|
567
|
+
}
|
|
568
|
+
|
|
422
569
|
/**
|
|
423
570
|
* Top-level dispatcher.
|
|
424
571
|
*/
|
|
425
572
|
export function cmdTokens(subcommand, projectDir, extraArgs = []) {
|
|
426
573
|
if (!subcommand) {
|
|
427
|
-
return err(
|
|
574
|
+
return err(
|
|
575
|
+
'Usage: apt-tools tokens <record|tally|compare|list|phase-summary> <project-dir> [flags]',
|
|
576
|
+
)
|
|
428
577
|
}
|
|
429
578
|
if (!projectDir) {
|
|
430
579
|
return err(`Usage: apt-tools tokens ${subcommand} <project-dir> [flags]`)
|
|
@@ -444,9 +593,11 @@ export function cmdTokens(subcommand, projectDir, extraArgs = []) {
|
|
|
444
593
|
return cmdCompare(targetDir, extraArgs)
|
|
445
594
|
case 'list':
|
|
446
595
|
return cmdList(targetDir, extraArgs)
|
|
596
|
+
case 'phase-summary':
|
|
597
|
+
return cmdPhaseSummary(targetDir, extraArgs)
|
|
447
598
|
default:
|
|
448
599
|
return err(
|
|
449
|
-
`Unknown tokens subcommand: ${subcommand}. Expected: record, tally, compare, list`,
|
|
600
|
+
`Unknown tokens subcommand: ${subcommand}. Expected: record, tally, compare, list, phase-summary`,
|
|
450
601
|
)
|
|
451
602
|
}
|
|
452
603
|
} catch (e) {
|