@open-agent-toolkit/cli 0.2.30 → 0.2.31

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 (72) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +41 -0
  3. package/assets/docs/contributing/code.md +19 -7
  4. package/assets/docs/contributing/explainer-kit-verification.md +9 -1
  5. package/assets/docs/contributing/skills.md +9 -0
  6. package/assets/docs/workflows/skills/explainer-kit-providers.md +16 -5
  7. package/assets/docs/workflows/skills/explainer-kit.md +98 -38
  8. package/assets/public-package-versions.json +4 -4
  9. package/assets/skills/explainer-kit/SKILL.md +33 -8
  10. package/assets/skills/explainer-kit/briefs/project-recap.md +25 -7
  11. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +72 -0
  12. package/assets/skills/explainer-kit/references/contracts.md +49 -17
  13. package/assets/skills/explainer-kit/references/destination-contract.md +141 -25
  14. package/assets/skills/explainer-kit/references/extension-contract.md +19 -10
  15. package/assets/skills/explainer-kit/references/visual-authoring.md +24 -0
  16. package/assets/skills/explainer-kit/references/visual-review.md +19 -5
  17. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +241 -0
  18. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +215 -0
  19. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +34 -0
  20. package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -1
  21. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +81 -0
  22. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +66 -0
  23. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +109 -3
  24. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +550 -17
  25. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +90 -8
  26. package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +5 -0
  27. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +538 -0
  28. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +129 -11
  29. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +54 -0
  30. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +2 -1
  31. package/assets/skills/explainer-kit/scripts/lib/records.mjs +139 -22
  32. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +353 -0
  33. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +237 -107
  34. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +1 -0
  35. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +157 -0
  36. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +26 -6
  37. package/assets/skills/explainer-kit/scripts/run.mjs +1006 -144
  38. package/assets/skills/oat-explainer-kit/SKILL.md +16 -3
  39. package/assets/skills/oat-explainer-kit/references/config-contract.md +13 -8
  40. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +50 -6
  41. package/assets/skills/oat-explainer-kit/references/migration.md +2 -1
  42. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +11 -0
  43. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +37 -15
  44. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +83 -0
  45. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +91 -0
  46. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +66 -10
  47. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +60 -21
  48. package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +52 -8
  49. package/assets/skills/oat-explainer-kit/scripts/run.mjs +271 -36
  50. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
  51. package/assets/skills/oat-project-complete/SKILL.md +19 -3
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
  53. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +8 -0
  54. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
  55. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
  56. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
  57. package/dist/commands/config/index.d.ts.map +1 -1
  58. package/dist/commands/config/index.js +18 -0
  59. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  60. package/dist/commands/project/archive/archive-utils.js +57 -7
  61. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +29 -0
  62. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +1 -0
  63. package/dist/commands/project/archive/explainer-terminal-evidence.js +37 -0
  64. package/dist/config/oat-config.d.ts +2 -0
  65. package/dist/config/oat-config.d.ts.map +1 -1
  66. package/dist/config/oat-config.js +4 -0
  67. package/dist/config/resolve.d.ts.map +1 -1
  68. package/dist/config/resolve.js +1 -0
  69. package/package.json +2 -2
  70. /package/assets/skills/explainer-kit/recipes/{project-recap.json → project-recap.v1.json} +0 -0
  71. /package/assets/skills/explainer-kit/schemas/{publish-receipt.schema.json → publish-receipt.v1.schema.json} +0 -0
  72. /package/assets/skills/explainer-kit/schemas/{publish-request.schema.json → publish-request.v1.schema.json} +0 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-explainer-kit
3
- version: 1.0.5
3
+ version: 1.0.6
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
@@ -37,7 +37,7 @@ rollback, and operator-owned real-wrapper gate, use `references/migration.md`.
37
37
 
38
38
  Before reading OAT config or invoking the core, call
39
39
  `scripts/check-core.mjs#checkCoreCompatibility` with this installed skill
