@open-agent-toolkit/cli 0.2.6 → 0.2.8

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "cli": "0.2.6",
3
- "docs-config": "0.2.6",
4
- "docs-theme": "0.2.6",
5
- "docs-transforms": "0.2.6"
2
+ "cli": "0.2.8",
3
+ "docs-config": "0.2.8",
4
+ "docs-theme": "0.2.8",
5
+ "docs-transforms": "0.2.8"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: explainer-kit
3
- version: 1.0.0
3
+ version: 1.0.1
4
4
  description: Use when building destination-neutral visual explainer artifacts from explicit, versioned inputs.
5
5
  user-invocable: true
6
6
  allowed-tools: Read, Write, Edit, Bash, Grep, Glob, Agent, mcp__*
@@ -55,6 +55,8 @@ discovery, theme resolution, rendering, QA, and manifest/build-record
55
55
  persistence. It runs without OAT files or ambient configuration. Supplied fact
56
56
  bases receive only lightweight consistency/freshness checks. Federated inputs
57
57
  require a provider-neutral critic callback and invoke it exactly once.
58
+ Optional claim `sections` tags route facts to matching recipe narrative
59
+ sections; untagged claims remain shared context for every required section.
58
60
 
59
61
  Unattended calls use explicit, already-approved source artifacts, persist their
60
62
  review provenance in `source/content-approval.json`, and never prompt.
@@ -36,7 +36,10 @@ contract.
36
36
  performs only the lightweight consistency and freshness check and never
37
37
  invokes the critic.
38
38
  - `factBase.mode: federated` names explicit source bindings. File locators
39
- contain JSON with a `claims` array of `{ "id", "text", "locator"? }`.
39
+ contain JSON with a `claims` array of
40
+ `{ "id", "text", "locator"?, "sections"? }`. Optional `sections` values
41
+ are recipe `requiredNarrative` IDs; untagged claims remain shared across
42
+ every required section.
40
43
  Non-file bindings require a caller-supplied `sourceLoader(source)` callback.
41
44
  Every binding names its recipe `role` and `sourceSetId`. Multiple documents
42
45
  may share one source-set ID; recipe cardinality counts distinct sets, not
@@ -58,12 +58,17 @@ Each source document contains a schema-compatible source and extracted claims:
58
58
  observedAt,
59
59
  authoritativeFor,
60
60
  },
61
- claims: [{ id, text, locator }],
61
+ claims: [{ id, text, locator, sections }],
62
62
  }],
63
63
  overrides: [{ claimId, decision, confirmedAt }],
64
64
  }
