@aperant/framework 0.8.4 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +185 -0
- package/agents/apt-planner.md +34 -3
- package/dist/cli/commands/coverage-check.d.mts.map +1 -1
- package/dist/cli/commands/coverage-check.mjs +74 -7
- package/dist/cli/commands/coverage-check.mjs.map +1 -1
- package/dist/cli/commands/detect-runtime.d.mts.map +1 -1
- package/dist/cli/commands/detect-runtime.mjs +18 -12
- package/dist/cli/commands/detect-runtime.mjs.map +1 -1
- package/dist/cli/coverage-check/user-outcomes.d.mts +84 -0
- package/dist/cli/coverage-check/user-outcomes.d.mts.map +1 -0
- package/dist/cli/coverage-check/user-outcomes.mjs +265 -0
- package/dist/cli/coverage-check/user-outcomes.mjs.map +1 -0
- package/dist/cli/host/detect.mjs +1 -1
- package/dist/cli/host/detect.mjs.map +1 -1
- package/dist/cli/install/install-from-source.mjs +3 -3
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +2 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/install/manifest.d.mts +13 -0
- package/dist/cli/install/manifest.d.mts.map +1 -1
- package/dist/cli/install/manifest.mjs +5 -0
- package/dist/cli/install/manifest.mjs.map +1 -1
- package/dist/cli/install/pipeline.d.mts +15 -0
- package/dist/cli/install/pipeline.d.mts.map +1 -1
- package/dist/cli/install/pipeline.mjs +27 -0
- package/dist/cli/install/pipeline.mjs.map +1 -1
- package/dist/cli/install/transforms/codex.d.mts +1 -1
- package/dist/cli/install/transforms/codex.d.mts.map +1 -1
- package/dist/cli/install/transforms/codex.mjs +43 -2
- package/dist/cli/install/transforms/codex.mjs.map +1 -1
- package/dist/cli/install/version-header.d.mts.map +1 -1
- package/dist/cli/install/version-header.mjs +7 -1
- package/dist/cli/install/version-header.mjs.map +1 -1
- package/dist/cli/verify-proof/idl/index.d.mts +1 -1
- package/dist/cli/verify-proof/idl/index.mjs +7 -8
- package/dist/cli/verify-proof/idl/index.mjs.map +1 -1
- package/dist/cli/verify-proof/idl/types.d.ts +5 -4
- package/dist/cli/verify-proof/idl/types.d.ts.map +1 -1
- package/dist/cli/verify-proof/idl/types.js +4 -3
- package/dist/cli/verify-proof/idl/types.js.map +1 -1
- package/dist/cli/verify-proof/resolver.d.mts +63 -1
- package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
- package/dist/cli/verify-proof/resolver.mjs +164 -4
- package/dist/cli/verify-proof/resolver.mjs.map +1 -1
- package/dist/cli/verify-proof/runtime-detect.d.mts +6 -3
- package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -1
- package/dist/cli/verify-proof/runtime-detect.mjs +104 -11
- package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -1
- package/dist/cli/verify-proof/trust.d.mts +1 -1
- package/dist/cli/verify-proof/trust.d.mts.map +1 -1
- package/dist/cli/verify-proof/trust.mjs +1 -1
- package/dist/driver-sdk/conformance.d.ts +41 -0
- package/dist/driver-sdk/conformance.d.ts.map +1 -0
- package/dist/driver-sdk/conformance.js +123 -0
- package/dist/driver-sdk/conformance.js.map +1 -0
- package/dist/driver-sdk/errors.d.ts +75 -0
- package/dist/driver-sdk/errors.d.ts.map +1 -0
- package/dist/driver-sdk/errors.js +80 -0
- package/dist/driver-sdk/errors.js.map +1 -0
- package/dist/driver-sdk/idl.d.ts +221 -0
- package/dist/driver-sdk/idl.d.ts.map +1 -0
- package/dist/driver-sdk/idl.js +140 -0
- package/dist/driver-sdk/idl.js.map +1 -0
- package/dist/driver-sdk/index.d.ts +28 -0
- package/dist/driver-sdk/index.d.ts.map +1 -0
- package/dist/driver-sdk/index.js +28 -0
- package/dist/driver-sdk/index.js.map +1 -0
- package/dist/driver-sdk/manifest.d.ts +93 -0
- package/dist/driver-sdk/manifest.d.ts.map +1 -0
- package/dist/driver-sdk/manifest.js +12 -0
- package/dist/driver-sdk/manifest.js.map +1 -0
- package/dist/driver-sdk/retry.d.ts +33 -0
- package/dist/driver-sdk/retry.d.ts.map +1 -0
- package/dist/driver-sdk/retry.js +50 -0
- package/dist/driver-sdk/retry.js.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +2 -1
- package/dist/plugin/agents/apt-planner.md +34 -3
- package/dist/plugin/skills/apt-plan/SKILL.md +50 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -1
- package/dist/plugin/skills/apt-research/SKILL.md +526 -0
- package/dist/plugin/skills/apt-research/appendices/budget-loop.md +397 -0
- package/dist/plugin/skills/apt-research/appendices/claim-graph-schema.md +206 -0
- package/dist/plugin/skills/apt-research/appendices/domain-tools.md +236 -0
- package/dist/plugin/skills/apt-spar/SKILL.md +19 -7
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +41 -2
- package/dist/schemas/quick-task.d.ts +17 -17
- package/drivers/.gitkeep +0 -0
- package/drivers/api/README.md +40 -0
- package/drivers/api/driver.mjs +59 -0
- package/drivers/api/manifest.json +26 -0
- package/drivers/browser/README.md +105 -0
- package/drivers/browser/driver.mjs +134 -0
- package/drivers/browser/manifest.json +35 -0
- package/drivers/cli/README.md +44 -0
- package/drivers/cli/driver.mjs +62 -0
- package/drivers/cli/manifest.json +28 -0
- package/drivers/electron/README.md +64 -0
- package/drivers/electron/driver.mjs +87 -0
- package/drivers/electron/manifest.json +37 -0
- package/package.json +7 -3
- package/skills/apt-plan/SKILL.md +50 -0
- package/skills/apt-planner.md +16 -0
- package/skills/apt-pr-review/SKILL.md +1 -1
- package/skills/apt-research/SKILL.md +526 -0
- package/skills/apt-research/appendices/budget-loop.md +397 -0
- package/skills/apt-research/appendices/claim-graph-schema.md +206 -0
- package/skills/apt-research/appendices/domain-tools.md +236 -0
- package/skills/apt-spar/SKILL.md +19 -7
- package/skills/apt-verify-proof/SKILL.md +41 -2
- package/src/cli/commands/coverage-check.mjs +126 -44
- package/src/cli/commands/detect-runtime.mjs +23 -14
- package/src/cli/coverage-check/user-outcomes.mjs +273 -0
- package/src/cli/host/detect.mjs +1 -1
- package/src/cli/install/install-from-source.mjs +3 -3
- package/src/cli/install/legacy-paths.mjs +2 -0
- package/src/cli/install/manifest.mjs +5 -0
- package/src/cli/install/pipeline.mjs +28 -0
- package/src/cli/install/transforms/codex.mjs +51 -2
- package/src/cli/install/version-header.mjs +7 -1
- package/src/cli/verify-proof/idl/index.mjs +7 -8
- package/src/cli/verify-proof/idl/types.ts +5 -4
- package/src/cli/verify-proof/manifest-schema.json +2 -1
- package/src/cli/verify-proof/resolver.mjs +171 -4
- package/src/cli/verify-proof/runtime-detect.mjs +99 -10
- package/src/cli/verify-proof/trust.mjs +1 -1
- package/templates/proof-verification.md +32 -3
- package/workflows/verify-proof.md +78 -9
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* coverage-check/user-outcomes.mjs — `## User Outcomes` PRD section parser.
|
|
3
|
+
*
|
|
4
|
+
* Pure, file-IO-free. Mirrors the `coverage-check/acceptance-criteria.mjs`
|
|
5
|
+
* pattern: takes spec.md content as a string and returns a structured
|
|
6
|
+
* `{header_present, body_kind, epic?, outcomes[], validation_warnings?}`
|
|
7
|
+
* envelope the coverage-check command consumes.
|
|
8
|
+
*
|
|
9
|
+
* The `## User Outcomes` section is the verify-proof input contract
|
|
10
|
+
* introduced in v0.8.6 (spec.md §ID-01 / §ID-07). The grammar:
|
|
11
|
+
*
|
|
12
|
+
* ## User Outcomes
|
|
13
|
+
*
|
|
14
|
+
* **Epic:** {one-sentence headline}
|
|
15
|
+
*
|
|
16
|
+
* - **ON** [surface]: {prose}
|
|
17
|
+
* - **ON** [surface]: {prose} *(requires ON N)*
|
|
18
|
+
*
|
|
19
|
+
* (refactor-only escape — INSTEAD of the outcome list:)
|
|
20
|
+
* _No user-observable changes — pure refactor._
|
|
21
|
+
*
|
|
22
|
+
* `surface` is the closed set: electron | web | cli | api | electron+web.
|
|
23
|
+
*
|
|
24
|
+
* **Positional ID synthesis (§ID-07 / M1).** The parser counts outcome
|
|
25
|
+
* rows in document order and synthesizes `id = "O" + (i+1)`. The literal
|
|
26
|
+
* `**ON**` regex stays — IDs are NOT author-written. The `requires N`
|
|
27
|
+
* digit captured from `*(requires ON N)*` is validated against the set
|
|
28
|
+
* of synthesized IDs; on mismatch the parser emits a
|
|
29
|
+
* `validation_warnings[]` entry of kind `unknown_requires_target`,
|
|
30
|
+
* skips the dep edge, but still includes the outcome row.
|
|
31
|
+
*
|
|
32
|
+
* See packages/framework/skills/apt-plan/SKILL.md Section 5.B for the
|
|
33
|
+
* authoring contract.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const VALID_SURFACES = new Set(['electron', 'web', 'cli', 'api', 'electron+web'])
|
|
37
|
+
|
|
38
|
+
const HEADING_RE = /^(#{1,6})\s+(.*)$/
|
|
39
|
+
const USER_OUTCOMES_HEADING_RE = /^user\s+outcomes\s*$/i
|
|
40
|
+
const EPIC_RE = /^\*\*Epic:\*\*\s+(.+)$/
|
|
41
|
+
const EMPTY_WITH_NOTE_RE = /^_No user-observable changes — pure refactor\._\s*$/
|
|
42
|
+
const OUTCOME_RE =
|
|
43
|
+
/^-\s+\*\*ON\*\*\s+\[(electron|web|cli|api|electron\+web)\]:\s+(.+?)(\s+\*\(requires\s+ON\s+(\d+)\)\*)?\s*$/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} UserOutcome
|
|
47
|
+
* @property {string} id — synthesized positional ID (O1, O2, ...)
|
|
48
|
+
* @property {('electron'|'web'|'cli'|'api'|'electron+web')} surface
|
|
49
|
+
* @property {string} text — outcome prose (sans the surface tag and requires suffix)
|
|
50
|
+
* @property {number} [requires] — the digit captured from `*(requires ON N)*`, when present and valid
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} ValidationWarning
|
|
55
|
+
* @property {'unknown_requires_target'} kind
|
|
56
|
+
* @property {string} outcome_id — synthesized ID of the outcome carrying the bad `requires`
|
|
57
|
+
* @property {number} requires — the offending digit
|
|
58
|
+
* @property {string} reason — human-readable explanation
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {Object} UserOutcomesParseResult
|
|
63
|
+
* @property {boolean} header_present
|
|
64
|
+
* @property {('absent'|'empty'|'empty-with-note'|'outcomes')} body_kind
|
|
65
|
+
* @property {string} [epic]
|
|
66
|
+
* @property {UserOutcome[]} outcomes
|
|
67
|
+
* @property {ValidationWarning[]} [validation_warnings]
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse the `## User Outcomes` section from a spec.md string.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} specContent
|
|
74
|
+
* @returns {UserOutcomesParseResult}
|
|
75
|
+
*/
|
|
76
|
+
export function parseUserOutcomes(specContent) {
|
|
77
|
+
if (typeof specContent !== 'string' || specContent.length === 0) {
|
|
78
|
+
return { header_present: false, body_kind: 'absent', outcomes: [] }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const lines = specContent.split('\n')
|
|
82
|
+
|
|
83
|
+
// Find the `## User Outcomes` heading + scope to its body (until the next
|
|
84
|
+
// heading of equal or lower depth).
|
|
85
|
+
let headingLine = -1
|
|
86
|
+
let headingLevel = 0
|
|
87
|
+
for (let i = 0; i < lines.length; i++) {
|
|
88
|
+
const m = lines[i].trim().match(HEADING_RE)
|
|
89
|
+
if (m && USER_OUTCOMES_HEADING_RE.test(m[2])) {
|
|
90
|
+
headingLine = i
|
|
91
|
+
headingLevel = m[1].length
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (headingLine === -1) {
|
|
97
|
+
return { header_present: false, body_kind: 'absent', outcomes: [] }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Collect body lines from heading+1 until the next heading of ANY
|
|
101
|
+
// depth. The User Outcomes section is intentionally flat — a
|
|
102
|
+
// downstream sub-heading (e.g. `### Acceptance Criteria` following
|
|
103
|
+
// directly without a `## ` separator) ends the body. This matches
|
|
104
|
+
// how authors write the section and avoids accidentally pulling
|
|
105
|
+
// stray prose from later sections into the outcomes pool.
|
|
106
|
+
const bodyLines = []
|
|
107
|
+
for (let i = headingLine + 1; i < lines.length; i++) {
|
|
108
|
+
const trimmed = lines[i].trim()
|
|
109
|
+
if (HEADING_RE.test(trimmed)) break
|
|
110
|
+
bodyLines.push(lines[i])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Classify body shape.
|
|
114
|
+
const nonBlank = bodyLines.map((l) => l.trim()).filter((l) => l.length > 0)
|
|
115
|
+
|
|
116
|
+
if (nonBlank.length === 0) {
|
|
117
|
+
return { header_present: true, body_kind: 'empty', outcomes: [] }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Empty-with-note escape — body MUST contain only the verbatim
|
|
121
|
+
// italic-underscore-em-dash-period sentinel (other lines may exist
|
|
122
|
+
// only if they are the Epic header or blank — but the spec says the
|
|
123
|
+
// sentinel REPLACES the outcome list, so we require it to be the only
|
|
124
|
+
// non-blank line OR the only non-Epic non-blank line).
|
|
125
|
+
let epic
|
|
126
|
+
const sentinelLines = []
|
|
127
|
+
const outcomeLines = []
|
|
128
|
+
for (const line of nonBlank) {
|
|
129
|
+
const epicMatch = line.match(EPIC_RE)
|
|
130
|
+
if (epicMatch) {
|
|
131
|
+
epic = epicMatch[1].trim()
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
if (EMPTY_WITH_NOTE_RE.test(line)) {
|
|
135
|
+
sentinelLines.push(line)
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
outcomeLines.push(line)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If the sentinel is present and there are NO outcome lines, this is
|
|
142
|
+
// the empty-with-note branch. The sentinel may co-exist with an Epic
|
|
143
|
+
// header — that's not required but not forbidden either.
|
|
144
|
+
if (sentinelLines.length > 0 && outcomeLines.length === 0) {
|
|
145
|
+
/** @type {UserOutcomesParseResult} */
|
|
146
|
+
const result = {
|
|
147
|
+
header_present: true,
|
|
148
|
+
body_kind: 'empty-with-note',
|
|
149
|
+
outcomes: [],
|
|
150
|
+
}
|
|
151
|
+
if (epic) result.epic = epic
|
|
152
|
+
return result
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// If neither the sentinel nor any outcome rows are present, the body
|
|
156
|
+
// is effectively empty (header present, no parseable content).
|
|
157
|
+
if (outcomeLines.length === 0) {
|
|
158
|
+
/** @type {UserOutcomesParseResult} */
|
|
159
|
+
const result = {
|
|
160
|
+
header_present: true,
|
|
161
|
+
body_kind: 'empty',
|
|
162
|
+
outcomes: [],
|
|
163
|
+
}
|
|
164
|
+
if (epic) result.epic = epic
|
|
165
|
+
return result
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Parse outcome rows in document order. Skip non-matching lines (e.g.
|
|
169
|
+
// stray prose between outcomes) — they don't count as outcomes.
|
|
170
|
+
/** @type {UserOutcome[]} */
|
|
171
|
+
const outcomes = []
|
|
172
|
+
const rawRequires = []
|
|
173
|
+
for (const line of outcomeLines) {
|
|
174
|
+
const m = line.match(OUTCOME_RE)
|
|
175
|
+
if (!m) continue
|
|
176
|
+
const surface = /** @type {UserOutcome['surface']} */ (m[1])
|
|
177
|
+
if (!VALID_SURFACES.has(surface)) continue
|
|
178
|
+
const text = m[2].trim()
|
|
179
|
+
const id = 'O' + (outcomes.length + 1)
|
|
180
|
+
const outcome = /** @type {UserOutcome} */ ({ id, surface, text })
|
|
181
|
+
if (m[4]) {
|
|
182
|
+
const n = Number.parseInt(m[4], 10)
|
|
183
|
+
rawRequires.push({ outcomeIndex: outcomes.length, requires: n })
|
|
184
|
+
}
|
|
185
|
+
outcomes.push(outcome)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (outcomes.length === 0) {
|
|
189
|
+
// Header present, prose present, but no parseable `**ON**` rows —
|
|
190
|
+
// effectively empty.
|
|
191
|
+
/** @type {UserOutcomesParseResult} */
|
|
192
|
+
const result = {
|
|
193
|
+
header_present: true,
|
|
194
|
+
body_kind: 'empty',
|
|
195
|
+
outcomes: [],
|
|
196
|
+
}
|
|
197
|
+
if (epic) result.epic = epic
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Validate `requires` cross-refs against the set of synthesized IDs.
|
|
202
|
+
/** @type {ValidationWarning[]} */
|
|
203
|
+
const warnings = []
|
|
204
|
+
for (const r of rawRequires) {
|
|
205
|
+
if (r.requires >= 1 && r.requires <= outcomes.length) {
|
|
206
|
+
outcomes[r.outcomeIndex].requires = r.requires
|
|
207
|
+
} else {
|
|
208
|
+
warnings.push({
|
|
209
|
+
kind: 'unknown_requires_target',
|
|
210
|
+
outcome_id: outcomes[r.outcomeIndex].id,
|
|
211
|
+
requires: r.requires,
|
|
212
|
+
reason: `requires ON ${r.requires} but only ${outcomes.length} outcome(s) parsed`,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** @type {UserOutcomesParseResult} */
|
|
218
|
+
const result = {
|
|
219
|
+
header_present: true,
|
|
220
|
+
body_kind: 'outcomes',
|
|
221
|
+
outcomes,
|
|
222
|
+
}
|
|
223
|
+
if (epic) result.epic = epic
|
|
224
|
+
if (warnings.length > 0) result.validation_warnings = warnings
|
|
225
|
+
return result
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Compute the coverage-check gate result for a parsed user-outcomes
|
|
230
|
+
* envelope. Per §ID-07 table — warn-not-block in v0.8.x, hard-block in
|
|
231
|
+
* v0.9.0.
|
|
232
|
+
*
|
|
233
|
+
* | header_present | body_kind | v0.8.x | v0.9.0 |
|
|
234
|
+
* |----------------|-------------------|--------|--------|
|
|
235
|
+
* | false | — | warn | block |
|
|
236
|
+
* | true | empty-with-note | pass | pass |
|
|
237
|
+
* | true | outcomes (≥1) | pass | pass |
|
|
238
|
+
* | true | empty | block | block |
|
|
239
|
+
*
|
|
240
|
+
* @param {UserOutcomesParseResult} parsed
|
|
241
|
+
* @param {string} frameworkVersion — semver string from the kernel's package.json (e.g. "0.8.6")
|
|
242
|
+
* @returns {('pass'|'warn'|'block')}
|
|
243
|
+
*/
|
|
244
|
+
export function userOutcomesGateResult(parsed, frameworkVersion) {
|
|
245
|
+
if (parsed.body_kind === 'empty-with-note' || parsed.body_kind === 'outcomes') {
|
|
246
|
+
return 'pass'
|
|
247
|
+
}
|
|
248
|
+
if (parsed.body_kind === 'empty') {
|
|
249
|
+
// Header present, body empty — author started the section and forgot
|
|
250
|
+
// to fill it in. Block at every version.
|
|
251
|
+
return 'block'
|
|
252
|
+
}
|
|
253
|
+
// Missing header (body_kind: 'absent'). Warn in 0.8.x, block in 0.9.0+.
|
|
254
|
+
const major = parseFrameworkMajorMinor(frameworkVersion)
|
|
255
|
+
if (major === null) return 'warn'
|
|
256
|
+
const { major: maj, minor: min } = major
|
|
257
|
+
if (maj > 0 || (maj === 0 && min >= 9)) return 'block'
|
|
258
|
+
return 'warn'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Parse the leading `major.minor` of a semver string. Returns null on
|
|
263
|
+
* malformed input (caller treats as "warn" — fail-soft).
|
|
264
|
+
*
|
|
265
|
+
* @param {string} version
|
|
266
|
+
* @returns {{major: number, minor: number} | null}
|
|
267
|
+
*/
|
|
268
|
+
function parseFrameworkMajorMinor(version) {
|
|
269
|
+
if (typeof version !== 'string') return null
|
|
270
|
+
const m = version.match(/^(\d+)\.(\d+)/)
|
|
271
|
+
if (!m) return null
|
|
272
|
+
return { major: Number.parseInt(m[1], 10), minor: Number.parseInt(m[2], 10) }
|
|
273
|
+
}
|
package/src/cli/host/detect.mjs
CHANGED
|
@@ -345,6 +345,6 @@ function detectRawCli(partner, pathEntries, exists) {
|
|
|
345
345
|
function rawCliInvocation(partner) {
|
|
346
346
|
if (partner === 'codex') return 'codex exec'
|
|
347
347
|
if (partner === 'claude') return 'claude -p'
|
|
348
|
-
if (partner === 'gemini') return 'gemini
|
|
348
|
+
if (partner === 'gemini') return 'gemini'
|
|
349
349
|
return partner
|
|
350
350
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `apt-tools install-from-source <project-dir>` for the Aperant-contributor
|
|
5
5
|
* self-host case. Resolves the workspace `@aperant/framework` chain — packs
|
|
6
|
-
* the framework itself AND every `workspace:*` dependency (
|
|
7
|
-
*
|
|
8
|
-
* list to a single `npm install --prefix` invocation so
|
|
6
|
+
* the framework itself AND every `workspace:*` dependency (none today after
|
|
7
|
+
* the 0.8.5 driver-sdk inline, but the walker is general). Hands the full
|
|
8
|
+
* tarball list to a single `npm install --prefix` invocation so any future
|
|
9
9
|
* unpublished-pair case stops blowing up with `EUNSUPPORTEDPROTOCOL`.
|
|
10
10
|
*
|
|
11
11
|
* Stable envelope:
|
|
@@ -109,6 +109,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
|
|
|
109
109
|
'agents/apt-prototype.md',
|
|
110
110
|
'agents/apt-quick.md',
|
|
111
111
|
'agents/apt-release-notes.md',
|
|
112
|
+
'agents/apt-research.md',
|
|
112
113
|
'agents/apt-resume.md',
|
|
113
114
|
'agents/apt-review.md',
|
|
114
115
|
'agents/apt-roadmap.md',
|
|
@@ -151,6 +152,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
|
|
|
151
152
|
'commands/apt-prototype.md',
|
|
152
153
|
'commands/apt-quick.md',
|
|
153
154
|
'commands/apt-release-notes.md',
|
|
155
|
+
'commands/apt-research.md',
|
|
154
156
|
'commands/apt-resume.md',
|
|
155
157
|
'commands/apt-review.md',
|
|
156
158
|
'commands/apt-roadmap.md',
|
|
@@ -39,6 +39,11 @@ export const MANIFEST_SCHEMA_VERSION = 1
|
|
|
39
39
|
* @property {string} installedHash
|
|
40
40
|
* @property {string} sourcePath
|
|
41
41
|
* @property {string} transformVersion
|
|
42
|
+
* @property {boolean} [headerless] true when the installed file legitimately
|
|
43
|
+
* carries no apt-skill-version/apt-hook-version header (e.g. Codex strips it).
|
|
44
|
+
* Drift detection skips the missing_header check for these — installedHash is
|
|
45
|
+
* the sole drift signal. Absent (undefined) on pre-0.8.6 manifests → treated
|
|
46
|
+
* as header-expected, preserving old behavior until re-install.
|
|
42
47
|
*/
|
|
43
48
|
|
|
44
49
|
/**
|
|
@@ -17,6 +17,7 @@ import { atomicWriteText } from './atomic-text.mjs'
|
|
|
17
17
|
import { addManifestEntry, newManifest, sha256, writeManifest } from './manifest.mjs'
|
|
18
18
|
import { getRuntime } from './runtime-detect.mjs'
|
|
19
19
|
import { classifyFileType } from './transforms/_shared.mjs'
|
|
20
|
+
import { parseVersionHeader } from './version-header.mjs'
|
|
20
21
|
|
|
21
22
|
const TRANSFORM_VERSION = '1'
|
|
22
23
|
|
|
@@ -67,6 +68,12 @@ function safeResolveWithin(installRoot, outputPath) {
|
|
|
67
68
|
* @property {string} canonicalRelPath
|
|
68
69
|
* @property {string} version
|
|
69
70
|
* @property {'skill'|'agent'|'hook-js'|'hook-sh'|'unknown'} fileType
|
|
71
|
+
* @property {Record<string, string>} [agentContentByName] canonical agent body
|
|
72
|
+
* keyed by agent name (basename sans `.md`, e.g. `apt-planner`). Lets a
|
|
73
|
+
* transform inline a referenced agent's prompt when the target runtime has
|
|
74
|
+
* no separate agent-spawn surface (Codex). The pipeline always supplies it
|
|
75
|
+
* (empty object when no agents discovered); optional here so unit tests can
|
|
76
|
+
* construct a TransformInput without it — transforms default it to `{}`.
|
|
70
77
|
*/
|
|
71
78
|
|
|
72
79
|
/**
|
|
@@ -180,6 +187,19 @@ export function runInstall({
|
|
|
180
187
|
const files = canonicalFiles ?? discoverCanonicalFiles(canonicalRoot)
|
|
181
188
|
const written = []
|
|
182
189
|
const skipped = []
|
|
190
|
+
// Map of agent name → canonical agent body, so a transform whose runtime
|
|
191
|
+
// has no separate agent-spawn surface (Codex) can inline a referenced
|
|
192
|
+
// agent's prompt. Keyed by basename sans `.md` (e.g. `apt-planner`). When
|
|
193
|
+
// the same name exists under both skills/ and agents/, last-wins — they are
|
|
194
|
+
// the same agent (see the de-dupe note below).
|
|
195
|
+
/** @type {Record<string, string>} */
|
|
196
|
+
const agentContentByName = {}
|
|
197
|
+
for (const f of files) {
|
|
198
|
+
if (f.fileType === 'agent') {
|
|
199
|
+
const name = f.relPath.replace(/^.*\//, '').replace(/\.md$/, '')
|
|
200
|
+
agentContentByName[name] = f.content
|
|
201
|
+
}
|
|
202
|
+
}
|
|
183
203
|
// De-dupe by output path — last-write-wins. Two canonical files can
|
|
184
204
|
// legitimately target the same output (e.g. skills/apt-planner.md and
|
|
185
205
|
// agents/apt-planner.md both producing agents/apt-planner.md). The
|
|
@@ -200,6 +220,7 @@ export function runInstall({
|
|
|
200
220
|
canonicalRelPath: f.relPath,
|
|
201
221
|
version,
|
|
202
222
|
fileType: f.fileType,
|
|
223
|
+
agentContentByName,
|
|
203
224
|
})
|
|
204
225
|
} catch (err) {
|
|
205
226
|
skipped.push(
|
|
@@ -225,12 +246,19 @@ export function runInstall({
|
|
|
225
246
|
}
|
|
226
247
|
atomicWriteText(absPath, out.content)
|
|
227
248
|
const installedHash = sha256(out.content)
|
|
249
|
+
// Record whether the installed file carries a version header. When a
|
|
250
|
+
// transform legitimately strips it (Codex frontmatter whitelist),
|
|
251
|
+
// drift detection must not flag it missing_header — installedHash is
|
|
252
|
+
// the drift signal. Only relevant for header-bearing file types.
|
|
253
|
+
const headerless =
|
|
254
|
+
/\.(md|js|mjs|sh)$/.test(out.outputPath) && parseVersionHeader(out.content) === null
|
|
228
255
|
entryByPath.set(out.outputPath, {
|
|
229
256
|
path: out.outputPath,
|
|
230
257
|
sourceHash,
|
|
231
258
|
installedHash,
|
|
232
259
|
sourcePath: `packages/framework/${f.relPath}`,
|
|
233
260
|
transformVersion: TRANSFORM_VERSION,
|
|
261
|
+
...(headerless ? { headerless: true } : {}),
|
|
234
262
|
})
|
|
235
263
|
if (!written.includes(out.outputPath)) written.push(out.outputPath)
|
|
236
264
|
}
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* against installRoot `.agents` to land at `.agents/skills/<folder>/SKILL.md`).
|
|
15
15
|
* Markdown agents are NOT a Codex surface (custom-agents are TOML
|
|
16
16
|
* under `.codex/agents/`); only skills are emitted.
|
|
17
|
+
* - agent inlining: Codex has no markdown agent-spawn surface, so a skill
|
|
18
|
+
* declaring `spawns_agent: true` + `agent_name: X` would, on Claude, spawn
|
|
19
|
+
* the X agent (whose prompt lives in agents/X.md). On Codex that file is
|
|
20
|
+
* never installed and the spawn directive is stripped by the whitelist —
|
|
21
|
+
* leaving the skill referencing a non-existent agent. To keep Codex at
|
|
22
|
+
* parity, the referenced agent's body is appended inline to the skill body
|
|
23
|
+
* so the model has the full methodology even without a spawn primitive.
|
|
17
24
|
*/
|
|
18
25
|
|
|
19
26
|
import { basename, dirname } from 'node:path'
|
|
@@ -36,16 +43,33 @@ const KEEP = ['name', 'description']
|
|
|
36
43
|
* @param {import('../pipeline.mjs').TransformInput} input
|
|
37
44
|
* @returns {import('../pipeline.mjs').TransformOutput[]}
|
|
38
45
|
*/
|
|
39
|
-
export function transform({
|
|
46
|
+
export function transform({
|
|
47
|
+
canonicalContent,
|
|
48
|
+
canonicalRelPath,
|
|
49
|
+
version,
|
|
50
|
+
fileType,
|
|
51
|
+
agentContentByName = {},
|
|
52
|
+
}) {
|
|
40
53
|
const subbed = substituteVersion(canonicalContent, version)
|
|
41
54
|
const parsed = parseFrontmatter(subbed)
|
|
42
55
|
let payload
|
|
43
56
|
if (parsed.hasFrontmatter && parsed.fm) {
|
|
57
|
+
// Read the spawn directive BEFORE the whitelist strips it. On Codex
|
|
58
|
+
// there's no agent-spawn surface, so we inline the agent body instead.
|
|
59
|
+
const spawnsAgent = parsed.fm.spawns_agent === true
|
|
60
|
+
const agentName =
|
|
61
|
+
typeof parsed.fm.agent_name === 'string' && parsed.fm.agent_name.length > 0
|
|
62
|
+
? parsed.fm.agent_name
|
|
63
|
+
: null
|
|
44
64
|
// Whitelist filter: drop every frontmatter key NOT in KEEP.
|
|
45
65
|
for (const k of Object.keys(parsed.fm)) {
|
|
46
66
|
if (!KEEP.includes(k)) delete parsed.fm[k]
|
|
47
67
|
}
|
|
48
|
-
|
|
68
|
+
let body = parsed.body
|
|
69
|
+
if (spawnsAgent && agentName && agentContentByName[agentName]) {
|
|
70
|
+
body = `${body}\n\n${inlineAgentSection(agentName, agentContentByName[agentName], version)}`
|
|
71
|
+
}
|
|
72
|
+
const wrappedBody = `<codex_agent_role>\n${body}\n</codex_agent_role>\n`
|
|
49
73
|
payload = serializeFrontmatter({ ...parsed, body: wrappedBody })
|
|
50
74
|
} else {
|
|
51
75
|
payload = `<codex_agent_role>\n${subbed}\n</codex_agent_role>\n`
|
|
@@ -63,3 +87,28 @@ export function transform({ canonicalContent, canonicalRelPath, version, fileTyp
|
|
|
63
87
|
// another dead surface. Drop the agent branch entirely.
|
|
64
88
|
return []
|
|
65
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the inlined-agent section appended to a Codex skill body. Strips the
|
|
93
|
+
* agent file's own frontmatter (Codex skills carry a single frontmatter block)
|
|
94
|
+
* and version-substitutes the body. The fenced header makes it unambiguous to
|
|
95
|
+
* the model that this is the methodology it should execute directly — there is
|
|
96
|
+
* no separate agent to spawn on Codex.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} agentName
|
|
99
|
+
* @param {string} agentContent raw canonical agent file (with frontmatter + {{APT_VERSION}})
|
|
100
|
+
* @param {string} version
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
function inlineAgentSection(agentName, agentContent, version) {
|
|
104
|
+
const agentBody = parseFrontmatter(substituteVersion(agentContent, version)).body.trim()
|
|
105
|
+
return [
|
|
106
|
+
`## Inlined agent: ${agentName}`,
|
|
107
|
+
'',
|
|
108
|
+
`Codex has no separate agent-spawn surface. Where this skill says to spawn`,
|
|
109
|
+
`the \`${agentName}\` agent, execute the methodology below directly — you ARE`,
|
|
110
|
+
`that agent for this task.`,
|
|
111
|
+
'',
|
|
112
|
+
agentBody,
|
|
113
|
+
].join('\n')
|
|
114
|
+
}
|
|
@@ -99,7 +99,13 @@ export function inspectManifestFiles(manifest, installRoot, opts = {}) {
|
|
|
99
99
|
continue
|
|
100
100
|
}
|
|
101
101
|
const declared = parseVersionHeader(buf.toString('utf-8'))
|
|
102
|
-
|
|
102
|
+
// `headerless` entries are files whose runtime transform legitimately
|
|
103
|
+
// strips the apt-skill-version header (e.g. Codex whitelists frontmatter
|
|
104
|
+
// to {name, description} and rejects unknown keys). For those, the
|
|
105
|
+
// installedHash check above is the sole drift signal — requiring a header
|
|
106
|
+
// would false-positive every install. The flag is recorded at install
|
|
107
|
+
// time by the pipeline from the post-transform content.
|
|
108
|
+
if (declared === null && !entry.headerless && /\.(md|js|mjs|sh)$/.test(entry.path)) {
|
|
103
109
|
stale.push({ path: entry.path, reason: 'missing_header' })
|
|
104
110
|
continue
|
|
105
111
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* idl/index.mjs — framework-side re-export of the IDL v1 runtime registry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* conformance kit)
|
|
8
|
-
*
|
|
4
|
+
* Single source of truth lives at packages/framework/src/driver-sdk/idl.ts
|
|
5
|
+
* (inlined in 0.8.5 — originally `@aperant/driver-sdk`). This file
|
|
6
|
+
* re-exports those runtime values so existing callers (bundled drivers,
|
|
7
|
+
* resolver, driver-doctor, conformance kit) keep working without a
|
|
8
|
+
* refactor of every callsite.
|
|
9
9
|
*
|
|
10
|
-
* If you add a verb, edit packages/framework
|
|
11
|
-
* file requires no maintenance.
|
|
10
|
+
* If you add a verb, edit packages/framework/src/driver-sdk/idl.ts.
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
export {
|
|
@@ -21,4 +20,4 @@ export {
|
|
|
21
20
|
isVerbResult,
|
|
22
21
|
LIFECYCLE_VERBS,
|
|
23
22
|
validateVerbArgs,
|
|
24
|
-
} from '@aperant/driver-sdk'
|
|
23
|
+
} from '@aperant/framework/driver-sdk'
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* idl/types.ts — re-export the SDK's IDL types for framework-internal use.
|
|
3
3
|
*
|
|
4
|
-
* Single source of truth lives
|
|
5
|
-
* code imports from here so future moves
|
|
6
|
-
*
|
|
4
|
+
* Single source of truth lives at packages/framework/src/driver-sdk/idl.ts
|
|
5
|
+
* (inlined in 0.8.5). Framework code imports from here so future moves
|
|
6
|
+
* (e.g. an internal-only IDL extension or re-extraction) only touch this
|
|
7
|
+
* re-export, not every call site.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
export type {
|
|
@@ -38,4 +39,4 @@ export type {
|
|
|
38
39
|
VerbResult,
|
|
39
40
|
WaitForEventArgs,
|
|
40
41
|
WaitForIdleArgs,
|
|
41
|
-
} from '@aperant/driver-sdk'
|
|
42
|
+
} from '@aperant/framework/driver-sdk'
|