@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/agents/apt-executor.md +12 -0
  3. package/agents/apt-team-docs-narrator.md +11 -0
  4. package/dist/cli/ci-watch/stop-matrix.d.mts +17 -4
  5. package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -1
  6. package/dist/cli/ci-watch/stop-matrix.mjs +41 -4
  7. package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -1
  8. package/dist/cli/commands/fan-out.d.mts +2 -0
  9. package/dist/cli/commands/fan-out.d.mts.map +1 -0
  10. package/dist/cli/commands/fan-out.mjs +123 -0
  11. package/dist/cli/commands/fan-out.mjs.map +1 -0
  12. package/dist/cli/commands/modes.d.mts +22 -0
  13. package/dist/cli/commands/modes.d.mts.map +1 -1
  14. package/dist/cli/commands/modes.mjs +4 -1
  15. package/dist/cli/commands/modes.mjs.map +1 -1
  16. package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
  17. package/dist/cli/commands/pr-review-audit-fixer.mjs +86 -6
  18. package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
  19. package/dist/cli/commands/roadmap.d.mts +1 -1
  20. package/dist/cli/commands/roadmap.d.mts.map +1 -1
  21. package/dist/cli/commands/roadmap.mjs +47 -4
  22. package/dist/cli/commands/roadmap.mjs.map +1 -1
  23. package/dist/cli/commands/task.d.mts.map +1 -1
  24. package/dist/cli/commands/task.mjs +15 -0
  25. package/dist/cli/commands/task.mjs.map +1 -1
  26. package/dist/cli/commands/validate-evidence.d.mts +1 -1
  27. package/dist/cli/commands/validate-evidence.d.mts.map +1 -1
  28. package/dist/cli/commands/validate-evidence.mjs +72 -47
  29. package/dist/cli/commands/validate-evidence.mjs.map +1 -1
  30. package/dist/cli/commands/wave.d.mts.map +1 -1
  31. package/dist/cli/commands/wave.mjs +91 -61
  32. package/dist/cli/commands/wave.mjs.map +1 -1
  33. package/dist/cli/commands/worktree.mjs +62 -3
  34. package/dist/cli/commands/worktree.mjs.map +1 -1
  35. package/dist/cli/config/load.mjs +1 -1
  36. package/dist/cli/config/load.mjs.map +1 -1
  37. package/dist/cli/config/parallelization.d.mts +14 -0
  38. package/dist/cli/config/parallelization.d.mts.map +1 -0
  39. package/dist/cli/config/parallelization.mjs +19 -0
  40. package/dist/cli/config/parallelization.mjs.map +1 -0
  41. package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
  42. package/dist/cli/coordination/agent-identity.mjs +7 -1
  43. package/dist/cli/coordination/agent-identity.mjs.map +1 -1
  44. package/dist/cli/dispatch.d.mts.map +1 -1
  45. package/dist/cli/dispatch.mjs +2 -0
  46. package/dist/cli/dispatch.mjs.map +1 -1
  47. package/dist/cli/roadmap/convex-mirror.d.mts +21 -0
  48. package/dist/cli/roadmap/convex-mirror.d.mts.map +1 -0
  49. package/dist/cli/roadmap/convex-mirror.mjs +133 -0
  50. package/dist/cli/roadmap/convex-mirror.mjs.map +1 -0
  51. package/dist/cli/roadmap/lifecycle.d.mts +1 -1
  52. package/dist/cli/roadmap/lifecycle.d.mts.map +1 -1
  53. package/dist/cli/roadmap/lifecycle.mjs +54 -0
  54. package/dist/cli/roadmap/lifecycle.mjs.map +1 -1
  55. package/dist/cli/wfrun/unit-shape.d.mts +27 -0
  56. package/dist/cli/wfrun/unit-shape.d.mts.map +1 -0
  57. package/dist/cli/wfrun/unit-shape.mjs +38 -0
  58. package/dist/cli/wfrun/unit-shape.mjs.map +1 -0
  59. package/package.json +1 -1
  60. package/skills/apt-execute/SKILL.md +13 -0
  61. package/skills/apt-fan-out/SKILL.md +124 -0
  62. package/skills/apt-pr-review/SKILL.md +8 -1
  63. package/skills/apt-review/SKILL.md +4 -0
  64. package/skills/apt-run/SKILL.md +20 -5
  65. package/skills/apt-spar/SKILL.md +12 -9
  66. package/skills/apt-verify/SKILL.md +11 -0
  67. package/skills/apt-watch-ci/SKILL.md +2 -2
  68. package/src/cli/ci-watch/stop-matrix.mjs +44 -4
  69. package/src/cli/commands/fan-out.mjs +129 -0
  70. package/src/cli/commands/modes.mjs +4 -1
  71. package/src/cli/commands/pr-review-audit-fixer.mjs +82 -6
  72. package/src/cli/commands/roadmap.mjs +48 -4
  73. package/src/cli/commands/task.mjs +15 -0
  74. package/src/cli/commands/validate-evidence.mjs +75 -50
  75. package/src/cli/commands/wave.mjs +91 -58
  76. package/src/cli/commands/worktree.mjs +56 -3
  77. package/src/cli/config/load.mjs +1 -1
  78. package/src/cli/config/parallelization.mjs +18 -0
  79. package/src/cli/coordination/agent-identity.mjs +9 -1
  80. package/src/cli/dispatch.mjs +2 -0
  81. package/src/cli/roadmap/convex-mirror.mjs +131 -0
  82. package/src/cli/roadmap/lifecycle.mjs +48 -0
  83. package/src/cli/wfrun/unit-shape.mjs +39 -0
  84. 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
+ }
@@ -85,7 +85,7 @@
85
85
  "task_isolation": {
86
86
  "worktree_per_task": false,
87
87
  "branch_prefix": "apt",
88
- "from_branch": "current",
88
+ "from_branch": "main",
89
89
  "push_on_create": true,
90
90
  "cleanup_on_close": "ask"
91
91
  },