40
- directory and minimum core version `2.0.3`. Continue only when it returns
40
+ directory and minimum core version `2.1.0`. Continue only when it returns
41
41
  `ok: true`.
42
42
 
43
43
  - Missing core: stop and show
@@ -58,7 +58,7 @@ Call `scripts/run.mjs#runOatExplainer` with the repository root, project
58
58
  invocation, active project path, recipe, slug, lifecycle mode, and any explicit
59
59
  runtime overrides. The adapter:
60
60
 
61
- 1. checks the user-scoped installed core at minimum version `2.0.3`;
61
+ 1. checks the user-scoped installed core at minimum version `2.1.0`;
62
62
  2. resolves only the public `explainers.*` and `workflow.explainers.*` keys;
63
63
  3. derives the canonical project output root;
64
64
  4. binds approved OAT artifacts to the recipe's single `project` source set;
@@ -72,6 +72,13 @@ Missing optional artifacts are omitted, but at least one approved lifecycle
72
72
  artifact is required. An explicit supplied fact-base path bypasses artifact
73
73
  federation and is passed through as `factBase.mode: supplied`.
74
74
 
75
+ New project-recap requests select immutable `project-recap@2`: one
76
+ navigational hub is the floor, while a diagram, walkthrough deck, or deep dive
77
+ is selected only when the planner can state its distinct reader question,
78
+ source evidence, and medium rationale. The installed core continues reading
79
+ `project-recap@1` for replay; the adapter never rewrites a retained request's
80
+ recipe selector.
81
+
75
82
  Before invocation, read `references/author-callback.md`. Unattended
76
83
  `project-recap` runs require exactly one provider-neutral set planner:
77
84
  in-process callers supply `planSet`, while JSON/CLI callers supply
@@ -99,6 +106,12 @@ matching exports. Bare browser callbacks and caller-authored runtime metadata
99
106
  are rejected. These are first-class adapter inputs; do not place either
100
107
  provider in `coreOptions`.
101
108
 
109
+ The critic judges typography, hierarchy, composition, density, medium leverage,
110
+ template repetition, diagram semantics, and cross-artifact cohesion from the
111
+ bound rendered evidence. Keep those criteria in prose. Preserve the core's
112
+ existing `visual-review-result/v1` contract and actionable `pass`/`correct`
113
+ behavior; do not add adapter-owned scores, layout thresholds, or result fields.
114
+
102
115
  The adapter resolves all executable providers before core invocation, enforces
103
116
  direct-versus-module mutual exclusion, and requires distinct identities for
104
117
  authoring, fact criticism, browser evidence, and visual criticism. A missing,
@@ -17,11 +17,12 @@ constructing an `explainer-kit.run-request/v1`.
17
17
  | `explainers.publish.s3Uri` | shared | unset |
18
18
  | `explainers.publish.publicBaseUrl` | shared | unset |
19
19
  | `explainers.publish.awsRegion` | shared | unset |
20
+ | `explainers.publish.publicAccess` | shared | `public` |
20
21
  | `explainers.publish.awsProfile` | local, user | unset |
21
22
  | `workflow.explainers.projectExplainer` | local, shared, user | `ask` |
22
23
  | `workflow.explainers.projectRecap` | local, shared, user | `ask` |
23
24
 
24
- Explicit runtime inputs may override these eleven keys for one invocation. They
25
+ Explicit runtime inputs may override these twelve keys for one invocation. They
25
26
  do not write config. Recipe, slug, fact-base path, output root, per-run art
26
27
  direction, and private wrapper lanes are invocation inputs rather than config
27
28
  keys and are rejected from the runtime config-override map.
@@ -44,12 +45,16 @@ warning. The adapter passes a resolved bundle as `theme.suppliedBundlePath`.
44
45
 
45
46
  ## Publish block
46
47
 
