@aperant/framework 0.6.4 → 0.6.6
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 +54 -234
- 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/commit.d.mts.map +1 -1
- package/dist/cli/commands/commit.mjs +25 -8
- package/dist/cli/commands/commit.mjs.map +1 -1
- 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 +125 -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 +221 -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 +393 -145
- 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 +263 -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/auto-emit-artifact-ready.d.mts +16 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.d.mts.map +1 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs +88 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -0
- 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/coordination/event-schema.d.mts +16 -0
- package/dist/cli/coordination/event-schema.d.mts.map +1 -0
- package/dist/cli/coordination/event-schema.mjs +161 -0
- package/dist/cli/coordination/event-schema.mjs.map +1 -0
- package/dist/cli/coordination/store.d.mts +6 -0
- package/dist/cli/coordination/store.d.mts.map +1 -1
- package/dist/cli/coordination/store.mjs +14 -0
- package/dist/cli/coordination/store.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 +3 -2
- 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/roadmap/conductor-view.d.mts +13 -0
- package/dist/cli/roadmap/conductor-view.d.mts.map +1 -0
- package/dist/cli/roadmap/conductor-view.mjs +31 -0
- package/dist/cli/roadmap/conductor-view.mjs.map +1 -0
- 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 +13 -1
- 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/ids.d.mts +7 -4
- package/dist/cli/task/ids.d.mts.map +1 -1
- package/dist/cli/task/ids.mjs +11 -10
- package/dist/cli/task/ids.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 +9 -1
- package/prompts/coder.md +2 -0
- package/prompts/conductor-status-check.md +23 -0
- package/prompts/conductor-sub-agent.md +57 -0
- package/prompts/conductor-system.md +172 -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 +159 -4
- package/skills/apt-plan/adapters/conductor.md +98 -0
- 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 +130 -2
- package/skills/apt-ship/SKILL.md +63 -1
- package/skills/apt-spar/SKILL.md +290 -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/commit.mjs +27 -8
- package/src/cli/commands/context.mjs +244 -0
- package/src/cli/commands/event.mjs +131 -24
- package/src/cli/commands/host-detect.mjs +33 -7
- package/src/cli/commands/modes.mjs +216 -0
- package/src/cli/commands/route.mjs +1 -1
- package/src/cli/commands/task.mjs +430 -170
- package/src/cli/commands/tokens.mjs +157 -6
- package/src/cli/commands/triage.mjs +286 -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/auto-emit-artifact-ready.mjs +96 -0
- package/src/cli/coordination/event-log.mjs +73 -2
- package/src/cli/coordination/event-schema.d.ts +13 -0
- package/src/cli/coordination/event-schema.mjs +174 -0
- package/src/cli/coordination/store.mjs +14 -0
- package/src/cli/dispatch.mjs +15 -1
- package/src/cli/gate/registry.mjs +14 -0
- package/src/cli/help.mjs +3 -2
- package/src/cli/host/detect.mjs +5 -0
- package/src/cli/roadmap/conductor-view.d.ts +10 -0
- package/src/cli/roadmap/conductor-view.mjs +31 -0
- package/src/cli/route/envelope.mjs +140 -106
- package/src/cli/route/skill-discover.mjs +14 -1
- package/src/cli/skill-author/contract.mjs +7 -0
- package/src/cli/task/ids.mjs +15 -13
- 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
|
@@ -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) {
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/triage.mjs — `apt-tools triage` (Pocock adoption ST-10, AC9).
|
|
3
|
+
*
|
|
4
|
+
* 5-state triage state machine with extensible metadata for v2 OSS
|
|
5
|
+
* contributor primitives. Backend-configured: writes to
|
|
6
|
+
* `.aperant/tasks/{task-id}/triage.json` by default (local-only); other
|
|
7
|
+
* backends (github-issues, app-inbox) ship as not-implemented stubs.
|
|
8
|
+
*
|
|
9
|
+
* Subcommands:
|
|
10
|
+
* apt-tools triage init <project-dir> --id <task-id> [--category bug|enhancement] [--extra '{...}']
|
|
11
|
+
* apt-tools triage transition <project-dir> --id <task-id> --to <state> [--reason <text>] [--by user|agent]
|
|
12
|
+
* apt-tools triage get <project-dir> --id <task-id>
|
|
13
|
+
*
|
|
14
|
+
* States: needs-triage | needs-info | ready-for-agent | ready-for-human | wontfix
|
|
15
|
+
* Categories: bug | enhancement
|
|
16
|
+
*
|
|
17
|
+
* Fast Path Guarantee (ID-05): QUICK-routed tasks NEVER enter triage.
|
|
18
|
+
* The CLI does NOT enforce this directly — the apt:triage SKILL.md
|
|
19
|
+
* routes around QUICK tasks before invoking the CLI. The CLI is the
|
|
20
|
+
* mechanical state-machine surface; the routing is the skill body's job.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
24
|
+
import { dirname, join, resolve } from 'node:path'
|
|
25
|
+
import { parseFlags } from '../util/args.mjs'
|
|
26
|
+
import { err, exitWith, ok } from '../util/result.mjs'
|
|
27
|
+
|
|
28
|
+
const STATES = ['needs-triage', 'needs-info', 'ready-for-agent', 'ready-for-human', 'wontfix']
|
|
29
|
+
|
|
30
|
+
const CATEGORIES = ['bug', 'enhancement']
|
|
31
|
+
|
|
32
|
+
// The 8 valid transitions per spec.md AC9. Anything else is rejected.
|
|
33
|
+
const VALID_TRANSITIONS = new Set([
|
|
34
|
+
'needs-triage->needs-info',
|
|
35
|
+
'needs-triage->ready-for-agent',
|
|
36
|
+
'needs-triage->ready-for-human',
|
|
37
|
+
'needs-triage->wontfix',
|
|
38
|
+
'needs-info->ready-for-agent',
|
|
39
|
+
'needs-info->ready-for-human',
|
|
40
|
+
'needs-info->wontfix',
|
|
41
|
+
'needs-info->needs-triage',
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
const BACKENDS = new Set(['local-only', 'github-issues', 'app-inbox'])
|
|
45
|
+
|
|
46
|
+
// Two task-id shapes coexist:
|
|
47
|
+
// - Legacy date-first: YY-MM-DD_slug-XXXX (preserved so the ~159
|
|
48
|
+
// pre-slug-first tasks already on disk keep resolving).
|
|
49
|
+
// - Current slug-first: slug_YY-MM-DD with optional -N counter
|
|
50
|
+
// emitted by generateTaskId since the slug-first rename.
|
|
51
|
+
const TASK_ID_RE_LEGACY = /^[0-9]{2}-[0-9]{2}-[0-9]{2}_[a-z0-9-]+-[a-z0-9]{4}$/
|
|
52
|
+
const TASK_ID_RE_SLUG_FIRST = /^[a-z0-9][a-z0-9-]*_[0-9]{2}-[0-9]{2}-[0-9]{2}(-\d+)?$/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read .aperant/config.json and extract task_tracking.backend.
|
|
56
|
+
* Defaults to 'local-only' if the file or the key is missing.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} absProjectDir
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function readBackend(absProjectDir) {
|
|
62
|
+
const configPath = join(absProjectDir, '.aperant', 'config.json')
|
|
63
|
+
if (!existsSync(configPath)) return 'local-only'
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
66
|
+
const backend = parsed?.task_tracking?.backend
|
|
67
|
+
if (typeof backend === 'string' && BACKENDS.has(backend)) return backend
|
|
68
|
+
return 'local-only'
|
|
69
|
+
} catch {
|
|
70
|
+
return 'local-only'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Triage record file path.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} absProjectDir
|
|
78
|
+
* @param {string} taskId
|
|
79
|
+
*/
|
|
80
|
+
function triagePath(absProjectDir, taskId) {
|
|
81
|
+
return join(absProjectDir, '.aperant', 'tasks', taskId, 'triage.json')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read the current triage record, or null if it doesn't exist.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} path
|
|
88
|
+
*/
|
|
89
|
+
function readTriage(path) {
|
|
90
|
+
if (!existsSync(path)) return null
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(readFileSync(path, 'utf-8'))
|
|
93
|
+
} catch {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Write a triage record.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} path
|
|
102
|
+
* @param {object} record
|
|
103
|
+
*/
|
|
104
|
+
function writeTriage(path, record) {
|
|
105
|
+
const dir = dirname(path)
|
|
106
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
107
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Apply the backend mirror step. local-only writes to disk; the other
|
|
112
|
+
* backends return a not-implemented envelope so callers can handle the
|
|
113
|
+
* stub gracefully.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} backend
|
|
116
|
+
* @param {string} absProjectDir
|
|
117
|
+
* @param {string} taskId
|
|
118
|
+
* @param {object} record
|
|
119
|
+
* @returns {null | { status: 'not-implemented', backend: string, v2_issue: string }}
|
|
120
|
+
*/
|
|
121
|
+
function mirrorToBackend(backend, absProjectDir, taskId, record) {
|
|
122
|
+
if (backend === 'local-only') {
|
|
123
|
+
writeTriage(triagePath(absProjectDir, taskId), record)
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
// v1 stubs: github-issues and app-inbox return not-implemented.
|
|
127
|
+
return { status: 'not-implemented', backend, v2_issue: '#TBD' }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Entry: `apt-tools triage <subcommand> <project-dir> [flags]`.
|
|
132
|
+
*/
|
|
133
|
+
export function cmdTriage(subcommand, projectDir, extraArgs) {
|
|
134
|
+
if (!subcommand || !['init', 'transition', 'get'].includes(subcommand)) {
|
|
135
|
+
return err(
|
|
136
|
+
'Usage: apt-tools triage <init|transition|get> <project-dir> --id <task-id> [...]',
|
|
137
|
+
1,
|
|
138
|
+
{ command: 'triage' },
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
if (!projectDir) {
|
|
142
|
+
return err('project-dir is required', 1, { command: `triage-${subcommand}` })
|
|
143
|
+
}
|
|
144
|
+
const absProjectDir = resolve(projectDir)
|
|
145
|
+
if (!existsSync(absProjectDir)) {
|
|
146
|
+
return err(`project directory not found: ${absProjectDir}`, 1, {
|
|
147
|
+
command: `triage-${subcommand}`,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const flags = parseFlags(extraArgs ?? [])
|
|
152
|
+
const taskId = flags.get('id')
|
|
153
|
+
if (typeof taskId !== 'string' || taskId.trim() === '') {
|
|
154
|
+
return err('--id <task-id> is required', 1, { command: `triage-${subcommand}` })
|
|
155
|
+
}
|
|
156
|
+
if (!TASK_ID_RE_LEGACY.test(taskId) && !TASK_ID_RE_SLUG_FIRST.test(taskId)) {
|
|
157
|
+
return err(
|
|
158
|
+
`--id must match pattern <slug_YY-MM-DD[-N]> (e.g. fix-login_24-01-15) or legacy <YY-MM-DD_slug-XXXX>`,
|
|
159
|
+
1,
|
|
160
|
+
{ command: `triage-${subcommand}` },
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
const backend = readBackend(absProjectDir)
|
|
164
|
+
|
|
165
|
+
if (subcommand === 'init') {
|
|
166
|
+
// Optional --category, optional --extra (JSON string).
|
|
167
|
+
const category = flags.get('category')
|
|
168
|
+
if (category !== undefined && !CATEGORIES.includes(String(category))) {
|
|
169
|
+
return err(`--category must be one of: ${CATEGORIES.join(' | ')}`, 1, {
|
|
170
|
+
command: 'triage-init',
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
let extra = {}
|
|
174
|
+
const extraRaw = flags.get('extra')
|
|
175
|
+
if (typeof extraRaw === 'string' && extraRaw.length > 0) {
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(extraRaw)
|
|
178
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
179
|
+
extra = parsed
|
|
180
|
+
} else {
|
|
181
|
+
return err('--extra must be a JSON object string', 1, { command: 'triage-init' })
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return err(`--extra is not valid JSON: ${e.message}`, 1, { command: 'triage-init' })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const existing = readTriage(triagePath(absProjectDir, taskId))
|
|
189
|
+
if (existing) {
|
|
190
|
+
return err(`triage record already exists for ${taskId}`, 1, {
|
|
191
|
+
command: 'triage-init',
|
|
192
|
+
current_state: existing.state,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const record = {
|
|
197
|
+
task_id: taskId,
|
|
198
|
+
state: 'needs-triage',
|
|
199
|
+
category: typeof category === 'string' ? category : null,
|
|
200
|
+
transitions: [],
|
|
201
|
+
extra,
|
|
202
|
+
}
|
|
203
|
+
const stub = mirrorToBackend(backend, absProjectDir, taskId, record)
|
|
204
|
+
if (stub) return exitWith({ command: 'triage-init', ...stub }, 0)
|
|
205
|
+
return ok({ status: 'ok', command: 'triage-init', backend, record })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (subcommand === 'transition') {
|
|
209
|
+
const toState = flags.get('to')
|
|
210
|
+
if (typeof toState !== 'string' || !STATES.includes(toState)) {
|
|
211
|
+
return err(`--to <state> is required (one of: ${STATES.join(' | ')})`, 1, {
|
|
212
|
+
command: 'triage-transition',
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
// SEC-003: cap --reason to 1000 chars to prevent unbounded disk writes.
|
|
216
|
+
const rawReason = flags.get('reason')
|
|
217
|
+
const reason = typeof rawReason === 'string' ? rawReason.slice(0, 1000) : null
|
|
218
|
+
const by =
|
|
219
|
+
typeof flags.get('by') === 'string' && ['user', 'agent'].includes(String(flags.get('by')))
|
|
220
|
+
? String(flags.get('by'))
|
|
221
|
+
: 'agent'
|
|
222
|
+
|
|
223
|
+
const current = readTriage(triagePath(absProjectDir, taskId))
|
|
224
|
+
if (!current) {
|
|
225
|
+
return err(`no triage record for ${taskId} — run \`triage init\` first`, 1, {
|
|
226
|
+
command: 'triage-transition',
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const from = current.state
|
|
231
|
+
const transitionKey = `${from}->${toState}`
|
|
232
|
+
if (!VALID_TRANSITIONS.has(transitionKey)) {
|
|
233
|
+
return exitWith(
|
|
234
|
+
{
|
|
235
|
+
status: 'rejected',
|
|
236
|
+
command: 'triage-transition',
|
|
237
|
+
reason: 'invalid-transition',
|
|
238
|
+
from,
|
|
239
|
+
to: toState,
|
|
240
|
+
valid_from_current: STATES.filter((s) => VALID_TRANSITIONS.has(`${from}->${s}`)),
|
|
241
|
+
},
|
|
242
|
+
1,
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const transition = {
|
|
247
|
+
from,
|
|
248
|
+
to: toState,
|
|
249
|
+
at: new Date().toISOString(),
|
|
250
|
+
by,
|
|
251
|
+
reason,
|
|
252
|
+
}
|
|
253
|
+
// SEC-002: Only carry known fields forward — do NOT spread the full
|
|
254
|
+
// disk-backed `current` object, which could contain arbitrary unknown
|
|
255
|
+
// keys from a hand-edited triage.json that would silently propagate.
|
|
256
|
+
const next = {
|
|
257
|
+
task_id: current.task_id,
|
|
258
|
+
state: toState,
|
|
259
|
+
category: current.category,
|
|
260
|
+
extra: current.extra ?? {},
|
|
261
|
+
transitions: [...(current.transitions || []), transition],
|
|
262
|
+
}
|
|
263
|
+
const stub = mirrorToBackend(backend, absProjectDir, taskId, next)
|
|
264
|
+
if (stub) return exitWith({ command: 'triage-transition', ...stub }, 0)
|
|
265
|
+
return ok({
|
|
266
|
+
status: 'ok',
|
|
267
|
+
command: 'triage-transition',
|
|
268
|
+
backend,
|
|
269
|
+
transition,
|
|
270
|
+
record: next,
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// subcommand === 'get'
|
|
275
|
+
const path = triagePath(absProjectDir, taskId)
|
|
276
|
+
const record = readTriage(path)
|
|
277
|
+
if (!record) {
|
|
278
|
+
return exitWith({ status: 'not-found', command: 'triage-get', task_id: taskId, file: path }, 0)
|
|
279
|
+
}
|
|
280
|
+
return ok({ status: 'ok', command: 'triage-get', record, backend })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Exported for testing.
|
|
284
|
+
export const _STATES = STATES
|
|
285
|
+
export const _CATEGORIES = CATEGORIES
|
|
286
|
+
export const _VALID_TRANSITIONS = VALID_TRANSITIONS
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
* post-merge-sweep.mjs — dispatch-time passive sweeper for stuck
|
|
3
3
|
* `shipped-pending-merge` tasks (C57 Piece 2).
|
|
4
4
|
*
|
|
5
|
+
* **Bookkeeping-only contract.** This hook runs the deterministic
|
|
6
|
+
* close half of the post-merge handoff: poll `gh pr view`, flip
|
|
7
|
+
* lifecycle_phase from `shipped-pending-merge` to `completed` via
|
|
8
|
+
* the same `task close` path, strip per-PR authorship overrides,
|
|
9
|
+
* and append a `pending_narration[]` ledger row. The LLM-side
|
|
10
|
+
* narration work (registry sync + ROADMAP prose) is explicitly
|
|
11
|
+
* out of scope here — the user drains the ledger via
|
|
12
|
+
* `/apt:close-task --narrate-only` (see packages/framework/skills/
|
|
13
|
+
* apt-close-task/SKILL.md §4) which is where those side effects belong.
|
|
14
|
+
*
|
|
5
15
|
* AI agents ship a PR and walk away. Tasks then rot in
|
|
6
16
|
* `shipped-pending-merge` because nothing automatic invokes
|
|
7
17
|
* `apt-tools task close-merged`. This hook runs on every `apt-tools`
|
|
@@ -11,8 +21,10 @@
|
|
|
11
21
|
* whose PRs have terminally landed.
|
|
12
22
|
*
|
|
13
23
|
* Non-blocking: never fails the command, never mutates exit code,
|
|
14
|
-
* never throws.
|
|
15
|
-
*
|
|
24
|
+
* never throws. Two stderr lines max per dispatch: the
|
|
25
|
+
* `Auto-sweeping N pending PR(s)` line on actual sweep kick, plus
|
|
26
|
+
* the `Post-merge narration pending` banner when the ledger has
|
|
27
|
+
* un-drained rows. Both lines are gated by `APT_SUPPRESS_MERGE_SWEEP=1`.
|
|
16
28
|
*
|
|
17
29
|
* Silenced by `APT_SUPPRESS_MERGE_SWEEP=1`. Skipped when
|
|
18
30
|
* `APT_INTERNAL_MERGE_SWEEP=1` is set in env (recursion guard for
|
|
@@ -90,6 +102,25 @@ export function readShippedCandidates(projectRoot) {
|
|
|
90
102
|
return candidates.length === 0 ? null : candidates
|
|
91
103
|
}
|
|
92
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Pure preflight — returns the integer length of state.pending_narration[].
|
|
107
|
+
* Returns 0 on read/parse failure or when the ledger is absent (the banner
|
|
108
|
+
* is suppressed in that case).
|
|
109
|
+
*
|
|
110
|
+
* @internal
|
|
111
|
+
*/
|
|
112
|
+
export function readPendingNarrationCount(projectRoot) {
|
|
113
|
+
const statePath = join(projectRoot, '.aperant', 'state.json')
|
|
114
|
+
if (!existsSync(statePath)) return 0
|
|
115
|
+
try {
|
|
116
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
117
|
+
const ledger = state?.pending_narration
|
|
118
|
+
return Array.isArray(ledger) ? ledger.length : 0
|
|
119
|
+
} catch {
|
|
120
|
+
return 0
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
93
124
|
/**
|
|
94
125
|
* Returns true if the cooldown lock is "fresh" (last sweep too recent).
|
|
95
126
|
* Lock content is `{pid}:{ms-since-epoch}` but we only consult mtime.
|
|
@@ -221,9 +252,24 @@ export function maybeSweepShippedMergedTasks(projectRoot, command, options = {})
|
|
|
221
252
|
return { kicked: false, reason: 'no-aperant-dir' }
|
|
222
253
|
}
|
|
223
254
|
|
|
255
|
+
// Narration-pending banner — fires on EVERY dispatch cycle when
|
|
256
|
+
// the ledger has un-drained rows, regardless of whether a sweep is
|
|
257
|
+
// kicked below. Cheap read, no spawn, no cooldown. Same suppression
|
|
258
|
+
// env as the sweep banner (APT_SUPPRESS_MERGE_SWEEP=1 hides both).
|
|
259
|
+
const pendingNarration = readPendingNarrationCount(root)
|
|
260
|
+
if (pendingNarration > 0) {
|
|
261
|
+
try {
|
|
262
|
+
process.stderr.write(
|
|
263
|
+
`[apt-tools] Post-merge narration pending (${pendingNarration}) — run /apt:close-task --narrate-only to refresh features registry + ROADMAP.\n`,
|
|
264
|
+
)
|
|
265
|
+
} catch {
|
|
266
|
+
/* stderr write failure is non-fatal */
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
224
270
|
const candidates = readShippedCandidates(root)
|
|
225
271
|
if (!candidates) {
|
|
226
|
-
return { kicked: false, reason: 'no-candidates' }
|
|
272
|
+
return { kicked: false, reason: 'no-candidates', pending_narration: pendingNarration }
|
|
227
273
|
}
|
|
228
274
|
|
|
229
275
|
const lockPath = join(root, '.aperant', '.merge-sweep.lock')
|
|
@@ -82,3 +82,17 @@ export function getRule(id) {
|
|
|
82
82
|
export function __resetRegistryForTests() {
|
|
83
83
|
registry.clear()
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Test-only helper — uniformly-named alias for the in-process cmd harness
|
|
88
|
+
* (run-cmd.mjs). Built-in rules self-register via the `rules/index.mjs`
|
|
89
|
+
* barrel at module-import time, which ESM evaluates exactly once per
|
|
90
|
+
* process — so the harness deliberately does NOT call this by default
|
|
91
|
+
* (clearing here would leave subsequent cmd invocations looking at an empty
|
|
92
|
+
* registry). Tests that need an empty registry import this name explicitly.
|
|
93
|
+
*
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
export function __resetCaches() {
|
|
97
|
+
registry.clear()
|
|
98
|
+
}
|
|
@@ -100,3 +100,15 @@ export function getAgentIdentity(projectDir) {
|
|
|
100
100
|
export function __resetAgentIdentityCacheForTests() {
|
|
101
101
|
cache.clear()
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Test-only helper — uniformly-named alias for the in-process cmd harness
|
|
106
|
+
* (run-cmd.mjs), which imports `__resetCaches` from each cache host with a
|
|
107
|
+
* single consistent name. Behaviour is identical to
|
|
108
|
+
* `__resetAgentIdentityCacheForTests`.
|
|
109
|
+
*
|
|
110
|
+
* @internal
|
|
111
|
+
*/
|
|
112
|
+
export function __resetCaches() {
|
|
113
|
+
cache.clear()
|
|
114
|
+
}
|