@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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ci-watch/stop-matrix.mjs — pure stop-matrix classifier (D-06).
|
|
3
|
+
*
|
|
4
|
+
* `classify({checks, state, prMetadata})` returns
|
|
5
|
+
* { action, counts_toward_budget, reason }
|
|
6
|
+
*
|
|
7
|
+
* No I/O, no fs, no gh CLI shell-outs — caller pre-fetches `checks` (the
|
|
8
|
+
* payload from `gh pr checks --json name,bucket,state,conclusion,detailsUrl`)
|
|
9
|
+
* and `prMetadata` (`gh pr view --json state,reviewDecision`) and passes them
|
|
10
|
+
* in. The orchestrator in commands/ci-watch.mjs handles the side effects
|
|
11
|
+
* implied by the action.
|
|
12
|
+
*
|
|
13
|
+
* Predicate order matters — short-circuits go first (merged/closed/green/cap
|
|
14
|
+
* fire before any per-check inspection). See spec.md §9 table for the full
|
|
15
|
+
* ordering.
|
|
16
|
+
*
|
|
17
|
+
* The 11 actions:
|
|
18
|
+
* merged, closed, green, attempts-cap, wall-cap, reviewer-pause,
|
|
19
|
+
* identical-log, flake-skip, infra-backoff, diverged-rebased,
|
|
20
|
+
* diverged-bailed, fix
|
|
21
|
+
*
|
|
22
|
+
* `counts_toward_budget` is `true` for fix attempts (including rebase
|
|
23
|
+
* attempts and rebase-bail); `false` for flake-skip / infra-backoff; `false`
|
|
24
|
+
* (with a sentinel meaning "n/a") for terminal actions.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const ATTEMPTS_CAP = 5
|
|
28
|
+
|
|
29
|
+
const INFRA_FAILURE_CLASSES = new Set(['runner-oom', 'runner-cancelled', 'gh-5xx', 'timeout'])
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} CheckRow
|
|
33
|
+
* @property {string} [name]
|
|
34
|
+
* @property {string} [conclusion] e.g. 'success' | 'failure' | 'cancelled' | 'timed_out' | 'neutral'
|
|
35
|
+
* @property {string} [state]
|
|
36
|
+
* @property {string} [bucket]
|
|
37
|
+
* @property {string} [failure_class] — optional pre-classified hint (test-only seam)
|
|
38
|
+
* @property {string} [log_fingerprint] — optional digest for identical-log detection
|
|
39
|
+
*
|
|
40
|
+
* @typedef {Object} ClassifyInput
|
|
41
|
+
* @property {CheckRow[]} checks
|
|
42
|
+
* @property {Object} state — per-PR state (see state.mjs §8)
|
|
43
|
+
* @property {Object} prMetadata — { state: 'OPEN'|'CLOSED'|'MERGED', reviewDecision?: string }
|
|
44
|
+
*
|
|
45
|
+
* @typedef {Object} ClassifyOutput
|
|
46
|
+
* @property {string} action
|
|
47
|
+
* @property {boolean} counts_toward_budget
|
|
48
|
+
* @property {string} reason
|
|
49
|
+
*
|
|
50
|
+
* @param {ClassifyInput} input
|
|
51
|
+
* @returns {ClassifyOutput}
|
|
52
|
+
*/
|
|
53
|
+
export function classify({ checks, state, prMetadata }) {
|
|
54
|
+
const pr = prMetadata || {}
|
|
55
|
+
const st = state || {}
|
|
56
|
+
const rows = Array.isArray(checks) ? checks : []
|
|
57
|
+
|
|
58
|
+
// 1. MERGED.
|
|
59
|
+
if (pr.state === 'MERGED') {
|
|
60
|
+
return { action: 'merged', counts_toward_budget: false, reason: 'pr is merged' }
|
|
61
|
+
}
|
|
62
|
+
// 2. CLOSED (not merged).
|
|
63
|
+
if (pr.state === 'CLOSED') {
|
|
64
|
+
return { action: 'closed', counts_toward_budget: false, reason: 'pr is closed unmerged' }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const failed = rows.filter((c) => isFailing(c))
|
|
68
|
+
const allGreen = rows.length > 0 && rows.every((c) => c.conclusion === 'success')
|
|
69
|
+
|
|
70
|
+
// 3. All checks green.
|
|
71
|
+
if (allGreen) {
|
|
72
|
+
return { action: 'green', counts_toward_budget: false, reason: 'all checks succeeded' }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 3b. No checks registered yet — CI hasn't enqueued jobs. Wait without
|
|
76
|
+
// burning a fix attempt.
|
|
77
|
+
if (rows.length === 0) {
|
|
78
|
+
return { action: 'wait', counts_toward_budget: false, reason: 'no checks registered yet' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 4. Attempts cap (only when red present — otherwise green/merged/closed would have fired).
|
|
82
|
+
const attempts = st.attempts || 0
|
|
83
|
+
if (attempts >= ATTEMPTS_CAP && failed.length > 0) {
|
|
84
|
+
return {
|
|
85
|
+
action: 'attempts-cap',
|
|
86
|
+
counts_toward_budget: false,
|
|
87
|
+
reason: `${attempts} fix attempts used; cap is ${ATTEMPTS_CAP}`,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 5. Wall-clock cap.
|
|
92
|
+
if (st.deadline && Date.now() > new Date(st.deadline).getTime() && failed.length > 0) {
|
|
93
|
+
return {
|
|
94
|
+
action: 'wall-cap',
|
|
95
|
+
counts_toward_budget: false,
|
|
96
|
+
reason: 'past 4h deadline',
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 6. Reviewer asked for changes.
|
|
101
|
+
if (pr.reviewDecision === 'CHANGES_REQUESTED') {
|
|
102
|
+
return {
|
|
103
|
+
action: 'reviewer-pause',
|
|
104
|
+
counts_toward_budget: false,
|
|
105
|
+
reason: 'reviewer requested changes — pausing',
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 7. Identical-log → non-converging fixer.
|
|
110
|
+
// Compare aggregate fingerprints (same format as the write side in ci-watch.mjs).
|
|
111
|
+
if (attempts > 0 && st.last_status && failed.length > 0) {
|
|
112
|
+
const currentFingerprint = failed
|
|
113
|
+
.map((f) => `${f.name}:${f.log_fingerprint ?? ''}`)
|
|
114
|
+
.sort()
|
|
115
|
+
.join('|')
|
|
116
|
+
if (currentFingerprint === st.last_status) {
|
|
117
|
+
return {
|
|
118
|
+
action: 'identical-log',
|
|
119
|
+
counts_toward_budget: false,
|
|
120
|
+
reason: 'failed-check fingerprint identical to last attempt',
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 8. Flake-skip — same check failed then passed on rerun, no commit between.
|
|
126
|
+
// The classifier sees this as an explicit hint from the caller: failed rows
|
|
127
|
+
// classified as `bucket === 'flake'` or `failure_class === 'flake'` are the
|
|
128
|
+
// flaky-test variety and must not burn a fix attempt. Fires when EVERY
|
|
129
|
+
// remaining failure is a flake — if any non-flake failure is present, we
|
|
130
|
+
// fall through and the real failure governs.
|
|
131
|
+
if (
|
|
132
|
+
failed.length > 0 &&
|
|
133
|
+
failed.every((c) => c.bucket === 'flake' || c.failure_class === 'flake')
|
|
134
|
+
) {
|
|
135
|
+
return {
|
|
136
|
+
action: 'flake-skip',
|
|
137
|
+
counts_toward_budget: false,
|
|
138
|
+
reason: 'check transient — passed on rerun',
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 9. Infra failure class.
|
|
143
|
+
if (failed.some((c) => INFRA_FAILURE_CLASSES.has(c.failure_class))) {
|
|
144
|
+
return {
|
|
145
|
+
action: 'infra-backoff',
|
|
146
|
+
counts_toward_budget: false,
|
|
147
|
+
reason: 'infra-class failure — backoff and retry',
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 10. Diverged branch — caller has already done the fetch + dry-rebase and
|
|
152
|
+
// passes the outcome in `prMetadata.diverged`:
|
|
153
|
+
// { rebased: true } → action 'diverged-rebased' (counts)
|
|
154
|
+
// { rebased: false } → action 'diverged-bailed' (counts)
|
|
155
|
+
if (pr.diverged && typeof pr.diverged === 'object') {
|
|
156
|
+
if (pr.diverged.rebased === true) {
|
|
157
|
+
return {
|
|
158
|
+
action: 'diverged-rebased',
|
|
159
|
+
counts_toward_budget: true,
|
|
160
|
+
reason: 'origin ahead; rebased successfully',
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
action: 'diverged-bailed',
|
|
165
|
+
counts_toward_budget: true,
|
|
166
|
+
reason: 'origin ahead; rebase conflict',
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 11. Default red — go fix.
|
|
171
|
+
return {
|
|
172
|
+
action: 'fix',
|
|
173
|
+
counts_toward_budget: true,
|
|
174
|
+
reason: 'red checks present — attempting fix',
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function isFailing(check) {
|
|
179
|
+
const c = check.conclusion
|
|
180
|
+
return c === 'failure' || c === 'cancelled' || c === 'timed_out' || c === 'action_required'
|
|
181
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli-wrappers/ci-watch.mjs — Layer 2 wrapper for the ci-watch cmd.
|
|
3
|
+
*
|
|
4
|
+
* Today this is a passthrough — DISPATCH calls wrap(cmdCiWatch) directly.
|
|
5
|
+
* Re-exporting under cliCiWatch keeps the file-per-cmd structural seam
|
|
6
|
+
* (mirrors task.mjs / pr-review.mjs).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { cmdCiWatch as cliCiWatch } from '../commands/ci-watch.mjs'
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/adr.mjs — `apt-tools adr write` (Pocock adoption ST-6, AC5).
|
|
3
|
+
*
|
|
4
|
+
* Writes an ADR under `docs/adr/NNNN-slug.md` ONLY when all three Nygard
|
|
5
|
+
* gates pass: hard-to-reverse AND surprising-without-context AND
|
|
6
|
+
* real-trade-off. When any gate fails, the writer returns a `filtered`
|
|
7
|
+
* envelope and writes nothing — the decision stays in
|
|
8
|
+
* `.aperant/context/notes/{task-id}.md` instead.
|
|
9
|
+
*
|
|
10
|
+
* CLI:
|
|
11
|
+
* apt-tools adr write <project-dir>
|
|
12
|
+
* --title <slug>
|
|
13
|
+
* --hard-to-reverse <true|false>
|
|
14
|
+
* --surprising-without-context <true|false>
|
|
15
|
+
* --real-trade-off <true|false>
|
|
16
|
+
* --decision <text>
|
|
17
|
+
* --consequences <text>
|
|
18
|
+
* [--context <text>]
|
|
19
|
+
* [--status Proposed|Accepted|Deprecated|Superseded]
|
|
20
|
+
*
|
|
21
|
+
* Output: JSON envelope.
|
|
22
|
+
* On filter: { status: 'filtered', failed_gates: string[], reason: 'three-gate-filter' }
|
|
23
|
+
* On write: { status: 'ok', command: 'adr-write', file: <abs path>, number: N, slug: '...' }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
|
|
27
|
+
import { join, resolve } from 'node:path'
|
|
28
|
+
import { parseFlags } from '../util/args.mjs'
|
|
29
|
+
import { err, exitWith, ok } from '../util/result.mjs'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse a flag value as a strict boolean. Returns null on unrecognized form
|
|
33
|
+
* so callers can distinguish "missing" from "false".
|
|
34
|
+
*
|
|
35
|
+
* @param {unknown} v
|
|
36
|
+
* @returns {boolean|null}
|
|
37
|
+
*/
|
|
38
|
+
function parseBool(v) {
|
|
39
|
+
if (v === true) return true
|
|
40
|
+
if (typeof v !== 'string') return null
|
|
41
|
+
const low = v.toLowerCase()
|
|
42
|
+
if (low === 'true' || low === 'yes' || low === '1') return true
|
|
43
|
+
if (low === 'false' || low === 'no' || low === '0') return false
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Slugify a title for the filename (NNNN-slug.md).
|
|
49
|
+
*
|
|
50
|
+
* @param {string} title
|
|
51
|
+
*/
|
|
52
|
+
function slugify(title) {
|
|
53
|
+
return title
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.trim()
|
|
56
|
+
.replace(/[^\w\s-]/g, '')
|
|
57
|
+
.replace(/\s+/g, '-')
|
|
58
|
+
.replace(/-+/g, '-')
|
|
59
|
+
.replace(/^-|-$/g, '')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find the next ADR sequence number by scanning `docs/adr/`.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} adrDir
|
|
66
|
+
* @returns {number}
|
|
67
|
+
*/
|
|
68
|
+
function nextAdrNumber(adrDir) {
|
|
69
|
+
if (!existsSync(adrDir)) return 1
|
|
70
|
+
const entries = readdirSync(adrDir)
|
|
71
|
+
let max = 0
|
|
72
|
+
for (const name of entries) {
|
|
73
|
+
const m = name.match(/^(\d{4})-/)
|
|
74
|
+
if (!m) continue
|
|
75
|
+
const n = Number.parseInt(m[1], 10)
|
|
76
|
+
if (n > max) max = n
|
|
77
|
+
}
|
|
78
|
+
return max + 1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Render the ADR markdown body.
|
|
83
|
+
*
|
|
84
|
+
* @param {{
|
|
85
|
+
* number: number,
|
|
86
|
+
* title: string,
|
|
87
|
+
* status: string,
|
|
88
|
+
* date: string,
|
|
89
|
+
* context: string,
|
|
90
|
+
* decision: string,
|
|
91
|
+
* consequences: string,
|
|
92
|
+
* hardToReverse: boolean,
|
|
93
|
+
* surprisingWithoutContext: boolean,
|
|
94
|
+
* realTradeOff: boolean,
|
|
95
|
+
* }} opts
|
|
96
|
+
*/
|
|
97
|
+
function renderAdr(opts) {
|
|
98
|
+
const paddedNumber = String(opts.number).padStart(4, '0')
|
|
99
|
+
return [
|
|
100
|
+
`# ADR-${paddedNumber}: ${opts.title}`,
|
|
101
|
+
'',
|
|
102
|
+
`**Status:** ${opts.status}`,
|
|
103
|
+
`**Date:** ${opts.date}`,
|
|
104
|
+
'',
|
|
105
|
+
'## Context',
|
|
106
|
+
'',
|
|
107
|
+
opts.context,
|
|
108
|
+
'',
|
|
109
|
+
'## Decision',
|
|
110
|
+
'',
|
|
111
|
+
opts.decision,
|
|
112
|
+
'',
|
|
113
|
+
'## Consequences',
|
|
114
|
+
'',
|
|
115
|
+
opts.consequences,
|
|
116
|
+
'',
|
|
117
|
+
'---',
|
|
118
|
+
'',
|
|
119
|
+
'## Nygard 3-gate filter (audit trail)',
|
|
120
|
+
'',
|
|
121
|
+
'This ADR was written via `apt-tools adr write` and passed all three of',
|
|
122
|
+
"Nygard's filter gates. Reversible / obvious / trade-off-free decisions",
|
|
123
|
+
'stay in `.aperant/context/notes/{task-id}.md` and do NOT become ADRs.',
|
|
124
|
+
'',
|
|
125
|
+
`- **Hard to reverse:** ${opts.hardToReverse}`,
|
|
126
|
+
`- **Surprising without context:** ${opts.surprisingWithoutContext}`,
|
|
127
|
+
`- **Real trade-off:** ${opts.realTradeOff}`,
|
|
128
|
+
'',
|
|
129
|
+
].join('\n')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* CLI entry: `apt-tools adr <subcommand> <project-dir> [flags]`.
|
|
134
|
+
*/
|
|
135
|
+
export function cmdAdr(subcommand, projectDir, extraArgs) {
|
|
136
|
+
if (subcommand !== 'write') {
|
|
137
|
+
return err(
|
|
138
|
+
'Usage: apt-tools adr write <project-dir> --title <slug> --hard-to-reverse <bool> --surprising-without-context <bool> --real-trade-off <bool> --decision <text> --consequences <text> [--context <text>] [--status <status>]',
|
|
139
|
+
1,
|
|
140
|
+
{ command: 'adr' },
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
if (!projectDir) {
|
|
144
|
+
return err('project-dir is required', 1, { command: 'adr-write' })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const flags = parseFlags(extraArgs ?? [])
|
|
148
|
+
const title = flags.get('title')
|
|
149
|
+
if (typeof title !== 'string' || title.trim() === '') {
|
|
150
|
+
return err('--title <slug> is required', 1, { command: 'adr-write' })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Three Nygard gates — every one must be parseable; missing means "not
|
|
154
|
+
// asked", which is the same as "no" for the filter.
|
|
155
|
+
const hardToReverse = parseBool(flags.get('hard-to-reverse'))
|
|
156
|
+
const surprisingWithoutContext = parseBool(flags.get('surprising-without-context'))
|
|
157
|
+
const realTradeOff = parseBool(flags.get('real-trade-off'))
|
|
158
|
+
|
|
159
|
+
for (const [name, value] of [
|
|
160
|
+
['--hard-to-reverse', hardToReverse],
|
|
161
|
+
['--surprising-without-context', surprisingWithoutContext],
|
|
162
|
+
['--real-trade-off', realTradeOff],
|
|
163
|
+
]) {
|
|
164
|
+
if (value === null) {
|
|
165
|
+
return err(`${name} <true|false> is required`, 1, { command: 'adr-write' })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const decision = flags.get('decision')
|
|
170
|
+
const consequences = flags.get('consequences')
|
|
171
|
+
if (typeof decision !== 'string' || decision.trim() === '') {
|
|
172
|
+
return err('--decision <text> is required', 1, { command: 'adr-write' })
|
|
173
|
+
}
|
|
174
|
+
if (typeof consequences !== 'string' || consequences.trim() === '') {
|
|
175
|
+
return err('--consequences <text> is required', 1, { command: 'adr-write' })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Three-gate filter — every gate must be true to write.
|
|
179
|
+
const failed = []
|
|
180
|
+
if (!hardToReverse) failed.push('hard-to-reverse')
|
|
181
|
+
if (!surprisingWithoutContext) failed.push('surprising-without-context')
|
|
182
|
+
if (!realTradeOff) failed.push('real-trade-off')
|
|
183
|
+
|
|
184
|
+
if (failed.length > 0) {
|
|
185
|
+
return exitWith(
|
|
186
|
+
{
|
|
187
|
+
status: 'filtered',
|
|
188
|
+
command: 'adr-write',
|
|
189
|
+
reason: 'three-gate-filter',
|
|
190
|
+
failed_gates: failed,
|
|
191
|
+
note: 'Decision stays in .aperant/context/notes/{task-id}.md. ADRs are reserved for hard-to-reverse, surprising, real-trade-off decisions.',
|
|
192
|
+
},
|
|
193
|
+
0,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// All three gates passed — write the ADR.
|
|
198
|
+
const absProjectDir = resolve(projectDir)
|
|
199
|
+
if (!existsSync(absProjectDir)) {
|
|
200
|
+
return err(`project directory not found: ${absProjectDir}`, 1, { command: 'adr-write' })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const adrDir = join(absProjectDir, 'docs', 'adr')
|
|
204
|
+
if (!existsSync(adrDir)) {
|
|
205
|
+
mkdirSync(adrDir, { recursive: true })
|
|
206
|
+
}
|
|
207
|
+
const number = nextAdrNumber(adrDir)
|
|
208
|
+
const slug = slugify(title)
|
|
209
|
+
const paddedNumber = String(number).padStart(4, '0')
|
|
210
|
+
const filename = `${paddedNumber}-${slug}.md`
|
|
211
|
+
const filePath = join(adrDir, filename)
|
|
212
|
+
|
|
213
|
+
const status = (typeof flags.get('status') === 'string' && flags.get('status')) || 'Proposed'
|
|
214
|
+
const context =
|
|
215
|
+
(typeof flags.get('context') === 'string' && flags.get('context')) ||
|
|
216
|
+
'{Context not supplied — provide via --context on a re-run.}'
|
|
217
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
218
|
+
|
|
219
|
+
const body = renderAdr({
|
|
220
|
+
number,
|
|
221
|
+
title,
|
|
222
|
+
status: String(status),
|
|
223
|
+
date,
|
|
224
|
+
context: String(context),
|
|
225
|
+
decision,
|
|
226
|
+
consequences,
|
|
227
|
+
hardToReverse: true,
|
|
228
|
+
surprisingWithoutContext: true,
|
|
229
|
+
realTradeOff: true,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
writeFileSync(filePath, body)
|
|
233
|
+
|
|
234
|
+
return ok({
|
|
235
|
+
status: 'ok',
|
|
236
|
+
command: 'adr-write',
|
|
237
|
+
file: filePath,
|
|
238
|
+
number,
|
|
239
|
+
slug,
|
|
240
|
+
title,
|
|
241
|
+
passed_gates: ['hard-to-reverse', 'surprising-without-context', 'real-trade-off'],
|
|
242
|
+
})
|
|
243
|
+
}
|