@aperant/framework 0.6.6 → 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.
- package/CHANGELOG.md +155 -0
- package/dist/cli/commands/check-version.d.mts.map +1 -1
- package/dist/cli/commands/check-version.mjs +76 -1
- package/dist/cli/commands/check-version.mjs.map +1 -1
- package/dist/cli/commands/ci-watch.d.mts.map +1 -1
- package/dist/cli/commands/ci-watch.mjs +34 -4
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts +8 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +91 -9
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/release-notes.d.mts +11 -0
- package/dist/cli/commands/release-notes.d.mts.map +1 -0
- package/dist/cli/commands/release-notes.mjs +173 -0
- package/dist/cli/commands/release-notes.mjs.map +1 -0
- package/dist/cli/config/load.d.mts +14 -0
- package/dist/cli/config/load.d.mts.map +1 -1
- package/dist/cli/config/load.mjs +40 -0
- package/dist/cli/config/load.mjs.map +1 -1
- package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
- package/dist/cli/config/upgrade-gitignore.mjs +6 -0
- package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.d.mts.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +4 -1
- package/dist/cli/consistency/parse-review.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/gate/gates/review-clean.d.mts.map +1 -1
- package/dist/cli/gate/gates/review-clean.mjs +4 -2
- package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
- package/dist/cli/install/update-chips.d.mts +23 -0
- package/dist/cli/install/update-chips.d.mts.map +1 -1
- package/dist/cli/install/update-chips.mjs +60 -0
- package/dist/cli/install/update-chips.mjs.map +1 -1
- package/dist/cli/release-notes/compile.d.mts +38 -0
- package/dist/cli/release-notes/compile.d.mts.map +1 -0
- package/dist/cli/release-notes/compile.mjs +244 -0
- package/dist/cli/release-notes/compile.mjs.map +1 -0
- package/dist/cli/release-notes/draft.d.mts +73 -0
- package/dist/cli/release-notes/draft.d.mts.map +1 -0
- package/dist/cli/release-notes/draft.mjs +120 -0
- package/dist/cli/release-notes/draft.mjs.map +1 -0
- package/dist/cli/release-notes/output-dir.d.mts +20 -0
- package/dist/cli/release-notes/output-dir.d.mts.map +1 -0
- package/dist/cli/release-notes/output-dir.mjs +42 -0
- package/dist/cli/release-notes/output-dir.mjs.map +1 -0
- package/dist/cli/release-notes/persona-filter.d.mts +51 -0
- package/dist/cli/release-notes/persona-filter.d.mts.map +1 -0
- package/dist/cli/release-notes/persona-filter.mjs +127 -0
- package/dist/cli/release-notes/persona-filter.mjs.map +1 -0
- package/dist/cli/release-notes/publish.d.mts +23 -0
- package/dist/cli/release-notes/publish.d.mts.map +1 -0
- package/dist/cli/release-notes/publish.mjs +125 -0
- package/dist/cli/release-notes/publish.mjs.map +1 -0
- package/dist/cli/release-notes/ship-autodraft.d.mts +37 -0
- package/dist/cli/release-notes/ship-autodraft.d.mts.map +1 -0
- package/dist/cli/release-notes/ship-autodraft.mjs +97 -0
- package/dist/cli/release-notes/ship-autodraft.mjs.map +1 -0
- package/dist/types/config.d.ts +11 -7
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +133 -133
- package/skills/apt-close-task/SKILL.md +25 -0
- package/skills/apt-diagram/SKILL.md +45 -9
- package/skills/apt-release-notes/SKILL.md +193 -0
- package/skills/apt-release-notes/appendices/persona-voice.md +59 -0
- package/skills/apt-setup/SKILL.md +146 -3
- package/skills/apt-ship/SKILL.md +47 -0
- package/skills/apt-update/SKILL.md +51 -9
- package/skills/apt-watch-ci/SKILL.md +2 -2
- package/src/cli/commands/check-version.mjs +73 -1
- package/src/cli/commands/ci-watch.mjs +35 -4
- package/src/cli/commands/init.mjs +101 -9
- package/src/cli/commands/release-notes.mjs +187 -0
- package/src/cli/config/load.mjs +37 -0
- package/src/cli/config/upgrade-gitignore.mjs +6 -0
- package/src/cli/consistency/parse-review.mjs +3 -1
- package/src/cli/dispatch.mjs +2 -0
- package/src/cli/gate/gates/review-clean.mjs +4 -2
- package/src/cli/install/update-chips.mjs +57 -0
- package/src/cli/release-notes/compile.mjs +261 -0
- package/src/cli/release-notes/draft.mjs +133 -0
- package/src/cli/release-notes/output-dir.mjs +52 -0
- package/src/cli/release-notes/persona-filter.mjs +126 -0
- package/src/cli/release-notes/publish.mjs +128 -0
- package/src/cli/release-notes/ship-autodraft.mjs +106 -0
- package/templates/config.json +28 -3
- package/workflows/docs.md +12 -0
|
@@ -16,7 +16,26 @@
|
|
|
16
16
|
* stale_files: [{ path, reason }],
|
|
17
17
|
* update_available: false
|
|
18
18
|
* }],
|
|
19
|
-
* legacy_install: false
|
|
19
|
+
* legacy_install: false, // true when only the old .apt-version exists
|
|
20
|
+
* // === 0.6.7 cache-merge fields (Bug 1 fix) ===
|
|
21
|
+
* // Merged from the SessionStart worker's cache at
|
|
22
|
+
* // ~/.cache/aperant/apt-update-check.json when fresh (< 1 h,
|
|
23
|
+
* // schema_version=1). When stale, missing, or malformed, the same
|
|
24
|
+
* // field names are present with null/false/0 sentinel values so
|
|
25
|
+
* // the skill's parser stays single-shape.
|
|
26
|
+
* //
|
|
27
|
+
* // Note: framework.installed_version is intentionally absent.
|
|
28
|
+
* // The live kernel version is always in the top-level
|
|
29
|
+
* // `framework_version` field (read from package.json, never from
|
|
30
|
+
* // the cache).
|
|
31
|
+
* framework: {
|
|
32
|
+
* latest_version: '0.6.7' | null
|
|
33
|
+
* },
|
|
34
|
+
* update_available: false,
|
|
35
|
+
* dev_install_ahead: false,
|
|
36
|
+
* not_published: false,
|
|
37
|
+
* stale_files_total: 0,
|
|
38
|
+
* lookup_error: null
|
|
20
39
|
* }
|
|
21
40
|
*
|
|
22
41
|
* Per-file `reason` values:
|
|
@@ -31,6 +50,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
31
50
|
import { fileURLToPath } from 'node:url'
|
|
32
51
|
import { readManifest } from '../install/manifest.mjs'
|
|
33
52
|
import { detectInstalledRuntimes } from '../install/runtime-detect.mjs'
|
|
53
|
+
import { readFreshUpdateCache } from '../install/update-chips.mjs'
|
|
34
54
|
import { inspectManifestFiles } from '../install/version-header.mjs'
|
|
35
55
|
import { err, ok } from '../util/result.mjs'
|
|
36
56
|
|
|
@@ -56,6 +76,10 @@ function inspectRuntime(targetDir, descriptor) {
|
|
|
56
76
|
runtime: descriptor.id,
|
|
57
77
|
installed_version: null,
|
|
58
78
|
stale_files: [],
|
|
79
|
+
// 0.6.7 — per-runtime `update_available` is retained as `false` for
|
|
80
|
+
// backward shape compatibility with pre-0.6.7 callers. The real
|
|
81
|
+
// npm-bump signal now lives on the TOP-LEVEL `update_available`
|
|
82
|
+
// field (merged from the SessionStart worker's cache below).
|
|
59
83
|
update_available: false,
|
|
60
84
|
}
|
|
61
85
|
}
|
|
@@ -67,10 +91,14 @@ function inspectRuntime(targetDir, descriptor) {
|
|
|
67
91
|
runtime: descriptor.id,
|
|
68
92
|
installed_version: manifest.framework_version,
|
|
69
93
|
stale_files: stale,
|
|
94
|
+
// See note above — top-level `update_available` carries the real signal.
|
|
70
95
|
update_available: false,
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
|
|
99
|
+
/** Cache freshness window — 1 hour, matches the worker's CACHE_TTL_MS. */
|
|
100
|
+
const CACHE_TTL_MS = 60 * 60 * 1_000
|
|
101
|
+
|
|
74
102
|
/**
|
|
75
103
|
* @param {string} projectDir
|
|
76
104
|
*/
|
|
@@ -106,6 +134,39 @@ export function cmdCheckVersion(projectDir) {
|
|
|
106
134
|
messageParts.push(`Skills are up to date for ${runtimes.length} runtime(s).`)
|
|
107
135
|
}
|
|
108
136
|
|
|
137
|
+
// 0.6.7 (Bug 1 fix) — merge the SessionStart worker's cache fields when
|
|
138
|
+
// the cache file is fresh (< 1 h, schema_version=1). On a stale / missing
|
|
139
|
+
// / malformed cache the same field names are emitted with null/false/0
|
|
140
|
+
// sentinel values so the skill's parser stays single-shape (ID-03).
|
|
141
|
+
//
|
|
142
|
+
// LOG-001: stale_files_total is always derived from the live per-runtime
|
|
143
|
+
// walk (totalStale). The cache value is intentionally NOT merged — it is
|
|
144
|
+
// up to 1 h old and would hide drift that occurred since the last worker
|
|
145
|
+
// run.
|
|
146
|
+
//
|
|
147
|
+
// LOG-002: installed_version always comes from the kernel's own
|
|
148
|
+
// package.json (frameworkVersion). The cache's installed_version is
|
|
149
|
+
// stale by definition and is NOT merged into the envelope. Consumers
|
|
150
|
+
// that need the live kernel version use the top-level `framework_version`
|
|
151
|
+
// field, which is identical.
|
|
152
|
+
const cacheResult = readFreshUpdateCache(CACHE_TTL_MS)
|
|
153
|
+
let latestVersion = null
|
|
154
|
+
let updateAvailable = false
|
|
155
|
+
let devInstallAhead = false
|
|
156
|
+
let notPublished = false
|
|
157
|
+
let lookupError = null
|
|
158
|
+
if (cacheResult.fresh) {
|
|
159
|
+
const c = cacheResult.cache
|
|
160
|
+
const fw = c.framework && typeof c.framework === 'object' ? c.framework : {}
|
|
161
|
+
// latest_version is semver-validated inside readFreshUpdateCache (SEC-001).
|
|
162
|
+
if (typeof fw.latest_version === 'string') latestVersion = fw.latest_version
|
|
163
|
+
if (typeof c.update_available === 'boolean') updateAvailable = c.update_available
|
|
164
|
+
if (typeof c.dev_install_ahead === 'boolean') devInstallAhead = c.dev_install_ahead
|
|
165
|
+
if (typeof c.not_published === 'boolean') notPublished = c.not_published
|
|
166
|
+
// stale_files_total intentionally NOT taken from cache (LOG-001).
|
|
167
|
+
if (typeof c.lookup_error === 'string') lookupError = c.lookup_error
|
|
168
|
+
}
|
|
169
|
+
|
|
109
170
|
return ok({
|
|
110
171
|
status,
|
|
111
172
|
command: 'check-version',
|
|
@@ -113,5 +174,16 @@ export function cmdCheckVersion(projectDir) {
|
|
|
113
174
|
runtimes,
|
|
114
175
|
legacy_install: legacyInstall,
|
|
115
176
|
message: messageParts.join(' '),
|
|
177
|
+
// Cache-merge fields (additive, nullable). Documented in the
|
|
178
|
+
// file-header schema comment.
|
|
179
|
+
framework: {
|
|
180
|
+
latest_version: latestVersion,
|
|
181
|
+
},
|
|
182
|
+
update_available: updateAvailable,
|
|
183
|
+
dev_install_ahead: devInstallAhead,
|
|
184
|
+
not_published: notPublished,
|
|
185
|
+
// stale_files_total: always the live sum from per-runtime walks (LOG-001).
|
|
186
|
+
stale_files_total: totalStale,
|
|
187
|
+
lookup_error: lookupError,
|
|
116
188
|
})
|
|
117
189
|
}
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* reads the gh payload from that file instead of shelling out. The stub
|
|
17
17
|
* shape is `{ checks: [...], prMetadata: {...} }` — matches the classifier
|
|
18
18
|
* input contract in stop-matrix.mjs.
|
|
19
|
+
*
|
|
20
|
+
* Production seam: when `--payload-file <path>` is passed, the tick path
|
|
21
|
+
* reads the gh payload from that file. Same shape as the stub. The
|
|
22
|
+
* orchestrator skill (apt-watch-ci §3) captures `gh pr checks ...` +
|
|
23
|
+
* `gh pr view ...` into the payload file BEFORE invoking the cmd — the cmd
|
|
24
|
+
* itself never shells out to gh.
|
|
19
25
|
*/
|
|
20
26
|
|
|
21
27
|
import { randomBytes } from 'node:crypto'
|
|
@@ -290,7 +296,8 @@ function doTick(root, flags) {
|
|
|
290
296
|
})
|
|
291
297
|
}
|
|
292
298
|
|
|
293
|
-
const
|
|
299
|
+
const payloadFile = flags.get('payload-file')
|
|
300
|
+
const payload = payloadFile ? loadPayloadFile(resolve(root, payloadFile)) : fetchGhPayload(pr)
|
|
294
301
|
if (payload.error) {
|
|
295
302
|
// gh CLI not available / auth failed → infra-backoff. Don't count it,
|
|
296
303
|
// double the next-tick interval (capped at MAX_TICK_INTERVAL_MS), and reschedule.
|
|
@@ -480,10 +487,34 @@ function fetchGhPayload(_pr) {
|
|
|
480
487
|
// Real gh shell-out lives in the skill body. The cmd's tick treats
|
|
481
488
|
// "no stub + no payload" as infra-backoff so unit tests can exercise the
|
|
482
489
|
// path without a live gh binary. The orchestrator skill is responsible
|
|
483
|
-
// for pre-fetching and re-invoking with APT_TEST_GH_STUB set
|
|
484
|
-
//
|
|
490
|
+
// for pre-fetching and re-invoking with APT_TEST_GH_STUB set (tests) OR
|
|
491
|
+
// `--payload-file <path>` (production — see loadPayloadFile below).
|
|
485
492
|
return {
|
|
486
|
-
error:
|
|
493
|
+
error:
|
|
494
|
+
'gh shell-out is the skill orchestrator boundary — set APT_TEST_GH_STUB for unit tests or pass --payload-file for production',
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Read a gh payload from disk for the production path (FRAMEWORK-BUG-010).
|
|
500
|
+
* Returns the same shape as fetchGhPayload's stub branch on success, or an
|
|
501
|
+
* { error } envelope on missing path / parse failure — the existing
|
|
502
|
+
* infra-backoff branch surfaces the error cleanly.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} path absolute or already-resolved path
|
|
505
|
+
*/
|
|
506
|
+
function loadPayloadFile(path) {
|
|
507
|
+
try {
|
|
508
|
+
if (!existsSync(path)) {
|
|
509
|
+
return { error: `payload-file path not found: ${path}` }
|
|
510
|
+
}
|
|
511
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'))
|
|
512
|
+
return {
|
|
513
|
+
checks: data.checks || [],
|
|
514
|
+
prMetadata: data.prMetadata || { state: 'OPEN' },
|
|
515
|
+
}
|
|
516
|
+
} catch (e) {
|
|
517
|
+
return { error: `payload-file parse error: ${e.message}` }
|
|
487
518
|
}
|
|
488
519
|
}
|
|
489
520
|
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1184
|
+
effectiveAnswers.runtimes,
|
|
1093
1185
|
results,
|
|
1094
|
-
|
|
1186
|
+
effectiveAnswers,
|
|
1095
1187
|
interactiveTty,
|
|
1096
1188
|
kernelResult,
|
|
1097
1189
|
)
|
|
1098
1190
|
if (interactiveTty) {
|
|
1099
|
-
await renderOutro(
|
|
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
|
|
@@ -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
|
+
}
|
package/src/cli/config/load.mjs
CHANGED
|
@@ -20,6 +20,43 @@ export function loadProjectConfig(targetDir) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Load `.aperant/config.json` deep-merged with `.aperant/config.local.json`.
|
|
25
|
+
* Local wins on key collision. Nested objects merge key-by-key; arrays and
|
|
26
|
+
* primitives replace wholesale.
|
|
27
|
+
*
|
|
28
|
+
* Returns null if config.json is missing/malformed (same as loadProjectConfig).
|
|
29
|
+
* Throws Error("config.local.json parse error: <msg>") if config.local.json
|
|
30
|
+
* exists but is malformed — this is a user-visible failure surface; silent
|
|
31
|
+
* fallback would mask the user's broken override. See FRAMEWORK-AUDIT-001.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} targetDir
|
|
34
|
+
* @returns {object | null}
|
|
35
|
+
*/
|
|
36
|
+
export function loadMergedProjectConfig(targetDir) {
|
|
37
|
+
const shared = loadProjectConfig(targetDir)
|
|
38
|
+
if (shared === null) return null
|
|
39
|
+
const localPath = join(targetDir, '.aperant', 'config.local.json')
|
|
40
|
+
if (!existsSync(localPath)) return shared
|
|
41
|
+
let local
|
|
42
|
+
try {
|
|
43
|
+
local = JSON.parse(readFileSync(localPath, 'utf-8'))
|
|
44
|
+
} catch (e) {
|
|
45
|
+
throw new Error(`config.local.json parse error: ${e.message}`)
|
|
46
|
+
}
|
|
47
|
+
return deepMerge(shared, local)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function deepMerge(a, b) {
|
|
51
|
+
if (a === null || typeof a !== 'object' || Array.isArray(a)) return b
|
|
52
|
+
if (b === null || typeof b !== 'object' || Array.isArray(b)) return b
|
|
53
|
+
const out = { ...a }
|
|
54
|
+
for (const k of Object.keys(b)) {
|
|
55
|
+
out[k] = k in a ? deepMerge(a[k], b[k]) : b[k]
|
|
56
|
+
}
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
23
60
|
/**
|
|
24
61
|
* Return a normalized task_isolation config object with safe fallbacks.
|
|
25
62
|
* Always returns a concrete object even when the project has no config at all,
|
|
@@ -183,6 +183,12 @@ export function buildAperantGitignoreTemplate(policy = DEFAULT_SHARE_POLICY) {
|
|
|
183
183
|
lines.push('events/')
|
|
184
184
|
lines.push('locks/')
|
|
185
185
|
lines.push('team-status/')
|
|
186
|
+
// config.local.json — per-user override file (FRAMEWORK-AUDIT-001). The
|
|
187
|
+
// deny-by-default `*` rule above already ignores it because no allow line
|
|
188
|
+
// exists for it; this explicit deny is defense in depth so a future
|
|
189
|
+
// maintainer cannot accidentally un-ignore it with a broad `!config*.json`
|
|
190
|
+
// allow rule.
|
|
191
|
+
lines.push('config.local.json')
|
|
186
192
|
lines.push('')
|
|
187
193
|
lines.push('# ── Derived markdown indexes (regenerated from JSON) ──────────────')
|
|
188
194
|
lines.push('tasks/INDEX.md')
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* `warning` -> `major`. `suggestion|nitpick|note|info` are dropped.
|
|
11
11
|
*
|
|
12
12
|
* Status normalizes to `fixed | deferred | open` (default `open`).
|
|
13
|
-
* `resolved: true`
|
|
13
|
+
* Both `fixed: true` and `resolved: true` are treated as `status: 'fixed'`
|
|
14
|
+
* (the reviewer skill writes `fixed: true`; `resolved: true` is legacy).
|
|
14
15
|
*
|
|
15
16
|
* Output: {findings: [{id, severity, status, title, file}]}
|
|
16
17
|
*/
|
|
@@ -89,6 +90,7 @@ function normalizeSeverity(sev) {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
function normalizeStatus(f) {
|
|
93
|
+
if (f.fixed === true) return 'fixed'
|
|
92
94
|
if (f.resolved === true) return 'fixed'
|
|
93
95
|
if (typeof f.status === 'string') {
|
|
94
96
|
const s = f.status.toLowerCase().trim()
|
package/src/cli/dispatch.mjs
CHANGED
|
@@ -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
|
|
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 {
|