@aperant/framework 0.6.3 → 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 +61 -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/artifacts/classification.d.mts.map +1 -1
- package/dist/cli/artifacts/classification.mjs +10 -0
- package/dist/cli/artifacts/classification.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/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +73 -5
- package/dist/cli/commands/init.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/pr-review-audit-fixer.d.mts +41 -2
- package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.mjs +91 -14
- package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
- package/dist/cli/commands/route.d.mts.map +1 -1
- package/dist/cli/commands/route.mjs +11 -2
- 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 +136 -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/design/frontmatter-schema.d.mts +3 -3
- package/dist/cli/design/frontmatter-schema.d.mts.map +1 -1
- package/dist/cli/design/frontmatter-schema.mjs +3 -1
- package/dist/cli/design/frontmatter-schema.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 +11 -0
- package/dist/cli/route/skill-discover.d.mts.map +1 -1
- package/dist/cli/route/skill-discover.mjs +46 -1
- package/dist/cli/route/skill-discover.mjs.map +1 -1
- package/dist/cli/skill-author/contract.d.mts +20 -0
- package/dist/cli/skill-author/contract.d.mts.map +1 -1
- package/dist/cli/skill-author/contract.mjs +27 -0
- package/dist/cli/skill-author/contract.mjs.map +1 -1
- package/dist/cli/skill-author/skill-template.d.mts.map +1 -1
- package/dist/cli/skill-author/skill-template.mjs +4 -3
- package/dist/cli/skill-author/skill-template.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 -122
- package/prompts/coder.md +2 -0
- package/prompts/planner.md +12 -0
- package/prompts/spec_writer.md +9 -0
- package/skills/apt/SKILL.md +112 -5
- package/skills/apt-author-skill/SKILL.md +11 -0
- package/skills/apt-bootstrap/SKILL.md +1 -0
- package/skills/apt-classify/SKILL.md +1 -0
- package/skills/apt-close-task/SKILL.md +33 -1
- package/skills/apt-create-docs/SKILL.md +1 -0
- package/skills/apt-debug/SKILL.md +41 -6
- package/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
- package/skills/apt-design/SKILL.md +2 -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 +27 -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-docs/SKILL.md +2 -0
- package/skills/apt-execute/SKILL.md +49 -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-mockup/SKILL.md +2 -0
- package/skills/apt-pause/SKILL.md +1 -0
- package/skills/apt-personas/SKILL.md +1 -0
- package/skills/apt-plan/SKILL.md +149 -4
- package/skills/apt-planner.md +42 -1
- package/skills/apt-pr-review/SKILL.md +47 -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 +32 -0
- package/skills/apt-resume/SKILL.md +1 -0
- package/skills/apt-review/SKILL.md +3 -0
- package/skills/apt-roadmap/SKILL.md +1 -0
- package/skills/apt-roundtable/SKILL.md +2 -0
- package/skills/apt-run/SKILL.md +33 -4
- package/skills/apt-scan/SKILL.md +1 -0
- package/skills/apt-setup/SKILL.md +129 -2
- package/skills/apt-ship/SKILL.md +52 -4
- package/skills/apt-stress-test/SKILL.md +1 -0
- package/skills/apt-terminal/SKILL.md +1 -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-update/SKILL.md +3 -0
- package/skills/apt-verify/SKILL.md +4 -0
- package/skills/apt-verify-proof/SKILL.md +4 -0
- package/skills/apt-watch-ci/SKILL.md +163 -0
- package/skills/apt-zoom-out/SKILL.md +130 -0
- package/src/cli/artifacts/classification.mjs +10 -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/init.mjs +83 -5
- package/src/cli/commands/modes.mjs +215 -0
- package/src/cli/commands/pr-review-audit-fixer.mjs +95 -16
- package/src/cli/commands/route.mjs +11 -2
- package/src/cli/commands/task.mjs +152 -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/design/frontmatter-schema.mjs +3 -1
- 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 +46 -1
- package/src/cli/skill-author/contract.mjs +29 -0
- package/src/cli/skill-author/skill-template.mjs +4 -3
- 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
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/ci-watch.mjs — Layer 1 cmd for `/apt:watch-ci` (C28).
|
|
3
|
+
*
|
|
4
|
+
* Subcommands: start | stop | status | tick
|
|
5
|
+
*
|
|
6
|
+
* Layer 1 rule: cmdCiWatch returns `{envelope, exitCode}` and MUST NOT call
|
|
7
|
+
* output() / process.exit() / fail() directly. The Layer 2 wrapper in
|
|
8
|
+
* dispatch.mjs is responsible for transport.
|
|
9
|
+
*
|
|
10
|
+
* `tick` is single-execution per invocation (D-01). The recurring beat is
|
|
11
|
+
* delivered externally — by CC's CronCreate, Pi's pi-schedule-prompt, or by
|
|
12
|
+
* the user re-invoking `--tick` on non-cron hosts. The cmd MUST NOT enter a
|
|
13
|
+
* polling loop.
|
|
14
|
+
*
|
|
15
|
+
* Test seam: when `APT_TEST_GH_STUB` is set to a path on disk, the tick path
|
|
16
|
+
* reads the gh payload from that file instead of shelling out. The stub
|
|
17
|
+
* shape is `{ checks: [...], prMetadata: {...} }` — matches the classifier
|
|
18
|
+
* input contract in stop-matrix.mjs.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { randomBytes } from 'node:crypto'
|
|
22
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
23
|
+
import { resolve } from 'node:path'
|
|
24
|
+
import { acquireLock, releaseLock } from '../ci-watch/lock.mjs'
|
|
25
|
+
import { deleteState, listWatches, readState, writeState } from '../ci-watch/state.mjs'
|
|
26
|
+
import { classify, isFailing } from '../ci-watch/stop-matrix.mjs'
|
|
27
|
+
import { parseFlags } from '../util/args.mjs'
|
|
28
|
+
import { err, exitWith, ok } from '../util/result.mjs'
|
|
29
|
+
|
|
30
|
+
const DEFAULT_TICK_INTERVAL_MS = 300_000 // 5 minutes
|
|
31
|
+
const MAX_TICK_INTERVAL_MS = 30 * 60 * 1000 // 30 min
|
|
32
|
+
const WALL_BUDGET_MS = 4 * 60 * 60 * 1000 // 4 hours
|
|
33
|
+
|
|
34
|
+
function newSessionId() {
|
|
35
|
+
return randomBytes(8).toString('hex')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function nowIso() {
|
|
39
|
+
return new Date().toISOString()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate `--pr` is a positive-integer string before it gets interpolated
|
|
44
|
+
* into a filesystem path. Without this guard, a value like `--pr ../escape`
|
|
45
|
+
* would write the per-PR state/lock file outside `.aperant/ci-watches/`
|
|
46
|
+
* (path traversal via `join(..., '../escape.json')`). PR numbers on GitHub
|
|
47
|
+
* are positive integers, so digits-only is the correct shape.
|
|
48
|
+
*
|
|
49
|
+
* Returns the validated PR string on success, or an err() CommandResult on
|
|
50
|
+
* failure — the caller returns it directly.
|
|
51
|
+
*
|
|
52
|
+
* @param {string|undefined} pr
|
|
53
|
+
* @returns {string | import('../util/result.mjs').CommandResult}
|
|
54
|
+
*/
|
|
55
|
+
function validatePr(pr) {
|
|
56
|
+
if (!pr) return err('Required: --pr <N>')
|
|
57
|
+
if (!/^[1-9]\d*$/.test(pr)) {
|
|
58
|
+
return err(`Invalid --pr value: ${JSON.stringify(pr)}. Expected a positive integer.`)
|
|
59
|
+
}
|
|
60
|
+
return pr
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} subcommand
|
|
65
|
+
* @param {string} projectDir
|
|
66
|
+
* @param {string[]} extraArgs
|
|
67
|
+
*/
|
|
68
|
+
export function cmdCiWatch(subcommand, projectDir, extraArgs) {
|
|
69
|
+
if (!subcommand) {
|
|
70
|
+
return err('Usage: apt-tools ci-watch <start|stop|status|tick> <project-dir> [flags]')
|
|
71
|
+
}
|
|
72
|
+
if (!projectDir) {
|
|
73
|
+
return err(`Usage: apt-tools ci-watch ${subcommand} <project-dir> [flags]`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const root = resolve(projectDir)
|
|
77
|
+
const flags = parseFlags(extraArgs)
|
|
78
|
+
|
|
79
|
+
switch (subcommand) {
|
|
80
|
+
case 'start':
|
|
81
|
+
return doStart(root, flags)
|
|
82
|
+
case 'stop':
|
|
83
|
+
return doStop(root, flags)
|
|
84
|
+
case 'status':
|
|
85
|
+
return doStatus(root, flags)
|
|
86
|
+
case 'tick':
|
|
87
|
+
return doTick(root, flags)
|
|
88
|
+
default:
|
|
89
|
+
return err(`Unknown ci-watch subcommand: ${subcommand}. Expected: start, stop, status, tick`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// start
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function doStart(root, flags) {
|
|
98
|
+
const prCheck = validatePr(flags.get('pr'))
|
|
99
|
+
if (typeof prCheck !== 'string') return prCheck
|
|
100
|
+
const pr = prCheck
|
|
101
|
+
const taskId = flags.get('task-id')
|
|
102
|
+
if (!taskId) return err('Required: --task-id <id>')
|
|
103
|
+
const host = flags.get('host') || 'unknown'
|
|
104
|
+
|
|
105
|
+
// Refuse to start a second watcher for the same PR — exit 2 with a
|
|
106
|
+
// busy envelope so the caller knows the PR is already armed. The
|
|
107
|
+
// orchestrator skill prints the holder's session and exits.
|
|
108
|
+
const existingState = readState(root, pr)
|
|
109
|
+
if (existingState && !existingState.error) {
|
|
110
|
+
return exitWith(
|
|
111
|
+
{
|
|
112
|
+
status: 'busy',
|
|
113
|
+
command: 'ci-watch-start',
|
|
114
|
+
pr: Number(pr),
|
|
115
|
+
reason: 'already-armed',
|
|
116
|
+
existing: {
|
|
117
|
+
task_id: existingState.task_id,
|
|
118
|
+
host: existingState.host,
|
|
119
|
+
attempts: existingState.attempts,
|
|
120
|
+
started_at: existingState.started_at,
|
|
121
|
+
deadline: existingState.deadline,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
2,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const sessionId = newSessionId()
|
|
129
|
+
const lockResult = acquireLock(root, pr, sessionId)
|
|
130
|
+
if (lockResult.status !== 'ok') {
|
|
131
|
+
return exitWith(
|
|
132
|
+
{
|
|
133
|
+
status: 'busy',
|
|
134
|
+
command: 'ci-watch-start',
|
|
135
|
+
pr: Number(pr),
|
|
136
|
+
reason: 'lock-held',
|
|
137
|
+
holder: lockResult.holder,
|
|
138
|
+
},
|
|
139
|
+
2,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const existing = readState(root, pr)
|
|
145
|
+
const existingObj = existing && !existing.error ? existing : {}
|
|
146
|
+
|
|
147
|
+
const startedAt = existingObj.started_at || nowIso()
|
|
148
|
+
const deadline = existingObj.deadline || new Date(Date.now() + WALL_BUDGET_MS).toISOString()
|
|
149
|
+
// Snapshot auto_merge from config at start so config changes mid-watch don't
|
|
150
|
+
// surprise the orchestrator (spec.md §8).
|
|
151
|
+
const autoMergeFromConfig = readAutoMergeFromConfig(root)
|
|
152
|
+
const state = {
|
|
153
|
+
pr: Number(pr),
|
|
154
|
+
task_id: taskId,
|
|
155
|
+
host,
|
|
156
|
+
scheduler_handle: existingObj.scheduler_handle ?? null,
|
|
157
|
+
attempts: existingObj.attempts || 0,
|
|
158
|
+
started_at: startedAt,
|
|
159
|
+
deadline,
|
|
160
|
+
last_poll: existingObj.last_poll || nowIso(),
|
|
161
|
+
last_status: existingObj.last_status ?? null,
|
|
162
|
+
next_tick_interval_ms: existingObj.next_tick_interval_ms || DEFAULT_TICK_INTERVAL_MS,
|
|
163
|
+
paused: existingObj.paused || false,
|
|
164
|
+
auto_merge_on_green: autoMergeFromConfig,
|
|
165
|
+
}
|
|
166
|
+
writeState(root, pr, state)
|
|
167
|
+
return ok({
|
|
168
|
+
status: 'ok',
|
|
169
|
+
command: 'ci-watch-start',
|
|
170
|
+
pr: Number(pr),
|
|
171
|
+
host,
|
|
172
|
+
scheduler_handle: state.scheduler_handle,
|
|
173
|
+
deadline: state.deadline,
|
|
174
|
+
})
|
|
175
|
+
} finally {
|
|
176
|
+
releaseLock(root, pr, sessionId)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// stop
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
function doStop(root, flags) {
|
|
185
|
+
const prCheck = validatePr(flags.get('pr'))
|
|
186
|
+
if (typeof prCheck !== 'string') return prCheck
|
|
187
|
+
const pr = prCheck
|
|
188
|
+
const reason = flags.get('reason') || 'user-stop'
|
|
189
|
+
|
|
190
|
+
const existing = readState(root, pr)
|
|
191
|
+
if (!existing || existing.error) {
|
|
192
|
+
// Idempotent — stopping a non-existent watch is a no-op.
|
|
193
|
+
return ok({
|
|
194
|
+
status: 'ok',
|
|
195
|
+
command: 'ci-watch-stop',
|
|
196
|
+
pr: Number(pr),
|
|
197
|
+
scheduler_handle_removed: false,
|
|
198
|
+
reason,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
const schedulerHandle = existing.scheduler_handle ?? null
|
|
202
|
+
deleteState(root, pr)
|
|
203
|
+
// Also remove any leftover lock file in case the watcher crashed mid-tick.
|
|
204
|
+
const stopSessionId = newSessionId()
|
|
205
|
+
const acq = acquireLock(root, pr, stopSessionId)
|
|
206
|
+
if (acq.status === 'ok') {
|
|
207
|
+
releaseLock(root, pr, stopSessionId)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return ok({
|
|
211
|
+
status: 'ok',
|
|
212
|
+
command: 'ci-watch-stop',
|
|
213
|
+
pr: Number(pr),
|
|
214
|
+
scheduler_handle_removed: schedulerHandle !== null,
|
|
215
|
+
reason,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// status
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
function doStatus(root, flags) {
|
|
224
|
+
if (flags.has('pr')) {
|
|
225
|
+
const prCheck = validatePr(flags.get('pr'))
|
|
226
|
+
if (typeof prCheck !== 'string') return prCheck
|
|
227
|
+
const pr = prCheck
|
|
228
|
+
const s = readState(root, pr)
|
|
229
|
+
if (!s || s.error) {
|
|
230
|
+
return ok({ status: 'ok', command: 'ci-watch-status', watches: [] })
|
|
231
|
+
}
|
|
232
|
+
return ok({
|
|
233
|
+
status: 'ok',
|
|
234
|
+
command: 'ci-watch-status',
|
|
235
|
+
watches: [summarize(s)],
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
const watches = listWatches(root).map(summarize)
|
|
239
|
+
return ok({ status: 'ok', command: 'ci-watch-status', watches })
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function summarize(s) {
|
|
243
|
+
return {
|
|
244
|
+
pr: s.pr,
|
|
245
|
+
attempts: s.attempts,
|
|
246
|
+
last_status: s.last_status,
|
|
247
|
+
deadline: s.deadline,
|
|
248
|
+
last_poll: s.last_poll,
|
|
249
|
+
paused: s.paused,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// tick
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
function doTick(root, flags) {
|
|
258
|
+
const prCheck = validatePr(flags.get('pr'))
|
|
259
|
+
if (typeof prCheck !== 'string') return prCheck
|
|
260
|
+
const pr = prCheck
|
|
261
|
+
|
|
262
|
+
const sessionId = newSessionId()
|
|
263
|
+
const lockResult = acquireLock(root, pr, sessionId)
|
|
264
|
+
if (lockResult.status !== 'ok') {
|
|
265
|
+
return exitWith(
|
|
266
|
+
{
|
|
267
|
+
status: 'busy',
|
|
268
|
+
command: 'ci-watch-tick',
|
|
269
|
+
pr: Number(pr),
|
|
270
|
+
reason: 'lock-held',
|
|
271
|
+
holder: lockResult.holder,
|
|
272
|
+
},
|
|
273
|
+
2,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const state = readState(root, pr)
|
|
279
|
+
if (!state || state.error) {
|
|
280
|
+
return err(`No active watch for PR ${pr} — run \`ci-watch start\` first`)
|
|
281
|
+
}
|
|
282
|
+
if (state.paused) {
|
|
283
|
+
return ok({
|
|
284
|
+
status: 'stopped',
|
|
285
|
+
command: 'ci-watch-tick',
|
|
286
|
+
pr: Number(pr),
|
|
287
|
+
action: 'reviewer-pause',
|
|
288
|
+
attempts_used: state.attempts,
|
|
289
|
+
reason: 'watch is paused (reviewer-pause); resume with /apt:watch-ci --resume',
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const payload = fetchGhPayload(pr)
|
|
294
|
+
if (payload.error) {
|
|
295
|
+
// gh CLI not available / auth failed → infra-backoff. Don't count it,
|
|
296
|
+
// double the next-tick interval (capped at MAX_TICK_INTERVAL_MS), and reschedule.
|
|
297
|
+
state.last_poll = nowIso()
|
|
298
|
+
state.next_tick_interval_ms = Math.min(
|
|
299
|
+
(state.next_tick_interval_ms || DEFAULT_TICK_INTERVAL_MS) * 2,
|
|
300
|
+
MAX_TICK_INTERVAL_MS,
|
|
301
|
+
)
|
|
302
|
+
writeState(root, pr, state)
|
|
303
|
+
return ok({
|
|
304
|
+
status: 'ok',
|
|
305
|
+
command: 'ci-watch-tick',
|
|
306
|
+
pr: Number(pr),
|
|
307
|
+
action: 'infra-backoff',
|
|
308
|
+
attempts_used: state.attempts,
|
|
309
|
+
next_tick_at: nextTickAt(state),
|
|
310
|
+
reason: `gh payload fetch failed: ${payload.error}`,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const result = classify({
|
|
315
|
+
checks: payload.checks,
|
|
316
|
+
state,
|
|
317
|
+
prMetadata: payload.prMetadata,
|
|
318
|
+
})
|
|
319
|
+
const { action, counts_toward_budget, reason } = result
|
|
320
|
+
|
|
321
|
+
// Apply action-specific state mutation.
|
|
322
|
+
if (action === 'green' || action === 'merged' || action === 'closed') {
|
|
323
|
+
// Terminal — delete state before releasing lock so no concurrent tick
|
|
324
|
+
// can slip in between the release and the delete.
|
|
325
|
+
deleteState(root, pr)
|
|
326
|
+
releaseLock(root, pr, sessionId)
|
|
327
|
+
return ok({
|
|
328
|
+
status: 'stopped',
|
|
329
|
+
command: 'ci-watch-tick',
|
|
330
|
+
pr: Number(pr),
|
|
331
|
+
action,
|
|
332
|
+
attempts_used: state.attempts,
|
|
333
|
+
reason,
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
if (action === 'attempts-cap' || action === 'wall-cap' || action === 'identical-log') {
|
|
337
|
+
deleteState(root, pr)
|
|
338
|
+
releaseLock(root, pr, sessionId)
|
|
339
|
+
return ok({
|
|
340
|
+
status: 'stopped',
|
|
341
|
+
command: 'ci-watch-tick',
|
|
342
|
+
pr: Number(pr),
|
|
343
|
+
action,
|
|
344
|
+
attempts_used: state.attempts,
|
|
345
|
+
reason,
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
if (action === 'reviewer-pause') {
|
|
349
|
+
state.paused = true
|
|
350
|
+
state.last_poll = nowIso()
|
|
351
|
+
writeState(root, pr, state)
|
|
352
|
+
return ok({
|
|
353
|
+
status: 'stopped',
|
|
354
|
+
command: 'ci-watch-tick',
|
|
355
|
+
pr: Number(pr),
|
|
356
|
+
action,
|
|
357
|
+
attempts_used: state.attempts,
|
|
358
|
+
reason,
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
if (action === 'flake-skip') {
|
|
362
|
+
state.last_poll = nowIso()
|
|
363
|
+
// Reset interval on a clean (non-infra) tick.
|
|
364
|
+
state.next_tick_interval_ms = DEFAULT_TICK_INTERVAL_MS
|
|
365
|
+
writeState(root, pr, state)
|
|
366
|
+
return ok({
|
|
367
|
+
status: 'ok',
|
|
368
|
+
command: 'ci-watch-tick',
|
|
369
|
+
pr: Number(pr),
|
|
370
|
+
action,
|
|
371
|
+
attempts_used: state.attempts,
|
|
372
|
+
next_tick_at: nextTickAt(state),
|
|
373
|
+
reason,
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
if (action === 'infra-backoff') {
|
|
377
|
+
state.last_poll = nowIso()
|
|
378
|
+
state.next_tick_interval_ms = Math.min(
|
|
379
|
+
(state.next_tick_interval_ms || DEFAULT_TICK_INTERVAL_MS) * 2,
|
|
380
|
+
MAX_TICK_INTERVAL_MS,
|
|
381
|
+
)
|
|
382
|
+
writeState(root, pr, state)
|
|
383
|
+
return ok({
|
|
384
|
+
status: 'ok',
|
|
385
|
+
command: 'ci-watch-tick',
|
|
386
|
+
pr: Number(pr),
|
|
387
|
+
action,
|
|
388
|
+
attempts_used: state.attempts,
|
|
389
|
+
next_tick_at: nextTickAt(state),
|
|
390
|
+
reason,
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
if (action === 'wait') {
|
|
394
|
+
state.last_poll = nowIso()
|
|
395
|
+
state.next_tick_interval_ms = DEFAULT_TICK_INTERVAL_MS
|
|
396
|
+
writeState(root, pr, state)
|
|
397
|
+
return ok({
|
|
398
|
+
status: 'ok',
|
|
399
|
+
command: 'ci-watch-tick',
|
|
400
|
+
pr: Number(pr),
|
|
401
|
+
action,
|
|
402
|
+
attempts_used: state.attempts,
|
|
403
|
+
next_tick_at: nextTickAt(state),
|
|
404
|
+
reason,
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
// fix / diverged-rebased / diverged-bailed — all count toward budget.
|
|
408
|
+
if (counts_toward_budget) {
|
|
409
|
+
state.attempts = (state.attempts || 0) + 1
|
|
410
|
+
}
|
|
411
|
+
state.last_poll = nowIso()
|
|
412
|
+
state.next_tick_interval_ms = DEFAULT_TICK_INTERVAL_MS
|
|
413
|
+
// Track the failed-check fingerprint aggregate for next-tick identical-log detection.
|
|
414
|
+
const failed = (payload.checks || []).filter(isFailing)
|
|
415
|
+
state.last_status =
|
|
416
|
+
failed.length === 0
|
|
417
|
+
? null
|
|
418
|
+
: failed
|
|
419
|
+
.map((f) => `${f.name}:${f.log_fingerprint ?? ''}`)
|
|
420
|
+
.sort()
|
|
421
|
+
.join('|')
|
|
422
|
+
writeState(root, pr, state)
|
|
423
|
+
return ok({
|
|
424
|
+
status: 'ok',
|
|
425
|
+
command: 'ci-watch-tick',
|
|
426
|
+
pr: Number(pr),
|
|
427
|
+
action,
|
|
428
|
+
attempts_used: state.attempts,
|
|
429
|
+
next_tick_at: nextTickAt(state),
|
|
430
|
+
reason,
|
|
431
|
+
})
|
|
432
|
+
} finally {
|
|
433
|
+
// `try/finally` so every branch releases the lock — even thrown errors.
|
|
434
|
+
// The terminal branches above release explicitly before deleting state
|
|
435
|
+
// (releaseLock no-ops on a missing file, so calling it twice is safe).
|
|
436
|
+
releaseLock(root, pr, sessionId)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// helpers
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
function nextTickAt(state) {
|
|
445
|
+
return new Date(
|
|
446
|
+
Date.now() + (state.next_tick_interval_ms || DEFAULT_TICK_INTERVAL_MS),
|
|
447
|
+
).toISOString()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Fetch the gh CLI payload OR read the test-stub JSON. Returns
|
|
452
|
+
* { checks, prMetadata } on success
|
|
453
|
+
* { error: '<message>' } when gh / stub failed.
|
|
454
|
+
*
|
|
455
|
+
* Note: this is a thin wrapper — the orchestrator skill is responsible for
|
|
456
|
+
* the actual `gh run view --log` + fix-CLI shell-out (spec.md §5.5). The
|
|
457
|
+
* cmd's tick only reads checks + classifies + updates state; the heavy
|
|
458
|
+
* fix-loop work happens in the skill body.
|
|
459
|
+
*/
|
|
460
|
+
function fetchGhPayload(_pr) {
|
|
461
|
+
const isTestRuntime =
|
|
462
|
+
process.env.NODE_ENV === 'test' ||
|
|
463
|
+
process.env.VITEST != null ||
|
|
464
|
+
process.env.APT_INTERNAL_TEST != null
|
|
465
|
+
const stub = isTestRuntime ? process.env.APT_TEST_GH_STUB : undefined
|
|
466
|
+
if (stub) {
|
|
467
|
+
try {
|
|
468
|
+
if (!existsSync(stub)) {
|
|
469
|
+
return { error: 'APT_TEST_GH_STUB path not found' }
|
|
470
|
+
}
|
|
471
|
+
const data = JSON.parse(readFileSync(stub, 'utf-8'))
|
|
472
|
+
return {
|
|
473
|
+
checks: data.checks || [],
|
|
474
|
+
prMetadata: data.prMetadata || { state: 'OPEN' },
|
|
475
|
+
}
|
|
476
|
+
} catch (_e) {
|
|
477
|
+
return { error: 'stub parse error' }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// Real gh shell-out lives in the skill body. The cmd's tick treats
|
|
481
|
+
// "no stub + no payload" as infra-backoff so unit tests can exercise the
|
|
482
|
+
// path without a live gh binary. The orchestrator skill is responsible
|
|
483
|
+
// for pre-fetching and re-invoking with APT_TEST_GH_STUB set, OR for
|
|
484
|
+
// passing real data via a future --payload-file flag (out of scope here).
|
|
485
|
+
return {
|
|
486
|
+
error: 'gh shell-out is the skill orchestrator boundary — set APT_TEST_GH_STUB for unit tests',
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Read `ci_watch.auto_merge_on_green` from `.aperant/config.json` (default
|
|
492
|
+
* false). Best-effort — missing file or unparsable JSON returns false.
|
|
493
|
+
*/
|
|
494
|
+
function readAutoMergeFromConfig(root) {
|
|
495
|
+
try {
|
|
496
|
+
const path = resolve(root, '.aperant', 'config.json')
|
|
497
|
+
if (!existsSync(path)) return false
|
|
498
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'))
|
|
499
|
+
return parsed?.ci_watch?.auto_merge_on_green === true
|
|
500
|
+
} catch {
|
|
501
|
+
return false
|
|
502
|
+
}
|
|
503
|
+
}
|