@aperant/framework 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +152 -0
- package/dist/cli/commands/ci-watch.mjs +49 -2
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/commit.mjs +3 -3
- package/dist/cli/commands/commit.mjs.map +1 -1
- package/dist/cli/commands/event.mjs +16 -16
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/merge-integrate.mjs +3 -3
- package/dist/cli/commands/merge-integrate.mjs.map +1 -1
- package/dist/cli/commands/produce.d.mts +9 -0
- package/dist/cli/commands/produce.d.mts.map +1 -0
- package/dist/cli/commands/produce.mjs +1345 -0
- package/dist/cli/commands/produce.mjs.map +1 -0
- package/dist/cli/commands/task.d.mts +16 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +434 -266
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -1
- package/dist/cli/coordination/event-schema.d.mts +5 -3
- package/dist/cli/coordination/event-schema.d.mts.map +1 -1
- package/dist/cli/coordination/event-schema.mjs +245 -21
- package/dist/cli/coordination/event-schema.mjs.map +1 -1
- package/dist/cli/coordination/store.d.mts +2 -2
- package/dist/cli/coordination/store.mjs +4 -4
- package/dist/cli/coordination/store.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +2 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +30 -0
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +2 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/produce/blind-probe.d.mts +85 -0
- package/dist/cli/produce/blind-probe.d.mts.map +1 -0
- package/dist/cli/produce/blind-probe.mjs +217 -0
- package/dist/cli/produce/blind-probe.mjs.map +1 -0
- package/dist/cli/produce/claim.d.mts +188 -0
- package/dist/cli/produce/claim.d.mts.map +1 -0
- package/dist/cli/produce/claim.mjs +518 -0
- package/dist/cli/produce/claim.mjs.map +1 -0
- package/dist/cli/produce/done-gate.d.mts +87 -0
- package/dist/cli/produce/done-gate.d.mts.map +1 -0
- package/dist/cli/produce/done-gate.mjs +200 -0
- package/dist/cli/produce/done-gate.mjs.map +1 -0
- package/dist/cli/produce/events.d.mts +77 -0
- package/dist/cli/produce/events.d.mts.map +1 -0
- package/dist/cli/produce/events.mjs +126 -0
- package/dist/cli/produce/events.mjs.map +1 -0
- package/dist/cli/produce/evidence-oracle.d.mts +63 -0
- package/dist/cli/produce/evidence-oracle.d.mts.map +1 -0
- package/dist/cli/produce/evidence-oracle.mjs +122 -0
- package/dist/cli/produce/evidence-oracle.mjs.map +1 -0
- package/dist/cli/produce/ledger.d.mts +140 -0
- package/dist/cli/produce/ledger.d.mts.map +1 -0
- package/dist/cli/produce/ledger.mjs +272 -0
- package/dist/cli/produce/ledger.mjs.map +1 -0
- package/dist/cli/produce/probe-family.d.mts +53 -0
- package/dist/cli/produce/probe-family.d.mts.map +1 -0
- package/dist/cli/produce/probe-family.mjs +160 -0
- package/dist/cli/produce/probe-family.mjs.map +1 -0
- package/dist/cli/produce/projection.d.mts +55 -0
- package/dist/cli/produce/projection.d.mts.map +1 -0
- package/dist/cli/produce/projection.mjs +97 -0
- package/dist/cli/produce/projection.mjs.map +1 -0
- package/dist/cli/produce/run-id.d.mts +42 -0
- package/dist/cli/produce/run-id.d.mts.map +1 -0
- package/dist/cli/produce/run-id.mjs +79 -0
- package/dist/cli/produce/run-id.mjs.map +1 -0
- package/dist/cli/produce/saga.d.mts +180 -0
- package/dist/cli/produce/saga.d.mts.map +1 -0
- package/dist/cli/produce/saga.mjs +290 -0
- package/dist/cli/produce/saga.mjs.map +1 -0
- package/dist/cli/produce/scheduler.d.mts +165 -0
- package/dist/cli/produce/scheduler.d.mts.map +1 -0
- package/dist/cli/produce/scheduler.mjs +399 -0
- package/dist/cli/produce/scheduler.mjs.map +1 -0
- package/dist/cli/produce/setpoint.d.mts +52 -0
- package/dist/cli/produce/setpoint.d.mts.map +1 -0
- package/dist/cli/produce/setpoint.mjs +113 -0
- package/dist/cli/produce/setpoint.mjs.map +1 -0
- package/dist/cli/produce/verification-ttl.d.mts +75 -0
- package/dist/cli/produce/verification-ttl.d.mts.map +1 -0
- package/dist/cli/produce/verification-ttl.mjs +169 -0
- package/dist/cli/produce/verification-ttl.mjs.map +1 -0
- package/dist/cli/roadmap/{conductor-view.d.mts → showrunner-view.d.mts} +3 -3
- package/dist/cli/roadmap/showrunner-view.d.mts.map +1 -0
- package/dist/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +7 -7
- package/dist/cli/roadmap/showrunner-view.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +2 -1
- package/dist/plugin/skills/apt/SKILL.md +112 -38
- package/dist/plugin/skills/apt-debug/SKILL.md +14 -24
- package/dist/plugin/skills/apt-fan-out/SKILL.md +4 -4
- package/dist/plugin/skills/apt-handoff/SKILL.md +1 -1
- package/dist/plugin/skills/apt-plan/SKILL.md +5 -5
- package/dist/plugin/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
- package/dist/plugin/skills/apt-produce/SKILL.md +606 -0
- package/dist/plugin/skills/apt-quick/SKILL.md +14 -22
- package/dist/plugin/skills/apt-run/SKILL.md +126 -3
- package/dist/plugin/skills/apt-ship/SKILL.md +2 -0
- package/dist/plugin/skills/apt-spar/SKILL.md +5 -3
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +4 -4
- package/package.json +4 -4
- package/prompts/{conductor-framework-context.md → showrunner-framework-context.md} +1 -1
- package/prompts/{conductor-status-check.md → showrunner-status-check.md} +1 -1
- package/prompts/{conductor-sub-agent.md → showrunner-sub-agent.md} +6 -6
- package/prompts/{conductor-system.md → showrunner-system.md} +8 -8
- package/skills/apt/SKILL.md +112 -38
- package/skills/apt-debug/SKILL.md +14 -24
- package/skills/apt-fan-out/SKILL.md +4 -4
- package/skills/apt-handoff/SKILL.md +1 -1
- package/skills/apt-plan/SKILL.md +5 -5
- package/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
- package/skills/apt-produce/SKILL.md +606 -0
- package/skills/apt-quick/SKILL.md +14 -22
- package/skills/apt-run/SKILL.md +126 -3
- package/skills/apt-ship/SKILL.md +2 -0
- package/skills/apt-spar/SKILL.md +5 -3
- package/skills/apt-watch-ci/SKILL.md +4 -4
- package/src/cli/commands/ci-watch.mjs +51 -2
- package/src/cli/commands/commit.mjs +3 -3
- package/src/cli/commands/event.mjs +16 -16
- package/src/cli/commands/merge-integrate.mjs +3 -3
- package/src/cli/commands/produce.mjs +1466 -0
- package/src/cli/commands/task.mjs +482 -285
- package/src/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
- package/src/cli/coordination/event-schema.d.ts +4 -2
- package/src/cli/coordination/event-schema.mjs +276 -21
- package/src/cli/coordination/store.mjs +4 -4
- package/src/cli/dispatch.mjs +2 -0
- package/src/cli/help.mjs +30 -0
- package/src/cli/install/legacy-paths.mjs +2 -0
- package/src/cli/produce/blind-probe.mjs +245 -0
- package/src/cli/produce/claim.mjs +543 -0
- package/src/cli/produce/done-gate.mjs +238 -0
- package/src/cli/produce/events.mjs +131 -0
- package/src/cli/produce/evidence-oracle.mjs +133 -0
- package/src/cli/produce/ledger.mjs +284 -0
- package/src/cli/produce/probe-family.mjs +168 -0
- package/src/cli/produce/projection.mjs +105 -0
- package/src/cli/produce/run-id.mjs +84 -0
- package/src/cli/produce/saga.mjs +303 -0
- package/src/cli/produce/scheduler.mjs +423 -0
- package/src/cli/produce/setpoint.mjs +122 -0
- package/src/cli/produce/verification-ttl.mjs +191 -0
- package/src/cli/roadmap/showrunner-view.d.ts +10 -0
- package/src/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +6 -6
- package/dist/cli/roadmap/conductor-view.d.mts.map +0 -1
- package/dist/cli/roadmap/conductor-view.mjs.map +0 -1
- package/src/cli/roadmap/conductor-view.d.ts +0 -10
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* produce/ledger.mjs — the acceptance-criteria ledger + Lyapunov math (M3).
|
|
4
|
+
*
|
|
5
|
+
* V (the Lyapunov function) = open acceptance criteria = declared − closed,
|
|
6
|
+
* folded from the append-only log on every read (ID-M3-01 / ID-M3-02). Each
|
|
7
|
+
* criterion is a `produce.criteria.declared` event bound to a milestone; it
|
|
8
|
+
* closes via `produce.criteria.closed`. `produce.scope.changed` is the ONLY
|
|
9
|
+
* sanctioned way V may rise after the run has begun closing criteria (ID-M3-04).
|
|
10
|
+
*
|
|
11
|
+
* This module is a SEPARATE fold parallel to `projection.mjs` — the roadmap
|
|
12
|
+
* projection still folds `produce.roadmap.*` ONLY, so criteria/scope events
|
|
13
|
+
* never enter the roadmap the Showrunner re-reads (the M1/M2 blindness invariant
|
|
14
|
+
* is preserved). It reads ONLY the event log through `events.mjs` (no `node:`
|
|
15
|
+
* beyond what events.mjs already uses) and holds NO model id (it is pure math).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { appendProduceEvent, readProduceRunEventsMatching } from './events.mjs'
|
|
19
|
+
import { slugFromOutcome } from './run-id.mjs'
|
|
20
|
+
|
|
21
|
+
/** The op-match for the criteria + scope ledger fold (never `produce.roadmap.*`). */
|
|
22
|
+
function isLedgerOp(op) {
|
|
23
|
+
return op.startsWith('produce.criteria.') || op === 'produce.scope.changed'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fold the run's acceptance-criteria ledger from the append-only log. A
|
|
28
|
+
* `produce.criteria.declared` seeds an OPEN criterion (in first-seen order); a
|
|
29
|
+
* `produce.criteria.closed` advances it to `closed`; `produce.scope.changed` is
|
|
30
|
+
* recorded on the scope-change trail. Deterministic — two folds of the same log
|
|
31
|
+
* yield the identical ledger; `updated_at` is the last folded event's timestamp.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} projectDir
|
|
34
|
+
* @param {string} runId
|
|
35
|
+
* @returns {{
|
|
36
|
+
* run_id: string,
|
|
37
|
+
* criteria: Array<{ criterion_id: string, milestone_id: string | null, text: string, status: 'open' | 'closed' }>,
|
|
38
|
+
* open_count: number,
|
|
39
|
+
* closed_count: number,
|
|
40
|
+
* scope_changes: Array<{ reason: string, milestone_id: string | null, t: string | null }>,
|
|
41
|
+
* updated_at: string | null,
|
|
42
|
+
* }}
|
|
43
|
+
*/
|
|
44
|
+
export function projectLedger(projectDir, runId) {
|
|
45
|
+
const events = readProduceRunEventsMatching(projectDir, runId, isLedgerOp)
|
|
46
|
+
const byId = new Map()
|
|
47
|
+
const order = []
|
|
48
|
+
const scopeChanges = []
|
|
49
|
+
let updatedAt = null
|
|
50
|
+
|
|
51
|
+
for (const e of events) {
|
|
52
|
+
const d = e.data || {}
|
|
53
|
+
if (e.op === 'produce.criteria.declared') {
|
|
54
|
+
const cid = typeof d.criterion_id === 'string' ? d.criterion_id : null
|
|
55
|
+
if (cid && !byId.has(cid)) {
|
|
56
|
+
byId.set(cid, {
|
|
57
|
+
criterion_id: cid,
|
|
58
|
+
milestone_id: typeof d.milestone_id === 'string' ? d.milestone_id : null,
|
|
59
|
+
text: typeof d.text === 'string' ? d.text : '',
|
|
60
|
+
status: 'open',
|
|
61
|
+
})
|
|
62
|
+
order.push(cid)
|
|
63
|
+
}
|
|
64
|
+
} else if (e.op === 'produce.criteria.closed') {
|
|
65
|
+
const cid = typeof d.criterion_id === 'string' ? d.criterion_id : null
|
|
66
|
+
if (cid && byId.has(cid)) byId.get(cid).status = 'closed'
|
|
67
|
+
} else if (e.op === 'produce.scope.changed') {
|
|
68
|
+
scopeChanges.push({
|
|
69
|
+
reason: typeof d.reason === 'string' ? d.reason : '',
|
|
70
|
+
milestone_id: typeof d.milestone_id === 'string' ? d.milestone_id : null,
|
|
71
|
+
t: typeof e.t === 'string' ? e.t : null,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
if (typeof e.t === 'string') updatedAt = e.t
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const criteria = order.map((cid) => byId.get(cid))
|
|
78
|
+
return {
|
|
79
|
+
run_id: runId,
|
|
80
|
+
criteria,
|
|
81
|
+
open_count: criteria.filter((c) => c.status === 'open').length,
|
|
82
|
+
closed_count: criteria.filter((c) => c.status === 'closed').length,
|
|
83
|
+
scope_changes: scopeChanges,
|
|
84
|
+
updated_at: updatedAt,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* V = open acceptance criteria = declared − closed, folded from the log.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} projectDir
|
|
92
|
+
* @param {string} runId
|
|
93
|
+
* @returns {number}
|
|
94
|
+
*/
|
|
95
|
+
export function computeOpenCriteria(projectDir, runId) {
|
|
96
|
+
return projectLedger(projectDir, runId).open_count
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The write-time anti-windup guard (ID-M3-03). `declare-criteria` raises V; it is
|
|
101
|
+
* REJECTED when the run is IN DESCENT (≥1 `produce.criteria.closed` already
|
|
102
|
+
* logged) AND no scope-change is supplied. Before descent, declaration is FREE
|
|
103
|
+
* (the baseline scope-definition window). This is the ONLY hard Lyapunov
|
|
104
|
+
* enforcement and it is fail-closed.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} projectDir
|
|
107
|
+
* @param {string} runId
|
|
108
|
+
* @param {{ hasScopeChange?: boolean }} [opts]
|
|
109
|
+
* @returns {{ allowed: boolean, reason: string, in_descent: boolean }}
|
|
110
|
+
*/
|
|
111
|
+
export function evaluateDeclareGuard(projectDir, runId, { hasScopeChange = false } = {}) {
|
|
112
|
+
const closes = readProduceRunEventsMatching(
|
|
113
|
+
projectDir,
|
|
114
|
+
runId,
|
|
115
|
+
(op) => op === 'produce.criteria.closed',
|
|
116
|
+
)
|
|
117
|
+
const inDescent = closes.length > 0
|
|
118
|
+
const allowed = !inDescent || !!hasScopeChange
|
|
119
|
+
let reason
|
|
120
|
+
if (!allowed) reason = 'in_descent_no_scope_change'
|
|
121
|
+
else if (!inDescent) reason = 'baseline_window'
|
|
122
|
+
else reason = 'scope_change'
|
|
123
|
+
return { allowed, reason, in_descent: inDescent }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compute the monotone-progress report (ID-M3-02 / AC6). Folds ALL of
|
|
128
|
+
* `produce.criteria.*` + `produce.scope.changed` +
|
|
129
|
+
* `produce.roadmap.milestone_status_changed` in append order, maintaining the
|
|
130
|
+
* running V. Every `milestone_status_changed{status:'done'}` is a checkpoint: it
|
|
131
|
+
* captures `v_before`, then consumes the contiguous run of `criteria.closed`
|
|
132
|
+
* events that ship triggered (appended immediately after the done event) to
|
|
133
|
+
* capture `v_after`. `delta = v_after − v_before` (≤ 0). The series is STRICTLY
|
|
134
|
+
* DECREASING iff every ship checkpoint had `delta < 0`; a flat ship (delta === 0)
|
|
135
|
+
* is a soft STALL, not a hard violation.
|
|
136
|
+
*
|
|
137
|
+
* Read-time defense (ID-M3-03): a `produce.criteria.declared` that raised V
|
|
138
|
+
* AFTER descent WITHOUT an accompanying (immediately-preceding)
|
|
139
|
+
* `produce.scope.changed` is a HARD `lyapunov_violation`. `progress` FLAGS — it
|
|
140
|
+
* never rejects (the rejection is the write-time guard).
|
|
141
|
+
*
|
|
142
|
+
* @param {string} projectDir
|
|
143
|
+
* @param {string} runId
|
|
144
|
+
* @returns {{
|
|
145
|
+
* run_id: string,
|
|
146
|
+
* v_open_now: number,
|
|
147
|
+
* v_series: Array<{ milestone_id: string | null, v_before: number, v_after: number, delta: number, decreased: boolean }>,
|
|
148
|
+
* strictly_decreasing: boolean,
|
|
149
|
+
* lyapunov_ok: boolean,
|
|
150
|
+
* lyapunov_violations: Array<{ criterion_id: string | null, milestone_id: string | null, reason: string, t: string | null }>,
|
|
151
|
+
* scope_changes: Array<{ reason: string, milestone_id: string | null, t: string | null }>,
|
|
152
|
+
* }}
|
|
153
|
+
*/
|
|
154
|
+
export function computeProgress(projectDir, runId) {
|
|
155
|
+
const events = readProduceRunEventsMatching(
|
|
156
|
+
projectDir,
|
|
157
|
+
runId,
|
|
158
|
+
(op) =>
|
|
159
|
+
op.startsWith('produce.criteria.') ||
|
|
160
|
+
op === 'produce.scope.changed' ||
|
|
161
|
+
op === 'produce.roadmap.milestone_status_changed',
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const status = new Map()
|
|
165
|
+
let v = 0
|
|
166
|
+
let inDescent = false
|
|
167
|
+
const vSeries = []
|
|
168
|
+
const violations = []
|
|
169
|
+
const scopeChanges = []
|
|
170
|
+
|
|
171
|
+
/** Apply a `criteria.closed` payload to V (idempotent; sets inDescent). */
|
|
172
|
+
const applyClose = (d) => {
|
|
173
|
+
const cid = typeof d.criterion_id === 'string' ? d.criterion_id : null
|
|
174
|
+
if (cid && status.get(cid) === 'open') {
|
|
175
|
+
status.set(cid, 'closed')
|
|
176
|
+
v -= 1
|
|
177
|
+
} else if (cid && !status.has(cid)) {
|
|
178
|
+
// Close of a never-declared criterion — record the state, no V change.
|
|
179
|
+
status.set(cid, 'closed')
|
|
180
|
+
}
|
|
181
|
+
inDescent = true
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < events.length; i++) {
|
|
185
|
+
const e = events[i]
|
|
186
|
+
const d = e.data || {}
|
|
187
|
+
|
|
188
|
+
if (e.op === 'produce.criteria.declared') {
|
|
189
|
+
if (inDescent) {
|
|
190
|
+
const prev = i > 0 ? events[i - 1] : null
|
|
191
|
+
const accompanied = !!prev && prev.op === 'produce.scope.changed'
|
|
192
|
+
if (!accompanied) {
|
|
193
|
+
violations.push({
|
|
194
|
+
criterion_id: typeof d.criterion_id === 'string' ? d.criterion_id : null,
|
|
195
|
+
milestone_id: typeof d.milestone_id === 'string' ? d.milestone_id : null,
|
|
196
|
+
reason: 'declared_after_descent_without_scope_change',
|
|
197
|
+
t: typeof e.t === 'string' ? e.t : null,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const cid = typeof d.criterion_id === 'string' ? d.criterion_id : null
|
|
202
|
+
if (cid && !status.has(cid)) {
|
|
203
|
+
status.set(cid, 'open')
|
|
204
|
+
v += 1
|
|
205
|
+
}
|
|
206
|
+
} else if (e.op === 'produce.criteria.closed') {
|
|
207
|
+
applyClose(d)
|
|
208
|
+
} else if (e.op === 'produce.scope.changed') {
|
|
209
|
+
scopeChanges.push({
|
|
210
|
+
reason: typeof d.reason === 'string' ? d.reason : '',
|
|
211
|
+
milestone_id: typeof d.milestone_id === 'string' ? d.milestone_id : null,
|
|
212
|
+
t: typeof e.t === 'string' ? e.t : null,
|
|
213
|
+
})
|
|
214
|
+
} else if (e.op === 'produce.roadmap.milestone_status_changed' && d.status === 'done') {
|
|
215
|
+
const vBefore = v
|
|
216
|
+
// Consume the contiguous run of criteria.closed this ship triggered
|
|
217
|
+
// (the real flow appends them immediately after the done event).
|
|
218
|
+
let j = i + 1
|
|
219
|
+
while (j < events.length && events[j].op === 'produce.criteria.closed') {
|
|
220
|
+
applyClose(events[j].data || {})
|
|
221
|
+
j++
|
|
222
|
+
}
|
|
223
|
+
i = j - 1
|
|
224
|
+
const vAfter = v
|
|
225
|
+
const delta = vAfter - vBefore
|
|
226
|
+
vSeries.push({
|
|
227
|
+
milestone_id: typeof d.milestone_id === 'string' ? d.milestone_id : null,
|
|
228
|
+
v_before: vBefore,
|
|
229
|
+
v_after: vAfter,
|
|
230
|
+
delta,
|
|
231
|
+
decreased: delta < 0,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
run_id: runId,
|
|
238
|
+
v_open_now: v,
|
|
239
|
+
v_series: vSeries,
|
|
240
|
+
strictly_decreasing: vSeries.every((s) => s.delta < 0),
|
|
241
|
+
lyapunov_ok: violations.length === 0,
|
|
242
|
+
lyapunov_violations: violations,
|
|
243
|
+
scope_changes: scopeChanges,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Close a shipped milestone's still-open acceptance criteria — the CONSEQUENCE
|
|
249
|
+
* of a milestone flipping to `done` (ID-M3-05, wired additively in
|
|
250
|
+
* `cmdSetMilestoneDone`). Folds the ledger, finds the milestone's OPEN criteria,
|
|
251
|
+
* and appends one `produce.criteria.closed` per criterion so V strictly
|
|
252
|
+
* decreases. Idempotent — an already-closed (or criteria-less) milestone appends
|
|
253
|
+
* nothing (`closed_count: 0`), so a criteria-less ship is byte-identical to M2.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} projectDir
|
|
256
|
+
* @param {string} runId
|
|
257
|
+
* @param {string} milestoneId
|
|
258
|
+
* @returns {{ closed_count: number, criterion_ids: string[] }}
|
|
259
|
+
*/
|
|
260
|
+
export function closeCriteriaForShippedMilestone(projectDir, runId, milestoneId) {
|
|
261
|
+
const ledger = projectLedger(projectDir, runId)
|
|
262
|
+
const open = ledger.criteria.filter((c) => c.milestone_id === milestoneId && c.status === 'open')
|
|
263
|
+
const criterionIds = []
|
|
264
|
+
for (const c of open) {
|
|
265
|
+
appendProduceEvent(projectDir, 'produce.criteria.closed', {
|
|
266
|
+
run_id: runId,
|
|
267
|
+
milestone_id: milestoneId,
|
|
268
|
+
criterion_id: c.criterion_id,
|
|
269
|
+
})
|
|
270
|
+
criterionIds.push(c.criterion_id)
|
|
271
|
+
}
|
|
272
|
+
return { closed_count: criterionIds.length, criterion_ids: criterionIds }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Mint a criterion id: `<slug-of-text>-<rand>`. Reuses `slugFromOutcome` and a
|
|
277
|
+
* short random suffix so two criteria minted from the same text never collide.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} text
|
|
280
|
+
* @returns {string}
|
|
281
|
+
*/
|
|
282
|
+
export function mintCriterionId(text) {
|
|
283
|
+
return `${slugFromOutcome(text)}-${Math.random().toString(36).slice(2, 8)}`
|
|
284
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* produce/probe-family.mjs — resolve a probe/oracle provider family DISJOINT
|
|
4
|
+
* from the implementer's, reusing the apt-spar cross-family mechanism (M2,
|
|
5
|
+
* ID-M2-01).
|
|
6
|
+
*
|
|
7
|
+
* The framework already selects + dispatches a different provider family in
|
|
8
|
+
* `apt:spar`: `host-detect` returns the current family, a partner-map picks a
|
|
9
|
+
* different one, and `spar-resolve-partner` resolves the partner's model LIVE
|
|
10
|
+
* from the user's own CLI config with ZERO baked default. M2 does NOT invent a
|
|
11
|
+
* second cross-family path — this module consumes the SAME `resolveCodexSparModel`
|
|
12
|
+
* primitive + the same partner-map shape and adds exactly ONE new rule: the
|
|
13
|
+
* resolved probe/oracle family must be DISJOINT from the *implementer* family
|
|
14
|
+
* (not merely "≠ host").
|
|
15
|
+
*
|
|
16
|
+
* Provider is an OUTPUT of selection; Aperant NEVER holds a model id (CLAUDE.md
|
|
17
|
+
* resolver rule / ID-M2-07). For codex the live model comes from
|
|
18
|
+
* `~/.codex/config.toml`; claude / gemini self-resolve (`model: null`). A
|
|
19
|
+
* setup where no disjoint family exists (host = implementer = only CLI) returns
|
|
20
|
+
* `available: false` fail-closed — a single-family setup cannot satisfy the
|
|
21
|
+
* cross-family rule by design.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
25
|
+
import { homedir } from 'node:os'
|
|
26
|
+
import { resolveCodexSparModel } from '../host/codex-config.mjs'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The provider families M2 selects across — the same three apt:spar supports.
|
|
30
|
+
* Ordered for a deterministic tiebreak when the partner-map default is not in
|
|
31
|
+
* the available pool.
|
|
32
|
+
*/
|
|
33
|
+
export const KNOWN_FAMILIES = ['claude', 'codex', 'gemini']
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The apt-spar §3 partner-map shape: each family's natural cross-family partner.
|
|
37
|
+
* Consulted as a PREFERENCE only — the disjoint + available + non-host rules
|
|
38
|
+
* decide the final pick; this just orders ties the way spar does.
|
|
39
|
+
*/
|
|
40
|
+
const PARTNER_MAP = {
|
|
41
|
+
claude: 'codex',
|
|
42
|
+
codex: 'claude',
|
|
43
|
+
gemini: 'codex',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize a provider/CLI family alias to one of KNOWN_FAMILIES. Returns null
|
|
48
|
+
* for an unrecognized family so it is excluded from selection (fail-closed).
|
|
49
|
+
*
|
|
50
|
+
* @param {unknown} family
|
|
51
|
+
* @returns {string | null}
|
|
52
|
+
*/
|
|
53
|
+
export function normalizeFamily(family) {
|
|
54
|
+
if (typeof family !== 'string') return null
|
|
55
|
+
const f = family.trim().toLowerCase()
|
|
56
|
+
if (f === 'claude' || f === 'claude-code' || f === 'anthropic') return 'claude'
|
|
57
|
+
if (f === 'codex' || f === 'openai' || f === 'gpt') return 'codex'
|
|
58
|
+
if (f === 'gemini' || f === 'google') return 'gemini'
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pick the probe/oracle family: disjoint from the implementer (the ONE new
|
|
64
|
+
* rule), preferring a family that is ALSO not the host (a genuine third party),
|
|
65
|
+
* then honoring the apt-spar partner-map default, then a deterministic order.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} implementerFamily — normalized implementer family.
|
|
68
|
+
* @param {string | null} hostFamily — normalized host family (or null).
|
|
69
|
+
* @param {string[]} normalizedAvailable — normalized available families.
|
|
70
|
+
* @returns {{ family: string | null, reason: string | null }}
|
|
71
|
+
*/
|
|
72
|
+
function pickProbeFamily(implementerFamily, hostFamily, normalizedAvailable) {
|
|
73
|
+
// The ONE new constraint over apt-spar: DISJOINT from the implementer.
|
|
74
|
+
const disjoint = normalizedAvailable.filter((f) => f !== implementerFamily)
|
|
75
|
+
if (disjoint.length === 0) return { family: null, reason: 'no_disjoint_family' }
|
|
76
|
+
// Prefer a disjoint family that is ALSO not the host — a genuine third-party
|
|
77
|
+
// reviewer. Fall back to a disjoint-but-host family only if that is all
|
|
78
|
+
// that's available (still satisfies the cross-family rule, which is about the
|
|
79
|
+
// implementer, not the host).
|
|
80
|
+
const nonHost = disjoint.filter((f) => f !== hostFamily)
|
|
81
|
+
const pool = nonHost.length > 0 ? nonHost : disjoint
|
|
82
|
+
// Honor the partner-map default when it is in the pool (apt-spar §3 shape).
|
|
83
|
+
const preferred = PARTNER_MAP[implementerFamily]
|
|
84
|
+
if (preferred && pool.includes(preferred)) return { family: preferred, reason: null }
|
|
85
|
+
// Deterministic tiebreak: first in KNOWN_FAMILIES order present in the pool.
|
|
86
|
+
const ordered = KNOWN_FAMILIES.filter((f) => pool.includes(f))
|
|
87
|
+
return { family: ordered[0] ?? pool[0], reason: null }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the probe/oracle family + LIVE model for a produce run.
|
|
92
|
+
*
|
|
93
|
+
* @param {{
|
|
94
|
+
* implementerFamily: string,
|
|
95
|
+
* hostFamily?: string | null,
|
|
96
|
+
* availableFamilies?: string[],
|
|
97
|
+
* codexDeps?: { home: string, existsSync: (p: string) => boolean, readFileSync: (p: string, enc: string) => string },
|
|
98
|
+
* }} args
|
|
99
|
+
* @returns {{
|
|
100
|
+
* available: boolean,
|
|
101
|
+
* probe_family: string | null,
|
|
102
|
+
* transport_hint: { prefer_self_resolving: boolean, recommended_walk: string[] } | null,
|
|
103
|
+
* model: string | null,
|
|
104
|
+
* source: string | null,
|
|
105
|
+
* reason: string | null,
|
|
106
|
+
* }}
|
|
107
|
+
*/
|
|
108
|
+
export function resolveProbeFamily({
|
|
109
|
+
implementerFamily,
|
|
110
|
+
hostFamily = null,
|
|
111
|
+
availableFamilies = KNOWN_FAMILIES,
|
|
112
|
+
codexDeps,
|
|
113
|
+
} = {}) {
|
|
114
|
+
const implementer = normalizeFamily(implementerFamily)
|
|
115
|
+
if (!implementer) {
|
|
116
|
+
return {
|
|
117
|
+
available: false,
|
|
118
|
+
probe_family: null,
|
|
119
|
+
transport_hint: null,
|
|
120
|
+
model: null,
|
|
121
|
+
source: null,
|
|
122
|
+
reason: 'unknown_implementer_family',
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const host = normalizeFamily(hostFamily)
|
|
127
|
+
const normalizedAvailable = [...new Set(availableFamilies.map(normalizeFamily).filter(Boolean))]
|
|
128
|
+
|
|
129
|
+
const { family, reason } = pickProbeFamily(implementer, host, normalizedAvailable)
|
|
130
|
+
if (!family) {
|
|
131
|
+
return {
|
|
132
|
+
available: false,
|
|
133
|
+
probe_family: null,
|
|
134
|
+
transport_hint: null,
|
|
135
|
+
model: null,
|
|
136
|
+
source: null,
|
|
137
|
+
reason: reason ?? 'no_disjoint_family',
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Resolve the model LIVE via the reused spar primitive (provider-is-output,
|
|
142
|
+
// zero baked id). codex reads ~/.codex/config.toml; claude/gemini self-resolve.
|
|
143
|
+
if (family === 'codex') {
|
|
144
|
+
const deps = codexDeps ?? { home: homedir(), existsSync, readFileSync }
|
|
145
|
+
const res = resolveCodexSparModel(deps)
|
|
146
|
+
return {
|
|
147
|
+
available: true,
|
|
148
|
+
probe_family: 'codex',
|
|
149
|
+
transport_hint: { prefer_self_resolving: true, recommended_walk: ['plugin', 'cli', 'mcp'] },
|
|
150
|
+
model: res.model,
|
|
151
|
+
source: res.source,
|
|
152
|
+
// A codex model that fails to resolve from config does NOT make the
|
|
153
|
+
// probe family unavailable — the self-resolving plugin/CLI rungs still
|
|
154
|
+
// run without a named model. Surface the model-resolution reason.
|
|
155
|
+
reason: res.reason,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// claude / gemini MCP rungs take only {prompt} — no model, no staleness vector.
|
|
160
|
+
return {
|
|
161
|
+
available: true,
|
|
162
|
+
probe_family: family,
|
|
163
|
+
transport_hint: { prefer_self_resolving: false, recommended_walk: ['mcp', 'plugin', 'cli'] },
|
|
164
|
+
model: null,
|
|
165
|
+
source: null,
|
|
166
|
+
reason: 'self_resolving',
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* produce/projection.mjs — event-sourced roadmap projection (M1, ID-01).
|
|
4
|
+
*
|
|
5
|
+
* The roadmap is DERIVED: `projectRoadmap` folds the run's `produce.roadmap.*`
|
|
6
|
+
* events into `{ run_id, milestones:[{id,title,status}], updated_at }` on every
|
|
7
|
+
* call. It is NEVER read as the source of truth — a persisted `roadmap.json`
|
|
8
|
+
* cache MAY be written for convenience but is always regenerable from the log,
|
|
9
|
+
* so the shared-mutable-blob lost-update bug is structurally impossible.
|
|
10
|
+
*
|
|
11
|
+
* `writeProjectionCache` is a SINGLE-WRITER-PER-RECORD write under withFileLock
|
|
12
|
+
* (a torn cache is recoverable by re-folding). The event log is the authority;
|
|
13
|
+
* the cache is disposable.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import { withFileLock } from '../util/fs-lock.mjs'
|
|
18
|
+
import { atomicWriteJson } from '../util/io.mjs'
|
|
19
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
20
|
+
import { readProduceEventsForRun } from './events.mjs'
|
|
21
|
+
import { resolveRunDir } from './run-id.mjs'
|
|
22
|
+
|
|
23
|
+
/** M1 milestone lifecycle — `done` is external-oracle-gated (M2, ID-03). */
|
|
24
|
+
const SEED_STATUS = 'planned'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Recompute the roadmap projection for a run by folding its append-only
|
|
28
|
+
* `produce.roadmap.*` events. `milestone_added` seeds a planned milestone (in
|
|
29
|
+
* first-seen order); `milestone_status_changed` advances its status. Idempotent
|
|
30
|
+
* re-adds keep the first seed (never clobber an advanced status). `updated_at`
|
|
31
|
+
* is derived from the last folded event's timestamp so the projection is
|
|
32
|
+
* DETERMINISTIC — two folds of the same log yield the identical roadmap.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} projectDir
|
|
35
|
+
* @param {string} runId
|
|
36
|
+
* @returns {{ run_id: string, milestones: Array<{ id: string, title: string, status: string }>, updated_at: string | null }}
|
|
37
|
+
*/
|
|
38
|
+
export function projectRoadmap(projectDir, runId) {
|
|
39
|
+
const events = readProduceEventsForRun(projectDir, runId)
|
|
40
|
+
const byId = new Map()
|
|
41
|
+
const order = []
|
|
42
|
+
let updatedAt = null
|
|
43
|
+
|
|
44
|
+
for (const e of events) {
|
|
45
|
+
const d = e.data || {}
|
|
46
|
+
const id = typeof d.milestone_id === 'string' ? d.milestone_id : null
|
|
47
|
+
if (e.op === 'produce.roadmap.milestone_added') {
|
|
48
|
+
if (id && !byId.has(id)) {
|
|
49
|
+
byId.set(id, { id, title: typeof d.title === 'string' ? d.title : id, status: SEED_STATUS })
|
|
50
|
+
order.push(id)
|
|
51
|
+
}
|
|
52
|
+
} else if (e.op === 'produce.roadmap.milestone_status_changed') {
|
|
53
|
+
if (id && byId.has(id) && typeof d.status === 'string') {
|
|
54
|
+
byId.get(id).status = d.status
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (typeof e.t === 'string') updatedAt = e.t
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
run_id: runId,
|
|
62
|
+
milestones: order.map((id) => byId.get(id)),
|
|
63
|
+
updated_at: updatedAt,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The derived roadmap.json cache path for a run.
|
|
69
|
+
* @param {string} projectDir
|
|
70
|
+
* @param {string} runId
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function roadmapCachePath(projectDir, runId) {
|
|
74
|
+
return join(resolveRunDir(projectDir, runId), 'roadmap.json')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Refresh the derived roadmap.json cache single-writer under withFileLock, and
|
|
79
|
+
* return the freshly-folded roadmap. Convenience only — the log is the
|
|
80
|
+
* authority; a corrupt cache is repaired by re-folding (ID-01).
|
|
81
|
+
*
|
|
82
|
+
* The fold happens INSIDE the lock (not precomputed then written): the write is
|
|
83
|
+
* a full idempotent regenerate from the authoritative log, never a
|
|
84
|
+
* read-modify-write of a shared blob. This guarantees the last writer to
|
|
85
|
+
* acquire the lock persists the COMPLETE set — a precompute-then-write pattern
|
|
86
|
+
* could persist a STALE fold if a concurrent O_APPEND landed between the
|
|
87
|
+
* precompute and the write, silently dropping that milestone from the cache
|
|
88
|
+
* (parent TD-06 / subtask 4). The event log stays lossless regardless.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} projectDir
|
|
91
|
+
* @param {string} runId
|
|
92
|
+
* @returns {{ run_id: string, milestones: Array<{ id: string, title: string, status: string }>, updated_at: string | null }}
|
|
93
|
+
*/
|
|
94
|
+
export function writeProjectionCache(projectDir, runId) {
|
|
95
|
+
const path = roadmapCachePath(projectDir, runId)
|
|
96
|
+
return withFileLock(
|
|
97
|
+
path,
|
|
98
|
+
() => {
|
|
99
|
+
const roadmap = projectRoadmap(projectDir, runId)
|
|
100
|
+
atomicWriteJson(path, roadmap)
|
|
101
|
+
return roadmap
|
|
102
|
+
},
|
|
103
|
+
{ projectRoot: resolveMainRepoRoot(projectDir) },
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* produce/run-id.mjs — mint + resolve produce run identifiers and run dirs (M1).
|
|
4
|
+
*
|
|
5
|
+
* A produce run is scoped to one frozen acceptance setpoint. Its run-id is a
|
|
6
|
+
* human-readable slug derived from the outcome plus a short timestamp and a
|
|
7
|
+
* random suffix (so two `start`s in the same second never collide). The run
|
|
8
|
+
* dir lives under the SHARED main-repo root at `.aperant/produce/<run-id>/`
|
|
9
|
+
* (resolveMainRepoRoot) so every worktree agent reads/writes one board
|
|
10
|
+
* (parent AC8 path / ID-06).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Slugify an outcome string into a short kebab identifier. Strips punctuation,
|
|
18
|
+
* lowercases, keeps the first `maxWords` words. Falls back to 'run' when empty.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} outcome
|
|
21
|
+
* @param {number} [maxWords=6]
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function slugFromOutcome(outcome, maxWords = 6) {
|
|
25
|
+
const words = String(outcome || '')
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
28
|
+
.trim()
|
|
29
|
+
.split(/\s+/)
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.slice(0, maxWords)
|
|
32
|
+
const slug = words.join('-').replace(/-+/g, '-')
|
|
33
|
+
return slug || 'run'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mint a fresh run-id: `<slug>-<YYYYMMDD-HHMMSS>-<rand>`. The random suffix
|
|
38
|
+
* guarantees uniqueness even for identical outcomes minted in the same second.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} outcome
|
|
41
|
+
* @param {Date} [now]
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function mintRunId(outcome, now = new Date()) {
|
|
45
|
+
const stamp = now
|
|
46
|
+
.toISOString()
|
|
47
|
+
.replace(/\.\d+Z$/, '')
|
|
48
|
+
.replace(/[-:]/g, '')
|
|
49
|
+
.replace('T', '-')
|
|
50
|
+
const rand = Math.random().toString(36).slice(2, 6)
|
|
51
|
+
return `${slugFromOutcome(outcome)}-${stamp}-${rand}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The `.aperant/produce/` root, resolved to the shared main repo from any cwd.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} projectDir
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function resolveProduceRoot(projectDir) {
|
|
61
|
+
return join(resolveMainRepoRoot(projectDir), '.aperant', 'produce')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The run dir `.aperant/produce/<run-id>/`, resolved to the shared main repo.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} projectDir
|
|
68
|
+
* @param {string} runId
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
export function resolveRunDir(projectDir, runId) {
|
|
72
|
+
return join(resolveProduceRoot(projectDir), runId)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Repo-relative setpoint path for a run — used as the `setpoint_path` event
|
|
77
|
+
* field. Forward-slash separated for portability across the event log.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} runId
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
export function relativeSetpointPath(runId) {
|
|
83
|
+
return ['.aperant', 'produce', runId, 'setpoint.json'].join('/')
|
|
84
|
+
}
|