@aperant/framework 0.6.3 → 0.6.5
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 +61 -0
- package/README.md +64 -10
- package/agents/apt-improver.md +99 -0
- package/agents/apt-planner.md +115 -10
- package/dist/__test-helpers/run-cmd.d.mts +4 -2
- package/dist/__test-helpers/run-cmd.d.mts.map +1 -1
- package/dist/__test-helpers/run-cmd.mjs +56 -15
- package/dist/__test-helpers/run-cmd.mjs.map +1 -1
- package/dist/cli/artifacts/classification.d.mts.map +1 -1
- package/dist/cli/artifacts/classification.mjs +10 -0
- package/dist/cli/artifacts/classification.mjs.map +1 -1
- package/dist/cli/ci-watch/lock.d.mts +53 -0
- package/dist/cli/ci-watch/lock.d.mts.map +1 -0
- package/dist/cli/ci-watch/lock.mjs +172 -0
- package/dist/cli/ci-watch/lock.mjs.map +1 -0
- package/dist/cli/ci-watch/state.d.mts +36 -0
- package/dist/cli/ci-watch/state.d.mts.map +1 -0
- package/dist/cli/ci-watch/state.mjs +103 -0
- package/dist/cli/ci-watch/state.mjs.map +1 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts +58 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -0
- package/dist/cli/ci-watch/stop-matrix.mjs +164 -0
- package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -0
- package/dist/cli/cli-wrappers/ci-watch.d.mts +2 -0
- package/dist/cli/cli-wrappers/ci-watch.d.mts.map +1 -0
- package/dist/cli/cli-wrappers/ci-watch.mjs +9 -0
- package/dist/cli/cli-wrappers/ci-watch.mjs.map +1 -0
- package/dist/cli/commands/adr.d.mts +5 -0
- package/dist/cli/commands/adr.d.mts.map +1 -0
- package/dist/cli/commands/adr.mjs +228 -0
- package/dist/cli/commands/adr.mjs.map +1 -0
- package/dist/cli/commands/ci-watch.d.mts +7 -0
- package/dist/cli/commands/ci-watch.d.mts.map +1 -0
- package/dist/cli/commands/ci-watch.mjs +465 -0
- package/dist/cli/commands/ci-watch.mjs.map +1 -0
- package/dist/cli/commands/context.d.mts +7 -0
- package/dist/cli/commands/context.d.mts.map +1 -0
- package/dist/cli/commands/context.mjs +224 -0
- package/dist/cli/commands/context.mjs.map +1 -0
- package/dist/cli/commands/event.d.mts.map +1 -1
- package/dist/cli/commands/event.mjs +59 -24
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/host-detect.d.mts +1 -1
- package/dist/cli/commands/host-detect.d.mts.map +1 -1
- package/dist/cli/commands/host-detect.mjs +30 -3
- package/dist/cli/commands/host-detect.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +73 -5
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/modes.d.mts +13 -0
- package/dist/cli/commands/modes.d.mts.map +1 -0
- package/dist/cli/commands/modes.mjs +220 -0
- package/dist/cli/commands/modes.mjs.map +1 -0
- package/dist/cli/commands/pr-review-audit-fixer.d.mts +41 -2
- package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.mjs +91 -14
- package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
- package/dist/cli/commands/route.d.mts.map +1 -1
- package/dist/cli/commands/route.mjs +11 -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 +136 -6
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/tokens.d.mts.map +1 -1
- package/dist/cli/commands/tokens.mjs +150 -6
- package/dist/cli/commands/tokens.mjs.map +1 -1
- package/dist/cli/commands/triage.d.mts +8 -0
- package/dist/cli/commands/triage.d.mts.map +1 -0
- package/dist/cli/commands/triage.mjs +259 -0
- package/dist/cli/commands/triage.mjs.map +1 -0
- package/dist/cli/config/post-merge-sweep.d.mts +18 -0
- package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
- package/dist/cli/config/post-merge-sweep.mjs +48 -3
- package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
- package/dist/cli/consistency/registry.d.mts +11 -0
- package/dist/cli/consistency/registry.d.mts.map +1 -1
- package/dist/cli/consistency/registry.mjs +13 -0
- package/dist/cli/consistency/registry.mjs.map +1 -1
- package/dist/cli/coordination/agent-identity.d.mts +9 -0
- package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
- package/dist/cli/coordination/agent-identity.mjs +11 -0
- package/dist/cli/coordination/agent-identity.mjs.map +1 -1
- package/dist/cli/coordination/event-log.d.mts +27 -0
- package/dist/cli/coordination/event-log.d.mts.map +1 -1
- package/dist/cli/coordination/event-log.mjs +72 -2
- package/dist/cli/coordination/event-log.mjs.map +1 -1
- package/dist/cli/design/frontmatter-schema.d.mts +3 -3
- package/dist/cli/design/frontmatter-schema.d.mts.map +1 -1
- package/dist/cli/design/frontmatter-schema.mjs +3 -1
- package/dist/cli/design/frontmatter-schema.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +14 -1
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/registry.d.mts +11 -0
- package/dist/cli/gate/registry.d.mts.map +1 -1
- package/dist/cli/gate/registry.mjs +13 -0
- package/dist/cli/gate/registry.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 +1 -0
- package/dist/cli/host/detect.d.mts.map +1 -1
- package/dist/cli/host/detect.mjs +5 -0
- package/dist/cli/host/detect.mjs.map +1 -1
- package/dist/cli/route/envelope.d.mts +68 -4
- package/dist/cli/route/envelope.d.mts.map +1 -1
- package/dist/cli/route/envelope.mjs +140 -103
- package/dist/cli/route/envelope.mjs.map +1 -1
- package/dist/cli/route/skill-discover.d.mts +11 -0
- package/dist/cli/route/skill-discover.d.mts.map +1 -1
- package/dist/cli/route/skill-discover.mjs +46 -1
- package/dist/cli/route/skill-discover.mjs.map +1 -1
- package/dist/cli/skill-author/contract.d.mts +20 -0
- package/dist/cli/skill-author/contract.d.mts.map +1 -1
- package/dist/cli/skill-author/contract.mjs +27 -0
- package/dist/cli/skill-author/contract.mjs.map +1 -1
- package/dist/cli/skill-author/skill-template.d.mts.map +1 -1
- package/dist/cli/skill-author/skill-template.mjs +4 -3
- package/dist/cli/skill-author/skill-template.mjs.map +1 -1
- package/dist/cli/task/worktree-cleanup.d.mts +9 -1
- package/dist/cli/task/worktree-cleanup.d.mts.map +1 -1
- package/dist/cli/task/worktree-cleanup.mjs +190 -9
- package/dist/cli/task/worktree-cleanup.mjs.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/apt-planner.md +1 -1
- package/dist/plugin/skills/apt/SKILL.md +111 -5
- package/dist/plugin/skills/apt-author-skill/SKILL.md +11 -0
- package/dist/plugin/skills/apt-bootstrap/SKILL.md +1 -0
- package/dist/plugin/skills/apt-classify/SKILL.md +1 -0
- package/dist/plugin/skills/apt-close-task/SKILL.md +1 -0
- package/dist/plugin/skills/apt-create-docs/SKILL.md +1 -0
- package/dist/plugin/skills/apt-debug/SKILL.md +2 -0
- package/dist/plugin/skills/apt-design/SKILL.md +2 -0
- package/dist/plugin/skills/apt-discuss/SKILL.md +2 -0
- package/dist/plugin/skills/apt-docs/SKILL.md +2 -0
- package/dist/plugin/skills/apt-execute/SKILL.md +1 -0
- package/dist/plugin/skills/apt-mockup/SKILL.md +2 -0
- package/dist/plugin/skills/apt-pause/SKILL.md +1 -0
- package/dist/plugin/skills/apt-personas/SKILL.md +1 -0
- package/dist/plugin/skills/apt-plan/SKILL.md +2 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -0
- package/dist/plugin/skills/apt-quick/SKILL.md +2 -0
- package/dist/plugin/skills/apt-resume/SKILL.md +1 -0
- package/dist/plugin/skills/apt-review/SKILL.md +1 -0
- package/dist/plugin/skills/apt-roadmap/SKILL.md +1 -0
- package/dist/plugin/skills/apt-roundtable/SKILL.md +2 -0
- package/dist/plugin/skills/apt-run/SKILL.md +1 -0
- package/dist/plugin/skills/apt-scan/SKILL.md +1 -0
- package/dist/plugin/skills/apt-setup/SKILL.md +1 -0
- package/dist/plugin/skills/apt-ship/SKILL.md +6 -5
- package/dist/plugin/skills/apt-stress-test/SKILL.md +1 -0
- package/dist/plugin/skills/apt-terminal/SKILL.md +1 -0
- package/dist/plugin/skills/apt-update/SKILL.md +5 -0
- package/dist/plugin/skills/apt-verify/SKILL.md +1 -0
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +1 -0
- package/dist/types/config.d.ts +85 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +125 -122
- package/prompts/coder.md +2 -0
- package/prompts/planner.md +12 -0
- package/prompts/spec_writer.md +9 -0
- package/skills/apt/SKILL.md +112 -5
- package/skills/apt-author-skill/SKILL.md +11 -0
- package/skills/apt-bootstrap/SKILL.md +1 -0
- package/skills/apt-classify/SKILL.md +1 -0
- package/skills/apt-close-task/SKILL.md +33 -1
- package/skills/apt-create-docs/SKILL.md +1 -0
- package/skills/apt-debug/SKILL.md +41 -6
- package/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
- package/skills/apt-design/SKILL.md +2 -0
- package/skills/apt-diagram/SKILL.md +342 -0
- package/skills/apt-diagram/appendices/design-discipline.md +97 -0
- package/skills/apt-discuss/SKILL.md +27 -0
- package/skills/apt-discuss/appendices/grill-discipline.md +104 -0
- package/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
- package/skills/apt-docs/SKILL.md +2 -0
- package/skills/apt-execute/SKILL.md +49 -5
- package/skills/apt-execute/appendices/tdd-mode.md +107 -0
- package/skills/apt-improve/DEEPENING.md +84 -0
- package/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
- package/skills/apt-improve/LANGUAGE.md +104 -0
- package/skills/apt-improve/SKILL.md +141 -0
- package/skills/apt-mockup/SKILL.md +2 -0
- package/skills/apt-pause/SKILL.md +1 -0
- package/skills/apt-personas/SKILL.md +1 -0
- package/skills/apt-plan/SKILL.md +149 -4
- package/skills/apt-planner.md +42 -1
- package/skills/apt-pr-review/SKILL.md +47 -16
- package/skills/apt-prototype/LOGIC.md +109 -0
- package/skills/apt-prototype/SKILL.md +143 -0
- package/skills/apt-prototype/UI.md +90 -0
- package/skills/apt-quick/SKILL.md +32 -0
- package/skills/apt-resume/SKILL.md +1 -0
- package/skills/apt-review/SKILL.md +3 -0
- package/skills/apt-roadmap/SKILL.md +1 -0
- package/skills/apt-roundtable/SKILL.md +2 -0
- package/skills/apt-run/SKILL.md +33 -4
- package/skills/apt-scan/SKILL.md +1 -0
- package/skills/apt-setup/SKILL.md +129 -2
- package/skills/apt-ship/SKILL.md +52 -4
- package/skills/apt-stress-test/SKILL.md +1 -0
- package/skills/apt-terminal/SKILL.md +1 -0
- package/skills/apt-triage/AGENT-BRIEF.md +84 -0
- package/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
- package/skills/apt-triage/SKILL.md +169 -0
- package/skills/apt-update/SKILL.md +3 -0
- package/skills/apt-verify/SKILL.md +4 -0
- package/skills/apt-verify-proof/SKILL.md +4 -0
- package/skills/apt-watch-ci/SKILL.md +163 -0
- package/skills/apt-zoom-out/SKILL.md +130 -0
- package/src/cli/artifacts/classification.mjs +10 -0
- package/src/cli/ci-watch/lock.mjs +178 -0
- package/src/cli/ci-watch/state.mjs +103 -0
- package/src/cli/ci-watch/stop-matrix.mjs +181 -0
- package/src/cli/cli-wrappers/ci-watch.mjs +9 -0
- package/src/cli/commands/adr.mjs +243 -0
- package/src/cli/commands/ci-watch.mjs +503 -0
- package/src/cli/commands/context.mjs +244 -0
- package/src/cli/commands/event.mjs +63 -24
- package/src/cli/commands/host-detect.mjs +33 -7
- package/src/cli/commands/init.mjs +83 -5
- package/src/cli/commands/modes.mjs +215 -0
- package/src/cli/commands/pr-review-audit-fixer.mjs +95 -16
- package/src/cli/commands/route.mjs +11 -2
- package/src/cli/commands/task.mjs +152 -18
- package/src/cli/commands/tokens.mjs +157 -6
- package/src/cli/commands/triage.mjs +277 -0
- package/src/cli/config/post-merge-sweep.mjs +49 -3
- package/src/cli/consistency/registry.mjs +14 -0
- package/src/cli/coordination/agent-identity.mjs +12 -0
- package/src/cli/coordination/event-log.mjs +73 -2
- package/src/cli/design/frontmatter-schema.mjs +3 -1
- package/src/cli/dispatch.mjs +15 -1
- package/src/cli/gate/registry.mjs +14 -0
- package/src/cli/help.mjs +1 -0
- package/src/cli/host/detect.mjs +5 -0
- package/src/cli/route/envelope.mjs +140 -106
- package/src/cli/route/skill-discover.mjs +46 -1
- package/src/cli/skill-author/contract.mjs +29 -0
- package/src/cli/skill-author/skill-template.mjs +4 -3
- package/src/cli/task/worktree-cleanup.mjs +191 -9
- package/templates/adr-format.md +56 -0
- package/templates/config.json +4 -0
- package/templates/context-format.md +34 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/modes.mjs — `apt-tools modes list` (Pocock adoption ST-13, AC12).
|
|
3
|
+
*
|
|
4
|
+
* Power-user discoverability tool. Returns a JSON envelope with one
|
|
5
|
+
* record per discovered skill, exposing modes / flags / content-formats /
|
|
6
|
+
* config-driven behavior so a power user can build a mental model of
|
|
7
|
+
* the framework without reading 30 SKILL.md files.
|
|
8
|
+
*
|
|
9
|
+
* Adding a new skill auto-appears in `modes list` output without any
|
|
10
|
+
* code changes — the discovery is filesystem-scan-based (mirrors the
|
|
11
|
+
* router's eager skill-discover module).
|
|
12
|
+
*
|
|
13
|
+
* CLI:
|
|
14
|
+
* apt-tools modes list [project-dir]
|
|
15
|
+
*
|
|
16
|
+
* Output envelope:
|
|
17
|
+
* { status: 'ok', command: 'modes-list',
|
|
18
|
+
* skills: [{ name, modes, flags, content_formats, config_keys, file_path }] }
|
|
19
|
+
*
|
|
20
|
+
* Field semantics:
|
|
21
|
+
* - name — frontmatter `name` (e.g. "apt:plan")
|
|
22
|
+
* - modes — frontmatter `execution_modes` (array)
|
|
23
|
+
* - flags — parsed from frontmatter `argument-hint`
|
|
24
|
+
* - content_formats — frontmatter `produces` (optional array; [] when absent)
|
|
25
|
+
* - config_keys — hardcoded skill→config-paths map maintained inline
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
29
|
+
import { join, resolve } from 'node:path'
|
|
30
|
+
import { parse as parseYaml } from 'yaml'
|
|
31
|
+
import { ok } from '../util/result.mjs'
|
|
32
|
+
|
|
33
|
+
const FRONTMATTER_RE = /^---\s*([\s\S]*?)\s*---/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hardcoded skill→config-paths map. Maintained inline so adding a new
|
|
37
|
+
* config-dependent skill is a one-line change.
|
|
38
|
+
*
|
|
39
|
+
* Per AC12, this is the mapping `modes list` consumes to enumerate
|
|
40
|
+
* which config keys a skill cares about. Skills that don't touch
|
|
41
|
+
* config are omitted (their config_keys is `[]`).
|
|
42
|
+
*
|
|
43
|
+
* @type {Record<string, string[]>}
|
|
44
|
+
*/
|
|
45
|
+
const SKILL_CONFIG_KEYS = {
|
|
46
|
+
'apt:execute': ['coding.tdd_default', 'tdd.iron_law'],
|
|
47
|
+
'apt:triage': [
|
|
48
|
+
'task_tracking.backend',
|
|
49
|
+
'task_tracking.publish_to_tracker',
|
|
50
|
+
'task_tracking.tracker_url',
|
|
51
|
+
'task_tracking.tracker_label_vocabulary',
|
|
52
|
+
],
|
|
53
|
+
'apt:prototype': ['coding.prototype_discipline'],
|
|
54
|
+
'apt:verify': ['verification.evidence_capture', 'verification.max_iterations'],
|
|
55
|
+
'apt:verify-proof': ['verification.open_report', 'verification.report_formats'],
|
|
56
|
+
'apt:review': ['pipeline.skip_review'],
|
|
57
|
+
'apt:ship': ['pipeline.auto_branch', 'pipeline.branch_prefix'],
|
|
58
|
+
'apt:setup': ['*'], // setup edits config — wildcard surface
|
|
59
|
+
'apt:run': ['parallelization.enabled', 'parallelization.max_agents'],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse the `argument-hint` frontmatter field for `--flag` tokens.
|
|
64
|
+
* Returns a deduplicated array.
|
|
65
|
+
*
|
|
66
|
+
* @param {string|undefined} hint
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
*/
|
|
69
|
+
function parseFlags(hint) {
|
|
70
|
+
if (typeof hint !== 'string') return []
|
|
71
|
+
const flags = new Set()
|
|
72
|
+
for (const m of hint.matchAll(/--[a-z][a-z0-9-]*/gi)) {
|
|
73
|
+
flags.add(m[0])
|
|
74
|
+
}
|
|
75
|
+
return [...flags]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Walk a skills root and return every apt-<slug>/SKILL.md it contains.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} root
|
|
82
|
+
*/
|
|
83
|
+
function walkSkillRoot(root) {
|
|
84
|
+
if (!existsSync(root)) return []
|
|
85
|
+
let stat
|
|
86
|
+
try {
|
|
87
|
+
stat = statSync(root)
|
|
88
|
+
} catch {
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
91
|
+
if (!stat.isDirectory()) return []
|
|
92
|
+
let entries
|
|
93
|
+
try {
|
|
94
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
95
|
+
} catch {
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
const out = []
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (!entry.name.startsWith('apt')) continue
|
|
101
|
+
if (!entry.isDirectory()) continue
|
|
102
|
+
const skillFile = join(root, entry.name, 'SKILL.md')
|
|
103
|
+
if (existsSync(skillFile)) out.push(skillFile)
|
|
104
|
+
}
|
|
105
|
+
return out
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse a single SKILL.md into a modes-list record. Returns null if
|
|
110
|
+
* the frontmatter is unparseable.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} file
|
|
113
|
+
*/
|
|
114
|
+
function readSkillForModes(file) {
|
|
115
|
+
let text
|
|
116
|
+
try {
|
|
117
|
+
text = readFileSync(file, 'utf-8')
|
|
118
|
+
} catch {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
const m = text.match(FRONTMATTER_RE)
|
|
122
|
+
if (!m) return null
|
|
123
|
+
let parsed
|
|
124
|
+
try {
|
|
125
|
+
parsed = parseYaml(m[1])
|
|
126
|
+
} catch {
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
if (parsed === null || typeof parsed !== 'object') return null
|
|
130
|
+
|
|
131
|
+
const name = typeof parsed.name === 'string' ? parsed.name : null
|
|
132
|
+
if (name === null) return null
|
|
133
|
+
|
|
134
|
+
const modes = Array.isArray(parsed.execution_modes)
|
|
135
|
+
? parsed.execution_modes.filter((m) => typeof m === 'string')
|
|
136
|
+
: []
|
|
137
|
+
const flags = parseFlags(parsed['argument-hint'])
|
|
138
|
+
const content_formats = Array.isArray(parsed.produces)
|
|
139
|
+
? parsed.produces.filter((p) => typeof p === 'string')
|
|
140
|
+
: []
|
|
141
|
+
// config_keys: prefer frontmatter `config_keys:` field when present; fall back
|
|
142
|
+
// to the hardcoded SKILL_CONFIG_KEYS map so skills not yet migrated still work.
|
|
143
|
+
const config_keys = Array.isArray(parsed.config_keys)
|
|
144
|
+
? parsed.config_keys.filter((k) => typeof k === 'string')
|
|
145
|
+
: SKILL_CONFIG_KEYS[name] || []
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
name,
|
|
149
|
+
modes,
|
|
150
|
+
flags,
|
|
151
|
+
content_formats,
|
|
152
|
+
config_keys,
|
|
153
|
+
file_path: file,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* CLI entry: `apt-tools modes <subcommand> [project-dir]`.
|
|
159
|
+
*
|
|
160
|
+
* Subcommands: `list` (the only v1 subcommand).
|
|
161
|
+
*
|
|
162
|
+
* The trusted-skill-roots discovery follows the same pattern as the
|
|
163
|
+
* router's skill-discover module, but at minimum scans
|
|
164
|
+
* `packages/framework/skills/` relative to the framework checkout. For
|
|
165
|
+
* project-installed skills, the project's `.aperant/deps/.../skills`
|
|
166
|
+
* tree is also scanned when present.
|
|
167
|
+
*/
|
|
168
|
+
export function cmdModes(subcommand, projectDir, _extraArgs) {
|
|
169
|
+
const cmd = subcommand || 'list'
|
|
170
|
+
if (cmd !== 'list') {
|
|
171
|
+
return ok({
|
|
172
|
+
status: 'error',
|
|
173
|
+
command: 'modes',
|
|
174
|
+
error: `Unknown subcommand: ${cmd}. Use 'list'.`,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Project-dir is optional for modes list — defaults to cwd.
|
|
179
|
+
const absProjectDir = projectDir ? resolve(projectDir) : process.cwd()
|
|
180
|
+
|
|
181
|
+
// Compute trusted skill roots. The framework's own skills root
|
|
182
|
+
// (relative to this file) is always included; project-installed
|
|
183
|
+
// roots (.aperant/deps/...) are added when present.
|
|
184
|
+
const roots = new Set()
|
|
185
|
+
// Framework checkout root: this file is at
|
|
186
|
+
// .../packages/framework/src/cli/commands/modes.mjs → 5 levels up
|
|
187
|
+
// to the framework root, then skills/.
|
|
188
|
+
const frameworkRoot = resolve(new URL('../../../skills', import.meta.url).pathname)
|
|
189
|
+
if (existsSync(frameworkRoot)) roots.add(frameworkRoot)
|
|
190
|
+
const projectSkillRoot = join(absProjectDir, 'packages/framework/skills')
|
|
191
|
+
if (existsSync(projectSkillRoot)) roots.add(projectSkillRoot)
|
|
192
|
+
const installedRoot = join(absProjectDir, '.aperant/deps/node_modules/@aperant/framework/skills')
|
|
193
|
+
if (existsSync(installedRoot)) roots.add(installedRoot)
|
|
194
|
+
|
|
195
|
+
const seen = new Set()
|
|
196
|
+
const skills = []
|
|
197
|
+
for (const root of roots) {
|
|
198
|
+
for (const file of walkSkillRoot(root)) {
|
|
199
|
+
const record = readSkillForModes(file)
|
|
200
|
+
if (!record) continue
|
|
201
|
+
if (seen.has(record.name)) continue // first-writer-wins
|
|
202
|
+
seen.add(record.name)
|
|
203
|
+
skills.push(record)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
skills.sort((a, b) => a.name.localeCompare(b.name))
|
|
207
|
+
|
|
208
|
+
return ok({
|
|
209
|
+
status: 'ok',
|
|
210
|
+
command: 'modes-list',
|
|
211
|
+
project_dir: absProjectDir,
|
|
212
|
+
skills,
|
|
213
|
+
scanned_roots: [...roots],
|
|
214
|
+
})
|
|
215
|
+
}
|
|
@@ -117,7 +117,8 @@ function extractTopLevelSection(markdown, heading) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* Parse a fixes-applied.md produced by apt-pr-review-fixer
|
|
120
|
+
* Parse a fixes-applied.md produced by apt-pr-review-fixer OR a
|
|
121
|
+
* self-review.md produced by apt-pr-review-self-reviewer. Returns
|
|
121
122
|
* `{ fixes: Array<{id, title, file, verification}>, skipped: Array<{id}>, exists: true }`.
|
|
122
123
|
* Non-existent or empty report returns `{ exists: false, fixes: [], skipped: [] }`.
|
|
123
124
|
*
|
|
@@ -125,24 +126,57 @@ function extractTopLevelSection(markdown, heading) {
|
|
|
125
126
|
* `### <id>` block. We strip any trailing `(created)` / `(modify)` etc
|
|
126
127
|
* annotation the fixer template shows as free-form text so the intersect
|
|
127
128
|
* step can match against `git diff --name-only` output cleanly.
|
|
129
|
+
*
|
|
130
|
+
* Dual-schema support (PR #95 Defect 2):
|
|
131
|
+
* - Fixer reports use `## Fixes Applied` with `### <ID> — title` headers
|
|
132
|
+
* (e.g. `### QUA-001 — title`). Real finding ids carry through.
|
|
133
|
+
* - Self-reviewer reports use `## Fixes Applied by Self-Reviewer` with
|
|
134
|
+
* `### Fix N: description` headers. These have no real finding id, so
|
|
135
|
+
* we synthesize `self-reviewer-fix-${N}` to satisfy the
|
|
136
|
+
* `report.fixes.length > 0` guard in decideVerdict rule 3 while
|
|
137
|
+
* letting the diff-intersect step (the real check) decide the verdict.
|
|
138
|
+
*
|
|
139
|
+
* The two heading texts are disjoint — fixers emit one, self-reviewers
|
|
140
|
+
* emit the other. If both somehow appear in the same file (not expected
|
|
141
|
+
* in practice), both are parsed and fixer entries come first.
|
|
128
142
|
*/
|
|
129
143
|
export function parseFixesAppliedMarkdown(md) {
|
|
130
144
|
if (!md || typeof md !== 'string' || !md.trim()) {
|
|
131
|
-
return { exists: false, fixes: [], skipped: [] }
|
|
145
|
+
return { exists: false, fixes: [], skipped: [], schema: 'fixer' }
|
|
132
146
|
}
|
|
133
147
|
const fixesSection = extractTopLevelSection(md, 'Fixes Applied')
|
|
148
|
+
const selfReviewerSection = extractTopLevelSection(md, 'Fixes Applied by Self-Reviewer')
|
|
134
149
|
const skippedSection = extractTopLevelSection(md, 'Skipped')
|
|
135
|
-
const fixes =
|
|
150
|
+
const fixes = [
|
|
151
|
+
...parseFixEntries(fixesSection),
|
|
152
|
+
...parseSelfReviewerFixEntries(selfReviewerSection),
|
|
153
|
+
]
|
|
136
154
|
const skipped = parseSkippedEntries(skippedSection)
|
|
137
|
-
|
|
155
|
+
// Detect which heading was present; fixer takes precedence when both appear.
|
|
156
|
+
const schema = fixesSection ? 'fixer' : 'self-reviewer'
|
|
157
|
+
return { exists: true, fixes, skipped, schema }
|
|
138
158
|
}
|
|
139
159
|
|
|
140
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Parse the body of a `## Fixes Applied by Self-Reviewer` section into
|
|
162
|
+
* entries shaped like fixer entries (id, title, file, files, verification).
|
|
163
|
+
*
|
|
164
|
+
* The self-reviewer agent emits `### Fix N: description` per fix instead
|
|
165
|
+
* of `### ID — title`, because the reviewer doesn't carry the original
|
|
166
|
+
* finding ids forward. We synthesize id `self-reviewer-fix-${N}` so the
|
|
167
|
+
* rest of the audit pipeline (decideVerdict rule 3 guard, audit.json
|
|
168
|
+
* envelope) stays schema-agnostic. The body shape — `- **File**:` /
|
|
169
|
+
* `- **Change**:` — is identical to fixer entries, so we reuse
|
|
170
|
+
* extractFileFields verbatim. Verification is not part of the self-
|
|
171
|
+
* reviewer schema (their PASS/FAIL signal lives in `## Verification
|
|
172
|
+
* Results` and is already covered by parseFixerStatusLine), so we leave
|
|
173
|
+
* `verification: null` and let the gate's diff-intersect step decide.
|
|
174
|
+
*/
|
|
175
|
+
function parseSelfReviewerFixEntries(section) {
|
|
141
176
|
if (!section) return []
|
|
142
|
-
// `###
|
|
143
|
-
// The fixer template uses em-dash + space; we tolerate either.
|
|
177
|
+
// `### Fix 1: description` / `### Fix 2: description`.
|
|
144
178
|
const entries = []
|
|
145
|
-
const headerRx = /^###\s+(
|
|
179
|
+
const headerRx = /^###\s+Fix\s+(\d+)(?:\s*:\s*(.+))?$/gm
|
|
146
180
|
const matches = [...section.matchAll(headerRx)]
|
|
147
181
|
for (let i = 0; i < matches.length; i++) {
|
|
148
182
|
const m = matches[i]
|
|
@@ -151,10 +185,8 @@ function parseFixEntries(section) {
|
|
|
151
185
|
const body = section.slice(start, nextStart)
|
|
152
186
|
const files = extractFileFields(body)
|
|
153
187
|
entries.push({
|
|
154
|
-
id: m[1]
|
|
188
|
+
id: `self-reviewer-fix-${m[1]}`,
|
|
155
189
|
title: m[2] ? m[2].trim() : null,
|
|
156
|
-
// Back-compat: single-path consumers + tests still see `file`
|
|
157
|
-
// (the first extracted path). New code should prefer `files`.
|
|
158
190
|
file: files[0] ?? null,
|
|
159
191
|
files,
|
|
160
192
|
verification: extractVerificationField(body),
|
|
@@ -163,6 +195,48 @@ function parseFixEntries(section) {
|
|
|
163
195
|
return entries
|
|
164
196
|
}
|
|
165
197
|
|
|
198
|
+
function parseFixEntries(section) {
|
|
199
|
+
if (!section) return []
|
|
200
|
+
// `### <id> — <title>` OR `### <id> -- <title>` OR `### <id>` alone.
|
|
201
|
+
// The fixer template uses em-dash + space; we tolerate either.
|
|
202
|
+
//
|
|
203
|
+
// PR #95 (2026-05-12) widened the capture to accept combined-id headers
|
|
204
|
+
// like `### QUA-003 + FIT-001 — title` where one body explains multiple
|
|
205
|
+
// findings. The locked separator alphabet is `+`, `,`, and ` and ` —
|
|
206
|
+
// the exact set observed in live fixer reports. We do NOT widen to a
|
|
207
|
+
// generic `\W+` splitter; the alphabet stays narrow so a typo like
|
|
208
|
+
// `### QUA-003-foo` cannot be mis-parsed as id `QUA-003`. Combined
|
|
209
|
+
// headers produce one entry per id, all sharing the same body fields
|
|
210
|
+
// (files, verification, title).
|
|
211
|
+
const entries = []
|
|
212
|
+
const headerRx =
|
|
213
|
+
/^###\s+([A-Z]+-\d+(?:\s*(?:\+|,|\s+and\s+)\s*[A-Z]+-\d+)*)(?:\s*[—-]{1,2}\s*(.+))?$/gm
|
|
214
|
+
const idSplitRx = /\s*(?:\+|,|\s+and\s+)\s*/
|
|
215
|
+
const matches = [...section.matchAll(headerRx)]
|
|
216
|
+
for (let i = 0; i < matches.length; i++) {
|
|
217
|
+
const m = matches[i]
|
|
218
|
+
const start = m.index
|
|
219
|
+
const nextStart = i + 1 < matches.length ? matches[i + 1].index : section.length
|
|
220
|
+
const body = section.slice(start, nextStart)
|
|
221
|
+
const files = extractFileFields(body)
|
|
222
|
+
const title = m[2] ? m[2].trim() : null
|
|
223
|
+
const verification = extractVerificationField(body)
|
|
224
|
+
const ids = m[1].split(idSplitRx)
|
|
225
|
+
for (const id of ids) {
|
|
226
|
+
entries.push({
|
|
227
|
+
id,
|
|
228
|
+
title,
|
|
229
|
+
// Back-compat: single-path consumers + tests still see `file`
|
|
230
|
+
// (the first extracted path). New code should prefer `files`.
|
|
231
|
+
file: files[0] ?? null,
|
|
232
|
+
files,
|
|
233
|
+
verification,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return entries
|
|
238
|
+
}
|
|
239
|
+
|
|
166
240
|
function parseSkippedEntries(section) {
|
|
167
241
|
if (!section) return []
|
|
168
242
|
const entries = []
|
|
@@ -518,12 +592,14 @@ export function decideVerdict({ statusLine, fixesApplied, diffFiles }) {
|
|
|
518
592
|
// fail-closed trap. The report could be scaffolded with headers but
|
|
519
593
|
// no actual entries.
|
|
520
594
|
if (claimed.fixed > 0 && report.fixes.length === 0) {
|
|
595
|
+
const heading =
|
|
596
|
+
report.schema === 'self-reviewer' ? '## Fixes Applied by Self-Reviewer' : '## Fixes Applied'
|
|
521
597
|
return {
|
|
522
598
|
verdict: 'hallucinated',
|
|
523
599
|
reason:
|
|
524
600
|
`fixer claimed FIXED=${claimed.fixed} but fixes-applied.md has zero ` +
|
|
525
601
|
`### <id>` +
|
|
526
|
-
` entries under
|
|
602
|
+
` entries under ${heading}`,
|
|
527
603
|
claimed,
|
|
528
604
|
matched: [],
|
|
529
605
|
unmatched: [],
|
|
@@ -551,10 +627,13 @@ export function decideVerdict({ statusLine, fixesApplied, diffFiles }) {
|
|
|
551
627
|
// Main rule: FIXED > 0 AND PASS — intersect claimed files with the
|
|
552
628
|
// worktree diff. Empty intersection = hallucination.
|
|
553
629
|
if (claimed.fixed > 0 && claimed.verification === 'PASS') {
|
|
554
|
-
const claimedFiles =
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
630
|
+
const claimedFiles = [
|
|
631
|
+
...new Set(
|
|
632
|
+
report.fixes.flatMap((f) => (Array.isArray(f.files) ? f.files : f.file ? [f.file] : [])),
|
|
633
|
+
),
|
|
634
|
+
]
|
|
635
|
+
const { matched: rawMatched, unmatched } = intersectClaimedAndStaged(claimedFiles, diffFiles)
|
|
636
|
+
const matched = [...new Set(rawMatched)]
|
|
558
637
|
if (matched.length === 0) {
|
|
559
638
|
return {
|
|
560
639
|
verdict: 'hallucinated',
|
|
@@ -138,7 +138,7 @@ const PR_REVIEW_SIGNAL = /(?:\bPR\s*#?\s*\d+|#\d+|\bpull\s*request\s*#?\s*\d+)\b
|
|
|
138
138
|
* so this keeps the two paths consistent.
|
|
139
139
|
*
|
|
140
140
|
* @param {import('../route/skill-discover.mjs').DiscoveredSkill[]} skills
|
|
141
|
-
* @returns {{ agentOf: (slug: string) => string|null, spawnsAgent: (slug: string) => boolean, knownSlugs: Set<string>, isUserInvocable: (slug: string) => boolean }}
|
|
141
|
+
* @returns {{ agentOf: (slug: string) => string|null, spawnsAgent: (slug: string) => boolean, taskContextOf: (slug: string) => string|null, defaultTrackOf: (slug: string) => string|null, knownSlugs: Set<string>, isUserInvocable: (slug: string) => boolean }}
|
|
142
142
|
*/
|
|
143
143
|
function skillIndex(skills) {
|
|
144
144
|
const byName = new Map()
|
|
@@ -160,6 +160,12 @@ function skillIndex(skills) {
|
|
|
160
160
|
spawnsAgent(slug) {
|
|
161
161
|
return Boolean(byName.get(slug)?.spawns_agent)
|
|
162
162
|
},
|
|
163
|
+
taskContextOf(slug) {
|
|
164
|
+
return byName.get(slug)?.task_context ?? null
|
|
165
|
+
},
|
|
166
|
+
defaultTrackOf(slug) {
|
|
167
|
+
return byName.get(slug)?.default_track ?? null
|
|
168
|
+
},
|
|
163
169
|
isUserInvocable(slug) {
|
|
164
170
|
return userInvocableSlugs.has(slug)
|
|
165
171
|
},
|
|
@@ -322,6 +328,7 @@ export function cmdRoute(projectDir, extraArgs) {
|
|
|
322
328
|
if (!isPrReviewFreetext && index.knownSlugs.has(firstWord)) {
|
|
323
329
|
const shouldSpawn = index.spawnsAgent(firstWord)
|
|
324
330
|
const gate_preview = previewGatesFor(firstWord)
|
|
331
|
+
const defaultTrack = index.defaultTrackOf(firstWord)
|
|
325
332
|
return ok({
|
|
326
333
|
status: 'ok',
|
|
327
334
|
command: 'route',
|
|
@@ -330,6 +337,8 @@ export function cmdRoute(projectDir, extraArgs) {
|
|
|
330
337
|
skill_args: remainingArgs || null,
|
|
331
338
|
spawn_agent: shouldSpawn,
|
|
332
339
|
agent: shouldSpawn ? index.agentOf(firstWord) || `apt-${firstWord}` : null,
|
|
340
|
+
task_context: index.taskContextOf(firstWord),
|
|
341
|
+
...(defaultTrack ? { default_track: defaultTrack } : {}),
|
|
333
342
|
host_capabilities,
|
|
334
343
|
task_isolation,
|
|
335
344
|
update_check,
|
|
@@ -430,7 +439,7 @@ export function cmdRoute(projectDir, extraArgs) {
|
|
|
430
439
|
{
|
|
431
440
|
status: 'error',
|
|
432
441
|
command: 'route',
|
|
433
|
-
error_code: '
|
|
442
|
+
error_code: 'skill_description_too_long',
|
|
434
443
|
reason: e instanceof Error ? e.message : String(e),
|
|
435
444
|
},
|
|
436
445
|
1,
|