@open-agent-toolkit/cli 0.2.20 → 0.2.22

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 (58) hide show
  1. package/assets/docs/workflows/projects/artifacts.md +1 -1
  2. package/assets/docs/workflows/skills/explainer-kit.md +188 -23
  3. package/assets/docs/workflows/skills/index.md +1 -1
  4. package/assets/public-package-versions.json +4 -4
  5. package/assets/skills/explainer-kit/SKILL.md +87 -25
  6. package/assets/skills/explainer-kit/briefs/deep-dive.md +35 -0
  7. package/assets/skills/explainer-kit/briefs/engineer-tour.md +45 -0
  8. package/assets/skills/explainer-kit/briefs/program-recap.md +41 -0
  9. package/assets/skills/explainer-kit/briefs/project-explainer.md +38 -0
  10. package/assets/skills/explainer-kit/briefs/project-page.md +38 -0
  11. package/assets/skills/explainer-kit/briefs/project-recap.md +47 -0
  12. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +28 -0
  13. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +35 -0
  14. package/assets/skills/explainer-kit/examples/project-recap/content.md +57 -14
  15. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +104 -0
  16. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +26 -4
  17. package/assets/skills/explainer-kit/recipes/engineer-tour.json +27 -10
  18. package/assets/skills/explainer-kit/recipes/program-recap.json +35 -11
  19. package/assets/skills/explainer-kit/recipes/project-explainer.json +27 -10
  20. package/assets/skills/explainer-kit/recipes/project-recap.json +43 -11
  21. package/assets/skills/explainer-kit/references/contracts.md +45 -19
  22. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +41 -0
  23. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +52 -0
  24. package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +442 -0
  25. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +223 -10
  26. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +28 -43
  27. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +237 -0
  28. package/assets/skills/explainer-kit/scripts/lib/html-safety.mjs +687 -0
  29. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +414 -0
  30. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +313 -14
  31. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +314 -41
  32. package/assets/skills/explainer-kit/scripts/lib/records.mjs +61 -0
  33. package/assets/skills/explainer-kit/scripts/lib/render.mjs +166 -12
  34. package/assets/skills/explainer-kit/scripts/render-qa.mjs +152 -2
  35. package/assets/skills/explainer-kit/scripts/run.mjs +789 -272
  36. package/assets/skills/explainer-kit/templates/deck-shell.html +25 -5
  37. package/assets/skills/explainer-kit/templates/diagram-shell.html +29 -7
  38. package/assets/skills/explainer-kit/templates/engineer-tour.html +133 -9
  39. package/assets/skills/explainer-kit/templates/house-style.html +82 -0
  40. package/assets/skills/oat-brainstorm/SKILL.md +1 -1
  41. package/assets/skills/oat-brainstorm/scripts/helper.js +18 -11
  42. package/assets/skills/oat-brainstorm/scripts/server.cjs +109 -55
  43. package/assets/skills/oat-explainer-kit/SKILL.md +16 -9
  44. package/assets/skills/oat-explainer-kit/references/author-callback.md +51 -0
  45. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +10 -8
  46. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +14 -0
  47. package/assets/skills/oat-explainer-kit/scripts/run.mjs +7 -11
  48. package/assets/skills/oat-project-complete/SKILL.md +18 -2
  49. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  50. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +7 -1
  51. package/assets/skills/oat-wave-execute/SKILL.md +12 -19
  52. package/assets/skills/oat-wave-program/SKILL.md +12 -13
  53. package/dist/commands/init/tools/index.d.ts.map +1 -1
  54. package/dist/commands/init/tools/index.js +4 -4
  55. package/dist/engine/compute-plan.js +3 -3
  56. package/package.json +2 -2
  57. package/assets/skills/explainer-kit/schemas/author-request.schema.json +0 -85
  58. package/assets/skills/explainer-kit/schemas/author-result.schema.json +0 -65
@@ -5,11 +5,14 @@ const path = require('path');
5
5
 
6
6
  // ========== WebSocket Protocol (RFC 6455) ==========
7
7
 
8
- const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
8
+ const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0a };
9
9
  const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
10
10
 
11
11
  function computeAcceptKey(clientKey) {
12
- return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
12
+ return crypto
13
+ .createHash('sha1')
14
+ .update(clientKey + WS_MAGIC)
15
+ .digest('base64');
13
16
  }