47
- No provider means build-only operation, even if unused destination fields are
48
- present. When `provider` is `s3-static`, `s3Uri`, `publicBaseUrl`, and
49
- `awsRegion` are all required. `awsProfile` is optional and uses the normal AWS
50
- credential chain when absent. Destination roots are normalized without trailing
51
- slashes. Lifecycle callers must explicitly select publish durability; config
52
- alone never starts publishing.
48
+ `provider`, `s3Uri`, `publicBaseUrl`, and `awsRegion` form one complete publish
49
+ block. If any field is absent, the adapter returns a structured build-only
50
+ report listing every missing field and does not construct publish config.
51
+ `awsProfile` is optional and uses the normal AWS credential chain when absent.
52
+ `publicAccess` accepts `public` or `protected` and defaults to `public`;
53
+ source metadata is retained and the resolved mode is carried explicitly in the
54
+ versioned core publish request. Destination roots are normalized without trailing
55
+ slashes. Lifecycle callers must explicitly select publish durability; complete
56
+ configuration only makes publication available and never authorizes it. The
57
+ existing human gate remains mandatory.
53
58
 
54
59
  ## Canonical output roots
55
60
 
@@ -68,5 +73,5 @@ core creates output.
68
73
  The adapter emits `explainer-kit.run-request/v1` with the requested recipe,
69
74
  slug, fact-base binding, mode, derived output root, resolved theme selection,
70
75
  privacy choice, and explicit durability strategy. Publish durability adds a
71
- complete `explainer-kit.publish-request/v1` whose `siteRoot` and
76
+ complete `explainer-kit.publish-request/v2` whose `siteRoot` and
72
77
  `manifestPath` point inside `<outputRoot>/<slug>/`.
@@ -86,6 +86,38 @@ The resolved callback is passed only as the `author` option to
86
86
  `run-request.json`, or another retained data contract. Interactive runs use the
87
87
  same author contract and differ only at the later approval gate.
88
88
 
89
+ ## Repository invocation sources
90
+
91
+ A repository invocation must provide `suppliedFactBasePath`. The adapter binds
92
+ that validated fact-base input with provenance from the exact reviewed
93
+ repository revision returned by `resolveReviewedRepository`; it never resolves
94
+ or reads an active project for this invocation. Missing supplied input fails
95
+ closed before core invocation with an actionable requirement.
96
+
97
+ Repository runs write beneath `.oat/repo/reference/explainers/<run-slug>/` and
98
+ use repository-level publish roots unchanged. Project invocations retain their
99
+ approved lifecycle-artifact binding and project-local output behavior.
100
+
101
+ When publish durability is explicitly selected, the adapter constructs the
102
+ core `explainer-kit.publish-request/v2` with the per-invocation derived `s3Uri`,
103
+ `publicBaseUrl`, and source-aware `publicAccess`. New adapter runs emit this v2
104
+ shape atomically; the request carries no project/repository topology fields and
105
+ no credential material. The human-gated publisher returns a complete
106
+ `explainer-kit.publish-receipt/v2`, which the core validates against the
107
+ finalized manifest and generated catalog before the adapter forwards the
108
+ publication summary without destination reinterpretation. The immutable
109
+ `publish-request/v1` and `publish-receipt/v1` contracts remain supported only
110
+ for replay; v2 does not mutate either v1 contract in place. Configuration never
111
+ bypasses the human publication gate.
112
+
113
+ The adapter forwards the immutable `explainer-kit.publish-summary/v2` lifecycle
114
+ handoff without destination reinterpretation. Every v2 artifact entry retains
115
+ its source identity, rendered path, S3 URI, canonical public URL, content hash,
116
+ object verification, and public verification evidence. A
117
+ `publish-receipt/v1` replay instead yields the reduced immutable
118
+ `explainer-kit.publish-summary/v1` shape, whose artifact entries retain only
119
+ the relative path and public URL. Neither summary contract is mutated in place.
120
+
89
121
  ## Browser and visual-review execution
90
122
 
91
123
  Every unattended `project-recap` must provide exactly one browser-evidence
@@ -109,10 +141,13 @@ request/result, and byte-bound whole-set review contracts.
109
141
 
110
142
  The core retains canonical 320, 768, and 1440 viewport screenshots, paired
