@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
@@ -6,6 +6,7 @@ import { appendFileSync, mkdirSync, readdirSync, readFileSync, writeFileSync } f
6
6
  import { hostname } from 'node:os'
7
7
  import { join, resolve } from 'node:path'
8
8
  import { readLifecycleEventsForDay } from '../coordination/event-log.mjs'
9
+ import { validateConductorEvent } from '../coordination/event-schema.mjs'
9
10
  import { createCoordinationStore } from '../coordination/store.mjs'
10
11
  import { detectWorktree } from '../git/worktree-detect.mjs'
11
12
  import { parseFlags } from '../util/args.mjs'
@@ -27,6 +28,21 @@ const VALID_EVENT_TYPES = [
27
28
  'ci-watch.tick',
28
29
  'ci-watch.fix-pushed',
29
30
  'ci-watch.stopped',
31
+ // Phase 2.5 — Conductor-readable ops emitted by APT skills so the
32
+ // Conductor's readEventLog tool can drive its observe loop. These
33
+ // events route to JSONL (like ci-watch.*) and carry terminal_id
34
+ // auto-stamped from APERANT_TERMINAL_ID when present.
35
+ 'phase.changed',
36
+ 'artifact.ready',
37
+ 'awaiting-input',
38
+ 'input-rescinded',
39
+ 'heartbeat',
40
+ 'budget.exceeded',
41
+ 'terminal.enrolled',
42
+ 'terminal.unenrolled',
43
+ 'conductor.intent',
44
+ 'conductor.cross_check',
45
+ 'conductor.realignment',
30
46
  ]
31
47
 