14
17
 
15
18
  function encodeFrame(opcode, payload) {
@@ -40,9 +43,9 @@ function decodeFrame(buffer) {
40
43
  if (buffer.length < 2) return null;
41
44
 
42
45
  const secondByte = buffer[1];
43
- const opcode = buffer[0] & 0x0F;
46
+ const opcode = buffer[0] & 0x0f;
44
47
  const masked = (secondByte & 0x80) !== 0;
45
- let payloadLen = secondByte & 0x7F;
48
+ let payloadLen = secondByte & 0x7f;
46
49
  let offset = 2;
47
50
 
48
51
  if (!masked) throw new Error('Client frames must be masked');
@@ -73,18 +76,29 @@ function decodeFrame(buffer) {
73
76
 
74
77
  // ========== Configuration ==========
75
78
 
76
- const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
79
+ const PORT =
80
+ process.env.BRAINSTORM_PORT || 49152 + Math.floor(Math.random() * 16383);
77
81
  const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
78
- const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
82
+ const URL_HOST =
83
+ process.env.BRAINSTORM_URL_HOST ||
84
+ (HOST === '127.0.0.1' ? 'localhost' : HOST);
79
85
  const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
80
86
  const CONTENT_DIR = path.join(SESSION_DIR, 'content');
81
87
  const STATE_DIR = path.join(SESSION_DIR, 'state');
82
- let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
88
+ let ownerPid = process.env.BRAINSTORM_OWNER_PID
89
+ ? Number(process.env.BRAINSTORM_OWNER_PID)
90
+ : null;
83
91
 
84
92
  const MIME_TYPES = {
85
- '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
86
- '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
87
- '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml'
93
+ '.html': 'text/html',
94
+ '.css': 'text/css',
95
+ '.js': 'application/javascript',
96
+ '.json': 'application/json',
97
+ '.png': 'image/png',
98
+ '.jpg': 'image/jpeg',
99
+ '.jpeg': 'image/jpeg',
100
+ '.gif': 'image/gif',
101
+ '.svg': 'image/svg+xml',
88
102
  };
89
103
 
90
104
  // ========== Templates and Constants ==========
@@ -98,9 +112,15 @@ h1 { color: #333; } p { color: #666; }</style>
98
112
  <body><h1>Brainstorm Companion</h1>
99
113
  <p>Waiting for the agent to push a screen...</p></body></html>`;
100
114
 
101
- const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
102
- const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
103
- const helperInjection = '<script>\n' + helperScript + '\n</script>';
115
+ const frameTemplate = fs.readFileSync(
116
+ path.join(__dirname, 'frame-template.html'),
117
+ 'utf-8',
118
+ );
119
+ const helperScript = fs.readFileSync(
120
+ path.join(__dirname, 'helper.js'),
121
+ 'utf-8',
122
+ );
123
+ const helperInjection = `<script>\n${helperScript}\n</script>`;
104
124
 
105
125
  // ========== Helper Functions ==========
106
126
 
@@ -114,9 +134,10 @@ function wrapInFrame(content) {
114
134
  }
115
135
 
116
136
  function getNewestScreen() {
117
- const files = fs.readdirSync(CONTENT_DIR)
118
- .filter(f => f.endsWith('.html'))
119
- .map(f => {
137
+ const files = fs
138
+ .readdirSync(CONTENT_DIR)
139
+ .filter((f) => f.endsWith('.html'))
140
+ .map((f) => {
120
141
  const fp = path.join(CONTENT_DIR, f);
121
142
  return { path: fp, mtime: fs.statSync(fp).mtime.getTime() };
122
143
  })
@@ -131,11 +152,13 @@ function handleRequest(req, res) {
131
152
  if (req.method === 'GET' && req.url === '/') {
132
153
  const screenFile = getNewestScreen();
133
154
  let html = screenFile
134
- ? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
155
+ ? ((raw) => (isFullDocument(raw) ? raw : wrapInFrame(raw)))(
156
+ fs.readFileSync(screenFile, 'utf-8'),
157
+ )
135
158
  : WAITING_PAGE;
136
159
 
137
160
  if (html.includes('</body>')) {
138
- html = html.replace('</body>', helperInjection + '\n</body>');
161
+ html = html.replace('</body>', `${helperInjection}\n</body>`);
139
162
  } else {
140
163
  html += helperInjection;
141
164
  }
@@ -166,14 +189,17 @@ const clients = new Set();
166
189
 
167
190
  function handleUpgrade(req, socket) {
168
191
  const key = req.headers['sec-websocket-key'];
169
- if (!key) { socket.destroy(); return; }
192
+ if (!key) {
193
+ socket.destroy();
194
+ return;
195
+ }
170
196
 
171
197
  const accept = computeAcceptKey(key);
172
198
  socket.write(
173
199
  'HTTP/1.1 101 Switching Protocols\r\n' +
174
- 'Upgrade: websocket\r\n' +
175
- 'Connection: Upgrade\r\n' +
176
- 'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n'
200
+ 'Upgrade: websocket\r\n' +
201
+ 'Connection: Upgrade\r\n' +
202
+ `Sec-WebSocket-Accept: ${accept}\r\n\r\n`,
177
203
  );
178
204
 
179
205
  let buffer = Buffer.alloc(0);
@@ -185,7 +211,7 @@ function handleUpgrade(req, socket) {
185
211
  let result;
186
212
  try {
187
213
  result = decodeFrame(buffer);
188
- } catch (e) {
214
+ } catch {
189
215
  socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
190
216
  clients.delete(socket);
191
217
  return;
@@ -233,14 +259,18 @@ function handleMessage(text) {
233
259
  console.log(JSON.stringify({ source: 'user-event', ...event }));
234
260
  if (event.choice) {
235
261
  const eventsFile = path.join(STATE_DIR, 'events');
236
- fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
262
+ fs.appendFileSync(eventsFile, `${JSON.stringify(event)}\n`);
237
263
  }
238
264
  }
239
265
 
240
266
  function broadcast(msg) {
241
267
  const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg)));
242
268
  for (const socket of clients) {
243
- try { socket.write(frame); } catch (e) { clients.delete(socket); }
269
+ try {
270
+ socket.write(frame);
271
+ } catch {
272
+ clients.delete(socket);
273
+ }
244
274
  }
245
275
  }
246
276
 
@@ -260,14 +290,15 @@ const debounceTimers = new Map();
260
290
  // ========== Server Startup ==========
261
291
 
262
292
  function startServer() {
263
- if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true });
293
+ if (!fs.existsSync(CONTENT_DIR))
294
+ fs.mkdirSync(CONTENT_DIR, { recursive: true });
264
295
  if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
265
296
 
266
297
  // Track known files to distinguish new screens from updates.
267
298
  // macOS fs.watch reports 'rename' for both new files and overwrites,
268
299
  // so we can't rely on eventType alone.
269
300
  const knownFiles = new Set(
270
- fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.html'))
301
+ fs.readdirSync(CONTENT_DIR).filter((f) => f.endsWith('.html')),
271
302
  );
272
303
 
273
304
  const server = http.createServer(handleRequest);
@@ -276,25 +307,31 @@ function startServer() {
276
307
  const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => {
277
308
  if (!filename || !filename.endsWith('.html')) return;
278
309
 
279
- if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename));
280
- debounceTimers.set(filename, setTimeout(() => {
281
- debounceTimers.delete(filename);
282
- const filePath = path.join(CONTENT_DIR, filename);
283
-
284
- if (!fs.existsSync(filePath)) return; // file was deleted
285
- touchActivity();
286
-
287
- if (!knownFiles.has(filename)) {
288
- knownFiles.add(filename);
289
- const eventsFile = path.join(STATE_DIR, 'events');
290
- if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
291
- console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
292
- } else {
293
- console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
294
- }
310
+ if (debounceTimers.has(filename))
311
+ clearTimeout(debounceTimers.get(filename));
312
+ debounceTimers.set(
313
+ filename,
314
+ setTimeout(() => {
315
+ debounceTimers.delete(filename);
316
+ const filePath = path.join(CONTENT_DIR, filename);
317
+
318
+ if (!fs.existsSync(filePath)) return; // file was deleted
319
+ touchActivity();
320
+
321
+ if (!knownFiles.has(filename)) {
322
+ knownFiles.add(filename);
323
+ const eventsFile = path.join(STATE_DIR, 'events');
324
+ if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
325
+ console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
326
+ } else {
327
+ console.log(
328
+ JSON.stringify({ type: 'screen-updated', file: filePath }),
329
+ );
330
+ }
295
331
 
296
- broadcast({ type: 'reload' });
297
- }, 100));
332
+ broadcast({ type: 'reload' });
333
+ }, 100),
334
+ );
298
335
  });
299
336
  watcher.on('error', (err) => console.error('fs.watch error:', err.message));
300
337
 
@@ -304,7 +341,7 @@ function startServer() {
304
341
  if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
305
342
  fs.writeFileSync(
306
343
  path.join(STATE_DIR, 'server-stopped'),
307
- JSON.stringify({ reason, timestamp: Date.now() }) + '\n'
344
+ `${JSON.stringify({ reason, timestamp: Date.now() })}\n`,
308
345
  );
309
346
  watcher.close();
310
347
  clearInterval(lifecycleCheck);
@@ -313,13 +350,19 @@ function startServer() {
313
350
 
314
351
  function ownerAlive() {
315
352
  if (!ownerPid) return true;
316
- try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
353
+ try {
354
+ process.kill(ownerPid, 0);
355
+ return true;
356
+ } catch (e) {
357
+ return e.code === 'EPERM';
358
+ }
317
359
  }
318
360
 
319
361
  // Check every 60s: exit if owner process died or idle for 30 minutes
320
362
  const lifecycleCheck = setInterval(() => {
321
363
  if (!ownerAlive()) shutdown('owner process exited');
322
- else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown('idle timeout');
364
+ else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS)
365
+ shutdown('idle timeout');
323
366
  }, 60 * 1000);
324
367
  lifecycleCheck.unref();
325
368
 
@@ -327,10 +370,17 @@ function startServer() {
327
370
  // was wrong (common on WSL, Tailscale SSH, and cross-user scenarios).
328
371
  // Disable monitoring and rely on the idle timeout instead.
329
372
  if (ownerPid) {
330
- try { process.kill(ownerPid, 0); }
331
- catch (e) {
373
+ try {
374
+ process.kill(ownerPid, 0);
375
+ } catch (e) {
332
376
  if (e.code !== 'EPERM') {
333
- console.log(JSON.stringify({ type: 'owner-pid-invalid', pid: ownerPid, reason: 'dead at startup' }));
377
+ console.log(
378
+ JSON.stringify({
379
+ type: 'owner-pid-invalid',
380
+ pid: ownerPid,
381
+ reason: 'dead at startup',
382
+ }),
383
+ );
334
384
  ownerPid = null;
335
385
  }
336
386
  }
@@ -338,12 +388,16 @@ function startServer() {
338
388
 
339
389
  server.listen(PORT, HOST, () => {
340
390
  const info = JSON.stringify({
341
- type: 'server-started', port: Number(PORT), host: HOST,
342
- url_host: URL_HOST, url: 'http://' + URL_HOST + ':' + PORT,
343
- screen_dir: CONTENT_DIR, state_dir: STATE_DIR
391
+ type: 'server-started',
392
+ port: Number(PORT),
393
+ host: HOST,
394
+ url_host: URL_HOST,
395
+ url: `http://${URL_HOST}:${PORT}`,
396
+ screen_dir: CONTENT_DIR,
397
+ state_dir: STATE_DIR,
344
398
  });
345
399
  console.log(info);
346
- fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n');
400
+ fs.writeFileSync(path.join(STATE_DIR, 'server-info'), `${info}\n`);
347
401
  });