111
143
  `explainer-kit.browser-evidence/v2` metrics with launched Chromium name,
112
- version, and capture identity, each review request/result, cohesion
144
+ version, and capture identity, each review request and closed local evidence
145
+ projection, cohesion
113
146
  observations, and any one-pass revision record. Missing, malformed, forged,
114
147
  cross-record mismatched, stale, or failed review-chain evidence terminates as
115
148
  `built-needs-review`; durability and publication remain blocked.
149
+ The review chain performs at most one correction and one final review. It never
150
+ starts a second rebuild to chase visual perfection.
116
151
 
117
152
  ## Tracked-run finalization
118
153
 
@@ -124,10 +159,15 @@ repository root, project name, an explicit compatible `coreRoot`, and, for
124
159
  dynamically loads the versioned package-coverage contract from that core root;
125
160
  it does not maintain an adapter-local path list.
126
161
 
127
- A core `built-needs-review` outcome is a terminal review gate, not a
128
- non-durable success. The planner rejects it before producing artifact,
129
- attestation, evidence-commit, or push commands. The recap must instead be
130
- reviewed and rebuilt to a passing visual-review outcome.
162
+ A core `built-needs-review` or `failed` outcome is terminal evidence, not a
163
+ non-durable success. Provider review and diagnostic prose exists only during
164
+ the in-memory correction attempt. Retained review attempts use the closed
165
+ `visual-review-evidence/v1` projection bound to the adjacent request hash and
166
+ attempt directory. The core retains `terminal-evidence.json` with run identity,
167
+ the manifest hash when available, bounded `stage`/`kind` reason tuples, and the
168
+ evidence disposition. The planner verifies that binding and returns complete
169
+ without artifact, attestation, evidence-commit, or push commands. This preserves
170
+ the handoff without promoting it to durable or publishable success.
131
171
 
132
172
  The returned stages must run in order:
133
173
 
@@ -188,7 +228,11 @@ complete. A later attestation may recover durability without repeating the
188
228
  archive.
189
229
 
190
230
  This recovery path applies only to `built-not-durable`.
191
- `built-needs-review` cannot be exported, attested, finalized, or pushed.
231
+ `built-needs-review` and `failed` packages may be exported so their compact
232
+ terminal evidence survives project deletion, but they cannot be attested into
233
+ `built-durable`, pushed by the finalizer, or published. Flagged, failed,
234
+ superseded, and `built-not-durable` runs are never publishable. Only a
235
+ review-clean `built-durable` run may cross the explicit human publication gate.
192
236
 
193
237
  Post-archive summary and PR recap links target `projectRecapExport.exportRoot`
194
238
  under `.oat/repo/reference/project-recaps/` on the current head branch. The
@@ -17,7 +17,8 @@ release candidate passes every acceptance gate.
17
17
  Google account data, authentication choices, and personal destination
18
18
  topology in private wrapper configuration.
19
19
  4. Invoke the core once, consume only `explainer-kit.manifest/v1` and the
20
- optional publish receipt, then perform Stoa notes, Google Docs sync, and
20
+ optional `explainer-kit.publish-receipt/v2` (while retaining v1 replay),
21
+ then perform Stoa notes, Google Docs sync, and
21
22
  private link maintenance as post-run work.
22
23
 
23
24
  Do not add a plugin, inject private work between core stages, or add private
@@ -51,6 +51,12 @@ viewport-matched screenshot and metrics hashes, observed cohesion claims, and
51
51
  the exact browser runtime and capture identity shared by every evidence record.
52
52
  Use the evidence reader to inspect the byte snapshot named by the request.
53
53
 
54
+ Judge the rendered set as prose-led visual work. Cover typography, hierarchy,
55
+ composition, density, medium leverage, template repetition, diagram semantics,
56
+ and cross-artifact cohesion. Use the set plan and shared ledger to distinguish
57
+ intentional variation from drift. Do not turn those judgments into numeric
58
+ scores, geometry thresholds, or deterministic style checks.
59
+
54
60
  Return `explainer-kit.visual-review-result/v1` with:
