@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.
Files changed (127) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/agents/apt-planner.md +34 -3
  3. package/dist/cli/commands/coverage-check.d.mts.map +1 -1
  4. package/dist/cli/commands/coverage-check.mjs +74 -7
  5. package/dist/cli/commands/coverage-check.mjs.map +1 -1
  6. package/dist/cli/commands/detect-runtime.d.mts.map +1 -1
  7. package/dist/cli/commands/detect-runtime.mjs +18 -12
  8. package/dist/cli/commands/detect-runtime.mjs.map +1 -1
  9. package/dist/cli/coverage-check/user-outcomes.d.mts +84 -0
  10. package/dist/cli/coverage-check/user-outcomes.d.mts.map +1 -0
  11. package/dist/cli/coverage-check/user-outcomes.mjs +265 -0
  12. package/dist/cli/coverage-check/user-outcomes.mjs.map +1 -0
  13. package/dist/cli/host/detect.mjs +1 -1
  14. package/dist/cli/host/detect.mjs.map +1 -1
  15. package/dist/cli/install/install-from-source.mjs +3 -3
  16. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  17. package/dist/cli/install/legacy-paths.mjs +2 -0
  18. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  19. package/dist/cli/install/manifest.d.mts +13 -0
  20. package/dist/cli/install/manifest.d.mts.map +1 -1
  21. package/dist/cli/install/manifest.mjs +5 -0
  22. package/dist/cli/install/manifest.mjs.map +1 -1
  23. package/dist/cli/install/pipeline.d.mts +15 -0
  24. package/dist/cli/install/pipeline.d.mts.map +1 -1
  25. package/dist/cli/install/pipeline.mjs +27 -0
  26. package/dist/cli/install/pipeline.mjs.map +1 -1
  27. package/dist/cli/install/transforms/codex.d.mts +1 -1
  28. package/dist/cli/install/transforms/codex.d.mts.map +1 -1
  29. package/dist/cli/install/transforms/codex.mjs +43 -2
  30. package/dist/cli/install/transforms/codex.mjs.map +1 -1
  31. package/dist/cli/install/version-header.d.mts.map +1 -1
  32. package/dist/cli/install/version-header.mjs +7 -1
  33. package/dist/cli/install/version-header.mjs.map +1 -1
  34. package/dist/cli/verify-proof/idl/index.d.mts +1 -1
  35. package/dist/cli/verify-proof/idl/index.mjs +7 -8
  36. package/dist/cli/verify-proof/idl/index.mjs.map +1 -1
  37. package/dist/cli/verify-proof/idl/types.d.ts +5 -4
  38. package/dist/cli/verify-proof/idl/types.d.ts.map +1 -1
  39. package/dist/cli/verify-proof/idl/types.js +4 -3
  40. package/dist/cli/verify-proof/idl/types.js.map +1 -1
  41. package/dist/cli/verify-proof/resolver.d.mts +63 -1
  42. package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
  43. package/dist/cli/verify-proof/resolver.mjs +164 -4
  44. package/dist/cli/verify-proof/resolver.mjs.map +1 -1
  45. package/dist/cli/verify-proof/runtime-detect.d.mts +6 -3
  46. package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -1
  47. package/dist/cli/verify-proof/runtime-detect.mjs +104 -11
  48. package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -1
  49. package/dist/cli/verify-proof/trust.d.mts +1 -1
  50. package/dist/cli/verify-proof/trust.d.mts.map +1 -1
  51. package/dist/cli/verify-proof/trust.mjs +1 -1
  52. package/dist/driver-sdk/conformance.d.ts +41 -0
  53. package/dist/driver-sdk/conformance.d.ts.map +1 -0
  54. package/dist/driver-sdk/conformance.js +123 -0
  55. package/dist/driver-sdk/conformance.js.map +1 -0
  56. package/dist/driver-sdk/errors.d.ts +75 -0
  57. package/dist/driver-sdk/errors.d.ts.map +1 -0
  58. package/dist/driver-sdk/errors.js +80 -0
  59. package/dist/driver-sdk/errors.js.map +1 -0
  60. package/dist/driver-sdk/idl.d.ts +221 -0
  61. package/dist/driver-sdk/idl.d.ts.map +1 -0
  62. package/dist/driver-sdk/idl.js +140 -0
  63. package/dist/driver-sdk/idl.js.map +1 -0
  64. package/dist/driver-sdk/index.d.ts +28 -0
  65. package/dist/driver-sdk/index.d.ts.map +1 -0
  66. package/dist/driver-sdk/index.js +28 -0
  67. package/dist/driver-sdk/index.js.map +1 -0
  68. package/dist/driver-sdk/manifest.d.ts +93 -0
  69. package/dist/driver-sdk/manifest.d.ts.map +1 -0
  70. package/dist/driver-sdk/manifest.js +12 -0
  71. package/dist/driver-sdk/manifest.js.map +1 -0
  72. package/dist/driver-sdk/retry.d.ts +33 -0
  73. package/dist/driver-sdk/retry.d.ts.map +1 -0
  74. package/dist/driver-sdk/retry.js +50 -0
  75. package/dist/driver-sdk/retry.js.map +1 -0
  76. package/dist/plugin/.claude-plugin/plugin.json +2 -1
  77. package/dist/plugin/agents/apt-planner.md +34 -3
  78. package/dist/plugin/skills/apt-plan/SKILL.md +50 -0
  79. package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -1
  80. package/dist/plugin/skills/apt-research/SKILL.md +526 -0
  81. package/dist/plugin/skills/apt-research/appendices/budget-loop.md +397 -0
  82. package/dist/plugin/skills/apt-research/appendices/claim-graph-schema.md +206 -0
  83. package/dist/plugin/skills/apt-research/appendices/domain-tools.md +236 -0
  84. package/dist/plugin/skills/apt-spar/SKILL.md +19 -7
  85. package/dist/plugin/skills/apt-verify-proof/SKILL.md +41 -2
  86. package/dist/schemas/quick-task.d.ts +17 -17
  87. package/drivers/.gitkeep +0 -0
  88. package/drivers/api/README.md +40 -0
  89. package/drivers/api/driver.mjs +59 -0
  90. package/drivers/api/manifest.json +26 -0
  91. package/drivers/browser/README.md +105 -0
  92. package/drivers/browser/driver.mjs +134 -0
  93. package/drivers/browser/manifest.json +35 -0
  94. package/drivers/cli/README.md +44 -0
  95. package/drivers/cli/driver.mjs +62 -0
  96. package/drivers/cli/manifest.json +28 -0
  97. package/drivers/electron/README.md +64 -0
  98. package/drivers/electron/driver.mjs +87 -0
  99. package/drivers/electron/manifest.json +37 -0
  100. package/package.json +7 -3
  101. package/skills/apt-plan/SKILL.md +50 -0
  102. package/skills/apt-planner.md +16 -0
  103. package/skills/apt-pr-review/SKILL.md +1 -1
  104. package/skills/apt-research/SKILL.md +526 -0
  105. package/skills/apt-research/appendices/budget-loop.md +397 -0
  106. package/skills/apt-research/appendices/claim-graph-schema.md +206 -0
  107. package/skills/apt-research/appendices/domain-tools.md +236 -0
  108. package/skills/apt-spar/SKILL.md +19 -7
  109. package/skills/apt-verify-proof/SKILL.md +41 -2
  110. package/src/cli/commands/coverage-check.mjs +126 -44
  111. package/src/cli/commands/detect-runtime.mjs +23 -14
  112. package/src/cli/coverage-check/user-outcomes.mjs +273 -0
  113. package/src/cli/host/detect.mjs +1 -1
  114. package/src/cli/install/install-from-source.mjs +3 -3
  115. package/src/cli/install/legacy-paths.mjs +2 -0
  116. package/src/cli/install/manifest.mjs +5 -0
  117. package/src/cli/install/pipeline.mjs +28 -0
  118. package/src/cli/install/transforms/codex.mjs +51 -2
  119. package/src/cli/install/version-header.mjs +7 -1
  120. package/src/cli/verify-proof/idl/index.mjs +7 -8
  121. package/src/cli/verify-proof/idl/types.ts +5 -4
  122. package/src/cli/verify-proof/manifest-schema.json +2 -1
  123. package/src/cli/verify-proof/resolver.mjs +171 -4
  124. package/src/cli/verify-proof/runtime-detect.mjs +99 -10
  125. package/src/cli/verify-proof/trust.mjs +1 -1
  126. package/templates/proof-verification.md +32 -3
  127. 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