348
402
  }
349
403
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-explainer-kit
3
- version: 1.0.1
3
+ version: 1.0.3
4
4
  description: Use when building project explainers or recaps from OAT configuration, state, and lifecycle artifacts.
5
5
  disable-model-invocation: false
6
6
  user-invocable: true
@@ -19,6 +19,7 @@ Adapt OAT project context into the versioned request consumed by the canonical
19
19
  - Derive canonical project or repository output roots.
20
20
  - Bind OAT lifecycle artifacts to generic recipe source roles.
21
21
  - Resolve project explainer and recap intent before invoking the core.
22
+ - Require lifecycle callers to construct a brief-aware author seam.
22
23
 
23
24
  ## Dependency Direction
24
25
 
@@ -33,7 +34,7 @@ rollback, and operator-owned real-wrapper gate, use `references/migration.md`.
33
34
 
34
35
  Before reading OAT config or invoking the core, call
35
36
  `scripts/check-core.mjs#checkCoreCompatibility` with this installed skill
36
- directory and minimum core version `1.0.0`. Continue only when it returns
37
+ directory and minimum core version `2.0.0`. Continue only when it returns
37
38
  `ok: true`.
38
39
 
39
40
  - Missing core: stop and show
@@ -54,7 +55,7 @@ Call `scripts/run.mjs#runOatExplainer` with the repository root, project
54
55
  invocation, active project path, recipe, slug, lifecycle mode, and any explicit