55
61
 
56
62
  - a safe `reviewId`;
@@ -60,6 +66,11 @@ Return `explainer-kit.visual-review-result/v1` with:
60
66
  - the complete reviewed `artifactIds`; and
61
67
  - structured `findings`.
62
68
 
69
+ Return `pass` only when the full set needs no required correction. Return
70
+ `correct` when one bounded review round can address every finding, and give
71
+ each finding an actionable correction tied to an artifact and visible evidence.
72
+ The result shape and its `pass`/`correct`/`fail` dispositions remain unchanged.
73
+
63
74
  The callback must not mutate rendered files, screenshots, or metrics. The core
64
75
  revalidates all bound bytes after the callback returns.
65
76
 
@@ -18,6 +18,12 @@ export async function bindProjectSources({
18
18
  suppliedFactBasePath,
19
19
  reviewedRepository,
20
20
  }) {
21
+ if (suppliedFactBasePath) {
22
+ const repository =
23
+ reviewedRepository ??
24
+ (repoRoot ? await resolveReviewedRepository(repoRoot) : null);
25
+ return bindSuppliedFactBase(suppliedFactBasePath, repository);
26
+ }
21
27
  if (!projectRoot) {
22
28
  throw new TypeError('projectRoot is required to bind OAT artifacts.');
23
29
  }
@@ -30,21 +36,6 @@ export async function bindProjectSources({
30
36
  const repository =
31
37
  reviewedRepository ??
32
38
  (repoRoot ? await resolveReviewedRepository(repoRoot) : null);
33
- if (suppliedFactBasePath) {
34
- const path = await realpath(suppliedFactBasePath);
35
- return {
36
- factBase: {
37
- mode: 'supplied',
38
- path,
39
- freshnessPolicy: 'live-wins',
40
- },
41
- reviewedSource: {
42
- kind: 'approved-fact-base',
43
- locator: repository?.repositoryUrl ?? path,
44
- ...(repository && repository),
45
- },
46
- };
47
- }
48
39
 
49
40
  const sourceSetId = basename(canonicalProjectRoot);
50
41
  const sources = [];
@@ -95,6 +86,21 @@ export async function bindProjectSources({
95
86
  };
96
87
  }
97
88
 
89
+ export async function bindRepositorySources({
90
+ repoRoot,
91
+ suppliedFactBasePath,
92
+ reviewedRepository,
93
+ }) {
94
+ if (!suppliedFactBasePath) {
95
+ throw new Error(
96
+ 'Repository invocation requires a caller-supplied fact base path.',
97
+ );
98
+ }
99
+ const repository =
100
+ reviewedRepository ?? (await resolveReviewedRepository(repoRoot));
101
+ return bindSuppliedFactBase(suppliedFactBasePath, repository);
102
+ }
103
+
98
104
  export async function resolveReviewedRepository(
99
105
  repoRoot,
100
106
  { command = execFile } = {},
@@ -119,6 +125,22 @@ export async function resolveReviewedRepository(
119
125
  };
120
126
  }
121
127
 
128
+ async function bindSuppliedFactBase(suppliedFactBasePath, repository) {
129
+ const path = await realpath(suppliedFactBasePath);
130
+ return {
131
+ factBase: {
132
+ mode: 'supplied',
133
+ path,
134
+ freshnessPolicy: 'live-wins',
135
+ },
136
+ reviewedSource: {
137
+ kind: 'approved-fact-base',
138
+ locator: repository?.repositoryUrl ?? path,
139
+ ...(repository && repository),
140
+ },
141
+ };
142
+ }
143
+
122
144
  async function loadOatArtifact(source) {
123
145
  return oatArtifactDocument(
124
146
  source,
@@ -0,0 +1,83 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { pathToFileURL } from 'node:url';
3
+
4
+ const TERMINAL_OUTCOMES = new Set([
5
+ 'built-durable',
6
+ 'built-not-durable',
7
+ 'built-needs-review',
8
+ 'failed',
9
+ ]);
10
+
11
+ export function checkTerminalOutcome({ intent, outcome }) {
12
+ if (intent === 'skip') {
13
+ return { ok: true, intent, outcome: null };
14
+ }
15
+ if (intent !== 'generate') {
16
+ throw recapOutcomeError('Recap intent must be generate or skip.');
17
+ }
18
+ if (!TERMINAL_OUTCOMES.has(outcome)) {
19
+ throw recapOutcomeError(
20
+ 'Generated project recaps require a terminal recap outcome before approval.',
21
+ );
22
+ }
23
+ return { ok: true, intent, outcome };
24
+ }
25
+
26
+ async function main(argv) {
27
+ const { intent, manifestPath } = parseArguments(argv);
28
+ let outcome;
29
+ if (manifestPath !== undefined) {
30
+ let manifest;
31
+ try {
32
+ manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
33
+ } catch (error) {
34
+ throw recapOutcomeError(
35
+ `Recap manifest could not be read: ${error.message}`,
36
+ );
37
+ }
38
+ outcome = manifest?.outcome;
39
+ }
40
+ return checkTerminalOutcome({ intent, outcome });
41
+ }
42
+
43
+ function parseArguments(argv) {
44
+ let intent;
45
+ let manifestPath;
46
+ for (let index = 0; index < argv.length; index += 2) {
47
+ const flag = argv[index];
48
+ const value = argv[index + 1];
49
+ if (value === undefined) {
50
+ throw recapOutcomeError(`Missing value for ${flag ?? 'argument'}.`);
51
+ }
52
+ if (flag === '--intent') {
53
+ intent = value;
54
+ } else if (flag === '--manifest') {
55
+ manifestPath = value;
56
+ } else {
57
+ throw recapOutcomeError(`Unsupported argument: ${flag}.`);
58
+ }
59
+ }
60
+ return { intent, manifestPath };
61
+ }
62
+
63
+ function recapOutcomeError(message) {
64
+ const error = new Error(message);
65
+ error.code = 'E_RECAP_OUTCOME';
66
+ return error;
67
+ }
68
+
69
+ if (
70
+ process.argv[1] &&
71
+ import.meta.url === pathToFileURL(process.argv[1]).href
72
+ ) {
73
+ main(process.argv.slice(2))
74
+ .then((result) => {
75
+ process.stdout.write(`${JSON.stringify(result)}\n`);
76
+ })
77
+ .catch((error) => {
78
+ process.stderr.write(
79
+ `${JSON.stringify({ ok: false, code: error.code, message: error.message })}\n`,
80
+ );
81
+ process.exitCode = 1;
82
+ });
83
+ }
@@ -0,0 +1,91 @@
1
+ const INVOCATIONS = new Set(['project', 'repo', 'direct']);
2
+
3
+ export function deriveExplainerDestination({
4
+ invocation,
5
+ projectSlug,
6
+ s3Uri,
7
+ publicBaseUrl,
8
+ }) {
9
+ if (!INVOCATIONS.has(invocation)) {
10
+ throw new Error(`Unsupported explainer invocation: ${invocation}`);
11
+ }
12
+ const roots = {
13
+ s3Uri: normalizeRoot(s3Uri, 's3Uri', 's3:'),
14
+ publicBaseUrl: normalizeRoot(publicBaseUrl, 'publicBaseUrl', 'https:'),
15
+ };
16
+ if (invocation !== 'project') {
17
+ return roots;
18
+ }
19
+
20
+ const encodedSlug = encodeProjectSlug(projectSlug);
21
+ return {
22
+ s3Uri: `${roots.s3Uri}/projects/${encodedSlug}`,
23
+ publicBaseUrl: `${roots.publicBaseUrl}/projects/${encodedSlug}`,
24
+ };
25
+ }
26
+
27
+ function encodeProjectSlug(projectSlug) {
28
+ if (
29
+ typeof projectSlug !== 'string' ||
30
+ !projectSlug.trim() ||
31
+ projectSlug === '.' ||
32
+ projectSlug === '..' ||
33
+ projectSlug.includes('/') ||
34
+ projectSlug.includes('\\') ||
35
+ projectSlug.includes('\0')
36
+ ) {
37
+ throw new Error(
38
+ 'Project slug must be a non-empty, safe path segment without traversal or separators.',
39
+ );
40
+ }
41
+ return encodeURIComponent(projectSlug);
42
+ }
43
+
44
+ function normalizeRoot(value, label, protocol) {
45
+ if (typeof value !== 'string') {
46
+ throw new Error(`${label} must be a valid destination root.`);
47
+ }
48
+ const root = value.trim();
49
+ const authorityStart = `${protocol}//`;
50
+ if (
51
+ !root.startsWith(authorityStart) ||
52
+ /\s|\\/.test(root) ||
53
+ root.includes('?') ||
54
+ root.includes('#')
55
+ ) {
56
+ throw new Error(`${label} must be a valid destination root.`);
57
+ }
58
+
59
+ let parsed;
60
+ try {
61
+ parsed = new URL(root);
62
+ } catch {
63
+ throw new Error(`${label} must be a valid destination root.`);
64
+ }
65
+ const authority = root.slice(authorityStart.length).split('/', 1)[0];
66
+ if (
67
+ parsed.protocol !== protocol ||
68
+ !isValidRawAuthority(authority, protocol) ||
69
+ !parsed.hostname ||
70
+ parsed.username ||
71
+ parsed.password ||
72
+ parsed.search ||
73
+ parsed.hash
74
+ ) {
75
+ throw new Error(`${label} must be a valid destination root.`);
76
+ }
77
+ return root.replace(/\/+$/, '');
78
+ }
79
+
80
+ function isValidRawAuthority(authority, protocol) {
81
+ if (!authority || authority.includes('@')) {
82
+ return false;
83
+ }
84
+ if (protocol === 's3:') {
85
+ return /^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(authority);
86
+ }
87
+ if (authority.startsWith('[')) {
88
+ return /^\[[^\]]+\](?::\d+)?$/.test(authority);
89
+ }
90
+ return /^[^:]+(?::\d+)?$/.test(authority);
91
+ }
@@ -7,6 +7,7 @@ const MODES = new Set(['dedicated', 'completion-bookkeeping']);
7
7
  const ARTIFACT_COMMIT_TOKEN = '$ARTIFACT_COMMIT';
8
8
  const SHA_PATTERN = /^[a-f0-9]{40}$/;
9
9
  const PACKAGE_COVERAGE_VERSION = 'explainer-kit.package-coverage/v2';
10
+ const TERMINAL_EVIDENCE_VERSION = 'explainer-kit.terminal-evidence/v1';
10
11
 
11
12
  export async function planTrackedRunFinalization(request, context = {}) {
12
13
  assertRequest(request);
@@ -24,10 +25,27 @@ export async function planTrackedRunFinalization(request, context = {}) {
24
25
  'Finalization requires an explainer-kit.manifest/v1 record.',
25
26
  );
26
27
  }
27
- if (manifest.outcome === 'built-needs-review') {
28
- throw new Error(
29
- 'built-needs-review requires a passing visual review before finalization.',
28
+ if (['built-needs-review', 'failed'].includes(manifest.outcome)) {
29
+ const terminalEvidenceContract = await loadTerminalEvidenceContract(
30
+ context.coreRoot,
30
31
  );
32
+ const { evidence: terminalEvidence } =
33
+ await terminalEvidenceContract.readTerminalEvidenceFile(runRoot, {
34
+ manifest,
35
+ });
36
+ return {
37
+ schemaVersion: 'oat-explainer-kit.finalization-plan/v1',
38
+ status: 'complete',
39
+ outcome: manifest.outcome,
40
+ commands: [],
41
+ push: null,
42
+ publicationAllowed: false,
43
+ evidenceDisposition: terminalEvidence.evidenceDisposition,
44
+ terminalEvidencePath: 'terminal-evidence.json',
45
+ ...(terminalEvidence.supersededBy && {
46
+ supersededBy: terminalEvidence.supersededBy,
47
+ }),
48
+ };
31
49
  }
32
50
  const packageCoverage = await loadPackageCoverage(context.coreRoot);
33
51
 
@@ -138,11 +156,21 @@ export async function planTrackedRunFinalization(request, context = {}) {
138
156
 
139
157
  export function verifyTrackedRunFinalization(plan, observation) {
140
158
  if (plan?.status === 'complete') {
159
+ if (
160
+ !['built-durable', 'built-needs-review', 'failed'].includes(plan.outcome)
161
+ ) {
162
+ return failed([
163
+ error(
164
+ 'invalid-plan',
165
+ 'Complete finalization plan has an unsupported outcome.',
166
+ ),
167
+ ]);
168
+ }
141
169
  return {
142
170
  ok: true,
143
- outcome: 'built-durable',
171
+ outcome: plan.outcome,
144
172
  pushAllowed: false,
145
- errors: [],
173
+ reasons: [],
146
174
  };
147
175
  }
148
176
  const errors = [];
@@ -240,7 +268,7 @@ export function verifyTrackedRunFinalization(plan, observation) {
240
268
  }
241
269
 
242
270
  return errors.length === 0
243
- ? { ok: true, outcome, pushAllowed: true, errors: [] }
271
+ ? { ok: true, outcome, pushAllowed: true, reasons: [] }
244
272
  : failed(errors);
245
273
  }
246
274
 
@@ -329,10 +357,9 @@ async function loadPackageCoverage(coreRoot) {
329
357
  let loaded;
330
358
  try {
331
359
  loaded = await import(pathToFileURL(modulePath).href);
332
- } catch (loadError) {
360
+ } catch {
333
361
  throw new Error(
334
- `Compatible explainer package coverage could not be loaded from coreRoot: ${loadError.message}`,
335
- { cause: loadError },
362
+ 'Compatible explainer package coverage could not be loaded from coreRoot.',
336
363
  );
337
364
  }
338
365
  if (
@@ -347,6 +374,29 @@ async function loadPackageCoverage(coreRoot) {
347
374
  return loaded;
348
375
  }
349
376
 
377
+ async function loadTerminalEvidenceContract(coreRoot) {
378
+ const root = await realpathRequired(coreRoot, 'coreRoot');
379
+ const modulePath = join(root, 'scripts', 'lib', 'terminal-evidence.mjs');
380
+ let loaded;
381
+ try {
382
+ loaded = await import(pathToFileURL(modulePath).href);
383
+ } catch {
384
+ throw new Error(
385
+ 'Compatible terminal evidence could not be loaded from coreRoot.',
386
+ );
387
+ }
388
+ if (
389
+ loaded.TERMINAL_EVIDENCE_VERSION !== TERMINAL_EVIDENCE_VERSION ||
390
+ typeof loaded.assertTerminalEvidence !== 'function' ||
391
+ typeof loaded.readTerminalEvidenceFile !== 'function'
392
+ ) {
393
+ throw new Error(
394
+ `coreRoot must provide ${TERMINAL_EVIDENCE_VERSION} confined validation.`,
395
+ );
396
+ }
397
+ return loaded;
398
+ }
399
+
350
400
  function resolveRunPath(runRoot, path) {
351
401
  const target = resolve(runRoot, path);
352
402
  assertWithin(runRoot, target, `Manifest path ${path}`);
@@ -470,6 +520,12 @@ function failed(errors) {
470
520
  ok: false,
471
521
  outcome: 'built-not-durable',
472
522
  pushAllowed: false,
473
- errors,
523
+ reasons: [
524
+ {
525
+ stage: 'finalization',
526
+ kind: 'pipeline-failure',
527
+ count: Math.min(Math.max(errors.length, 1), 50),
528
+ },
529
+ ],
474
530
  };
475
531
  }