@aperant/framework 0.6.6 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +215 -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 +73 -5
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/health-check.mjs +1 -1
- package/dist/cli/commands/health-check.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts +17 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +183 -11
- 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/commands/route.d.mts.map +1 -1
- package/dist/cli/commands/route.mjs +37 -2
- package/dist/cli/commands/route.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +48 -0
- package/dist/cli/commands/task.mjs.map +1 -1
- 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/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +1 -0
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/host/detect.d.mts +122 -9
- package/dist/cli/host/detect.d.mts.map +1 -1
- package/dist/cli/host/detect.mjs +132 -0
- package/dist/cli/host/detect.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts +38 -0
- package/dist/cli/install/legacy-paths.d.mts.map +1 -0
- package/dist/cli/install/legacy-paths.mjs +69 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -0
- package/dist/cli/install/runtime-migrate.d.mts +84 -0
- package/dist/cli/install/runtime-migrate.d.mts.map +1 -0
- package/dist/cli/install/runtime-migrate.mjs +244 -0
- package/dist/cli/install/runtime-migrate.mjs.map +1 -0
- 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/cli/route/drift-detect.d.mts +20 -0
- package/dist/cli/route/drift-detect.d.mts.map +1 -0
- package/dist/cli/route/drift-detect.mjs +107 -0
- package/dist/cli/route/drift-detect.mjs.map +1 -0
- package/dist/cli/util/aperant-section.d.mts +34 -0
- package/dist/cli/util/aperant-section.d.mts.map +1 -0
- package/dist/cli/util/aperant-section.mjs +127 -0
- package/dist/cli/util/aperant-section.mjs.map +1 -0
- package/dist/cli/util/copy.d.mts +28 -1
- package/dist/cli/util/copy.d.mts.map +1 -1
- package/dist/cli/util/copy.mjs +43 -55
- package/dist/cli/util/copy.mjs.map +1 -1
- package/dist/cli/util/semver.d.mts +17 -0
- package/dist/cli/util/semver.d.mts.map +1 -0
- package/dist/cli/util/semver.mjs +29 -0
- package/dist/cli/util/semver.mjs.map +1 -0
- package/dist/cli/util/skill-installs.d.mts +65 -9
- package/dist/cli/util/skill-installs.d.mts.map +1 -1
- package/dist/cli/util/skill-installs.mjs +130 -21
- package/dist/cli/util/skill-installs.mjs.map +1 -1
- package/dist/cli/util/version-preflight.d.mts +44 -0
- package/dist/cli/util/version-preflight.d.mts.map +1 -0
- package/dist/cli/util/version-preflight.mjs +66 -0
- package/dist/cli/util/version-preflight.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 +30 -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 +63 -4
- package/skills/apt-spar/SKILL.md +36 -11
- package/skills/apt-update/SKILL.md +77 -10
- package/skills/apt-watch-ci/SKILL.md +6 -3
- package/src/cli/commands/check-version.mjs +73 -1
- package/src/cli/commands/ci-watch.mjs +74 -5
- package/src/cli/commands/health-check.mjs +1 -1
- package/src/cli/commands/init.mjs +202 -10
- package/src/cli/commands/release-notes.mjs +187 -0
- package/src/cli/commands/route.mjs +38 -2
- package/src/cli/commands/task.mjs +49 -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/help.mjs +1 -0
- package/src/cli/host/detect.mjs +135 -0
- package/src/cli/install/legacy-paths.mjs +69 -0
- package/src/cli/install/runtime-migrate.mjs +252 -0
- 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/src/cli/route/drift-detect.mjs +107 -0
- package/src/cli/util/aperant-section.mjs +136 -0
- package/src/cli/util/copy.mjs +43 -56
- package/src/cli/util/semver.mjs +28 -0
- package/src/cli/util/skill-installs.mjs +134 -21
- package/src/cli/util/version-preflight.mjs +65 -0
- package/templates/aperant-claude-md-appendix.md +37 -0
- package/templates/config.json +28 -3
- package/workflows/docs.md +12 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* util/aperant-section.mjs — generator for the CLAUDE.md / AGENTS.md
|
|
3
|
+
* "## Aperant Framework" section (FRAMEWORK-BUG-023).
|
|
4
|
+
*
|
|
5
|
+
* Replaces the previously-hardcoded `APT_SECTION` constant in copy.mjs.
|
|
6
|
+
* The skill TABLE is rebuilt from `discoverSkills` (filesystem source of
|
|
7
|
+
* truth — same helper the router uses at runtime) so install and runtime
|
|
8
|
+
* can never disagree about the catalog.
|
|
9
|
+
*
|
|
10
|
+
* Curated-doc risk: the appendix template
|
|
11
|
+
* (`templates/aperant-claude-md-appendix.md`) still contains hand-written
|
|
12
|
+
* sub-sections — "Fast Path Guarantee", "Discoverability —
|
|
13
|
+
* `apt-tools modes list`", "Feature Registry", "Config". These are small
|
|
14
|
+
* (~30 lines), rarely change, and rot would be visible (sections about
|
|
15
|
+
* non-existent features). Acceptable tradeoff per plan §Approach (c).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from 'node:fs'
|
|
19
|
+
import { join } from 'node:path'
|
|
20
|
+
import { getTrustedSkillSources } from '../config/load.mjs'
|
|
21
|
+
import { discoverSkills } from '../route/skill-discover.mjs'
|
|
22
|
+
|
|
23
|
+
const SECTION_START = '<!-- APT:framework-start -->'
|
|
24
|
+
const SECTION_END = '<!-- APT:framework-end -->'
|
|
25
|
+
const SECTION_HEADING = '## Aperant Framework'
|
|
26
|
+
const APPENDIX_REL_PATH = ['templates', 'aperant-claude-md-appendix.md']
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Escape a SKILL `description` for safe inclusion as a markdown table cell.
|
|
30
|
+
* The only character we have to defend against is the pipe (`|`) — it
|
|
31
|
+
* terminates the table column. `apt:triage` ships with a literal `|` in
|
|
32
|
+
* its description, so this is a live case, not a theoretical one.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} text
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function escapePipes(text) {
|
|
38
|
+
return text.replace(/\|/g, '\\|')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Deterministic sort: the bare `apt` router skill first, then ASCII-sorted
|
|
43
|
+
* by `name`. The router goes first so the generated table mirrors the user's
|
|
44
|
+
* mental model — the entry point, then everything reachable from it.
|
|
45
|
+
*
|
|
46
|
+
* @param {{ name: string }} a
|
|
47
|
+
* @param {{ name: string }} b
|
|
48
|
+
* @returns {number}
|
|
49
|
+
*/
|
|
50
|
+
function sortSkills(a, b) {
|
|
51
|
+
if (a.name === 'apt' && b.name !== 'apt') return -1
|
|
52
|
+
if (b.name === 'apt' && a.name !== 'apt') return 1
|
|
53
|
+
return a.name.localeCompare(b.name)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build the full "## Aperant Framework" section, wrapped in the
|
|
58
|
+
* `<!-- APT:framework-* -->` markers.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} opts
|
|
61
|
+
* @param {string} opts.canonicalRoot Absolute path to the framework root
|
|
62
|
+
* (the directory that contains
|
|
63
|
+
* `templates/aperant-claude-md-appendix.md`).
|
|
64
|
+
* @param {string} opts.targetDir Project root (passed to discoverSkills).
|
|
65
|
+
* @param {string} [opts.cli] Host CLI label (forwarded to
|
|
66
|
+
* getTrustedSkillSources so the install
|
|
67
|
+
* root for the runtime is included).
|
|
68
|
+
* @returns {string} The full section, including markers
|
|
69
|
+
* and trailing newline before the
|
|
70
|
+
* end-marker. No leading newline.
|
|
71
|
+
*/
|
|
72
|
+
export function buildAptSection({ canonicalRoot, targetDir, cli }) {
|
|
73
|
+
const trustedSkillSources = getTrustedSkillSources(targetDir, cli)
|
|
74
|
+
const discovered = discoverSkills({ targetDir, trustedSkillSources })
|
|
75
|
+
const skills = discovered.skills
|
|
76
|
+
.filter((s) => s.user_invocable === true && s.internal === false)
|
|
77
|
+
.slice()
|
|
78
|
+
.sort(sortSkills)
|
|
79
|
+
|
|
80
|
+
const rows = skills.map((s) => `| \`/${s.name}\` | ${escapePipes(s.description)} |`)
|
|
81
|
+
|
|
82
|
+
const tableBlock = [
|
|
83
|
+
'### Available Skills',
|
|
84
|
+
'| Skill | Description |',
|
|
85
|
+
'|-------|-------------|',
|
|
86
|
+
...rows,
|
|
87
|
+
].join('\n')
|
|
88
|
+
|
|
89
|
+
const appendixPath = join(canonicalRoot, ...APPENDIX_REL_PATH)
|
|
90
|
+
let appendix
|
|
91
|
+
try {
|
|
92
|
+
appendix = readFileSync(appendixPath, 'utf-8').replace(/\n+$/, '')
|
|
93
|
+
} catch (cause) {
|
|
94
|
+
const err = new Error(
|
|
95
|
+
`aperant-section: template not found at ${appendixPath} — re-install the framework package`,
|
|
96
|
+
{ cause },
|
|
97
|
+
)
|
|
98
|
+
err.code = 'aperant-section.template-missing'
|
|
99
|
+
throw err
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const body = [SECTION_HEADING, '', appendix, '', tableBlock].join('\n')
|
|
103
|
+
|
|
104
|
+
return `${SECTION_START}\n${body}\n${SECTION_END}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract the `/apt:<slug>` (or bare `/apt`) row IDs from the
|
|
109
|
+
* `<!-- APT:framework-* -->` section of an existing instruction file.
|
|
110
|
+
* Returns `[]` when the file has no APT section.
|
|
111
|
+
*
|
|
112
|
+
* Used by `injectInstructionFile` to detect foreign rows that would be
|
|
113
|
+
* silently clobbered by a regeneration — see `--force-overwrite-catalog`.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} text Full instruction-file contents.
|
|
116
|
+
* @returns {string[]} Row IDs in file order, e.g. `['/apt', '/apt:quick', …]`.
|
|
117
|
+
*/
|
|
118
|
+
export function extractAptSectionRowIds(text) {
|
|
119
|
+
const match = text.match(/<!-- APT:framework-start -->([\s\S]*?)<!-- APT:framework-end -->/)
|
|
120
|
+
if (!match) return []
|
|
121
|
+
// Strip HTML comments (including multiline) before scanning for row IDs so
|
|
122
|
+
// that commented-out example rows don't trigger false-positive foreign-row exits.
|
|
123
|
+
const section = match[1].replace(/<!--[\s\S]*?-->/g, '')
|
|
124
|
+
const ids = []
|
|
125
|
+
// Match table rows whose first cell is `/apt` or `/apt:<slug>` wrapped in
|
|
126
|
+
// backticks. The leading `|` plus optional whitespace anchors us to a
|
|
127
|
+
// markdown table cell; the trailing backtick + ` |` anchors the column
|
|
128
|
+
// boundary so we don't match prose mentions like "the `/apt` router".
|
|
129
|
+
const rowRe = /^\|\s*`(\/apt(?::[a-z0-9-]+)?)`\s*\|/gm
|
|
130
|
+
let m = rowRe.exec(section)
|
|
131
|
+
while (m !== null) {
|
|
132
|
+
ids.push(m[1])
|
|
133
|
+
m = rowRe.exec(section)
|
|
134
|
+
}
|
|
135
|
+
return ids
|
|
136
|
+
}
|
package/src/cli/util/copy.mjs
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* copy.mjs — filesystem copy + instruction-file injection helpers (C29).
|
|
3
3
|
*
|
|
4
|
-
* `copyDirRecursive` is a pure leaf
|
|
5
|
-
* CLAUDE.md / AGENTS.md "<!-- APT:framework-* -->" section
|
|
4
|
+
* `copyDirRecursive` is a pure leaf. `injectInstructionFile` owns the
|
|
5
|
+
* CLAUDE.md / AGENTS.md "<!-- APT:framework-* -->" section write path.
|
|
6
|
+
*
|
|
7
|
+
* The APT section TEXT is no longer assembled here — it is generated by
|
|
8
|
+
* `util/aperant-section.mjs#buildAptSection` and passed in (FRAMEWORK-BUG-023).
|
|
9
|
+
* Callers compute the section once (from the filesystem skill catalog) and
|
|
10
|
+
* hand it to this helper. The defensive foreign-row check stays in this
|
|
11
|
+
* file so the read/compare/write happens atomically against `filePath`.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
import {
|
|
@@ -14,6 +20,7 @@ import {
|
|
|
14
20
|
writeFileSync,
|
|
15
21
|
} from 'node:fs'
|
|
16
22
|
import { join } from 'node:path'
|
|
23
|
+
import { extractAptSectionRowIds } from './aperant-section.mjs'
|
|
17
24
|
|
|
18
25
|
export function copyDirRecursive(src, dest) {
|
|
19
26
|
mkdirSync(dest, { recursive: true })
|
|
@@ -28,68 +35,48 @@ export function copyDirRecursive(src, dest) {
|
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
| \`/apt:stress-test\` | Scenario simulation to catch gaps in plans |
|
|
53
|
-
| \`/apt:ship\` | Create PR with QA scores and decision traceability |
|
|
54
|
-
| \`/apt:pause\` | Human-initiated session handoff — save state when YOU need to stop (not for agent-initiated bailouts) |
|
|
55
|
-
| \`/apt:resume\` | Restore context from a paused session and route to next action |
|
|
56
|
-
| \`/apt:docs\` | Generate/update documentation verified against the actual codebase |
|
|
57
|
-
| \`/apt:setup\` | Change framework preferences (verification style, multi-model, branching) |
|
|
58
|
-
|
|
59
|
-
### Feature Registry
|
|
60
|
-
The feature registry (\`.aperant/features/\`) is a living inventory of every interactive
|
|
61
|
-
feature in the app. It's the source of truth for what to verify and review.
|
|
62
|
-
|
|
63
|
-
- **Created by:** \`/apt:scan\` (initial scan or full refresh)
|
|
64
|
-
- **Updated by:** \`/apt:execute\` post-step (\`apt-tools features-audit --apply-stubs\` auto-registers new UI/core/hook/IPC files per the docs-coherence policy)
|
|
65
|
-
- **Consumed by:** \`/apt:verify-proof\` (generates test cases from registry)
|
|
66
|
-
- **Checked by:** \`/apt:review\` Pass 6 (flags unregistered new components, applies stubs for auto-update policies)
|
|
67
|
-
|
|
68
|
-
Run \`/apt:scan\` after setting up Aperant on a brownfield project, or after major changes.
|
|
69
|
-
|
|
70
|
-
### Config
|
|
71
|
-
- Settings: \`.aperant/config.json\` (run \`/apt:setup\` to change)
|
|
72
|
-
- Features: \`.aperant/features/\` (feature registry -- run \`/apt:scan\` to create)
|
|
73
|
-
- State: \`.aperant/state.json\` (execution state)
|
|
74
|
-
- Evidence: \`.aperant/proof/\` (proof reports and screenshots)
|
|
75
|
-
- Prompts: \`.aperant/prompts/\` (agent prompt templates)
|
|
76
|
-
<!-- APT:framework-end -->`
|
|
77
|
-
|
|
78
|
-
export function injectInstructionFile(filePath) {
|
|
38
|
+
/**
|
|
39
|
+
* Write the (already-generated) APT section into `filePath`. Creates the
|
|
40
|
+
* file if missing; replaces the section in-place if the markers are
|
|
41
|
+
* present.
|
|
42
|
+
*
|
|
43
|
+
* FRAMEWORK-BUG-023 — before overwriting an existing section, extract the
|
|
44
|
+
* row IDs in the on-disk file and compare to the row IDs in the freshly
|
|
45
|
+
* generated section. If the file contains rows that are NOT in the
|
|
46
|
+
* generated set ("foreign rows"), the regen would silently clobber them.
|
|
47
|
+
* In that case we abort and let the caller surface a structured error.
|
|
48
|
+
* The `--force-overwrite-catalog` escape hatch flips `force: true`.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} filePath
|
|
51
|
+
* @param {Object} opts
|
|
52
|
+
* @param {string} opts.aptSection Output of buildAptSection() — wrapped
|
|
53
|
+
* in `<!-- APT:framework-* -->` markers.
|
|
54
|
+
* @param {boolean} [opts.force=false]
|
|
55
|
+
* Bypass the foreign-row check.
|
|
56
|
+
* @returns {{ status: 'created' | 'updated' | 'aborted-foreign-rows', written: boolean, foreignRowIds?: string[] }}
|
|
57
|
+
*/
|
|
58
|
+
export function injectInstructionFile(filePath, { aptSection, force = false }) {
|
|
79
59
|
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : ''
|
|
80
60
|
|
|
81
61
|
if (existing.includes('<!-- APT:framework-start -->')) {
|
|
82
|
-
|
|
62
|
+
if (!force) {
|
|
63
|
+
const existingIds = new Set(extractAptSectionRowIds(existing))
|
|
64
|
+
const generatedIds = new Set(extractAptSectionRowIds(aptSection))
|
|
65
|
+
const foreignRowIds = [...existingIds].filter((id) => !generatedIds.has(id))
|
|
66
|
+
if (foreignRowIds.length > 0) {
|
|
67
|
+
return { status: 'aborted-foreign-rows', written: false, foreignRowIds }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
83
70
|
const updated = existing.replace(
|
|
84
71
|
/<!-- APT:framework-start -->[\s\S]*?<!-- APT:framework-end -->/,
|
|
85
|
-
|
|
72
|
+
aptSection,
|
|
86
73
|
)
|
|
87
74
|
writeFileSync(filePath, updated, 'utf-8')
|
|
88
|
-
return 'updated'
|
|
75
|
+
return { status: 'updated', written: true }
|
|
89
76
|
}
|
|
90
77
|
|
|
91
|
-
// Append
|
|
78
|
+
// Append (greenfield create — no foreign-row concern).
|
|
92
79
|
const separator = existing.length > 0 ? '\n\n' : ''
|
|
93
|
-
writeFileSync(filePath, existing + separator +
|
|
94
|
-
return 'created'
|
|
80
|
+
writeFileSync(filePath, existing + separator + aptSection + '\n', 'utf-8')
|
|
81
|
+
return { status: 'created', written: true }
|
|
95
82
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* util/semver.mjs — shared semver helpers used by route/drift-detect.mjs
|
|
3
|
+
* and util/version-preflight.mjs. Extracted to avoid duplication (QUA-002).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Semver-shape pattern — mirrors install/update-chips.mjs:64. */
|
|
7
|
+
export const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compare two semver-shape strings. Returns -1 / 0 / 1. Ignores
|
|
11
|
+
* pre-release + build metadata (sufficient for "min version" checks
|
|
12
|
+
* where we only care about the MAJOR.MINOR.PATCH triple).
|
|
13
|
+
*
|
|
14
|
+
* @param {string} a
|
|
15
|
+
* @param {string} b
|
|
16
|
+
* @returns {-1 | 0 | 1}
|
|
17
|
+
*/
|
|
18
|
+
export function compareVersions(a, b) {
|
|
19
|
+
const pa = a.split(/[-+]/, 1)[0].split('.').map(Number)
|
|
20
|
+
const pb = b.split(/[-+]/, 1)[0].split('.').map(Number)
|
|
21
|
+
for (let i = 0; i < 3; i++) {
|
|
22
|
+
const av = pa[i] ?? 0
|
|
23
|
+
const bv = pb[i] ?? 0
|
|
24
|
+
if (av < bv) return -1
|
|
25
|
+
if (av > bv) return 1
|
|
26
|
+
}
|
|
27
|
+
return 0
|
|
28
|
+
}
|
|
@@ -1,37 +1,150 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* skill-installs.mjs — shared skill install surfaces per
|
|
2
|
+
* skill-installs.mjs — shared skill install surfaces per runtime (BUG-022).
|
|
3
|
+
*
|
|
4
|
+
* Source of truth for "what does the framework have installed on this
|
|
5
|
+
* project, per runtime, and is it current?" — the post-022 implementation
|
|
6
|
+
* is manifest-aware:
|
|
7
|
+
*
|
|
8
|
+
* 1. Per surface, prefer `apt-file-manifest.json` (the v0.6.7+ runtime
|
|
9
|
+
* manifest). When present and valid, `framework_version` is the
|
|
10
|
+
* authoritative answer.
|
|
11
|
+
* 2. Manifest present but malformed → reported as `installed: true`,
|
|
12
|
+
* `stale: true`, `source: 'manifest-malformed'`. Don't silently
|
|
13
|
+
* report healthy.
|
|
14
|
+
* 3. Else fall back to `legacy_version_file` (the pre-manifest
|
|
15
|
+
* `.apt-version` stamp; only defined for codex via
|
|
16
|
+
* `LEGACY_INSTALL_PATHS` today). `source: 'legacy_version_file'`.
|
|
17
|
+
* 4. Else → not installed for this surface.
|
|
18
|
+
*
|
|
19
|
+
* The surface array is derived from `RUNTIMES` (single source of truth in
|
|
20
|
+
* `runtime-detect.mjs`) so adding a new runtime there auto-extends every
|
|
21
|
+
* downstream diagnostic. The legacy fallback path is only populated for
|
|
22
|
+
* runtimes that declare a legacy topology in `LEGACY_INSTALL_PATHS`.
|
|
23
|
+
*
|
|
24
|
+
* Backward-compat: `host` field stays `'claude-code'` for the claude
|
|
25
|
+
* runtime (the statusline chip + several other consumers key on this
|
|
26
|
+
* exact string). The new `runtime` field exposes the descriptor id
|
|
27
|
+
* (`'claude'`, `'codex'`, …) for callers that want the canonical name.
|
|
3
28
|
*/
|
|
4
29
|
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
30
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
6
31
|
import { join, resolve } from 'node:path'
|
|
32
|
+
import { LEGACY_INSTALL_PATHS } from '../install/legacy-paths.mjs'
|
|
33
|
+
import { MANIFEST_FILENAME, readManifest } from '../install/manifest.mjs'
|
|
34
|
+
import { RUNTIMES } from '../install/runtime-detect.mjs'
|
|
7
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Map a runtime descriptor id to the `host` value historical consumers
|
|
38
|
+
* expect. The Claude runtime is the only one with a non-identity mapping
|
|
39
|
+
* (`'claude' → 'claude-code'`) — every other id is its own host string.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} runtimeId
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function hostFor(runtimeId) {
|
|
45
|
+
return runtimeId === 'claude' ? 'claude-code' : runtimeId
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} SkillInstallSurface
|
|
50
|
+
* @property {string} host Backward-compat host id.
|
|
51
|
+
* @property {string} runtime Canonical runtime id (from RUNTIMES).
|
|
52
|
+
* @property {string} install_root Absolute path of the runtime install root.
|
|
53
|
+
* @property {string} manifest_path Absolute path of apt-file-manifest.json.
|
|
54
|
+
* @property {string|null} legacy_version_file Absolute path of the legacy .apt-version stamp, or null.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Return one install-surface descriptor per runtime in `RUNTIMES`.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} projectDir
|
|
61
|
+
* @returns {SkillInstallSurface[]}
|
|
62
|
+
*/
|
|
8
63
|
export function getSkillInstallSurfaces(projectDir) {
|
|
9
64
|
const root = resolve(projectDir)
|
|
10
|
-
return
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}))
|
|
65
|
+
return RUNTIMES.map((descriptor) => {
|
|
66
|
+
const install_root = join(root, descriptor.installRoot)
|
|
67
|
+
const legacyTopology = LEGACY_INSTALL_PATHS[descriptor.id]
|
|
68
|
+
const legacy_version_file = legacyTopology ? join(root, legacyTopology.version_file) : null
|
|
69
|
+
return {
|
|
70
|
+
host: hostFor(descriptor.id),
|
|
71
|
+
runtime: descriptor.id,
|
|
72
|
+
install_root,
|
|
73
|
+
manifest_path: join(install_root, MANIFEST_FILENAME),
|
|
74
|
+
legacy_version_file,
|
|
75
|
+
}
|
|
76
|
+
})
|
|
23
77
|
}
|
|
24
78
|
|
|
79
|
+
/**
|
|
80
|
+
* @typedef {Object} SkillInstallState
|
|
81
|
+
* @property {string} host
|
|
82
|
+
* @property {string} runtime
|
|
83
|
+
* @property {string} install_root
|
|
84
|
+
* @property {string} manifest_path
|
|
85
|
+
* @property {string|null} legacy_version_file
|
|
86
|
+
* @property {boolean} installed
|
|
87
|
+
* @property {string|null} installed_version
|
|
88
|
+
* @property {boolean} stale
|
|
89
|
+
* @property {'manifest' | 'manifest-malformed' | 'legacy_version_file' | null} source
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read each surface's install state. See the file header for the
|
|
94
|
+
* precedence rules.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} projectDir
|
|
97
|
+
* @param {string} frameworkVersion
|
|
98
|
+
* @returns {SkillInstallState[]}
|
|
99
|
+
*/
|
|
25
100
|
export function readSkillInstallState(projectDir, frameworkVersion) {
|
|
26
101
|
return getSkillInstallSurfaces(projectDir).map((surface) => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
102
|
+
const manifestExists = existsSync(surface.manifest_path)
|
|
103
|
+
if (manifestExists) {
|
|
104
|
+
const manifest = readManifest(surface.install_root)
|
|
105
|
+
if (manifest && typeof manifest.framework_version === 'string') {
|
|
106
|
+
return {
|
|
107
|
+
...surface,
|
|
108
|
+
installed: true,
|
|
109
|
+
installed_version: manifest.framework_version,
|
|
110
|
+
stale: manifest.framework_version !== frameworkVersion,
|
|
111
|
+
source: /** @type {const} */ ('manifest'),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Manifest file present but parser rejected it (malformed JSON,
|
|
115
|
+
// missing required fields, …). Don't silently report healthy.
|
|
116
|
+
return {
|
|
117
|
+
...surface,
|
|
118
|
+
installed: true,
|
|
119
|
+
installed_version: null,
|
|
120
|
+
stale: true,
|
|
121
|
+
source: /** @type {const} */ ('manifest-malformed'),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (surface.legacy_version_file && existsSync(surface.legacy_version_file)) {
|
|
126
|
+
try {
|
|
127
|
+
if (statSync(surface.legacy_version_file).isFile()) {
|
|
128
|
+
const version = readFileSync(surface.legacy_version_file, 'utf-8').trim()
|
|
129
|
+
return {
|
|
130
|
+
...surface,
|
|
131
|
+
installed: true,
|
|
132
|
+
installed_version: version,
|
|
133
|
+
stale: version !== frameworkVersion,
|
|
134
|
+
source: /** @type {const} */ ('legacy_version_file'),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
/* fall through to "not installed" */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
30
142
|
return {
|
|
31
143
|
...surface,
|
|
32
|
-
installed:
|
|
33
|
-
installed_version,
|
|
34
|
-
stale:
|
|
144
|
+
installed: false,
|
|
145
|
+
installed_version: null,
|
|
146
|
+
stale: false,
|
|
147
|
+
source: null,
|
|
35
148
|
}
|
|
36
149
|
})
|
|
37
150
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* util/version-preflight.mjs — FRAMEWORK-BUG-017 (rung 2 of 3).
|
|
3
|
+
*
|
|
4
|
+
* Pure helper for cmds that depend on a flag / behavior introduced in a
|
|
5
|
+
* specific framework version. When the installed kernel predates that
|
|
6
|
+
* version, returning a structured `version_drift` envelope is strictly
|
|
7
|
+
* better than the silent infra-backoff loop that bit us in
|
|
8
|
+
* FRAMEWORK-BUG-010.
|
|
9
|
+
*
|
|
10
|
+
* Scope (Codex review edit 1): this is a **future-guard** for the next
|
|
11
|
+
* framework fix that introduces a new flag. A stale 0.6.6 install
|
|
12
|
+
* obviously cannot execute 0.6.8 preflight code (the helper doesn't
|
|
13
|
+
* exist there). The actual catch-point for already-stale installs is
|
|
14
|
+
* the route-side `detectSourceWorkspaceDrift`. This helper makes the
|
|
15
|
+
* NEXT bug bundle cheaper to ship safely.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { compareVersions, SEMVER_RE } from './semver.mjs'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Preflight check: is the installed kernel new enough to support the
|
|
22
|
+
* named feature?
|
|
23
|
+
*
|
|
24
|
+
* - Returns `null` when the install is at-or-above `introducedIn` (pass).
|
|
25
|
+
* - Returns `null` when `installedVersion` is malformed (fail-open — a
|
|
26
|
+
* corrupted `package.json` shouldn't lock users out of every cmd; the
|
|
27
|
+
* route-level drift check still catches the same install via the
|
|
28
|
+
* version mismatch path).
|
|
29
|
+
* - Returns a `version_drift` error envelope when the install predates
|
|
30
|
+
* `introducedIn`. Callers `exitWith(envelope, 1)`.
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} args
|
|
33
|
+
* @param {string} args.feature Short name of the feature being gated.
|
|
34
|
+
* @param {string} args.introducedIn Minimum framework version that ships the feature.
|
|
35
|
+
* @param {string} args.installedVersion The installed kernel's version.
|
|
36
|
+
* @param {string} args.missingFlag The CLI flag that requires this version (e.g. '--payload-file').
|
|
37
|
+
* @returns {null | {
|
|
38
|
+
* status: 'error',
|
|
39
|
+
* command: 'version-preflight',
|
|
40
|
+
* error_code: 'version_drift',
|
|
41
|
+
* feature: string,
|
|
42
|
+
* introduced_in: string,
|
|
43
|
+
* installed_version: string,
|
|
44
|
+
* missing_flag: string,
|
|
45
|
+
* reason: string,
|
|
46
|
+
* }}
|
|
47
|
+
*/
|
|
48
|
+
export function requireMinVersion({ feature, introducedIn, installedVersion, missingFlag }) {
|
|
49
|
+
if (!installedVersion || typeof installedVersion !== 'string') return null
|
|
50
|
+
if (!SEMVER_RE.test(installedVersion)) return null
|
|
51
|
+
if (!SEMVER_RE.test(introducedIn)) return null
|
|
52
|
+
|
|
53
|
+
if (compareVersions(installedVersion, introducedIn) >= 0) return null
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
status: 'error',
|
|
57
|
+
command: 'version-preflight',
|
|
58
|
+
error_code: 'version_drift',
|
|
59
|
+
feature,
|
|
60
|
+
introduced_in: introducedIn,
|
|
61
|
+
installed_version: installedVersion,
|
|
62
|
+
missing_flag: missingFlag,
|
|
63
|
+
reason: `cmd needs feature ${feature} introduced in version ${introducedIn}; installed version is ${installedVersion} — run /apt:update`,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
This project uses the [Aperant Framework](https://github.com/Mikalsen-AI/aperant-cloud-desktop/tree/main/packages/framework) for AI-assisted development.
|
|
2
|
+
|
|
3
|
+
### Fast Path Guarantee (Pocock adoption ID-05)
|
|
4
|
+
|
|
5
|
+
QUICK track is **hard-exempt** from PRD-shaped spec.md, HITL/AFK metadata,
|
|
6
|
+
vertical-slice subtask schema, TDD iron law (`apt:execute --tdd`), triage
|
|
7
|
+
prompts, and any new heavy interaction discipline. `apt:quick` and any
|
|
8
|
+
task router-classified as QUICK run the existing one-shot pattern
|
|
9
|
+
unchanged. This is a **constraint on every implementing change**, not a
|
|
10
|
+
configuration toggle. Dedicated `*-quick-exempt.test.ts` regression tests
|
|
11
|
+
in `packages/framework/src/__tests__/` enforce the exemption.
|
|
12
|
+
|
|
13
|
+
### Discoverability — `apt-tools modes list`
|
|
14
|
+
|
|
15
|
+
Power-user surface that enumerates every skill's `modes`, `flags`,
|
|
16
|
+
`content_formats` (from optional `produces:` frontmatter), and
|
|
17
|
+
`config_keys`. Adding a new skill auto-appears via filesystem scan
|
|
18
|
+
without code changes. Mental-model builder for "what can the framework
|
|
19
|
+
do?" without reading 30 SKILL.md files.
|
|
20
|
+
|
|
21
|
+
### Feature Registry
|
|
22
|
+
The feature registry (`.aperant/features/`) is a living inventory of every interactive
|
|
23
|
+
feature in the app. It's the source of truth for what to verify and review.
|
|
24
|
+
|
|
25
|
+
- **Created by:** `/apt:scan` (initial scan or full refresh)
|
|
26
|
+
- **Updated by:** `/apt:execute` post-step (`apt-tools features-audit --apply-stubs` auto-registers new UI/core/hook/IPC files per the docs-coherence policy)
|
|
27
|
+
- **Consumed by:** `/apt:verify-proof` (generates test cases from registry)
|
|
28
|
+
- **Checked by:** `/apt:review` Pass 6 (flags unregistered new components, applies stubs for auto-update policies)
|
|
29
|
+
|
|
30
|
+
Run `/apt:scan` after setting up Aperant on a brownfield project, or after major changes.
|
|
31
|
+
|
|
32
|
+
### Config
|
|
33
|
+
- Settings: `.aperant/config.json` (run `/apt:setup` to change)
|
|
34
|
+
- Features: `.aperant/features/` (feature registry -- run `/apt:scan` to create)
|
|
35
|
+
- State: `.aperant/state.json` (execution state)
|
|
36
|
+
- Evidence: `.aperant/proof/` (proof reports and screenshots)
|
|
37
|
+
- Prompts: `.aperant/prompts/` (agent prompt templates)
|
package/templates/config.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 5,
|
|
3
3
|
"project_type": "unknown",
|
|
4
4
|
"share": {
|
|
5
5
|
"$comment": "Artifact-sharing policy (C35). Source of truth: packages/framework/src/cli/config/share-policy.mjs. visibility = 'solo' | 'team' | 'oss' preset; individual share.* toggles override it. share.digests = null inherits from config.digests.enabled.",
|
|
@@ -107,12 +107,37 @@
|
|
|
107
107
|
"debate_mode": "auto"
|
|
108
108
|
},
|
|
109
109
|
"diagram": {
|
|
110
|
-
"mode": "file"
|
|
111
|
-
"mcpConsent": null
|
|
110
|
+
"mode": "file"
|
|
112
111
|
},
|
|
113
112
|
"docs_policy": {
|
|
114
113
|
"$comment": "C33 docs-coherence policy. Override via /apt:setup Batch 7. Keys are globs/paths; values are 'auto-update' | 'flag-drift' | 'skip'. Missing block → defaults from packages/framework/templates/docs-policy-defaults.json apply."
|
|
115
114
|
},
|
|
115
|
+
"changelog": {
|
|
116
|
+
"$comment": "v5 — persona-aware changelog + release-notes system. Two surfaces: dev changelog (CHANGELOG.md from commits, default ON) and user-facing release notes (docs/releases, default OFF). Opt in per project via /apt:setup Batch 9. See packages/framework/skills/apt-release-notes/SKILL.md.",
|
|
117
|
+
"dev": {
|
|
118
|
+
"enabled": true,
|
|
119
|
+
"file": "CHANGELOG.md",
|
|
120
|
+
"format": "keep-a-changelog",
|
|
121
|
+
"source": "commits"
|
|
122
|
+
},
|
|
123
|
+
"release_notes": {
|
|
124
|
+
"enabled": false,
|
|
125
|
+
"audience": null,
|
|
126
|
+
"output_dir": "docs/releases",
|
|
127
|
+
"persona_filter": "primary+secondary",
|
|
128
|
+
"require_pr_field": true,
|
|
129
|
+
"draft": {
|
|
130
|
+
"on_pr_merge": true,
|
|
131
|
+
"fragments_dir": "docs/releases/_unreleased"
|
|
132
|
+
},
|
|
133
|
+
"compile": {
|
|
134
|
+
"on_tag": true
|
|
135
|
+
},
|
|
136
|
+
"publish": {
|
|
137
|
+
"targets": ["github-release"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
116
141
|
"router": {
|
|
117
142
|
"llm": {
|
|
118
143
|
"enabled": true,
|
package/workflows/docs.md
CHANGED
|
@@ -90,6 +90,18 @@ Gather facts from the actual codebase. The depth depends on doc type:
|
|
|
90
90
|
- Conventional commit messages to categorize changes
|
|
91
91
|
- Breaking changes (commits with `BREAKING CHANGE` or `!:`)
|
|
92
92
|
|
|
93
|
+
> **Two-skill split (v0.7.0+).** `/apt:docs changelog` owns the
|
|
94
|
+
> dev-flavored `CHANGELOG.md` surface — internal task IDs, file paths,
|
|
95
|
+
> schema-bump notes, process trivia. For the user-facing complement,
|
|
96
|
+
> projects can opt in to `/apt:release-notes` via `/apt:setup` →
|
|
97
|
+
> Batch 9. That skill produces persona-voice-tuned release notes
|
|
98
|
+
> (towncrier-style fragments at PR-merge time, compiled per tag, pushed
|
|
99
|
+
> to GitHub Releases). The two skills coexist intentionally: a single
|
|
100
|
+
> `--audience` flag inside `/apt:docs changelog` drifts to a
|
|
101
|
+
> lowest-common-denominator artifact that satisfies neither audience.
|
|
102
|
+
> See `packages/framework/skills/apt-release-notes/SKILL.md` for the
|
|
103
|
+
> opt-in surface and the three modes (draft / compile / publish).
|
|
104
|
+
|
|
93
105
|
### For Contributing
|
|
94
106
|
- Development setup steps (verified by checking file existence)
|
|
95
107
|
- Test commands (verified by running them)
|