65
65
  ```
66
66
 
67
+ Optional `sections` entries are recipe `requiredNarrative` IDs. Tagged claims
68
+ are routed only to those sections; untagged claims remain shared context for
69
+ every required section. Federated reconciliation preserves shared scope when
70
+ any agreeing selected observation is untagged.
71
+
67
72
  For conflicting text under one claim ID, an `authoritativeFor` declaration
68
73
  wins first. Otherwise the newest `observedAt` wins. A tie remains
69
74
  `contradictory`. Operator overrides take final precedence, produce an
@@ -46,6 +46,15 @@
46
46
  "type": "string",
47
47
  "pattern": "^sha256:[a-f0-9]{64}$"
48
48
  },
49
+ "sectionIds": {
50
+ "type": "array",
51
+ "items": {
52
+ "type": "string",
53
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
54
+ },
55
+ "minItems": 1,
56
+ "uniqueItems": true
57
+ },
49
58
  "source": {
50
59
  "type": "object",
51
60
  "additionalProperties": false,
@@ -85,6 +94,7 @@
85
94
  "id": { "type": "string", "minLength": 1 },
86
95
  "text": { "type": "string", "minLength": 1 },
87
96
  "status": { "enum": ["confirmed", "overridden"] },
97
+ "sections": { "$ref": "#/$defs/sectionIds" },
88
98
  "citations": {
89
99
  "type": "array",
90
100
  "items": { "$ref": "#/$defs/citation" },
@@ -108,6 +118,7 @@
108
118
  "needs-confirmation"
109
119
  ]
110
120
  },
121
+ "sections": { "$ref": "#/$defs/sectionIds" },
111
122
  "citations": {
112
123
  "type": "array",
113
124
  "items": { "$ref": "#/$defs/citation" },
@@ -166,10 +166,10 @@ export async function verifyRebuildability(artifact, runRoot) {
166
166
  await writeFileAtomic(runRoot, artifact.renderedPath, original);
167
167
  }
168
168
  return { verified: true, reason: null };
169
- } catch (error) {
169
+ } catch (caught) {
170
170
  return {
171
171
  verified: false,
172
- reason: `Deterministic replay failed: ${errorMessage(error)}`,
172
+ reason: `Deterministic replay failed: ${errorMessage(caught)}`,
173
173
  };
174
174
  }
175
175
  }
@@ -2,6 +2,7 @@ import { canonicalHash, validateContract } from './contracts.mjs';
2
2
 
3
3
  const FACT_BASE_VERSION = 'explainer-kit.fact-base/v1';
4
4
  const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
5
+ const SECTION_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5
6
  const FINDING_REASONS = new Set([
6
7
  'contradictory',
7
8
  'stale',
@@ -107,6 +108,7 @@ async function processFederated(binding, { critic, now }) {
107
108
  text: override.decision,
108
109
  status: 'overridden',
109
110
  citations: citationsFor(entries),
111
+ ...sectionMetadata(entries),
110
112
  });
111
113
  continue;
112
114
  }
@@ -251,7 +253,8 @@ function collectObservations(documents, sourceById) {
251
253
  typeof claim.id !== 'string' ||
252
254
  claim.id.length === 0 ||
253
255
  typeof claim.text !== 'string' ||
254
- claim.text.length === 0
256
+ claim.text.length === 0 ||
257
+ !validSections(claim.sections)
255
258
  ) {
256
259
  throw new Error(
257
260
  `Source ${document.source.id} contains an invalid claim.`,
@@ -263,6 +266,7 @@ function collectObservations(documents, sourceById) {
263
266
  text: claim.text,
264
267
  source: sourceById.get(document.source.id),
265
268
  locator: claim.locator ?? document.source.locator,
269
+ ...(claim.sections && { sections: [...claim.sections] }),
266
270
  });
267
271
  observations.set(claim.id, entries);
268
272
  }
@@ -307,6 +311,7 @@ function resolveObservations(claimId, entries) {
307
311
  text: entries[0].text,
308
312
  status: 'confirmed',
309
313
  citations: citationsFor(entries),
314
+ ...sectionMetadata(entries),
310
315
  },
311
316
  };
312
317
  }
@@ -341,6 +346,9 @@ function resolveObservations(claimId, entries) {
341
346
  citations: citationsFor(
342
347
  entries.filter(({ text }) => text === winner.entry.text),
343
348
  ),
349
+ ...sectionMetadata(
350
+ entries.filter(({ text }) => text === winner.entry.text),
351
+ ),
344
352
  },
345
353
  };
346
354
  }
@@ -352,6 +360,7 @@ function resolveObservations(claimId, entries) {
352
360
  text: `Conflicting values: ${[...byText.keys()].sort().join(' | ')}`,
353
361
  reason: 'contradictory',
354
362
  citations: citationsFor(entries),
363
+ ...sectionMetadata(entries),
355
364
  },
356
365
  };
357
366
  }
@@ -441,6 +450,7 @@ function integrateCriticFindings({
441
450
  }
442
451
 
443
452
  const claimIndex = claims.findIndex(({ id }) => id === finding.claimId);
453
+ const existingClaim = claimIndex >= 0 ? claims[claimIndex] : null;
444
454
  if (claimIndex >= 0) {
445
455
  claims.splice(claimIndex, 1);
446
456
  }
@@ -449,6 +459,7 @@ function integrateCriticFindings({
449
459
  text: finding.text,
450
460
  reason: finding.classification,
451
461
  citations,
462
+ ...(existingClaim?.sections && { sections: existingClaim.sections }),
452
463
  });
453
464
  }
454
465
  }
@@ -489,6 +500,29 @@ function uniqueCitations(citations) {
489
500
  ];
490
501
  }
491
502
 
503
+ function validSections(sections) {
504
+ return (
505
+ sections === undefined ||
506
+ (Array.isArray(sections) &&
507
+ sections.length > 0 &&
508
+ new Set(sections).size === sections.length &&
509
+ sections.every(
510
+ (section) =>
511
+ typeof section === 'string' && SECTION_ID_PATTERN.test(section),
512
+ ))
513
+ );
514
+ }
515
+
516
+ function sectionMetadata(entries) {
517
+ if (entries.some(({ sections }) => sections === undefined)) {
518
+ return {};
519
+ }
520
+ const sections = [
521
+ ...new Set(entries.flatMap((entry) => entry.sections ?? [])),
522
+ ].sort();
523
+ return sections.length > 0 ? { sections } : {};
524
+ }
525
+
492
526
  function byId(left, right) {
493
527
  return left.id.localeCompare(right.id);
494
528
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
2
+ import { readFile, readdir, rm } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
 
5
5
  import { validateContract } from './contracts.mjs';
@@ -70,6 +70,7 @@ export async function initializeRun(request) {
70
70
  request: normalizedRequest,
71
71
  };
72
72
 
73
+ await clearRunRoot(run);
73
74
  await writeJsonAtomic(run.runRoot, 'build-record.json', buildRecord);
74
75
  await writeJsonAtomic(
75
76
  run.runRoot,
@@ -226,6 +227,58 @@ function assertRun(run) {
226
227
  }
227
228
  }
228
229
 
230
+ async function clearRunRoot(run) {
231
+ const entries = await readdir(run.runRoot, { withFileTypes: true });
232
+ if (entries.length === 0) return;
233
+ await assertOwnedRunRoot(run);
234
+ const removable = [];
235
+ for (const entry of entries) {
236
+ const path = join(run.runRoot, entry.name);
237
+ if (!(await containsSymlink(path, entry))) {
238
+ removable.push(path);
239
+ }
240
+ }
241
+ await Promise.all(
242
+ removable.map((path) => rm(path, { recursive: true, force: true })),
243
+ );
244
+ }
245
+
246
+ async function assertOwnedRunRoot(run) {
247
+ try {
248
+ const [persistedRequest, persistedRecord] = await Promise.all([
249
+ readJson(join(run.runRoot, 'run-request.json')),
250
+ readJson(join(run.runRoot, 'build-record.json')),
251
+ ]);
252
+ assertValidContract('run-request', persistedRequest);
253
+ assertValidContract('build-record', persistedRecord);
254
+ if (
255
+ persistedRequest.slug !== run.slug ||
256
+ persistedRequest.outputRoot !== run.outputRoot
257
+ ) {
258
+ throw new Error('Prior run identity does not match this slug.');
259
+ }
260
+ } catch {
261
+ throw new Error(
262
+ 'Existing slug directory is not a prior Explainer Kit run; refusing to clear it.',
263
+ );
264
+ }
265
+ }
266
+
267
+ async function containsSymlink(path, entry) {
268
+ if (entry.isSymbolicLink()) return true;
269
+ if (!entry.isDirectory()) return false;
270
+ for (const child of await readdir(path, { withFileTypes: true })) {
271
+ if (await containsSymlink(join(path, child.name), child)) {
272
+ return true;
273
+ }
274
+ }
275
+ return false;
276
+ }
277
+
278
+ async function readJson(path) {
279
+ return JSON.parse(await readFile(path, 'utf8'));
280
+ }
281
+
229
282
  function isObject(value) {
230
283
  return typeof value === 'object' && value !== null && !Array.isArray(value);
231
284
  }
@@ -227,7 +227,9 @@ async function loadResumableRun(request) {
227
227
  persistedRequest.slug !== normalized.slug ||
228
228
  persistedRequest.recipe?.id !== normalized.recipe.id ||
229
229
  persistedRequest.recipe?.version !== normalized.recipe.version ||
230
- persistedRequest.mode !== normalized.mode
230
+ persistedRequest.mode !== normalized.mode ||
231
+ canonicalHash(persistedRequest.factBase) !==
232
+ canonicalHash(normalized.factBase)
231
233
  ) {
232
234
  throw codedError(
233
235
  'E_APPROVAL_RESUME',
@@ -551,12 +553,25 @@ function manifestFor(state, buildRecord, createdAt, immutableHashes) {
551
553
 
552
554
  function createContentModel(recipe, artifact, slug, factBase) {
553
555
  const facts = [
554
- ...factBase.claims.map(({ text }) => text),
555
- ...factBase.unresolvedClaims.map(
556
- ({ text }) => `Needs confirmation: ${text}`,
556
+ ...factBase.claims.map(({ text, sections }) => ({ text, sections })),
557
+ ...factBase.unresolvedClaims.map(({ text, sections }) => ({
558
+ text: `Needs confirmation: ${text}`,
559
+ sections,
560
+ })),
561
+ ];
562
+ const unknownSections = [
563
+ ...new Set(
564
+ facts
565
+ .flatMap(({ sections }) => sections ?? [])
566
+ .filter((section) => !recipe.requiredNarrative.includes(section)),
557
567
  ),
558
568
  ];
559
- const summary = facts.length > 0 ? facts.join(' ') : 'No confirmed facts.';
569
+ if (unknownSections.length > 0) {
570
+ throw codedError(
571
+ 'E_CONTENT',
572
+ `Unknown narrative section tags: ${unknownSections.join(', ')}`,
573
+ );
574
+ }
560
575
  return {
561
576
  artifactId: artifact.id,
562
577
  slug,
@@ -564,11 +579,19 @@ function createContentModel(recipe, artifact, slug, factBase) {
564
579
  description: `Approved-source ${humanize(recipe.id).toLowerCase()}.`,
565
580
  eyebrow: 'Explainer Kit',
566
581
  footer: 'Generated from the retained reconciled fact base.',
567
- sections: recipe.requiredNarrative.map((id) => ({
568
- id,
569
- title: humanize(id),
570
- content: summary,
571
- })),
582
+ sections: recipe.requiredNarrative.map((id) => {
583
+ const sectionFacts = facts
584
+ .filter(({ sections }) => !sections || sections.includes(id))
585
+ .map(({ text }) => text);
586
+ return {
587
+ id,
588
+ title: humanize(id),
589
+ content:
590
+ sectionFacts.length > 0
591
+ ? sectionFacts.join(' ')
592
+ : 'No confirmed facts.',
593
+ };
594
+ }),
572
595
  artifactLinks: [],
573
596
  };
574
597
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-wave-execute
3
- version: 1.6.1
3
+ version: 1.6.2
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "private": false,
5
5
  "description": "Open Agent Toolkit CLI",
6
6
  "homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
@@ -34,7 +34,7 @@
34
34
  "ora": "^9.0.0",
35
35
  "yaml": "2.8.2",
36
36
  "zod": "^3.25.76",
37
- "@open-agent-toolkit/control-plane": "0.2.6"
37
+ "@open-agent-toolkit/control-plane": "0.2.8"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",
@@ -1,34 +0,0 @@
1
- {
2
- "schemaVersion": "explainer-kit.recipe/v1",
3
- "id": "program-recap",
4
- "version": "1",
5
- "sourceRoles": [
6
- {
7
- "role": "program",
8
- "required": true,
9
- "accepts": ["file", "directory", "git"],
10
- "minBindings": 1,
11
- "maxBindings": 1
12
- }
13
- ],
14
- "requiredNarrative": [
15
- "program-overview",
16
- "wave-map",
17
- "per-wave-outcomes",
18
- "convention-evolution",
19
- "aggregate-numbers",
20
- "follow-up-ledger"
21
- ],
22
- "artifacts": [
23
- {
24
- "id": "program-recap",
25
- "type": "hub",
26
- "template": "house-style",
27
- "required": true
28
- }
29
- ],
30
- "discoveryLimits": {
31
- "consecutiveNoNewFindingsRounds": 2,
32
- "maxRounds": 8
33
- }
34
- }