@aperant/framework 0.6.5 → 0.6.7

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 (149) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +17 -251
  3. package/dist/cli/commands/check-version.d.mts.map +1 -1
  4. package/dist/cli/commands/check-version.mjs +76 -1
  5. package/dist/cli/commands/check-version.mjs.map +1 -1
  6. package/dist/cli/commands/ci-watch.d.mts.map +1 -1
  7. package/dist/cli/commands/ci-watch.mjs +34 -4
  8. package/dist/cli/commands/ci-watch.mjs.map +1 -1
  9. package/dist/cli/commands/commit.d.mts.map +1 -1
  10. package/dist/cli/commands/commit.mjs +25 -8
  11. package/dist/cli/commands/commit.mjs.map +1 -1
  12. package/dist/cli/commands/event.d.mts.map +1 -1
  13. package/dist/cli/commands/event.mjs +66 -0
  14. package/dist/cli/commands/event.mjs.map +1 -1
  15. package/dist/cli/commands/init.d.mts +8 -1
  16. package/dist/cli/commands/init.d.mts.map +1 -1
  17. package/dist/cli/commands/init.mjs +91 -9
  18. package/dist/cli/commands/init.mjs.map +1 -1
  19. package/dist/cli/commands/modes.d.mts.map +1 -1
  20. package/dist/cli/commands/modes.mjs +2 -1
  21. package/dist/cli/commands/modes.mjs.map +1 -1
  22. package/dist/cli/commands/release-notes.d.mts +11 -0
  23. package/dist/cli/commands/release-notes.d.mts.map +1 -0
  24. package/dist/cli/commands/release-notes.mjs +173 -0
  25. package/dist/cli/commands/release-notes.mjs.map +1 -0
  26. package/dist/cli/commands/task.d.mts.map +1 -1
  27. package/dist/cli/commands/task.mjs +285 -139
  28. package/dist/cli/commands/task.mjs.map +1 -1
  29. package/dist/cli/commands/triage.d.mts.map +1 -1
  30. package/dist/cli/commands/triage.mjs +9 -5
  31. package/dist/cli/commands/triage.mjs.map +1 -1
  32. package/dist/cli/config/load.d.mts +14 -0
  33. package/dist/cli/config/load.d.mts.map +1 -1
  34. package/dist/cli/config/load.mjs +40 -0
  35. package/dist/cli/config/load.mjs.map +1 -1
  36. package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
  37. package/dist/cli/config/upgrade-gitignore.mjs +6 -0
  38. package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
  39. package/dist/cli/consistency/parse-review.d.mts.map +1 -1
  40. package/dist/cli/consistency/parse-review.mjs +4 -1
  41. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  42. package/dist/cli/coordination/auto-emit-artifact-ready.d.mts +16 -0
  43. package/dist/cli/coordination/auto-emit-artifact-ready.d.mts.map +1 -0
  44. package/dist/cli/coordination/auto-emit-artifact-ready.mjs +88 -0
  45. package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -0
  46. package/dist/cli/coordination/event-schema.d.mts +16 -0
  47. package/dist/cli/coordination/event-schema.d.mts.map +1 -0
  48. package/dist/cli/coordination/event-schema.mjs +161 -0
  49. package/dist/cli/coordination/event-schema.mjs.map +1 -0
  50. package/dist/cli/coordination/store.d.mts +6 -0
  51. package/dist/cli/coordination/store.d.mts.map +1 -1
  52. package/dist/cli/coordination/store.mjs +14 -0
  53. package/dist/cli/coordination/store.mjs.map +1 -1
  54. package/dist/cli/dispatch.d.mts.map +1 -1
  55. package/dist/cli/dispatch.mjs +2 -0
  56. package/dist/cli/dispatch.mjs.map +1 -1
  57. package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
  58. package/dist/cli/gate/gates/review-clean.mjs +4 -2
  59. package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
  60. package/dist/cli/help.mjs +2 -2
  61. package/dist/cli/help.mjs.map +1 -1
  62. package/dist/cli/install/update-chips.d.mts +23 -0
  63. package/dist/cli/install/update-chips.d.mts.map +1 -1
  64. package/dist/cli/install/update-chips.mjs +60 -0
  65. package/dist/cli/install/update-chips.mjs.map +1 -1
  66. package/dist/cli/release-notes/compile.d.mts +38 -0
  67. package/dist/cli/release-notes/compile.d.mts.map +1 -0
  68. package/dist/cli/release-notes/compile.mjs +244 -0
  69. package/dist/cli/release-notes/compile.mjs.map +1 -0
  70. package/dist/cli/release-notes/draft.d.mts +73 -0
  71. package/dist/cli/release-notes/draft.d.mts.map +1 -0
  72. package/dist/cli/release-notes/draft.mjs +120 -0
  73. package/dist/cli/release-notes/draft.mjs.map +1 -0
  74. package/dist/cli/release-notes/output-dir.d.mts +20 -0
  75. package/dist/cli/release-notes/output-dir.d.mts.map +1 -0
  76. package/dist/cli/release-notes/output-dir.mjs +42 -0
  77. package/dist/cli/release-notes/output-dir.mjs.map +1 -0
  78. package/dist/cli/release-notes/persona-filter.d.mts +51 -0
  79. package/dist/cli/release-notes/persona-filter.d.mts.map +1 -0
  80. package/dist/cli/release-notes/persona-filter.mjs +127 -0
  81. package/dist/cli/release-notes/persona-filter.mjs.map +1 -0
  82. package/dist/cli/release-notes/publish.d.mts +23 -0
  83. package/dist/cli/release-notes/publish.d.mts.map +1 -0
  84. package/dist/cli/release-notes/publish.mjs +125 -0
  85. package/dist/cli/release-notes/publish.mjs.map +1 -0
  86. package/dist/cli/release-notes/ship-autodraft.d.mts +37 -0
  87. package/dist/cli/release-notes/ship-autodraft.d.mts.map +1 -0
  88. package/dist/cli/release-notes/ship-autodraft.mjs +97 -0
  89. package/dist/cli/release-notes/ship-autodraft.mjs.map +1 -0
  90. package/dist/cli/roadmap/conductor-view.d.mts +13 -0
  91. package/dist/cli/roadmap/conductor-view.d.mts.map +1 -0
  92. package/dist/cli/roadmap/conductor-view.mjs +31 -0
  93. package/dist/cli/roadmap/conductor-view.mjs.map +1 -0
  94. package/dist/cli/route/skill-discover.d.mts.map +1 -1
  95. package/dist/cli/route/skill-discover.mjs +2 -1
  96. package/dist/cli/route/skill-discover.mjs.map +1 -1
  97. package/dist/cli/task/ids.d.mts +7 -4
  98. package/dist/cli/task/ids.d.mts.map +1 -1
  99. package/dist/cli/task/ids.mjs +11 -10
  100. package/dist/cli/task/ids.mjs.map +1 -1
  101. package/dist/types/config.d.ts +11 -7
  102. package/dist/types/config.d.ts.map +1 -1
  103. package/package.json +9 -1
  104. package/prompts/conductor-status-check.md +23 -0
  105. package/prompts/conductor-sub-agent.md +57 -0
  106. package/prompts/conductor-system.md +172 -0
  107. package/skills/apt-close-task/SKILL.md +25 -0
  108. package/skills/apt-diagram/SKILL.md +45 -9
  109. package/skills/apt-plan/SKILL.md +12 -0
  110. package/skills/apt-plan/adapters/conductor.md +98 -0
  111. package/skills/apt-release-notes/SKILL.md +193 -0
  112. package/skills/apt-release-notes/appendices/persona-voice.md +59 -0
  113. package/skills/apt-setup/SKILL.md +148 -3
  114. package/skills/apt-ship/SKILL.md +74 -12
  115. package/skills/apt-spar/SKILL.md +290 -0
  116. package/skills/apt-update/SKILL.md +51 -9
  117. package/skills/apt-watch-ci/SKILL.md +2 -2
  118. package/src/cli/commands/check-version.mjs +73 -1
  119. package/src/cli/commands/ci-watch.mjs +35 -4
  120. package/src/cli/commands/commit.mjs +27 -8
  121. package/src/cli/commands/event.mjs +68 -0
  122. package/src/cli/commands/init.mjs +101 -9
  123. package/src/cli/commands/modes.mjs +2 -1
  124. package/src/cli/commands/release-notes.mjs +187 -0
  125. package/src/cli/commands/task.mjs +305 -152
  126. package/src/cli/commands/triage.mjs +14 -5
  127. package/src/cli/config/load.mjs +37 -0
  128. package/src/cli/config/upgrade-gitignore.mjs +6 -0
  129. package/src/cli/consistency/parse-review.mjs +3 -1
  130. package/src/cli/coordination/auto-emit-artifact-ready.mjs +96 -0
  131. package/src/cli/coordination/event-schema.d.ts +13 -0
  132. package/src/cli/coordination/event-schema.mjs +174 -0
  133. package/src/cli/coordination/store.mjs +14 -0
  134. package/src/cli/dispatch.mjs +2 -0
  135. package/src/cli/gate/gates/review-clean.mjs +4 -2
  136. package/src/cli/help.mjs +2 -2
  137. package/src/cli/install/update-chips.mjs +57 -0
  138. package/src/cli/release-notes/compile.mjs +261 -0
  139. package/src/cli/release-notes/draft.mjs +133 -0
  140. package/src/cli/release-notes/output-dir.mjs +52 -0
  141. package/src/cli/release-notes/persona-filter.mjs +126 -0
  142. package/src/cli/release-notes/publish.mjs +128 -0
  143. package/src/cli/release-notes/ship-autodraft.mjs +106 -0
  144. package/src/cli/roadmap/conductor-view.d.ts +10 -0
  145. package/src/cli/roadmap/conductor-view.mjs +31 -0
  146. package/src/cli/route/skill-discover.mjs +2 -1
  147. package/src/cli/task/ids.mjs +15 -13
  148. package/templates/config.json +28 -3
  149. package/workflows/docs.md +12 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @internal
