@aperant/framework 0.6.4 → 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 +38 -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/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/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/route.mjs +1 -1
- 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 +108 -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/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 +9 -0
- package/dist/cli/route/skill-discover.d.mts.map +1 -1
- package/dist/cli/route/skill-discover.mjs +11 -0
- package/dist/cli/route/skill-discover.mjs.map +1 -1
- package/dist/cli/skill-author/contract.d.mts +1 -0
- package/dist/cli/skill-author/contract.d.mts.map +1 -1
- package/dist/cli/skill-author/contract.mjs +7 -0
- package/dist/cli/skill-author/contract.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 -125
- package/prompts/coder.md +2 -0
- package/prompts/planner.md +12 -0
- package/prompts/spec_writer.md +9 -0
- package/skills/apt/SKILL.md +1 -0
- package/skills/apt-close-task/SKILL.md +32 -1
- package/skills/apt-debug/SKILL.md +39 -6
- package/skills/apt-debug/appendices/diagnose-discipline.md +119 -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 +25 -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-execute/SKILL.md +48 -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-plan/SKILL.md +147 -4
- package/skills/apt-planner.md +42 -1
- package/skills/apt-pr-review/SKILL.md +46 -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 +30 -0
- package/skills/apt-review/SKILL.md +2 -0
- package/skills/apt-run/SKILL.md +32 -4
- package/skills/apt-setup/SKILL.md +128 -2
- package/skills/apt-ship/SKILL.md +47 -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-verify/SKILL.md +3 -0
- package/skills/apt-verify-proof/SKILL.md +3 -0
- package/skills/apt-watch-ci/SKILL.md +163 -0
- package/skills/apt-zoom-out/SKILL.md +130 -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/modes.mjs +215 -0
- package/src/cli/commands/route.mjs +1 -1
- package/src/cli/commands/task.mjs +125 -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/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 +12 -0
- package/src/cli/skill-author/contract.mjs +7 -0
- 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,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/context.mjs — `apt-tools context write` (Pocock adoption ST-5, AC4).
|
|
3
|
+
*
|
|
4
|
+
* Upserts a term-block into the project's CONTEXT.md (the persistent
|
|
5
|
+
* project glossary). Format ported from Pocock's CONTEXT-FORMAT.md
|
|
6
|
+
* (see packages/framework/templates/context-format.md for the MIT
|
|
7
|
+
* attribution + field semantics).
|
|
8
|
+
*
|
|
9
|
+
* Idempotent: re-running with the same `--term` updates the existing
|
|
10
|
+
* block in place. Fields not explicitly overridden via flags are
|
|
11
|
+
* preserved from the prior block.
|
|
12
|
+
*
|
|
13
|
+
* CLI:
|
|
14
|
+
* apt-tools context write <project-dir>
|
|
15
|
+
* --term <bold-term>
|
|
16
|
+
* --definition <one-line>
|
|
17
|
+
* [--aliases-to-avoid alias1,alias2]
|
|
18
|
+
* [--cardinality 'one-to-many' | 'one-to-one' | 'many-to-many']
|
|
19
|
+
* [--flagged-ambiguity 'note']
|
|
20
|
+
* [--example-dialogue 'example']
|
|
21
|
+
*
|
|
22
|
+
* Output: JSON envelope { status: 'ok' | 'error',
|
|
23
|
+
* command: 'context-write',
|
|
24
|
+
* file: <absolute path>,
|
|
25
|
+
* term: <term>,
|
|
26
|
+
* operation: 'created' | 'updated' }.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
30
|
+
import { resolve } from 'node:path'
|
|
31
|
+
import { parseFlags } from '../util/args.mjs'
|
|
32
|
+
import { err, ok } from '../util/result.mjs'
|
|
33
|
+
|
|
34
|
+
const HEADER = '# Project glossary (CONTEXT.md)'
|
|
35
|
+
const HEADER_NOTE =
|
|
36
|
+
'<!-- Managed by `apt-tools context write`. Hand-edits are preserved across upserts. -->'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse a single term-block out of CONTEXT.md.
|
|
40
|
+
*
|
|
41
|
+
* A block looks like:
|
|
42
|
+
* ## {Term}
|
|
43
|
+
*
|
|
44
|
+
* {Definition line}
|
|
45
|
+
*
|
|
46
|
+
* - **Aliases to avoid:** ...
|
|
47
|
+
* - **Cardinality:** ...
|
|
48
|
+
* - **Flagged ambiguities:** ...
|
|
49
|
+
* - **Example dialogue:** ...
|
|
50
|
+
*
|
|
51
|
+
* Returns `{ definition, aliasesToAvoid, cardinality, flaggedAmbiguity, exampleDialogue }`
|
|
52
|
+
* with each field as a string or null.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} blockText
|
|
55
|
+
*/
|
|
56
|
+
function parseBlock(blockText) {
|
|
57
|
+
const lines = blockText.split('\n')
|
|
58
|
+
// First non-empty line after the ## header is the definition.
|
|
59
|
+
let definition = ''
|
|
60
|
+
let i = 0
|
|
61
|
+
while (i < lines.length && lines[i].trim() === '') i++
|
|
62
|
+
if (i < lines.length && !lines[i].startsWith('- ')) {
|
|
63
|
+
definition = lines[i].trim()
|
|
64
|
+
i++
|
|
65
|
+
}
|
|
66
|
+
const fields = {
|
|
67
|
+
aliasesToAvoid: null,
|
|
68
|
+
cardinality: null,
|
|
69
|
+
flaggedAmbiguity: null,
|
|
70
|
+
exampleDialogue: null,
|
|
71
|
+
}
|
|
72
|
+
for (; i < lines.length; i++) {
|
|
73
|
+
const line = lines[i]
|
|
74
|
+
const m = line.match(/^- \*\*([^*]+):\*\*\s*(.*)$/)
|
|
75
|
+
if (!m) continue
|
|
76
|
+
const key = m[1].trim()
|
|
77
|
+
const value = m[2].trim()
|
|
78
|
+
switch (key) {
|
|
79
|
+
case 'Aliases to avoid':
|
|
80
|
+
fields.aliasesToAvoid = value
|
|
81
|
+
break
|
|
82
|
+
case 'Cardinality':
|
|
83
|
+
fields.cardinality = value
|
|
84
|
+
break
|
|
85
|
+
case 'Flagged ambiguities':
|
|
86
|
+
fields.flaggedAmbiguity = value
|
|
87
|
+
break
|
|
88
|
+
case 'Example dialogue':
|
|
89
|
+
fields.exampleDialogue = value
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { definition, ...fields }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Render a term-block to markdown.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} term
|
|
100
|
+
* @param {{
|
|
101
|
+
* definition: string,
|
|
102
|
+
* aliasesToAvoid: string|null,
|
|
103
|
+
* cardinality: string|null,
|
|
104
|
+
* flaggedAmbiguity: string|null,
|
|
105
|
+
* exampleDialogue: string|null,
|
|
106
|
+
* }} block
|
|
107
|
+
*/
|
|
108
|
+
function renderBlock(term, block) {
|
|
109
|
+
const lines = [`## ${term}`, '', block.definition, '']
|
|
110
|
+
const fields = [
|
|
111
|
+
['Aliases to avoid', block.aliasesToAvoid],
|
|
112
|
+
['Cardinality', block.cardinality],
|
|
113
|
+
['Flagged ambiguities', block.flaggedAmbiguity],
|
|
114
|
+
['Example dialogue', block.exampleDialogue],
|
|
115
|
+
]
|
|
116
|
+
for (const [key, value] of fields) {
|
|
117
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
118
|
+
lines.push(`- **${key}:** ${value}`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return lines.join('\n')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find an existing block by term in the source markdown.
|
|
126
|
+
* Returns `{ start, end, prior }` or `null` if no such block exists.
|
|
127
|
+
* `start` and `end` are inclusive/exclusive offsets into `source`.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} source
|
|
130
|
+
* @param {string} term
|
|
131
|
+
*/
|
|
132
|
+
function findBlock(source, term) {
|
|
133
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
134
|
+
const headerRe = new RegExp(`^##\\s+${escaped}\\s*$`, 'm')
|
|
135
|
+
const m = headerRe.exec(source)
|
|
136
|
+
if (!m) return null
|
|
137
|
+
const start = m.index
|
|
138
|
+
// Find the next ## or # header or EOF. LOG-004: including H1 prevents a
|
|
139
|
+
// manually-inserted `# Heading` from being absorbed into the block's content.
|
|
140
|
+
const tail = source.slice(m.index + m[0].length)
|
|
141
|
+
const nextRe = /^#{1,2}\s+/m
|
|
142
|
+
const nextMatch = nextRe.exec(tail)
|
|
143
|
+
const end = nextMatch ? m.index + m[0].length + nextMatch.index : source.length
|
|
144
|
+
const blockText = source.slice(m.index + m[0].length, end)
|
|
145
|
+
return { start, end, prior: parseBlock(blockText) }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* CLI entry: `apt-tools context <subcommand> <project-dir> [flags]`.
|
|
150
|
+
*
|
|
151
|
+
* Only `write` is supported in v1; other subcommands return a usage error.
|
|
152
|
+
*/
|
|
153
|
+
export function cmdContext(subcommand, projectDir, extraArgs) {
|
|
154
|
+
if (subcommand !== 'write') {
|
|
155
|
+
return err(
|
|
156
|
+
'Usage: apt-tools context write <project-dir> --term <term> --definition <line> [--aliases-to-avoid ...] [--cardinality ...] [--flagged-ambiguity ...] [--example-dialogue ...]',
|
|
157
|
+
1,
|
|
158
|
+
{ command: 'context' },
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
if (!projectDir) {
|
|
162
|
+
return err(
|
|
163
|
+
'Usage: apt-tools context write <project-dir> --term <term> --definition <line>',
|
|
164
|
+
1,
|
|
165
|
+
{
|
|
166
|
+
command: 'context-write',
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const flags = parseFlags(extraArgs ?? [])
|
|
172
|
+
const term = flags.get('term')
|
|
173
|
+
const definition = flags.get('definition')
|
|
174
|
+
if (typeof term !== 'string' || term.trim() === '') {
|
|
175
|
+
return err('--term <bold-term> is required', 1, { command: 'context-write' })
|
|
176
|
+
}
|
|
177
|
+
if (typeof definition !== 'string' || definition.trim() === '') {
|
|
178
|
+
return err('--definition <one-line> is required', 1, { command: 'context-write' })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const absProjectDir = resolve(projectDir)
|
|
182
|
+
if (!existsSync(absProjectDir)) {
|
|
183
|
+
return err(`project directory not found: ${absProjectDir}`, 1, {
|
|
184
|
+
command: 'context-write',
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const contextPath = resolve(absProjectDir, 'CONTEXT.md')
|
|
189
|
+
let source = ''
|
|
190
|
+
if (existsSync(contextPath)) {
|
|
191
|
+
source = readFileSync(contextPath, 'utf-8')
|
|
192
|
+
} else {
|
|
193
|
+
source = `${HEADER}\n\n${HEADER_NOTE}\n\n`
|
|
194
|
+
}
|
|
195
|
+
// Ensure the file starts with the header even if it existed pre-Pocock.
|
|
196
|
+
if (!source.startsWith(HEADER)) {
|
|
197
|
+
source = `${HEADER}\n\n${HEADER_NOTE}\n\n${source}`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const existing = findBlock(source, term)
|
|
201
|
+
const incoming = {
|
|
202
|
+
definition,
|
|
203
|
+
aliasesToAvoid:
|
|
204
|
+
typeof flags.get('aliases-to-avoid') === 'string' ? flags.get('aliases-to-avoid') : null,
|
|
205
|
+
cardinality: typeof flags.get('cardinality') === 'string' ? flags.get('cardinality') : null,
|
|
206
|
+
flaggedAmbiguity:
|
|
207
|
+
typeof flags.get('flagged-ambiguity') === 'string' ? flags.get('flagged-ambiguity') : null,
|
|
208
|
+
exampleDialogue:
|
|
209
|
+
typeof flags.get('example-dialogue') === 'string' ? flags.get('example-dialogue') : null,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Merge: prior block fields are preserved unless explicitly overridden.
|
|
213
|
+
const merged = existing
|
|
214
|
+
? {
|
|
215
|
+
definition: incoming.definition, // definition always overrides
|
|
216
|
+
aliasesToAvoid: incoming.aliasesToAvoid ?? existing.prior.aliasesToAvoid,
|
|
217
|
+
cardinality: incoming.cardinality ?? existing.prior.cardinality,
|
|
218
|
+
flaggedAmbiguity: incoming.flaggedAmbiguity ?? existing.prior.flaggedAmbiguity,
|
|
219
|
+
exampleDialogue: incoming.exampleDialogue ?? existing.prior.exampleDialogue,
|
|
220
|
+
}
|
|
221
|
+
: incoming
|
|
222
|
+
|
|
223
|
+
const rendered = renderBlock(term, merged)
|
|
224
|
+
let updated
|
|
225
|
+
if (existing) {
|
|
226
|
+
// Replace the existing block (header + body) with the rendered one.
|
|
227
|
+
const before = source.slice(0, existing.start).replace(/\n+$/, '')
|
|
228
|
+
const after = source.slice(existing.end).replace(/^\n+/, '')
|
|
229
|
+
updated = `${before}\n\n${rendered}\n${after.length > 0 ? `\n${after}` : ''}`
|
|
230
|
+
} else {
|
|
231
|
+
// Append a new block at the end.
|
|
232
|
+
updated = `${source.replace(/\n+$/, '')}\n\n${rendered}\n`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
writeFileSync(contextPath, updated)
|
|
236
|
+
|
|
237
|
+
return ok({
|
|
238
|
+
status: 'ok',
|
|
239
|
+
command: 'context-write',
|
|
240
|
+
file: contextPath,
|
|
241
|
+
term,
|
|
242
|
+
operation: existing ? 'updated' : 'created',
|
|
243
|
+
})
|
|
244
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* commands/event.mjs — append-only event log for coordination history (C29).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { appendFileSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
6
6
|
import { hostname } from 'node:os'
|
|
7
7
|
import { join, resolve } from 'node:path'
|
|
8
8
|
import { readLifecycleEventsForDay } from '../coordination/event-log.mjs'
|
|
@@ -11,6 +11,47 @@ import { detectWorktree } from '../git/worktree-detect.mjs'
|
|
|
11
11
|
import { parseFlags } from '../util/args.mjs'
|
|
12
12
|
import { err, ok } from '../util/result.mjs'
|
|
13
13
|
|
|
14
|
+
// QUA-003 / PRF-002 — single source of truth for valid event types.
|
|
15
|
+
// VALID_EVENT_TYPES is the authoritative list; CI_WATCH_EVENT_TYPES is the O(1)
|
|
16
|
+
// membership Set used to short-circuit ci-watch.* events to the JSONL path (D-04).
|
|
17
|
+
const VALID_EVENT_TYPES = [
|
|
18
|
+
'task-claimed',
|
|
19
|
+
'task-completed',
|
|
20
|
+
'task-failed',
|
|
21
|
+
'wave-started',
|
|
22
|
+
'wave-completed',
|
|
23
|
+
'install.started',
|
|
24
|
+
'install.ok',
|
|
25
|
+
'install.failed',
|
|
26
|
+
'ci-watch.armed',
|
|
27
|
+
'ci-watch.tick',
|
|
28
|
+
'ci-watch.fix-pushed',
|
|
29
|
+
'ci-watch.stopped',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const CI_WATCH_EVENT_TYPES = new Set([
|
|
33
|
+
'ci-watch.armed',
|
|
34
|
+
'ci-watch.tick',
|
|
35
|
+
'ci-watch.fix-pushed',
|
|
36
|
+
'ci-watch.stopped',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Append a ci-watch event to the per-day JSONL file (D-04).
|
|
41
|
+
* O(1) — no directory scan. Creates the events dir if missing.
|
|
42
|
+
* @param {string} eventsDir
|
|
43
|
+
* @param {object} payload
|
|
44
|
+
* @returns {{ jsonlFile: string, jsonlPath: string }}
|
|
45
|
+
*/
|
|
46
|
+
function appendToJsonl(eventsDir, payload) {
|
|
47
|
+
mkdirSync(eventsDir, { recursive: true })
|
|
48
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
49
|
+
const jsonlFile = `${today}.jsonl`
|
|
50
|
+
const jsonlPath = join(eventsDir, jsonlFile)
|
|
51
|
+
appendFileSync(jsonlPath, `${JSON.stringify(payload)}\n`, 'utf-8')
|
|
52
|
+
return { jsonlFile, jsonlPath }
|
|
53
|
+
}
|
|
54
|
+
|
|
14
55
|
export function cmdEvent(subcommand, projectDir, extraArgs) {
|
|
15
56
|
if (!subcommand)
|
|
16
57
|
return err('Usage: apt-tools event <append|list|rebuild-state> <project-dir> [flags]')
|
|
@@ -38,18 +79,12 @@ export function cmdEvent(subcommand, projectDir, extraArgs) {
|
|
|
38
79
|
// install.started — { task_id, ecosystem, pid, started_at, log_path }
|
|
39
80
|
// install.ok — { task_id, ecosystem, duration_ms, log_path }
|
|
40
81
|
// install.failed — { task_id, ecosystem, exit_code, duration_ms, reason, log_path }
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
'
|
|
47
|
-
'install.started',
|
|
48
|
-
'install.ok',
|
|
49
|
-
'install.failed',
|
|
50
|
-
]
|
|
51
|
-
if (!validTypes.includes(type)) {
|
|
52
|
-
return err(`Invalid event type: ${type}. Valid types: ${validTypes.join(', ')}`)
|
|
82
|
+
// C28 (post-ship CI watcher D-04) — added ci-watch.{armed,tick,fix-pushed,stopped}
|
|
83
|
+
// so /apt:watch-ci tick / start / stop can stream events into
|
|
84
|
+
// .aperant/events/{today}.jsonl. The rebuild-state switch below
|
|
85
|
+
// silently ignores them (they're informational, not task-lifecycle).
|
|
86
|
+
if (!VALID_EVENT_TYPES.includes(type)) {
|
|
87
|
+
return err(`Invalid event type: ${type}. Valid types: ${VALID_EVENT_TYPES.join(', ')}`)
|
|
53
88
|
}
|
|
54
89
|
|
|
55
90
|
let data = {}
|
|
@@ -61,6 +96,20 @@ export function cmdEvent(subcommand, projectDir, extraArgs) {
|
|
|
61
96
|
}
|
|
62
97
|
}
|
|
63
98
|
|
|
99
|
+
// PRF-002 — ci-watch.* events go to the per-day JSONL path (D-04).
|
|
100
|
+
// O(1) append; no directory scan needed.
|
|
101
|
+
if (CI_WATCH_EVENT_TYPES.has(type)) {
|
|
102
|
+
const event = {
|
|
103
|
+
type,
|
|
104
|
+
subtask,
|
|
105
|
+
worker: `${hostname()}-${process.pid}`,
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
data,
|
|
108
|
+
}
|
|
109
|
+
const { jsonlFile } = appendToJsonl(eventsDir, event)
|
|
110
|
+
return ok({ status: 'ok', command: 'event-append', event_file: jsonlFile, event })
|
|
111
|
+
}
|
|
112
|
+
|
|
64
113
|
// Find next sequence number — scan existing files for highest number
|
|
65
114
|
const existingFiles = readdirSync(eventsDir).filter((f) => f.endsWith('.json'))
|
|
66
115
|
let maxId = 0
|
|
@@ -169,17 +218,7 @@ export function cmdEvent(subcommand, projectDir, extraArgs) {
|
|
|
169
218
|
const event = JSON.parse(readFileSync(join(eventsDir, f), 'utf-8'))
|
|
170
219
|
|
|
171
220
|
// Validate event type before processing (T-11-04 mitigation)
|
|
172
|
-
|
|
173
|
-
'task-claimed',
|
|
174
|
-
'task-completed',
|
|
175
|
-
'task-failed',
|
|
176
|
-
'wave-started',
|
|
177
|
-
'wave-completed',
|
|
178
|
-
'install.started',
|
|
179
|
-
'install.ok',
|
|
180
|
-
'install.failed',
|
|
181
|
-
]
|
|
182
|
-
if (!validTypes.includes(event.type)) continue
|
|
221
|
+
if (!VALID_EVENT_TYPES.includes(event.type)) continue
|
|
183
222
|
|
|
184
223
|
eventsProcessed++
|
|
185
224
|
|
|
@@ -22,14 +22,30 @@ export function cmdHostDetect() {
|
|
|
22
22
|
})
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Parse `--track <quick|standard|deep>` from positional args. Returns the
|
|
27
|
+
* lowercased track or null when the flag is absent. Unknown values are
|
|
28
|
+
* silently ignored to preserve backward-compat — the caller falls back to
|
|
29
|
+
* global phase_delegation in that case.
|
|
30
|
+
*/
|
|
31
|
+
function parseTrackFlag(args) {
|
|
32
|
+
if (!Array.isArray(args)) return null
|
|
33
|
+
const idx = args.indexOf('--track')
|
|
34
|
+
if (idx === -1 || idx === args.length - 1) return null
|
|
35
|
+
const val = String(args[idx + 1]).toLowerCase()
|
|
36
|
+
return val === 'quick' || val === 'standard' || val === 'deep' ? val : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function cmdReconcile(projectDir, extraArgs = []) {
|
|
40
|
+
if (!projectDir)
|
|
41
|
+
return err('Usage: apt-tools reconcile <project-dir> [--track quick|standard|deep]')
|
|
27
42
|
const targetDir = resolve(projectDir)
|
|
28
43
|
|
|
29
44
|
if (!existsSync(targetDir)) return err(`Directory does not exist: ${targetDir}`)
|
|
30
45
|
|
|
31
46
|
const host = detectHost()
|
|
32
47
|
const { capabilities } = host
|
|
48
|
+
const track = parseTrackFlag(extraArgs)
|
|
33
49
|
|
|
34
50
|
// Read orchestration config — default all fields to 'auto' if missing
|
|
35
51
|
let orchestrationConfig = {
|
|
@@ -37,6 +53,7 @@ export function cmdReconcile(projectDir) {
|
|
|
37
53
|
review_mode: 'auto',
|
|
38
54
|
debate_mode: 'auto',
|
|
39
55
|
}
|
|
56
|
+
let perTrack = null
|
|
40
57
|
|
|
41
58
|
const configPath = join(targetDir, '.aperant', 'config.json')
|
|
42
59
|
if (existsSync(configPath)) {
|
|
@@ -48,18 +65,22 @@ export function cmdReconcile(projectDir) {
|
|
|
48
65
|
review_mode: cfg.orchestration.review_mode ?? 'auto',
|
|
49
66
|
debate_mode: cfg.orchestration.debate_mode ?? 'auto',
|
|
50
67
|
}
|
|
68
|
+
perTrack = cfg.orchestration.per_track ?? null
|
|
51
69
|
}
|
|
52
70
|
} catch {
|
|
53
71
|
// Malformed config — use defaults
|
|
54
72
|
}
|
|
55
73
|
}
|
|
56
74
|
|
|
75
|
+
// Per-track phase_delegation override (roundtable 2026-05-14 R7):
|
|
76
|
+
// when --track is passed AND per_track[track] is set, use that as
|
|
77
|
+
// the requested mode for phase_delegation. Otherwise fall back to
|
|
78
|
+
// the global value. review_mode and debate_mode are not track-scoped.
|
|
79
|
+
const requestedPhaseDelegation =
|
|
80
|
+
track && perTrack?.[track] ? perTrack[track] : orchestrationConfig.phase_delegation
|
|
81
|
+
|
|
57
82
|
// Resolve each dimension
|
|
58
|
-
const pdResult = resolveMode(
|
|
59
|
-
orchestrationConfig.phase_delegation,
|
|
60
|
-
capabilities,
|
|
61
|
-
'phase_delegation',
|
|
62
|
-
)
|
|
83
|
+
const pdResult = resolveMode(requestedPhaseDelegation, capabilities, 'phase_delegation')
|
|
63
84
|
const rmResult = resolveMode(orchestrationConfig.review_mode, capabilities, 'review_mode')
|
|
64
85
|
const dmResult = resolveMode(orchestrationConfig.debate_mode, capabilities, 'debate_mode')
|
|
65
86
|
|
|
@@ -79,6 +100,11 @@ export function cmdReconcile(projectDir) {
|
|
|
79
100
|
resolved,
|
|
80
101
|
}
|
|
81
102
|
|
|
103
|
+
if (track) {
|
|
104
|
+
result.track = track
|
|
105
|
+
result.track_source = perTrack && perTrack[track] ? 'per_track' : 'fallback'
|
|
106
|
+
}
|
|
107
|
+
|
|
82
108
|
if (downgrades.length > 0) {
|
|
83
109
|
result.downgrades = downgrades
|
|
84
110
|
}
|
|
@@ -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
|
+
}
|