55
56
  runtime overrides. The adapter:
56
57
 
57
- 1. checks the user-scoped installed core at minimum version `1.0.0`;
58
+ 1. checks the user-scoped installed core at minimum version `2.0.0`;
58
59
  2. resolves only the public `explainers.*` and `workflow.explainers.*` keys;
59
60
  3. derives the canonical project output root;
60
61
  4. binds approved OAT artifacts to the recipe's single `project` source set;
@@ -68,13 +69,19 @@ Missing optional artifacts are omitted, but at least one approved lifecycle
68
69
  artifact is required. An explicit supplied fact-base path bypasses artifact
69
70
  federation and is passed through as `factBase.mode: supplied`.
70
71
 
72
+ Before invocation, read `references/author-callback.md` and construct exactly
73
+ one provider-neutral author seam in both modes: in-process callers supply
74
+ `author`, while JSON/CLI callers supply `authorModulePath` naming a module with
75
+ an `author` function export. The callback consumes `author-request/v2` with the
76
+ recipe brief and fact base inlined, plus theme and any recipe-selected artistic
77
+ shell. It may propose expansion only through recipe profile IDs. The adapter
78
+ validates and resolves that executable input before passing it to
79
+ `core.runExplainer`; callbacks and module paths never enter the persisted run
80
+ request.
81
+
71
82
  Unattended project runs pass `approved-oat-artifacts` provenance to the core's