+ }
@@ -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 -m'
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 (currently
7
- * `@aperant/driver-sdk`, but the walker is general). Hands the full tarball
8
- * list to a single `npm install --prefix` invocation so the same-PR
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({ canonicalContent, canonicalRelPath, version, fileType }) {
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
- const wrappedBody = `<codex_agent_role>\n${parsed.body}\n</codex_agent_role>\n`
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
- if (declared === null && /\.(md|js|mjs|sh)$/.test(entry.path)) {
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
- * The SINGLE SOURCE OF TRUTH lives in `@aperant/driver-sdk` (idl.ts) — see
5
- * ID-04 in spec.md. This file just re-exports those runtime values so
6
- * framework-internal callers (bundled drivers, resolver, driver-doctor,
7
- * conformance kit) can import from the existing relative path without
8
- * paying the cost of refactoring every callsite.
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-driver-sdk/src/idl.ts. This
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 in `@aperant/driver-sdk/idl.ts`. Framework
5
- * code imports from here so future moves (e.g. an internal-only IDL
6
- * extension) only touch this re-export, not every call site.
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'
@@ -164,7 +164,8 @@
164
164
  "type": "object",
165
165
  "required": ["mcp_server_id"],
166
166
  "properties": {
167
- "mcp_server_id": { "type": "string" }
167
+ "mcp_server_id": { "type": "string" },
168
+ "skill_id": { "type": "string" }
168
169
  }
169
170
  },
170
171
  {