3
+ * auto-emit-artifact-ready.mjs — code-enforced plan-ready signal.
4
+ *
5
+ * Promotes the `artifact.ready{kind:'plan'}` emit from a markdown contract
6
+ * in `skills/apt-plan/adapters/conductor.md` to a framework-level
7
+ * postcondition of `apt-tools commit`. This closes the Conductor v2 gap
8
+ * exposed during live verification 2026-05-15: the planner's QUICK /
9
+ * SIMPLE track is hard-exempt from "new interaction discipline"
10
+ * (Pocock Fast Path Guarantee ID-05), so the markdown emit step was
11
+ * never executed for tasks router-classified as simple. The fleet
12
+ * never saw artifact.ready, the PlanReadyCard never auto-promoted.
13
+ *
14
+ * The fix lives in `framework` rather than the skill so:
15
+ * - it works for QUICK + STANDARD + DEEP + COMPLEX uniformly
16
+ * - it doesn't violate the Fast Path Guarantee (a passive log emit
17
+ * is not interaction discipline)
18
+ * - it's the same code path every CLI consumer (Claude Code, Gemini,
19
+ * OpenCode, Codex) traverses via `apt-tools commit`
20
+ *
21
+ * Conditions for emit (ALL must hold — otherwise no-op):
22
+ * 1. `APERANT_TERMINAL_ID` is set in process.env (Conductor PTY context).
23
+ * 2. The committed files include a path matching
24
+ * `.aperant/tasks/{task-id}/implementation_plan.json`.
25
+ *
26
+ * The emit re-uses the same `event-schema` validator and JSONL writer
27
+ * `commands/event.mjs` already uses, so the envelope structure stays
28
+ * byte-identical with manually-emitted ones.
29
+ */
30
+
31
+ import { createHash } from 'node:crypto'
32
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs'
33
+ import { hostname } from 'node:os'
34
+ import { join } from 'node:path'
35
+ import { validateConductorEvent } from './event-schema.mjs'
36
+
37
+ const PLAN_PATH_RE = /^(?:\.\/)?\.aperant\/tasks\/([^/]+)\/implementation_plan\.json$/
38
+
39
+ let _incarnationCounter = 0
40
+
41
+ /**
42
+ * @param {{ files: string[], projectRoot: string }} input
43
+ * @returns {{ emitted: false, reason: string } | { emitted: true, envelope: object, file: string }}
44
+ */
45
+ export function maybeAutoEmitPlanReady({ files, projectRoot }) {
46
+ const terminalId = process.env.APERANT_TERMINAL_ID
47
+ if (!terminalId) return { emitted: false, reason: 'APERANT_TERMINAL_ID unset' }
48
+
49
+ const match = files.map((f) => f.replace(/\\/g, '/').match(PLAN_PATH_RE)).find((m) => m !== null)
50
+ if (!match) return { emitted: false, reason: 'no implementation_plan.json in committed files' }
51
+ const taskId = match[1]
52
+
53
+ const planRelPath = `.aperant/tasks/${taskId}/implementation_plan.json`
54
+ const absolutePath = join(projectRoot, planRelPath)
55
+
56
+ let hash
57
+ try {
58
+ const body = readFileSync(absolutePath)
59
+ hash = createHash('sha256').update(body).digest('hex')
60
+ } catch (err) {
61
+ return { emitted: false, reason: `plan file unreadable: ${(err && err.message) || err}` }
62
+ }
63
+
64
+ const data = {
65
+ kind: 'plan',
66
+ path: planRelPath,
67
+ hash,
68
+ request_id: `plan-${taskId}-${Date.now()}`,
69
+ task_id: taskId,
70
+ terminal_id: terminalId,
71
+ }
72
+
73
+ const validation = validateConductorEvent('artifact.ready', data)
74
+ if (!validation.ok) {
75
+ return { emitted: false, reason: `schema violation: ${validation.error}` }
76
+ }
77
+
78
+ const envelope = {
79
+ t: new Date().toISOString(),
80
+ agent: `${hostname()}-${process.pid}`,
81
+ pid: process.pid,
82
+ hostname: hostname(),
83
+ incarnation: ++_incarnationCounter,
84
+ op: 'artifact.ready',
85
+ task: taskId,
86
+ data,
87
+ }
88
+
89
+ const eventsDir = join(projectRoot, '.aperant', 'events')
90
+ mkdirSync(eventsDir, { recursive: true })
91
+ const dayFile = `${new Date().toISOString().slice(0, 10)}.jsonl`
92
+ const fullPath = join(eventsDir, dayFile)
93
+ appendFileSync(fullPath, `${JSON.stringify(envelope)}\n`, 'utf-8')
94
+
95
+ return { emitted: true, envelope, file: dayFile }
96
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Type declarations for the .mjs validator. Hand-authored because the
3
+ * import path points at src/.mjs directly (no dist roundtrip).
4
+ */
5
+
6
+ export type ValidateResult = { ok: true } | { ok: false; error: string }
7
+
8
+ export function validateConductorEvent(
9
+ op: string,
10
+ data: Record<string, unknown> | undefined | null,
11
+ ): ValidateResult
12
+
13
+ export const CONDUCTOR_VALIDATED_OPS: ReadonlySet<string>
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @internal
3
+ * event-schema.mjs — single source of truth for conductor event payload shapes.
4
+ *
5
+ * Why this lives in `framework`, not `core`:
6
+ * `packages/core` depends on `@aperant/framework`. The schema is shared by
7
+ * the CLI write path (`commands/event.mjs`), the framework lifecycle write
8
+ * seam (`coordination/store.mjs:appendEvent`), and the core IPC write seam
9
+ * (`packages/core/src/runtime/orchestration/conductor/event-log.ts`).
10
+ * Putting it in the lower layer keeps a single rule table that every
11
+ * writer can import.
12
+ *
13
+ * Shape: zero-dep plain JS validator returning a `{ ok, error? }` result.
14
+ * We deliberately avoid zod here so `.mjs` callers (event.mjs, store.mjs)
15
+ * don't need a build step, and so the core TS writer can use the same
16
+ * identity-shaped result without translating between zod and a custom
17
+ * error type.
18
+ *
19
+ * Per Codex roundtable lock 2026-05-15:
20
+ * - `phase.changed` is reserved for PTY busy/idle transitions ONLY.
21
+ * Plan completion uses `artifact.ready{kind:'plan'}`, not phase.changed.
22
+ * - `awaiting-input` / `input-rescinded` MUST carry both terminal_id and
23
+ * request_id so the drawer can pair cards strictly.
24
+ * - `artifact.ready{kind:'plan'}` is the canonical plan-completion signal
25
+ * consumed by the conductor's reviewer queue (Step 5's reviewPlan tool).
26
+ */
27
+
28
+ /** Conductor ops the schema enforces. Other ops pass through unchanged. */
29
+ const CONDUCTOR_OPS = new Set([
30
+ 'phase.changed',
31
+ 'artifact.ready',
32
+ 'awaiting-input',
33
+ 'input-rescinded',
34
+ 'heartbeat',
35
+ 'budget.exceeded',
36
+ 'terminal.enrolled',
37
+ 'terminal.unenrolled',
38
+ 'conductor.intent',
39
+ 'conductor.cross_check',
40
+ 'conductor.realignment',
41
+ // c-4 (action-capable) — gate verdict + boot-replay audit trail
42
+ 'conductor.action.executed',
43
+ 'conductor.action.gate_blocked',
44
+ 'conductor.boot_reconciled',
45
+ ])
46
+
47
+ /** Allowed values for `phase.changed.to` — PTY busy/idle only. */
48
+ const ALLOWED_PHASE_VALUES = new Set(['busy', 'idle'])
49
+
50
+ /** Allowed values for `artifact.ready.kind`. */
51
+ const ALLOWED_ARTIFACT_KINDS = new Set(['plan', 'spec', 'build', 'review'])
52
+
53
+ /**
54
+ * Validate the data payload of a conductor event against the op's contract.
55
+ *
56
+ * @param {string} op
57
+ * @param {Record<string, unknown> | undefined | null} data
58
+ * @returns {{ ok: true } | { ok: false, error: string }}
59
+ */
60
+ export function validateConductorEvent(op, data) {
61
+ if (!CONDUCTOR_OPS.has(op)) return { ok: true }
62
+ const d = data && typeof data === 'object' ? data : {}
63
+
64
+ switch (op) {
65
+ case 'awaiting-input':
66
+ case 'input-rescinded':
67
+ return requireFields(op, d, ['terminal_id', 'request_id'])
68
+
69
+ case 'phase.changed': {
70
+ const r = requireFields(op, d, ['from', 'to', 'terminal_id', 'request_id'])
71
+ if (!r.ok) return r
72
+ if (!ALLOWED_PHASE_VALUES.has(String(d.to))) {
73
+ return {
74
+ ok: false,
75
+ error: `phase.changed.to must be one of [${[...ALLOWED_PHASE_VALUES].join(', ')}], got: ${String(
76
+ d.to,
77
+ )}. Plan completion must use artifact.ready{kind:'plan'}.`,
78
+ }
79
+ }
80
+ return { ok: true }
81
+ }
82
+
83
+ case 'artifact.ready': {
84
+ const r = requireFields(op, d, [
85
+ 'kind',
86
+ 'path',
87
+ 'hash',
88
+ 'request_id',
89
+ 'terminal_id',
90
+ 'task_id',
91
+ ])
92
+ if (!r.ok) return r
93
+ if (!ALLOWED_ARTIFACT_KINDS.has(String(d.kind))) {
94
+ return {
95
+ ok: false,
96
+ error: `artifact.ready.kind must be one of [${[...ALLOWED_ARTIFACT_KINDS].join(
97
+ ', ',
98
+ )}], got: ${String(d.kind)}`,
99
+ }
100
+ }
101
+ return { ok: true }
102
+ }
103
+
104
+ case 'conductor.intent':
105
+ return requireFields(op, d, ['terminal_id', 'request_id', 'action', 'reasoning'])
106
+
107
+ case 'conductor.cross_check':
108
+ return requireFields(op, d, ['terminal_id', 'request_id'])
109
+
110
+ case 'conductor.realignment': {
111
+ const r = requireFields(op, d, ['terminal_id', 'request_id_orig'])
112
+ if (!r.ok) return r
113
+ if (d.attempt_n === undefined || d.attempt_n === null)
114
+ return { ok: false, error: `${op} missing required field(s): attempt_n` }
115
+ return { ok: true }
116
+ }
117
+
118
+ case 'terminal.enrolled':
119
+ case 'terminal.unenrolled':
120
+ case 'heartbeat':
121
+ case 'budget.exceeded':
122
+ return requireFields(op, d, ['terminal_id'])
123
+
124
+ case 'conductor.action.executed':
125
+ return requireFields(op, d, ['terminal_id', 'request_id', 'action_kind', 'idempotency_key'])
126
+
127
+ case 'conductor.action.gate_blocked':
128
+ return requireFields(op, d, [
129
+ 'terminal_id',
130
+ 'request_id',
131
+ 'action_kind',
132
+ 'idempotency_key',
133
+ 'blocked_reason',
134
+ ])
135
+
136
+ case 'conductor.boot_reconciled': {
137
+ // Fleet-wide event — numeric fields only. All optional but if
138
+ // present must be finite numbers (zero allowed).
139
+ const numFields = ['cleaned_leases', 'replayed', 'start_offset']
140
+ for (const f of numFields) {
141
+ if (d[f] === undefined) continue
142
+ if (typeof d[f] !== 'number' || !Number.isFinite(d[f])) {
143
+ return {
144
+ ok: false,
145
+ error: `${op} field ${f} must be a finite number, got: ${String(d[f])}`,
146
+ }
147
+ }
148
+ }
149
+ return { ok: true }
150
+ }
151
+
152
+ default:
153
+ return { ok: true }
154
+ }
155
+ }
156
+
157
+ /** Set of ops the schema validates — exported for test enumeration. */
158
+ export const CONDUCTOR_VALIDATED_OPS = CONDUCTOR_OPS
159
+
160
+ /**
161
+ * @param {string} op
162
+ * @param {Record<string, unknown>} d
163
+ * @param {string[]} fields
164
+ * @returns {{ ok: true } | { ok: false, error: string }}
165
+ */
166
+ function requireFields(op, d, fields) {
167
+ const missing = fields.filter((f) => {
168
+ const v = d[f]
169
+ if (typeof v !== 'string' || v.length === 0) return true
170
+ return false
171
+ })
172
+ if (missing.length === 0) return { ok: true }
173
+ return { ok: false, error: `${op} missing required field(s): ${missing.join(', ')}` }
174
+ }
@@ -32,6 +32,7 @@ import { withFileLock } from '../util/fs-lock.mjs'
32
32
  import { atomicWriteJson } from '../util/io.mjs'