32
48
  const CI_WATCH_EVENT_TYPES = new Set([
@@ -36,6 +52,20 @@ const CI_WATCH_EVENT_TYPES = new Set([
36
52
  'ci-watch.stopped',
37
53
  ])
38
54
 
55
+ const CONDUCTOR_EVENT_TYPES = new Set([
56
+ 'phase.changed',
57
+ 'artifact.ready',
58
+ 'awaiting-input',
59
+ 'input-rescinded',
60
+ 'heartbeat',
61
+ 'budget.exceeded',
62
+ 'terminal.enrolled',
63
+ 'terminal.unenrolled',
64
+ 'conductor.intent',
65
+ 'conductor.cross_check',
66
+ 'conductor.realignment',
67
+ ])
68
+
39
69
  /**
40
70
  * Append a ci-watch event to the per-day JSONL file (D-04).
41
71
  * O(1) — no directory scan. Creates the events dir if missing.
@@ -110,6 +140,44 @@ export function cmdEvent(subcommand, projectDir, extraArgs) {
110
140
  return ok({ status: 'ok', command: 'event-append', event_file: jsonlFile, event })
111
141
  }
112
142
 
143
+ // Phase 2.5 — Conductor-readable events route to the per-day JSONL
144
+ // path with the canonical envelope the Conductor's readEventLog
145
+ // expects: { t, agent, pid, hostname, incarnation, op, task, data }.
146
+ // terminal_id is auto-stamped from APERANT_TERMINAL_ID env when set,
147
+ // so APT skills running in a Conductor-spawned PTY automatically
148
+ // tag their events with the originating terminal.
149
+ if (CONDUCTOR_EVENT_TYPES.has(type)) {
150
+ const terminalId = process.env.APERANT_TERMINAL_ID
151
+ if (terminalId && data && typeof data === 'object' && !data.terminal_id) {
152
+ data.terminal_id = terminalId
153
+ }
154
+ // Step 1 — schema gate: reject payloads that violate the per-op
155
+ // contract before the line lands on disk. The same validator
156
+ // runs at the framework store seam and the core conductor
157
+ // writer; gating here catches CLI emitters (APT skills).
158
+ const validation = validateConductorEvent(type, data)
159
+ if (!validation.ok) {
160
+ return err(`Schema violation for ${type}: ${validation.error}`)
161
+ }
162
+ const envelope = {
163
+ t: new Date().toISOString(),
164
+ agent: `${hostname()}-${process.pid}`,
165
+ pid: process.pid,
166
+ hostname: hostname(),
167
+ incarnation: 1,
168
+ op: type,
169
+ task: subtask || null,
170
+ data: data || {},
171
+ }
172
+ const { jsonlFile } = appendToJsonl(eventsDir, envelope)
173
+ return ok({
174
+ status: 'ok',
175
+ command: 'event-append',
176
+ event_file: jsonlFile,
177
+ event: envelope,
178
+ })
179
+ }
180
+
113
181
  // Find next sequence number — scan existing files for highest number
114
182
  const existingFiles = readdirSync(eventsDir).filter((f) => f.endsWith('.json'))
115
183
  let maxId = 0
@@ -82,8 +82,14 @@ const TRANSFORM_LOADERS = new Map(
82
82
  * a missing runtimes flag should block (no flag, no `--yes`) or not
83
83
  * (flag passed, or `--yes`).
84
84
  *
85
+ * 0.6.7 — `--force-runtimes` (Bug 2 fix) bypasses the `upgrade_noop`
86
+ * short-circuit so `/apt:update` can re-stamp runtime manifests even
87
+ * when `.aperant/config.json` is at the current schema. Strictly
88
+ * additive: callers without the flag still get the no-op short-circuit
89
+ * (preserves `init-upgrade-drift.test.ts` case (a)).
90
+ *
85
91
  * @param {string[]} args
86
- * @returns {{ runtimes: string[], runtimes_explicit: boolean, target: string|null, interactive: boolean, skipBootstrap: boolean, visibility?: 'solo'|'team', autonomy?: 0|1|2|3, accept_new_defaults: boolean, yes: boolean }}
92
+ * @returns {{ runtimes: string[], runtimes_explicit: boolean, target: string|null, interactive: boolean, skipBootstrap: boolean, visibility?: 'solo'|'team', autonomy?: 0|1|2|3, accept_new_defaults: boolean, yes: boolean, forceRuntimes: boolean }}
87
93
  */
88
94
  export function parseInstallFlags(args) {
89
95
  const flagSet = new Set(args.filter((a) => a.startsWith('--')))
@@ -96,12 +102,16 @@ export function parseInstallFlags(args) {
96
102
  // C47 D-04 — opt out of the PROJECT.md auto-chain. Forwarded as
97
103
  // `answers.skipBootstrap` into both interview paths.
98
104
  const skipBootstrap = flagSet.has('--skip-bootstrap')
105
+ // 0.6.7 (Bug 2 fix) — opt-in bypass of the upgrade_noop short-circuit
106
+ // so /apt:update can re-stamp runtime files on a no-drift upgrade.
107
+ const forceRuntimes = flagSet.has('--force-runtimes')
99
108
 
100
109
  const classFlags = parseClassFlags(args)
101
110
 
102
111
  const base = {
103
112
  target,
104
113
  skipBootstrap,
114
+ forceRuntimes,
105
115
  visibility: classFlags.visibility,
106
116
  autonomy: classFlags.autonomy,
107
117
  accept_new_defaults: classFlags.accept_new_defaults,
@@ -326,6 +336,33 @@ function applyCollapsedClassAToMigration(
326
336
  }
327
337
  }
328
338
 
339
+ /**
340
+ * Resolve the effective runtimes list from flags, with fallback to
341
+ * `suggestRuntimes` and a final fallback to `['claude']`.
342
+ *
343
+ * Shared by the `nonInteractiveAnswers` upgrade path and the
344
+ * `--force-runtimes` synthesis block in `cmdInit` to avoid duplication.
345
+ *
346
+ * Emits a one-line stderr warning when the final fallback fires (no
347
+ * explicit flags and `suggestRuntimes` returned empty) so the choice is
348
+ * visible without breaking automated callers.
349
+ *
350
+ * @param {string} targetDir
351
+ * @param {ReturnType<typeof parseInstallFlags>} parsedFlags
352
+ * @returns {string[]}
353
+ */
354
+ function selectRuntimes(targetDir, parsedFlags) {
355
+ if (parsedFlags.runtimes_explicit) return parsedFlags.runtimes
356
+ const suggested = new Set(suggestRuntimes(targetDir))
357
+ if (suggested.size === 0) {
358
+ process.stderr.write(
359
+ '[apt-tools] No runtime specified and config has no suggestRuntimes; defaulting to claude.\n',
360
+ )
361
+ suggested.add('claude')
362
+ }
363
+ return [...suggested]
364
+ }
365
+
329
366
  /**
330
367
  * Non-TTY / scripted path — derive answers from flags + sensible defaults,
331
368
  * gated by the Class A/B classifier (spec R2 + R3).
@@ -427,9 +464,7 @@ function nonInteractiveAnswers(targetDir, canonicalRoot, archetype, parsedFlags)
427
464
  const visibility = migration.migrated?.share?.visibility === 'team' ? 'team' : 'solo'
428
465
  const autonomyDefault = migration.migrated?.autonomy?.default ?? 1
429
466
  const xverify = migration.migrated?.multi_model?.cross_verification?.tools ?? []
430
- const suggested = new Set(suggestRuntimes(targetDir))
431
- if (suggested.size === 0) suggested.add('claude')
432
- const runtimes = parsedFlags.runtimes_explicit ? parsedFlags.runtimes : [...suggested]
467
+ const runtimes = selectRuntimes(targetDir, parsedFlags)
433
468
  return {
434
469
  runtimes,
435
470
  autonomyDefault,
@@ -599,6 +634,12 @@ async function interactiveInterview(targetDir, version, canonicalRoot, archetype
599
634
  migration.applied_defaults.length === 0 &&
600
635
  migration.auto_added.length === 0
601
636
  if (noDrift) {
637
+ // 0.6.7 (Bug 2 fix) — `--force-runtimes` bypasses the TTY
638
+ // "Nothing to do." outro so the runtime walker re-stamps every
639
+ // manifest; cmdInit's force-runtimes synthesis path takes over.
640
+ if (parsedFlags.forceRuntimes) {
641
+ return { upgrade_noop: true, skipBootstrap, migration }
642
+ }
602
643
  process.stdout.write(`\n${aperantBanner(version)}\n\n`)
603
644
  clack.intro('install')
604
645
  clack.note(`Already up to date — v${version}`, 'No drift')
@@ -907,6 +948,22 @@ function applyInterviewAnswers(config, answers) {
907
948
  }
908
949
  config.project_type =
909
950
  answers.archetype.type === 'brownfield' ? 'unknown' : config.project_type || 'unknown'
951
+
952
+ // Onboarding marker (task 15-05-26_apt-setup-onboarding-marker-7q9c).
953
+ // `/apt:setup` dispatches on this marker to decide review-mode vs full
954
+ // Batch 1-8 flow — file existence alone cannot disambiguate "fresh seed
955
+ // from template" from "fully tuned by setup" because templates/config.json
956
+ // ships with every key populated. Only the fresh-seed branch reaches this
957
+ // function (finishInit:1331); the upgrade branch (finishInit:1338-1357)
958
+ // writes answers.migration.migrated directly and never calls
959
+ // applyInterviewAnswers, so legacy unmarked configs correctly stay
960
+ // unmarked on upgrade. --yes / --accept-new-defaults walk this same fresh
961
+ // path but do NOT walk Batch 1-8 questions, so completed stays false.
962
+ config.onboarding = {
963
+ completed: false,
964
+ seeded_from_template: true,
965
+ seeded_at: new Date().toISOString(),
966
+ }
910
967
  }
911
968
 
912
969
  /**
@@ -1061,7 +1118,13 @@ export async function cmdInit(projectDir, extraArgs = []) {
1061
1118
  // idempotent — preserves PROJECT.md, refreshes sidecar from it).
1062
1119
  // In TTY mode clack already rendered "Nothing to do." to stdout,
1063
1120
  // so suppress the JSON envelope (matches the cancelled path).
1064
- if (answers?.upgrade_noop) {
1121
+ //
1122
+ // 0.6.7 (Bug 2 fix) — `--force-runtimes` bypasses this short-circuit
1123
+ // so `/apt:update` can re-stamp every runtime manifest even when
1124
+ // `.aperant/config.json` is at the current schema. We synthesize a
1125
+ // real InstallAnswers shape from the existing config so
1126
+ // installRuntimes + finishInit run their normal upgrade path.
1127
+ if (answers?.upgrade_noop && !parsedFlags.forceRuntimes) {
1065
1128
  if (interactiveTty) return rawText('', 0)
1066
1129
  const bootstrap = answers.skipBootstrap
1067
1130
  ? { status: 'skipped', reason: '--skip-bootstrap flag' }
@@ -1077,7 +1140,36 @@ export async function cmdInit(projectDir, extraArgs = []) {
1077
1140
  })
1078
1141
  }
1079
1142
 
1080
- const results = await installRuntimes(answers.runtimes, canonicalRoot, installTarget, version)
1143
+ // 0.6.7 (Bug 2 fix) when forceRuntimes is set on a no-drift run,
1144
+ // synthesize a real InstallAnswers shape from the existing config so
1145
+ // the runtime walker re-stamps every manifest. Visibility/autonomy/
1146
+ // cross-verification are read from answers.migration.migrated (already
1147
+ // in memory from nonInteractiveAnswers — avoids a second config read);
1148
+ // runtimes resolved via selectRuntimes helper shared with the upgrade path.
1149
+ let effectiveAnswers = answers
1150
+ if (answers?.upgrade_noop && parsedFlags.forceRuntimes) {
1151
+ const existing = answers.migration?.migrated || {}
1152
+ const runtimes = selectRuntimes(installTarget, parsedFlags)
1153
+ effectiveAnswers = {
1154
+ runtimes,
1155
+ autonomyDefault: existing?.autonomy?.default ?? 1,
1156
+ visibility: existing?.share?.visibility === 'team' ? 'team' : 'solo',
1157
+ crossVerificationTools: existing?.multi_model?.cross_verification?.tools ?? [],
1158
+ scanNow: false,
1159
+ skipBootstrap: answers.skipBootstrap,
1160
+ archetype: detectArchetype(installTarget),
1161
+ agent_runtime: detectAgentRuntime(),
1162
+ applied_defaults: [],
1163
+ explicit_inputs: collectExplicitInputs(parsedFlags),
1164
+ }
1165
+ }
1166
+
1167
+ const results = await installRuntimes(
1168
+ effectiveAnswers.runtimes,
1169
+ canonicalRoot,
1170
+ installTarget,
1171
+ version,
1172
+ )
1081
1173
 
1082
1174
  // Provision the project-local kernel BEFORE finishInit runs its JSON
1083
1175
  // output — that way the "structure" summary can reflect the kernel
@@ -1089,14 +1181,14 @@ export async function cmdInit(projectDir, extraArgs = []) {
1089
1181
  installTarget,
1090
1182
  canonicalRoot,
1091
1183
  version,
1092
- answers.runtimes,
1184
+ effectiveAnswers.runtimes,
1093
1185
  results,
1094
- answers,
1186
+ effectiveAnswers,
1095
1187
  interactiveTty,
1096
1188
  kernelResult,
1097
1189
  )
1098
1190
  if (interactiveTty) {
1099
- await renderOutro(answers, results, version, kernelResult)
1191
+ await renderOutro(effectiveAnswers, results, version, kernelResult)
1100
1192
  // TTY mode: envelope was persisted to disk + suppressed from stdout
1101
1193
  // because the clack outro already rendered the user-facing summary.
1102
1194
  // Return the rawText shape with empty payload so wrap() routes us
@@ -28,6 +28,7 @@
28
28
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
29
29
  import { join, resolve } from 'node:path'
30
30
  import { parse as parseYaml } from 'yaml'
31
+ import { sanitizeAptVersionForYamlParse } from '../install/version-header.mjs'
31
32
  import { ok } from '../util/result.mjs'
32
33
 
33
34
  const FRONTMATTER_RE = /^---\s*([\s\S]*?)\s*---/
@@ -122,7 +123,7 @@ function readSkillForModes(file) {
122
123
  if (!m) return null
123
124
  let parsed
124
125
  try {
125
- parsed = parseYaml(m[1])
126
+ parsed = parseYaml(sanitizeAptVersionForYamlParse(m[1]))
126
127
  } catch {
127
128
  return null
128
129
  }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * commands/release-notes.mjs — `apt-tools release-notes <sub>` (subtask 2-3
3
+ * of persona-aware-changelog-and-release-note_16-05-26).
4
+ *
5
+ * Deterministic I/O surface for the apt:release-notes skill. NEVER calls
6
+ * an LLM. The SKILL.md body invokes the host LLM to author the note text
7
+ * and hands the structured payload to this CLI for persistence + compile.
8
+ *
9
+ * Sub-verb menu:
10
+ *
11
+ * apt-tools release-notes draft <project-dir> --pr <n> --note "<text>"
12
+ * [--persona <id>] [--output-dir <path>] [--commits <sha,sha,...>]
13
+ * [--date <YYYY-MM-DD>]
14
+ * → write `<output_dir>/_unreleased/PR-<n>.md`. Idempotent.
15
+ *
16
+ * apt-tools release-notes compile <project-dir> --tag <tag>
17
+ * [--output-dir <path>]
18
+ * → read every `_unreleased/PR-*.md`, filter by persona_filter, write
19
+ * `<output_dir>/<tag>.md`, clear `_unreleased/` only on success.
20
+ *
21
+ * apt-tools release-notes publish <project-dir> --tag <tag>
22
+ * [--output-dir <path>] [--dry-run]
23
+ * → read `<output_dir>/<tag>.md` + shell `gh release create/edit`.
24
+ *
25
+ * Return shape: always `{ status, command, ... }`. The wrap() helper in
26
+ * dispatch.mjs translates `{ status: 'error' }` envelopes into exit-1.
27
+ */
28
+
29
+ import { resolve } from 'node:path'
30
+
31
+ import { compileTag } from '../release-notes/compile.mjs'
32
+ import { writeDraft } from '../release-notes/draft.mjs'
33
+ import { publishTag } from '../release-notes/publish.mjs'
34
+ import { getFlagValue } from '../util/common-flags.mjs'
35
+ import { err, ok } from '../util/result.mjs'
36
+
37
+ /**
38
+ * Dispatch shape (mirrors `audit`): `release-notes: (args) =>
39
+ * wrap(cmdReleaseNotes)(args[1], args[2], args.slice(3))` — args[1] is the
40
+ * sub-verb, args[2] is the project dir.
41
+ *
42
+ * @param {string | undefined} subcommand
43
+ * @param {string | undefined} projectDirArg
44
+ * @param {string[] | undefined} extraArgs
45
+ */
46
+ export async function cmdReleaseNotes(subcommand, projectDirArg, extraArgs) {
47
+ const args = extraArgs ?? []
48
+ if (!subcommand) {
49
+ return err(
50
+ 'Usage: apt-tools release-notes <draft|compile|publish> <project-dir> [flags]\n' +
51
+ ' draft write docs/releases/_unreleased/PR-<n>.md\n' +
52
+ ' compile read fragments → docs/releases/<tag>.md, clear _unreleased/\n' +
53
+ ' publish shell gh release create/edit',
54
+ )
55
+ }
56
+ const projectDir = resolve(projectDirArg && projectDirArg.length > 0 ? projectDirArg : '.')
57
+
58
+ switch (subcommand) {
59
+ case 'draft':
60
+ return runDraft(projectDir, args)
61
+ case 'compile':
62
+ return runCompile(projectDir, args)
63
+ case 'publish':
64
+ return runPublish(projectDir, args)
65
+ default:
66
+ return err(
67
+ `apt-tools release-notes: unknown subcommand "${subcommand}". Known: draft, compile, publish.`,
68
+ )
69
+ }
70
+ }
71
+
72
+ /**
73
+ * draft — write a single PR fragment.
74
+ *
75
+ * @param {string} projectDir
76
+ * @param {string[]} args
77
+ */
78
+ function runDraft(projectDir, args) {
79
+ const prRaw = getFlagValue(args, '--pr')
80
+ const note = getFlagValue(args, '--note')
81
+ const persona = getFlagValue(args, '--persona')
82
+ const outputDirFlag = getFlagValue(args, '--output-dir')
83
+ const commitsFlag = getFlagValue(args, '--commits')
84
+ const dateFlag = getFlagValue(args, '--date')
85
+
86
+ if (!prRaw) {
87
+ return err('release-notes draft: --pr <number> is required')
88
+ }
89
+ const pr = Number.parseInt(prRaw, 10)
90
+ if (!Number.isFinite(pr) || pr <= 0) {
91
+ return err(`release-notes draft: --pr must be a positive integer (got "${prRaw}")`)
92
+ }
93
+ if (!note || note.trim().length === 0) {
94
+ return err('release-notes draft: --note "<text>" is required (and must be non-empty)')
95
+ }
96
+
97
+ const commits = commitsFlag
98
+ ? commitsFlag
99
+ .split(',')
100
+ .map((s) => s.trim())
101
+ .filter((s) => s.length > 0)
102
+ : null
103
+
104
+ const result = writeDraft({
105
+ projectDir,
106
+ pr,
107
+ note,
108
+ persona,
109
+ outputDir: outputDirFlag,
110
+ commits,
111
+ date: dateFlag,
112
+ })
113
+
114
+ return ok({
115
+ status: 'ok',
116
+ command: 'release-notes-draft',
117
+ projectDir,
118
+ pr,
119
+ written: result.written,
120
+ reason: result.reason,
121
+ path: result.path,
122
+ })
123
+ }
124
+
125
+ /**
126
+ * compile — produce the per-tag narrative file from accumulated fragments.
127
+ *
128
+ * @param {string} projectDir
129
+ * @param {string[]} args
130
+ */
131
+ function runCompile(projectDir, args) {
132
+ const tag = getFlagValue(args, '--tag')
133
+ if (!tag) {
134
+ return err('release-notes compile: --tag <tag> is required')
135
+ }
136
+ const outputDirFlag = getFlagValue(args, '--output-dir')
137
+ const result = compileTag({ projectDir, tag, outputDir: outputDirFlag })
138
+ return ok({
139
+ status: 'ok',
140
+ command: 'release-notes-compile',
141
+ projectDir,
142
+ tag,
143
+ written: result.written,
144
+ path: result.path,
145
+ fragments_total: result.fragmentsTotal,
146
+ fragments_in_body: result.fragmentsInBody,
147
+ fragments_in_triage: result.fragmentsInTriage,
148
+ fragments_filtered: result.fragmentsFiltered,
149
+ cleared_unreleased: result.clearedUnreleased,
150
+ })
151
+ }
152
+
153
+ /**
154
+ * publish — shell `gh release create/edit`. Returns a graceful
155
+ * `{status: 'skipped', reason: 'gh-not-found'}` when gh is unavailable.
156
+ *
157
+ * @param {string} projectDir
158
+ * @param {string[]} args
159
+ */
160
+ function runPublish(projectDir, args) {
161
+ const tag = getFlagValue(args, '--tag')
162
+ if (!tag) {
163
+ return err('release-notes publish: --tag <tag> is required')
164
+ }
165
+ const outputDirFlag = getFlagValue(args, '--output-dir')
166
+ const dryRun = args.includes('--dry-run')
167
+ const result = publishTag({ projectDir, tag, outputDir: outputDirFlag, dryRun })
168
+ if (result.status === 'skipped') {
169
+ return ok({
170
+ status: 'skipped',
171
+ command: 'release-notes-publish',
172
+ projectDir,
173
+ tag,
174
+ reason: result.reason,
175
+ notes_path: result.notesPath ?? null,
176
+ })
177
+ }
178
+ return ok({
179
+ status: 'ok',
180
+ command: 'release-notes-publish',
181
+ projectDir,
182
+ tag,
183
+ mode: result.mode,
184
+ dry_run: dryRun,
185
+ notes_path: result.notesPath,
186
+ })
187
+ }