@aperant/framework 0.7.3 → 0.8.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 +66 -0
- package/agents/apt-executor.md +12 -0
- package/agents/apt-team-docs-narrator.md +11 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts +17 -4
- package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -1
- package/dist/cli/ci-watch/stop-matrix.mjs +41 -4
- package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -1
- package/dist/cli/commands/fan-out.d.mts +2 -0
- package/dist/cli/commands/fan-out.d.mts.map +1 -0
- package/dist/cli/commands/fan-out.mjs +123 -0
- package/dist/cli/commands/fan-out.mjs.map +1 -0
- package/dist/cli/commands/modes.d.mts +22 -0
- package/dist/cli/commands/modes.d.mts.map +1 -1
- package/dist/cli/commands/modes.mjs +4 -1
- package/dist/cli/commands/modes.mjs.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.mjs +86 -6
- package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
- package/dist/cli/commands/roadmap.d.mts +1 -1
- package/dist/cli/commands/roadmap.d.mts.map +1 -1
- package/dist/cli/commands/roadmap.mjs +47 -4
- package/dist/cli/commands/roadmap.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +15 -0
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/validate-evidence.d.mts +1 -1
- package/dist/cli/commands/validate-evidence.d.mts.map +1 -1
- package/dist/cli/commands/validate-evidence.mjs +72 -47
- package/dist/cli/commands/validate-evidence.mjs.map +1 -1
- package/dist/cli/commands/wave.d.mts.map +1 -1
- package/dist/cli/commands/wave.mjs +91 -61
- package/dist/cli/commands/wave.mjs.map +1 -1
- package/dist/cli/commands/worktree.mjs +62 -3
- package/dist/cli/commands/worktree.mjs.map +1 -1
- package/dist/cli/config/load.mjs +1 -1
- package/dist/cli/config/load.mjs.map +1 -1
- package/dist/cli/config/parallelization.d.mts +14 -0
- package/dist/cli/config/parallelization.d.mts.map +1 -0
- package/dist/cli/config/parallelization.mjs +19 -0
- package/dist/cli/config/parallelization.mjs.map +1 -0
- package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
- package/dist/cli/coordination/agent-identity.mjs +7 -1
- package/dist/cli/coordination/agent-identity.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/roadmap/convex-mirror.d.mts +21 -0
- package/dist/cli/roadmap/convex-mirror.d.mts.map +1 -0
- package/dist/cli/roadmap/convex-mirror.mjs +133 -0
- package/dist/cli/roadmap/convex-mirror.mjs.map +1 -0
- package/dist/cli/roadmap/lifecycle.d.mts +1 -1
- package/dist/cli/roadmap/lifecycle.d.mts.map +1 -1
- package/dist/cli/roadmap/lifecycle.mjs +54 -0
- package/dist/cli/roadmap/lifecycle.mjs.map +1 -1
- package/dist/cli/wfrun/unit-shape.d.mts +27 -0
- package/dist/cli/wfrun/unit-shape.d.mts.map +1 -0
- package/dist/cli/wfrun/unit-shape.mjs +38 -0
- package/dist/cli/wfrun/unit-shape.mjs.map +1 -0
- package/package.json +1 -1
- package/skills/apt-execute/SKILL.md +13 -0
- package/skills/apt-fan-out/SKILL.md +124 -0
- package/skills/apt-pr-review/SKILL.md +8 -1
- package/skills/apt-review/SKILL.md +4 -0
- package/skills/apt-run/SKILL.md +20 -5
- package/skills/apt-spar/SKILL.md +12 -9
- package/skills/apt-verify/SKILL.md +11 -0
- package/skills/apt-watch-ci/SKILL.md +2 -2
- package/src/cli/ci-watch/stop-matrix.mjs +44 -4
- package/src/cli/commands/fan-out.mjs +129 -0
- package/src/cli/commands/modes.mjs +4 -1
- package/src/cli/commands/pr-review-audit-fixer.mjs +82 -6
- package/src/cli/commands/roadmap.mjs +48 -4
- package/src/cli/commands/task.mjs +15 -0
- package/src/cli/commands/validate-evidence.mjs +75 -50
- package/src/cli/commands/wave.mjs +91 -58
- package/src/cli/commands/worktree.mjs +56 -3
- package/src/cli/config/load.mjs +1 -1
- package/src/cli/config/parallelization.mjs +18 -0
- package/src/cli/coordination/agent-identity.mjs +9 -1
- package/src/cli/dispatch.mjs +2 -0
- package/src/cli/roadmap/convex-mirror.mjs +131 -0
- package/src/cli/roadmap/lifecycle.mjs +48 -0
- package/src/cli/wfrun/unit-shape.mjs +39 -0
- package/templates/config.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 5 ST14 / AC10 / ID-06 — `apt-tools roadmap convex-mirror`.
|
|
3
|
+
*
|
|
4
|
+
* Helper that POSTs the rendered narrator prose to the Next.js
|
|
5
|
+
* /api/roadmap/narrator-mirror endpoint. Markdown is canonical
|
|
6
|
+
* (ADR-0006); this call is the Convex mirror.
|
|
7
|
+
*
|
|
8
|
+
* Fail-soft contract:
|
|
9
|
+
* - 200 {mirrored:true} → exit 0, status='ok'
|
|
10
|
+
* - 200 {mirrored:false,queued:true} → exit 0, status='queued'
|
|
11
|
+
* - 401 → exit 1, status='error' (auth misconfig)
|
|
12
|
+
* - 4xx (other than 401) → exit 1, status='error'
|
|
13
|
+
* - 5xx / network error → exit 0, status='queued', appends a
|
|
14
|
+
* pending JSONL row to
|
|
15
|
+
* .aperant/digests/narrator-mirror-pending.jsonl
|
|
16
|
+
* so a follow-up narrator invocation
|
|
17
|
+
* can replay locally if the cron
|
|
18
|
+
* drainer hasn't caught up.
|
|
19
|
+
*
|
|
20
|
+
* Token resolution order (per ID-07):
|
|
21
|
+
* 1. process.env.MCP_NARRATOR_TOKEN
|
|
22
|
+
* 2. .aperant/team.json :: narratorMirrorToken
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
26
|
+
import { dirname, join } from 'node:path'
|
|
27
|
+
|
|
28
|
+
function resolveToken(projectDir) {
|
|
29
|
+
const envToken = process.env.MCP_NARRATOR_TOKEN
|
|
30
|
+
if (envToken && envToken.length > 0) return envToken
|
|
31
|
+
const teamJsonPath = join(projectDir, '.aperant', 'team.json')
|
|
32
|
+
if (!existsSync(teamJsonPath)) return null
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(readFileSync(teamJsonPath, 'utf-8'))
|
|
35
|
+
if (typeof parsed.narratorMirrorToken === 'string' && parsed.narratorMirrorToken.length > 0) {
|
|
36
|
+
return parsed.narratorMirrorToken
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Bad JSON — fall through to null
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function appendPendingJsonl(projectDir, row) {
|
|
45
|
+
const path = join(projectDir, '.aperant', 'digests', 'narrator-mirror-pending.jsonl')
|
|
46
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
47
|
+
appendFileSync(path, `${JSON.stringify(row)}\n`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run the convex-mirror call.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} projectDir
|
|
54
|
+
* @param {{scope: string, phaseId: string, anchorId: string, proseBody: string, mergedPr?: number, baseUrl?: string, fetchImpl?: typeof fetch}} args
|
|
55
|
+
* @returns {Promise<{status: 'ok'|'queued'|'error', code?: number, message?: string}>}
|
|
56
|
+
*/
|
|
57
|
+
export async function runConvexMirror(projectDir, args) {
|
|
58
|
+
const { scope, phaseId, anchorId, proseBody, mergedPr, baseUrl, fetchImpl } = args
|
|
59
|
+
if (!scope || !phaseId || !anchorId || typeof proseBody !== 'string') {
|
|
60
|
+
return { status: 'error', message: 'missing required: scope, phaseId, anchorId, proseBody' }
|
|
61
|
+
}
|
|
62
|
+
const token = resolveToken(projectDir)
|
|
63
|
+
if (!token) {
|
|
64
|
+
return {
|
|
65
|
+
status: 'error',
|
|
66
|
+
message:
|
|
67
|
+
'No narrator token (set MCP_NARRATOR_TOKEN or .aperant/team.json::narratorMirrorToken)',
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const url = `${(baseUrl || process.env.APERANT_BASE_URL || 'https://app.aperant.com').replace(/\/$/, '')}/api/roadmap/narrator-mirror`
|
|
71
|
+
const body = {
|
|
72
|
+
scope,
|
|
73
|
+
phaseId,
|
|
74
|
+
narratorAnchorId: anchorId,
|
|
75
|
+
narratorProse: proseBody,
|
|
76
|
+
mergedPr,
|
|
77
|
+
}
|
|
78
|
+
const f = fetchImpl ?? fetch
|
|
79
|
+
let res
|
|
80
|
+
try {
|
|
81
|
+
res = await f(url, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {
|
|
84
|
+
Authorization: `Bearer ${token}`,
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
})
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Network failure → fail-soft queue.
|
|
91
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
92
|
+
appendPendingJsonl(projectDir, {
|
|
93
|
+
...body,
|
|
94
|
+
lastError: `network: ${msg}`,
|
|
95
|
+
ts: new Date().toISOString(),
|
|
96
|
+
})
|
|
97
|
+
return { status: 'queued', message: `network error: ${msg}` }
|
|
98
|
+
}
|
|
99
|
+
if (res.status === 401) {
|
|
100
|
+
return { status: 'error', code: 401, message: 'auth: invalid or missing token' }
|
|
101
|
+
}
|
|
102
|
+
if (res.status >= 500) {
|
|
103
|
+
// 5xx → fail-soft queue.
|
|
104
|
+
appendPendingJsonl(projectDir, {
|
|
105
|
+
...body,
|
|
106
|
+
lastError: `http ${res.status}`,
|
|
107
|
+
ts: new Date().toISOString(),
|
|
108
|
+
})
|
|
109
|
+
return { status: 'queued', code: res.status, message: 'server error — enqueued' }
|
|
110
|
+
}
|
|
111
|
+
if (res.status >= 400) {
|
|
112
|
+
let detail = ''
|
|
113
|
+
try {
|
|
114
|
+
detail = await res.text()
|
|
115
|
+
} catch {
|
|
116
|
+
// ignore
|
|
117
|
+
}
|
|
118
|
+
return { status: 'error', code: res.status, message: detail || `http ${res.status}` }
|
|
119
|
+
}
|
|
120
|
+
// 200/2xx
|
|
121
|
+
let parsed
|
|
122
|
+
try {
|
|
123
|
+
parsed = await res.json()
|
|
124
|
+
} catch {
|
|
125
|
+
parsed = { mirrored: true, queued: false }
|
|
126
|
+
}
|
|
127
|
+
if (parsed?.queued) {
|
|
128
|
+
return { status: 'queued', code: res.status, message: 'enqueued server-side' }
|
|
129
|
+
}
|
|
130
|
+
return { status: 'ok', code: res.status }
|
|
131
|
+
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lifecycle.mjs — shared lifecycle-phase side-effect helper (C29).
|
|
3
|
+
*
|
|
4
|
+
* FRAMEWORK-BUG-033 defect 1/5: when `spec.to === 'completed'`, the helper
|
|
5
|
+
* enumerates sibling tasks in the same scope+milestone+phase and aborts
|
|
6
|
+
* the flip (returns null — idempotent no-op shape) when any sibling
|
|
7
|
+
* remains open (lifecycle_phase !== 'closed'). The guard lives inside the
|
|
8
|
+
* helper so every caller (`task update --lifecycle-phase`, `task close
|
|
9
|
+
* --verdict approved`, the drain wrapper, and `task close-merged`
|
|
10
|
+
* transitively via its `task close` child) inherits it.
|
|
3
11
|
*/
|
|
4
12
|
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
5
15
|
import { loadRoadmapForScope, saveRoadmapForScope } from './io.mjs'
|
|
6
16
|
import { applyRollup } from './rollup.mjs'
|
|
7
17
|
import { syncRoadmapInProcess } from './sync.mjs'
|
|
@@ -53,6 +63,44 @@ export function applyLifecyclePhaseSideEffect(targetDir, task, spec, reasonPrefi
|
|
|
53
63
|
// From-state guard: idempotent no-op when phase is not at `spec.from`.
|
|
54
64
|
if (phase.status !== spec.from) return null
|
|
55
65
|
|
|
66
|
+
// FRAMEWORK-BUG-033 defect 1/5 — sibling-count guard. Only gates the
|
|
67
|
+
// `→ completed` direction (other transitions like `→ in-progress` are
|
|
68
|
+
// unaffected per ID-02 / AC7). Enumerates state.active_tasks filtered by
|
|
69
|
+
// scope+milestone+phase. Any open sibling (lifecycle_phase !== 'closed'
|
|
70
|
+
// and id !== task.id) blocks the flip with the same idempotent no-op
|
|
71
|
+
// shape as the from-state guard above.
|
|
72
|
+
//
|
|
73
|
+
// State source: caller may pass `opts.stateBlob` when already holding
|
|
74
|
+
// `withFileLock(state.json)` to avoid a re-read race; otherwise we read
|
|
75
|
+
// state.json from disk once. On the `task close` path the task has
|
|
76
|
+
// already been deleted from state.active_tasks before this helper runs,
|
|
77
|
+
// so the `t.id !== task.id` filter is a defensive no-op there.
|
|
78
|
+
if (spec.to === 'completed') {
|
|
79
|
+
let stateBlob = opts.stateBlob
|
|
80
|
+
if (!stateBlob) {
|
|
81
|
+
const statePath = join(targetDir, '.aperant', 'state.json')
|
|
82
|
+
if (existsSync(statePath)) {
|
|
83
|
+
try {
|
|
84
|
+
stateBlob = JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
85
|
+
} catch {
|
|
86
|
+
// Fail-secure: cannot verify sibling state → assume siblings might exist → abort the flip.
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const activeTasks = stateBlob?.active_tasks || {}
|
|
92
|
+
for (const sibling of Object.values(activeTasks)) {
|
|
93
|
+
if (!sibling || typeof sibling !== 'object') continue
|
|
94
|
+
if (sibling.scope !== scope) continue
|
|
95
|
+
if (sibling.milestone_id !== task.milestone_id) continue
|
|
96
|
+
if (sibling.phase_id !== task.phase_id) continue
|
|
97
|
+
if (sibling.id === task.id) continue
|
|
98
|
+
if (sibling.lifecycle_phase === 'closed') continue
|
|
99
|
+
// Open sibling — abort the flip.
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
56
104
|
const transition = {
|
|
57
105
|
op: reasonPrefix,
|
|
58
106
|
scope,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wfrun/unit-shape.mjs — FRAMEWORK-BUG-033 capability gap (ID-03).
|
|
3
|
+
*
|
|
4
|
+
* Normalize a wave-engine plan blob to the unit-agnostic
|
|
5
|
+
* `{units, unit_kind}` shape. Two input forms accepted:
|
|
6
|
+
* - Subtask-shaped (back-compat): `{subtasks: [...]}`.
|
|
7
|
+
* - Task-shaped (new): `{unit_kind: "task", tasks: [...]}`.
|
|
8
|
+
*
|
|
9
|
+
* Internally the wave engine operates purely on `{id, dependencies,
|
|
10
|
+
* files}` records — only the I/O boundary cares about the rename.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Detect whether a plan blob is task-shaped, subtask-shaped, or invalid.
|
|
15
|
+
*
|
|
16
|
+
* @param {unknown} plan
|
|
17
|
+
* @returns {{ units: Array<{id: string, dependencies: string[], files?: string[]}>, unit_kind: 'subtask' | 'task', error: string | null }}
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeToUnits(plan) {
|
|
20
|
+
if (!plan || typeof plan !== 'object') {
|
|
21
|
+
return { units: [], unit_kind: 'subtask', error: 'Plan must be an object' }
|
|
22
|
+
}
|
|
23
|
+
const isTaskShape =
|
|
24
|
+
plan.unit_kind === 'task' || (Array.isArray(plan.tasks) && !Array.isArray(plan.subtasks))
|
|
25
|
+
if (isTaskShape) {
|
|
26
|
+
if (!Array.isArray(plan.tasks)) {
|
|
27
|
+
return { units: [], unit_kind: 'task', error: 'Plan must contain a "tasks" array' }
|
|
28
|
+
}
|
|
29
|
+
return { units: plan.tasks, unit_kind: 'task', error: null }
|
|
30
|
+
}
|
|
31
|
+
if (!Array.isArray(plan.subtasks)) {
|
|
32
|
+
return {
|
|
33
|
+
units: [],
|
|
34
|
+
unit_kind: 'subtask',
|
|
35
|
+
error: 'Plan must contain a "subtasks" array',
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { units: plan.subtasks, unit_kind: 'subtask', error: null }
|
|
39
|
+
}
|