33
33
  import { getAgentIdentity, resolveSharedRoot } from './agent-identity.mjs'
34
34
  import { appendLifecycleEvent } from './event-log.mjs'
35
+ import { validateConductorEvent } from './event-schema.mjs'
35
36
  import {
36
37
  listActive as teamStatusListActive,
37
38
  removeSelf as teamStatusRemoveSelf,
@@ -128,9 +129,22 @@ export function createCoordinationStore(projectRoot) {
128
129
  * @internal — R10 append-only event log. Emits one JSONL line per
129
130
  * call via appendLifecycleEvent. Best-effort: filesystem errors are
130
131
  * swallowed so a missing events/ dir never breaks task lifecycle.
132
+ *
133
+ * Step 1 — schema gate: conductor ops written via the store seam are
134
+ * validated against `event-schema.mjs` before they reach disk.
135
+ * Schema violations throw (caught here and surfaced via console.warn)
136
+ * because a malformed conductor event would silently break the
137
+ * drawer's pairing logic — we want it loud, not best-effort.
131
138
  */
132
139
  appendEvent(evt) {
133
140
  try {
141
+ if (evt && typeof evt.op === 'string') {
142
+ const validation = validateConductorEvent(evt.op, evt.data)
143
+ if (!validation.ok) {
144
+ console.warn(`[store.appendEvent] schema violation: ${validation.error}`)
145
+ return null
146
+ }
147
+ }
134
148
  return appendLifecycleEvent(projectRoot, evt)
135
149
  } catch {
136
150
  /* best-effort — never block a lifecycle transition on the log */
@@ -71,6 +71,7 @@ import { cmdPrReview } from './commands/pr-review.mjs'
71
71
  import { cmdPrecompactHook } from './commands/precompact-hook.mjs'
72
72
  import { cmdProductContext } from './commands/product-context.mjs'
73
73
  import { cmdProfile } from './commands/profile.mjs'
74
+ import { cmdReleaseNotes } from './commands/release-notes.mjs'
74
75
  import { cmdRiskCheck } from './commands/risk-check.mjs'
75
76
  import { cmdRoadmap } from './commands/roadmap.mjs'
76
77
  import { cmdRoute } from './commands/route.mjs'
@@ -253,6 +254,7 @@ export const DISPATCH = {
253
254
  'variant-pick': (args) => wrap(cmdVariantPick)(args[1], args[2], args.slice(3)),
254
255
  worktree: (args) => wrap(cmdWorktree)(args[1], args[2], args.slice(3)),
255
256
  audit: (args) => wrap(cmdAudit)(args[1], args[2], args.slice(3)),
257
+ 'release-notes': (args) => wrap(cmdReleaseNotes)(args[1], args[2], args.slice(3)),
256
258
  team: (args) => wrap(cmdTeam)(args[1], args[2], args.slice(3)),
257
259
  active: (args) => wrap(cmdActive)(args[1], args[2], args.slice(3)),
258
260
  roadmap: (args) => wrap(cmdRoadmap)(args[1], args[2], args.slice(3)),
@@ -4,7 +4,9 @@
4
4
  *
5
5
  * Skipped when {task_dir}/review.json is absent (review is optional for some
6
6
  * tracks). When it is present, fails if any finding has
7
- * severity ∈ {"critical", "warning"} AND resolved !== true.
7
+ * severity ∈ {"critical", "warning"} AND neither `resolved === true` nor
8
+ * `fixed === true`. The reviewer skill writes resolution as `fixed: true`
9
+ * paired with `fix_commit`; `resolved: true` is a legacy alias.
8
10
  *
9
11
  * The findings array may be under `findings` or `issues` depending on the
10
12
  * reviewer format — we accept either.
@@ -47,7 +49,7 @@ const gate = {
47
49
  ? doc.issues
48
50
  : []
49
51
  const unresolved = findings.filter(
50
- (f) => BLOCKING_SEVERITIES.has(f?.severity) && f?.resolved !== true,
52
+ (f) => BLOCKING_SEVERITIES.has(f?.severity) && f?.resolved !== true && f?.fixed !== true,
51
53
  )
52
54
  if (unresolved.length > 0) {
53
55
  return {
package/src/cli/help.mjs CHANGED
@@ -28,7 +28,7 @@ Commands:
28
28
  task create <dir> --description <text> [--scope <s>] [--milestone <id>] [--phase <id>] [--roadmap-ref-url <url>] [--depends-on <refs>] [--track <T>] [--autonomy <N>] Create isolated task
29
29
  task list <dir> [--scope <s>] [--blocked-by <id>] List active tasks (optionally filtered)
30
30
  task get <dir> --id <task-id> Get task details and artifacts
31
- task update <dir> --id <id> [--lifecycle-phase <p>] [--subtasks-total <N>] [--depends-on <refs>] Update non-linkage task state
31
+ task update <dir> --id <id> [--lifecycle-phase <p>] [--pr-url <url>] [--subtasks-total <N>] [--depends-on <refs>] Update non-linkage task state; --pr-url may arm ci-watch per ci_watch.after_ship
32
32
  task move <dir> --id <id> [--to-scope <s>] [--to-milestone <id>] [--to-phase <id>] [--to-roadmap-ref-url <url>] [--reason <text>] [--force] Reassign scope/milestone/phase with history
33
33
  task close <dir> --id <task-id> [--verdict <v>] Close task and move to completed
34
34
  task narration-drain <dir> --task <task-id> Remove a row from state.pending_narration[] under withFileLock (called by /apt:close-task --narrate-only after a successful narrator spawn)
@@ -131,5 +131,5 @@ Flags:
131
131
 
132
132
  All output is JSON.`
133
133
 
134
- process.stdout.write(help + '\n')
134
+ process.stdout.write(`${help}\n`)
135
135
  }
@@ -60,6 +60,63 @@ export function readUpdateCache() {
60
60
  }
61
61
  }
62
62
 
63
+ /** Semver-shape pattern: MAJOR.MINOR.PATCH with optional pre-release and build metadata. */
64
+ const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
65
+
66
+ /**
67
+ * Read the worker's cache file with TTL and schema validation. Returns
68
+ * `{ fresh: true, cache }` when the cache exists, parses, declares
69
+ * `schema_version === 1`, and was written within `ttlMs` milliseconds.
70
+ * Returns `{ fresh: false }` when missing, stale, or schema mismatch.
71
+ *
72
+ * Security: `latest_version` and `installed_version` inside
73
+ * `cache.framework` are validated as semver strings. Invalid values are
74
+ * dropped (set to null) so they never propagate into shell-interpolated
75
+ * contexts.
76
+ *
77
+ * ENOENT and JSON parse errors stay silent (expected when the cache is
78
+ * missing or being rewritten). Unexpected errors are logged to stderr.
79
+ *
80
+ * @param {number} ttlMs Maximum age in milliseconds before cache is treated as stale.
81
+ * @returns {{ fresh: true, cache: Record<string, unknown> } | { fresh: false }}
82
+ */
83
+ export function readFreshUpdateCache(ttlMs) {
84
+ const path = updateCachePath()
85
+ if (!existsSync(path)) return { fresh: false }
86
+ let data
87
+ try {
88
+ data = JSON.parse(readFileSync(path, 'utf-8'))
89
+ } catch (err) {
90
+ // SyntaxError = malformed JSON (expected); anything else is unexpected.
91
+ if (!(err instanceof SyntaxError)) {
92
+ console.error('[update-cache] read failed:', err.message)
93
+ }
94
+ return { fresh: false }
95
+ }
96
+ try {
97
+ if (!data || typeof data !== 'object') return { fresh: false }
98
+ if (data.schema_version !== 1) return { fresh: false }
99
+ if (typeof data.checked_at !== 'string') return { fresh: false }
100
+ const age = Date.now() - new Date(data.checked_at).getTime()
101
+ if (!Number.isFinite(age) || age < 0 || age >= ttlMs) return { fresh: false }
102
+ // Validate semver fields in framework block — reject non-semver strings.
103
+ const fw = data.framework && typeof data.framework === 'object' ? data.framework : {}
104
+ const sanitized = { ...data, framework: { ...fw } }
105
+ for (const field of ['latest_version', 'installed_version']) {
106
+ if (
107
+ typeof sanitized.framework[field] === 'string' &&
108
+ !SEMVER_RE.test(sanitized.framework[field])
109
+ ) {
110
+ sanitized.framework[field] = null
111
+ }
112
+ }
113
+ return { fresh: true, cache: sanitized }
114
+ } catch (err) {
115
+ console.error('[update-cache] read failed:', err.message)
116
+ return { fresh: false }
117
+ }
118
+ }
119
+
63
120
  /**
64
121
  * Build the chip string array for a given cache. Empty array when there
65
122
  * are no chips to show. Order: stale_files first (most urgent), then