72
- content-approval seam and never prompt. Every unattended run also requires
73
- exactly one provider-neutral author seam: in-process callers supply `author`,
74
- while JSON/CLI callers supply `authorModulePath` naming a module with an
75
- `author` function export. The adapter validates and resolves that executable
76
- input before passing it to `core.runExplainer`; callbacks and module paths never
77
- enter the persisted run request.
83
+ content-approval seam and never prompt. Automated completion and
84
+ implementation-tail recaps always use `mode: unattended`.
78
85
 
79
86
  Federated runs still require an explicit provider-neutral critic callback.
80
87
  In-process callers may supply `critic` (or `coreOptions.critic` for
@@ -0,0 +1,51 @@
1
+ # Author callback contract
2
+
3
+ Lifecycle callers own the executable author seam; the explainer core owns
4
+ construction of each author request. In-process callers pass an `author`
5
+ callback to `runOatExplainer`. JSON-only and official CLI callers pass
6
+ `authorModulePath`, which must name a module with an `author` function export.
7
+ Provide exactly one of those inputs in both interactive and unattended modes.
8
+ The callback or module may use any provider, but its request and result remain
9
+ provider-neutral.
10
+
11
+ ## Request handling
12
+
13
+ The callback receives one `explainer-kit.author-request/v2` per artifact. The
14
+ core constructs that request from recipe-owned policy:
15
+
16
+ - `brief` contains the prose loaded from the floor entry or expansion profile's
17
+ `briefRef`; the author must treat the inlined brief as its editorial contract.
18
+ - `factBase` contains the reconciled evidence for the run. Ground claims in that
19
+ fact base and do not replace it with ambient project context.
20
+ - `authoring` selects the output path. Return Markdown for `markdown` and a
21
+ complete HTML document for `html`.
22
+ - `theme` is attached to every request. HTML requests also carry the
23
+ recipe-selected `shell`; use it as the starting canvas while following the
24
+ brief's license to elaborate.
25
+ - A floor request may include `floor.requiredNarrative`, which identifies the
26
+ narrative coverage checked later as non-blocking guidelines.
27
+
28
+ Return one matching `explainer-kit.author-result/v2`: preserve `artifactId`,
29
+ set exactly one of `content.markdown` or `content.html`, and include
30
+ `provenance`. Do not persist provider credentials or callback configuration in
31
+ the result.
32
+
33
+ ## Expansion
34
+
35
+ Only a floor result may propose expansion. Put each proposal in
36
+ `proposedArtifacts` as `{id, profileId, rationale}`. Do not choose an artifact
37
+ type, authoring mode, brief, or shell in the proposal; the referenced recipe
38
+ profile, selected by `profileId`, owns those values.
39
+
40
+ The core validates proposal IDs, profile membership, collisions, and finite
41
+ caps. It rejects malformed proposals, records over-limit proposals as warnings,
42
+ and issues a new author request for every accepted artifact. Handle each
43
+ follow-up request independently and do not recursively propose more artifacts.
44
+
45
+ ## Lifecycle invocation
46
+
47
+ Automated project-completion and implementation-tail recaps always call
48
+ `runOatExplainer` with `mode: unattended`. They construct the author seam before
49
+ invocation and supply it alongside the existing provider-neutral `critic` or
50
+ validated `criticModulePath`. Interactive invocations use the same author
51
+ contract; only the later approval behavior differs.
@@ -71,18 +71,20 @@ On a stale-write conflict, the caller must re-read state, resolve precedence
71
71
  again, and decide whether a write is still required. It must not retry the old
72
72
  record blindly.
73
73
 
74
- ## Unattended author execution
74
+ ## Author execution
75
75
 
76
- Every unattended adapter run must provide exactly one provider-neutral author
77
- seam. In-process callers pass `author`; JSON-only and official CLI callers put
78
- `authorModulePath` in the adapter context, naming a module whose `author` export
79
- is a function. Missing module files, invalid exports, and direct-plus-module
80
- conflicts fail at the adapter boundary.
76
+ Every adapter run in both interactive and unattended modes must provide exactly
77
+ one provider-neutral author seam. In-process callers pass `author`; JSON-only
78
+ and official CLI callers put `authorModulePath` in the adapter context, naming
79
+ a module whose `author` export is a function. Missing module files, invalid
80
+ exports, and direct-plus-module conflicts fail at the adapter boundary. See
81
+ `author-callback.md` for brief, fact-base, shell, theme, result, and expansion
82
+ handling.
81
83
 
82
84
  The resolved callback is passed only as the `author` option to
83
85
  `core.runExplainer`. It is never copied into `ExplainerRunRequestV1`,
84
- `run-request.json`, or another retained data contract. Interactive runs may
85
- omit an author and retain their existing reviewed-source path.
86
+ `run-request.json`, or another retained data contract. Interactive runs use the
87
+ same author contract and differ only at the later approval gate.
86
88
 
87
89
  ## Tracked-run finalization
88
90
 
@@ -130,6 +130,20 @@ export function validateIntentRecord(product, record) {
130
130
  return record;
131
131
  }
132
132
 
133
+ export function explainerModeForIntent(intent) {
134
+ if (
135
+ !intent ||
136
+ typeof intent !== 'object' ||
137
+ intent.product !== 'projectRecap' ||
138
+ intent.decision !== 'generate'
139
+ ) {
140
+ throw new Error(
141
+ 'Only a generated projectRecap intent can select completion-chain explainer mode.',
142
+ );
143
+ }
144
+ return 'unattended';
145
+ }
146
+
133
147
  function resolveAutonomous({
134
148
  product,
135
149
  state,
@@ -12,7 +12,7 @@ import {
12
12
  } from './resolve-config.mjs';
13
13
  import { resolveExplainerOutputRoot } from './resolve-paths.mjs';
14
14
 
15
- const MINIMUM_CORE_VERSION = '1.0.0';
15
+ const MINIMUM_CORE_VERSION = '2.0.0';
16
16
  const ADAPTER_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
17
17
 
18
18
  export async function runOatExplainer({
@@ -114,7 +114,6 @@ export async function runOatExplainer({
114
114
  author,
115
115
  authorModulePath,
116
116
  coreOptions,
117
- mode: request.mode,
118
117
  });
119
118
  const lifecycleCritic = await resolveLifecycleCritic({
120
119
  critic,
@@ -141,6 +140,7 @@ export async function runOatExplainer({
141
140
  request,
142
141
  manifest,
143
142
  result,
143
+ marking: result.marking ?? null,
144
144
  outputRoot,
145
145
  };
146
146
  }
@@ -149,7 +149,6 @@ async function resolveLifecycleAuthor({
149
149
  author,
150
150
  authorModulePath,
151
151
  coreOptions,
152
- mode,
153
152
  }) {
154
153
  if (coreOptions?.author !== undefined) {
155
154
  throw new TypeError(
@@ -166,14 +165,11 @@ async function resolveLifecycleAuthor({
166
165
  }
167
166
 
168
167
  if (author === undefined && authorModulePath === undefined) {
169
- if (mode === 'unattended') {
170
- const error = new Error(
171
- 'Unattended OAT explainer runs require exactly one provider-neutral author callback or author module entry point.',
172
- );
173
- error.code = 'E_AUTHOR_REQUIRED';
174
- throw error;
175
- }
176
- return null;
168
+ const error = new Error(
169
+ 'Unattended and interactive OAT explainer runs require exactly one provider-neutral author callback or author module entry point.',
170
+ );
171
+ error.code = 'E_AUTHOR_REQUIRED';
172
+ throw error;
177
173
  }
178
174
  if (authorModulePath === undefined) {
179
175
  return author;
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-project-complete
3
- version: 1.5.4
3
+ version: 1.6.0
4
4
  description: Use when all implementation work is finished and the project is ready to close. Marks the OAT project lifecycle as complete.
5
5
  disable-model-invocation: true
6
6
  user-invocable: true
@@ -282,7 +282,23 @@ When `SHOULD_GENERATE_RECAP="true"`, inspect manifests under
282
282
  `{PROJECT_PATH}/explainers/` before generating. A fresh `project-recap` manifest for the current completed implementation is reused without invoking the adapter again. Fresh means the manifest identifies recipe `project-recap`, belongs to this project, has a terminal outcome, and its recorded source hashes match the current approved implementation inputs, including the refreshed summary when present.
283
283
 
284
284
  If no fresh recap exists, invoke `scripts/run.mjs#runOatExplainer` exactly once with recipe `project-recap`, project invocation, the active project, and unattended lifecycle mode so approved OAT artifacts do not trigger a second content prompt. A failed adapter run warns but does not block completion. Use a returned valid terminal `project-recap` manifest as the selected run; do not rerun to improve its outcome.
285
- Supply the provider-neutral critic callback (or validated critic module entry point for JSON/CLI invocation) on every federated adapter run.
285
+ Before that invocation, construct exactly one brief-aware, provider-neutral
286
+ author seam as documented by
287
+ `oat-explainer-kit/references/author-callback.md`. In-process callers pass
288
+ `author`; JSON/CLI callers pass a validated `authorModulePath`. Supply it
289
+ alongside the existing `critic` callback (or validated
290
+ `criticModulePath`), and invoke the recap with `mode: unattended`.
291
+
292
+ The author seam is the recap's quality mechanism, so derive its output from the
293
+ request rather than from ambient context or a stock recap shape. Cover every
294
+ `floor.requiredNarrative` section, ground each claim in the supplied `factBase`,
295
+ and follow the inlined `brief` for structure — evidence tables for the
296
+ implementation and validation sections, at least one high-level architecture
297
+ diagram, and lists where material is enumerable. A recap whose warnings include
298
+ `guideline-narrative-coverage-missing`, `guideline-structured-depth-missing`, or
299
+ `guideline-architecture-diagram-missing` is thin: it still completes, but treat
300
+ those warnings as the signal that the authored content did not use the evidence
301
+ it was given.
286
302
 
287
303
  Set `SELECTED_PROJECT_RECAP_RUN` only to the final selected `project-recap` run. The value must be project-relative in the form `explainers/<run-slug>` so it can be passed safely to the archive CLI. An incomplete, stale, wrong-project, or `project-explainer` manifest is never selected as the final recap.
288
304
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-project-implement
3
- version: 2.1.9
3
+ version: 2.2.0
4
4
  description: Use when plan.md is ready for execution. Dispatches one phase implementer per phase, owns independent phase review and bounded fix routing, and supports plan-declared worktree-isolated parallel phases.
5
5
  oat_gateable: true
6
6
  argument-hint: '[--retry-limit <N>] [--dry-run]'
@@ -751,7 +751,13 @@ Before generating, inspect the active project's explainer runs. A fresh `project
751
751
  Resolve recap intent through `oat-explainer-kit`. When `OAT_AUTONOMOUS=1` and no fresh recap exists, attempt `project-recap` exactly once; missing or stale persisted intent cannot suppress this autonomous attempt. Interactive mode honors the adapter's resolved persisted or workflow intent.
752
752
 
753
753
  Invoke the `oat-explainer-kit` adapter first, then run its shared tracked-run finalizer in `dedicated` mode for a successful build. Use the adapter result and finalizer result as returned; do not improvise commits, durability evidence, or reruns. Outcomes `failed` and `built-not-durable` are recorded warnings, never blockers for final HiLL approval, completion reporting, or later PR steps.
754
- Supply the provider-neutral critic callback (or validated critic module entry point for JSON/CLI invocation) on every federated adapter run.
754
+ For an adapter invocation, construct exactly one brief-aware,
755
+ provider-neutral author seam using
756
+ `oat-explainer-kit/references/author-callback.md`. In-process callers pass
757
+ `author`; JSON/CLI callers pass a validated `authorModulePath`. Supply it
758
+ alongside the existing `critic` callback (or validated
759
+ `criticModulePath`), and always invoke this implementation-tail recap with
760
+ `mode: unattended`.
755
761
 
756
762
  Always include the selected or attempted recap's outcome and run path in the
757
763
  implementation completion report. If `summary.md` exists, append or refresh its
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-wave-execute
3
- version: 1.7.1
3
+ version: 1.8.0
4
4
  description: Use when executing a wave of external implementation plans as a wrapper OAT project — scaffolding, drift refresh, parallel worktree groups, briefs, gates, merge choreography, and closeout.
5
5
  argument-hint: '<wave-id> [plan-names...] (e.g. wave-2 http-listener-before-indexing ...)'
6
6
  disable-model-invocation: false
@@ -411,24 +411,17 @@ exactly:
411
411
  `schemaVersion, generatedAt, mode, freshnessPolicy, sources, claims, unresolvedClaims, overrides`.
412
412
 
413
413
  The caller also owns CONTENT AUTHORING, exactly as it owns critic execution and
414
- fact-base synthesis: the kit's pipeline validates structure and fact
415
- consistency, but nothing in it owns prose quality. An unattended recap run
416
- without a caller-supplied authoring path emits raw federated artifact text as
417
- deck prose (stoa W6 live evidence, run-19af6e55: implementation.md pasted
418
- verbatim, frontmatter included, tables flattened to run-on prose every
419
- automated gate passed it). The explainer-kit now enforces this seam: every
420
- unattended run requires exactly one provider-neutral author seam in-process
421
- callers supply an `author(request)` callback; JSON/CLI callers supply
422
- `authorModulePath` naming a module with an `author` function export. The core
423
- invokes it once per recipe artifact with an `explainer-kit.author-request/v1`
424
- document and expects an `explainer-kit.author-result/v1` reply; runs fail if
425
- the author is absent, returns an invalid result, or copies excessive verbatim
426
- source text. Recap callers MUST satisfy that seam by authoring content from
427
- the synthesized fact base plus the recipe outline (LLM-authored from
428
- summary/synthesis material, as the operator-approved W6 rebuild demonstrates)
429
- or NOT run the unattended build, recording the skip disposition per the
430
- optional-step rule. Callbacks and module paths never enter the persisted run
431
- request.
414
+ fact-base synthesis: the kit validates structure and fact consistency, but
415
+ nothing in it owns prose quality. Every recap run requires exactly one
416
+ provider-neutral author seam in-process callers supply an `author(request)`
417
+ callback; JSON/CLI callers supply `authorModulePath` naming a module with an
418
+ `author` function export. The core invokes it for every floor and accepted
419
+ expansion artifact with `author-request/v2`: the artifact brief is inlined,
420
+ the fact base is attached, and artistic requests include the resolved theme
421
+ and hash-pinned shell. Authors may propose only recipe-declared expansion
422
+ profiles and return `author-result/v2`; runs fail on absent or invalid author
423
+ results or excessive verbatim source copying. Callbacks and module paths never
424
+ enter persisted run requests.
432
425
 
433
426
  The mechanical caller constructs an `explainer-kit.run-request/v1` document whose
434
427
  required keys are exactly: