@open-agent-toolkit/cli 0.2.25 → 0.2.27

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 (87) hide show
  1. package/assets/NOTICES.md +156 -0
  2. package/assets/docs/cli-utilities/configuration.md +42 -11
  3. package/assets/docs/contributing/explainer-kit-verification.md +125 -0
  4. package/assets/docs/contributing/index.md +1 -0
  5. package/assets/docs/reference/troubleshooting.md +47 -0
  6. package/assets/docs/workflows/projects/artifacts.md +24 -6
  7. package/assets/docs/workflows/projects/dispatch-ceiling.md +67 -16
  8. package/assets/docs/workflows/skills/explainer-kit-providers.md +144 -0
  9. package/assets/docs/workflows/skills/explainer-kit.md +121 -69
  10. package/assets/docs/workflows/skills/index.md +1 -0
  11. package/assets/public-package-versions.json +4 -4
  12. package/assets/skills/explainer-kit/SKILL.md +18 -3
  13. package/assets/skills/explainer-kit/recipes/project-recap.json +43 -16
  14. package/assets/skills/explainer-kit/references/contracts.md +167 -20
  15. package/assets/skills/explainer-kit/references/golden-conformance.md +80 -0
  16. package/assets/skills/explainer-kit/references/visual-authoring.md +92 -0
  17. package/assets/skills/explainer-kit/references/visual-review.md +57 -0
  18. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +172 -1
  19. package/assets/skills/explainer-kit/schemas/build-record.schema.json +7 -1
  20. package/assets/skills/explainer-kit/schemas/fact-base.schema.json +38 -2
  21. package/assets/skills/explainer-kit/schemas/manifest.schema.json +25 -1
  22. package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -0
  23. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +149 -0
  24. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +117 -0
  25. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +80 -0
  26. package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +148 -4
  27. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +243 -0
  28. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +586 -8
  29. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +285 -8
  30. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +35 -0
  31. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +144 -8
  32. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +379 -0
  33. package/assets/skills/explainer-kit/scripts/lib/png.mjs +287 -0
  34. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +280 -8
  35. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +132 -3
  36. package/assets/skills/explainer-kit/scripts/lib/records.mjs +513 -21
  37. package/assets/skills/explainer-kit/scripts/lib/render.mjs +67 -3
  38. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +43 -1
  39. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +208 -0
  40. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +218 -0
  41. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +380 -0
  42. package/assets/skills/explainer-kit/scripts/render-qa.mjs +48 -10
  43. package/assets/skills/explainer-kit/scripts/run.mjs +859 -134
  44. package/assets/skills/oat-explainer-kit/SKILL.md +40 -12
  45. package/assets/skills/oat-explainer-kit/references/author-callback.md +12 -10
  46. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +40 -2
  47. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +72 -0
  48. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +167 -5
  49. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +92 -5
  50. package/assets/skills/oat-explainer-kit/scripts/run.mjs +324 -2
  51. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
  53. package/assets/skills/oat-project-implement/SKILL.md +9 -11
  54. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +18 -9
  55. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
  56. package/assets/skills/oat-project-implement/references/phase-execution.md +13 -4
  57. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
  58. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
  59. package/dist/commands/config/index.d.ts.map +1 -1
  60. package/dist/commands/config/index.js +27 -3
  61. package/dist/commands/project/archive/archive-utils.d.ts +1 -0
  62. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  63. package/dist/commands/project/archive/archive-utils.js +109 -42
  64. package/dist/commands/project/archive/explainer-package-coverage.d.ts +14 -0
  65. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -0
  66. package/dist/commands/project/archive/explainer-package-coverage.js +27 -0
  67. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +18 -0
  68. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +1 -0
  69. package/dist/commands/project/archive/explainer-source-backlinks.js +27 -0
  70. package/dist/commands/project/archive/push-runner.d.ts +2 -1
  71. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  72. package/dist/commands/project/archive/push-runner.js +5 -1
  73. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  74. package/dist/commands/project/dispatch-ceiling/index.js +90 -0
  75. package/dist/config/dispatch-notices.d.ts +8 -0
  76. package/dist/config/dispatch-notices.d.ts.map +1 -0
  77. package/dist/config/dispatch-notices.js +79 -0
  78. package/dist/config/dispatch-policy-options.d.ts +2 -0
  79. package/dist/config/dispatch-policy-options.d.ts.map +1 -1
  80. package/dist/config/dispatch-policy-options.js +14 -2
  81. package/dist/providers/identity/dispatch-report.d.ts +17 -0
  82. package/dist/providers/identity/dispatch-report.d.ts.map +1 -1
  83. package/dist/providers/identity/dispatch-report.js +30 -0
  84. package/dist/release/public-package-contract.d.ts +6 -0
  85. package/dist/release/public-package-contract.d.ts.map +1 -1
  86. package/dist/release/public-package-contract.js +75 -0
  87. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-explainer-kit
3
- version: 1.0.3
3
+ version: 1.0.5
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,7 +19,10 @@ 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
+ - Resolve one provider-neutral set planner for unattended project recaps.
22
23
  - Require lifecycle callers to construct a brief-aware author seam.
24
+ - Require first-class browser-evidence and whole-set visual-review providers
25
+ for unattended project recaps.
23
26
 
24
27
  ## Dependency Direction
25
28
 
@@ -34,7 +37,7 @@ rollback, and operator-owned real-wrapper gate, use `references/migration.md`.
34
37
 
35
38
  Before reading OAT config or invoking the core, call
36
39
  `scripts/check-core.mjs#checkCoreCompatibility` with this installed skill
37
- directory and minimum core version `2.0.0`. Continue only when it returns
40
+ directory and minimum core version `2.0.3`. Continue only when it returns
38
41
  `ok: true`.
39
42
 
40
43
  - Missing core: stop and show
@@ -55,7 +58,7 @@ Call `scripts/run.mjs#runOatExplainer` with the repository root, project
55
58
  invocation, active project path, recipe, slug, lifecycle mode, and any explicit
56
59
  runtime overrides. The adapter:
57
60
 
58
- 1. checks the user-scoped installed core at minimum version `2.0.0`;
61
+ 1. checks the user-scoped installed core at minimum version `2.0.3`;
59
62
  2. resolves only the public `explainers.*` and `workflow.explainers.*` keys;
60
63
  3. derives the canonical project output root;
61
64
  4. binds approved OAT artifacts to the recipe's single `project` source set;
@@ -69,15 +72,40 @@ Missing optional artifacts are omitted, but at least one approved lifecycle
69
72
  artifact is required. An explicit supplied fact-base path bypasses artifact
70
73
  federation and is passed through as `factBase.mode: supplied`.
71
74
 
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.
75
+ Before invocation, read `references/author-callback.md`. Unattended
76
+ `project-recap` runs require exactly one provider-neutral set planner:
77
+ in-process callers supply `planSet`, while JSON/CLI callers supply
78
+ `planSetModulePath` naming a module with a `planSet` function export. The
79
+ adapter resolves this executable capability and passes only the callback to the
80
+ core. It never persists provider configuration or module paths.
81
+
82
+ Construct exactly one provider-neutral author seam in both modes: in-process
83
+ callers supply `author`, while JSON/CLI callers supply `authorModulePath` naming
84
+ a module with an `author` function export. The core invokes that callback once
85
+ per planned artifact. Every request carries the immutable shared `setContext`,
86
+ the matching `plannedArtifact`, the recipe brief, theme, and any
87
+ recipe-selected artistic shell. Those bundled inputs are the required
88
+ medium-specific guidance; an optional visual-explainer installation may enhance
89
+ provider execution but is never required. The adapter validates and resolves
90
+ executable inputs before passing them to `core.runExplainer`; callbacks and
91
+ module paths never enter the persisted run request.
92
+
93
+ Also read `references/visual-review-callback.md`. Unattended `project-recap`
94
+ runs require exactly one trusted launched-Chromium browser session and one
95
+ whole-set visual critic. In-process callers supply a branded `browserSession`
96
+ created by the compatible core and `visualCritic`; JSON/CLI callers supply
97
+ `browserSessionModulePath` and `visualCriticModulePath`, naming modules with
98
+ matching exports. Bare browser callbacks and caller-authored runtime metadata
99
+ are rejected. These are first-class adapter inputs; do not place either
100
+ provider in `coreOptions`.
101
+
102
+ The adapter resolves all executable providers before core invocation, enforces
103
+ direct-versus-module mutual exclusion, and requires distinct identities for
104
+ authoring, fact criticism, browser evidence, and visual criticism. A missing,
105
+ forged, or deterministic fixture session fails production validation before the
106
+ core runs. A runtime browser or visual-review failure is retained by the core
107
+ as `built-needs-review`, never as a successful durability or publication
108
+ outcome.
81
109
 
82
110
  Unattended project runs pass `approved-oat-artifacts` provenance to the core's
83
111
  content-approval seam and never prompt. Automated completion and
@@ -15,6 +15,10 @@ core constructs that request from recipe-owned policy:
15
15
 
16
16
  - `brief` contains the prose loaded from the floor entry or expansion profile's
17
17
  `briefRef`; the author must treat the inlined brief as its editorial contract.
18
+ - `visualAuthoringGuidance` contains the installed core's bundled
19
+ representation, hierarchy, responsive-navigation, table, diagram, and deck
20
+ rules. Treat it as the medium baseline; do not read ambient or home-directory
21
+ guidance.
18
22
  - `factBase` contains the reconciled evidence for the run. Ground claims in that
19
23
  fact base and do not replace it with ambient project context.
20
24
  - `authoring` selects the output path. Return Markdown for `markdown` and a
@@ -24,23 +28,21 @@ core constructs that request from recipe-owned policy:
24
28
  brief's license to elaborate.
25
29
  - A floor request may include `floor.requiredNarrative`, which identifies the
26
30
  narrative coverage checked later as non-blocking guidelines.
31
+ - `setContext` is the complete immutable planner-owned portfolio and shared
32
+ ledger. `plannedArtifact` is the exact entry assigned to this callback;
33
+ preserve its identity, sources, draft, and visual intent.
27
34
 
28
35
  Return one matching `explainer-kit.author-result/v2`: preserve `artifactId`,
29
36
  set exactly one of `content.markdown` or `content.html`, and include
30
37
  `provenance`. Do not persist provider credentials or callback configuration in
31
38
  the result.
32
39
 
33
- ## Expansion
40
+ ## Planner-owned expansion
34
41
 
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.
42
+ The set planner finalizes the complete floor and expansion portfolio before any
43
+ author runs. Authors must not return `proposedArtifacts` or otherwise add,
44
+ remove, replace, or reorder portfolio entries. Handle only the supplied
45
+ `plannedArtifact`; the core rejects author-driven expansion after planning.
44
46
 
45
47
  ## Lifecycle invocation
46
48
 
@@ -86,13 +86,48 @@ 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
+ ## Browser and visual-review execution
90
+
91
+ Every unattended `project-recap` must provide exactly one browser-evidence
92
+ session and exactly one whole-set visual critic through the adapter's
93
+ first-class boundary. In-process callers pass `browserSession`, created by the
94
+ compatible core's `createBrowserProbeSession()`, and `visualCritic`. JSON-only
95
+ and official CLI callers pass `browserSessionModulePath` and
96
+ `visualCriticModulePath`, naming modules whose matching exports are
97
+ `browserSession` and `visualCritic`. Direct-plus-module conflicts, invalid
98
+ exports, bare `browserProbe` inputs, and the legacy
99
+ `coreOptions.browserProbe`, `coreOptions.browserSession`, or
100
+ `coreOptions.visualCritic` routes fail before core invocation.
101
+
102
+ The author, fact critic, branded session probe, and visual critic must have
103
+ distinct callback identities. The adapter asks the loaded core to validate the
104
+ session's private brand and launched-Chromium runtime before passing it to
105
+ `core.runExplainer`. Executable callbacks, descriptors, and module paths never
106
+ enter the retained run request. Deterministic fixture sessions are rejected for
107
+ unattended project recaps. See `visual-review-callback.md` for the session,
108
+ request/result, and byte-bound whole-set review contracts.
109
+
110
+ The core retains canonical 320, 768, and 1440 viewport screenshots, paired
111
+ `explainer-kit.browser-evidence/v2` metrics with launched Chromium name,
112
+ version, and capture identity, each review request/result, cohesion
113
+ observations, and any one-pass revision record. Missing, malformed, forged,
114
+ cross-record mismatched, stale, or failed review-chain evidence terminates as
115
+ `built-needs-review`; durability and publication remain blocked.
116
+
89
117
  ## Tracked-run finalization
90
118
 
91
119
  `planTrackedRunFinalization(request, context)` is the shared command planner for
92
120
  tracked project explainer and recap runs. The request contains `runRoot`,
93
121
  `manifestPath`, `commitMode`, and optional `relocatedFrom`. Context supplies the
94
- repository root, project name, and, for `completion-bookkeeping`, the existing
95
- full artifact commit SHA.
122
+ repository root, project name, an explicit compatible `coreRoot`, and, for
123
+ `completion-bookkeeping`, the existing full artifact commit SHA. Finalization
124
+ dynamically loads the versioned package-coverage contract from that core root;
125
+ it does not maintain an adapter-local path list.
126
+
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.
96
131
 
97
132
  The returned stages must run in order:
98
133
 
@@ -152,6 +187,9 @@ export remains committed, the mutable records retain the warning and
152
187
  complete. A later attestation may recover durability without repeating the
153
188
  archive.
154
189
 
190
+ This recovery path applies only to `built-not-durable`.
191
+ `built-needs-review` cannot be exported, attested, finalized, or pushed.
192
+
155
193
  Post-archive summary and PR recap links target `projectRecapExport.exportRoot`
156
194
  under `.oat/repo/reference/project-recaps/` on the current head branch. The
157
195
  tracked summary export and the PR body may carry that link; the gitignored
@@ -0,0 +1,72 @@
1
+ # Browser and visual-review callbacks
2
+
3
+ Unattended project recaps require two provider-neutral executable seams. Supply
4
+ each seam either directly or through a module path, never both:
5
+
6
+ | Role | Direct input | Module-path input | Required export |
7
+ | ---------------- | ---------------- | -------------------------- | ---------------- |
8
+ | Browser evidence | `browserSession` | `browserSessionModulePath` | `browserSession` |
9
+ | Whole-set review | `visualCritic` | `visualCriticModulePath` | `visualCritic` |
10
+
11
+ Do not route these callbacks through `coreOptions`. The adapter resolves and
12
+ validates both providers before core invocation and keeps callbacks and module
13
+ paths out of retained requests.
14
+
15
+ ## Browser evidence
16
+
17
+ Create the direct descriptor with the compatible core's
18
+ `createBrowserProbeSession()`. A module-path provider exports that branded
19
+ descriptor as `browserSession`; it does not export a bare callback or a
20
+ caller-authored metadata object. The core derives Chromium type and version
21
+ from the launched `Browser` instance and validates the private session brand.
22
+ Callers remain responsible for closing the session.
23
+
24
+ The descriptor's probe receives one provider-neutral request for each artifact,
25
+ viewport, and required interaction scenario. The request includes the rendered
26
+ artifact, exact viewport dimensions, scenario, interaction expectations, and,
27
+ for the default scenario, an absolute `screenshotPath`. The probe writes a PNG
28
+ to that path and returns measured browser facts:
29
+
30
+ - `pageOverflowX` as a boolean;
31
+ - `clippedX`, `viewportClipped`, and `unreadableHeadings` as arrays;
32
+ - `reducedMotion` as a boolean;
33
+ - `keyboard` as an object describing observed key behavior; and
34
+ - applicable deck, theme-toggle, animation, and layout measurements.
35
+
36
+ For unattended project recaps the core chooses the canonical 320, 768, and 1440
37
+ widths. The retained PNG signature and IHDR dimensions must match the requested
38
+ viewport. The core writes paired metrics JSON itself; callback assertions do
39
+ not substitute for retained evidence. Those
40
+ `explainer-kit.browser-evidence/v2` records retain the launched Chromium name,
41
+ version, capture settings, and stable capture identity. Deterministic fixture
42
+ sessions are explicit test-only descriptors and are rejected by unattended
43
+ project-recap production paths.
44
+
45
+ ## Whole-set visual review
46
+
47
+ The visual critic receives one
48
+ `explainer-kit.visual-review-request/v1` plus a confined evidence reader. The
49
+ request contains every rendered artifact, exact rendered-content hashes,
50
+ viewport-matched screenshot and metrics hashes, observed cohesion claims, and
51
+ the exact browser runtime and capture identity shared by every evidence record.
52
+ Use the evidence reader to inspect the byte snapshot named by the request.
53
+
54
+ Return `explainer-kit.visual-review-result/v1` with:
55
+
56
+ - a safe `reviewId`;
57
+ - the request's exact `requestId` and `requestHash`;
58
+ - an ISO `reviewedAt`;
59
+ - `disposition` equal to `pass`, `correct`, or `fail`;
60
+ - the complete reviewed `artifactIds`; and
61
+ - structured `findings`.
62
+
63
+ The callback must not mutate rendered files, screenshots, or metrics. The core
64
+ revalidates all bound bytes after the callback returns.
65
+
66
+ ## Correction and terminal behavior
67
+
68
+ `correct` permits one correction callback and exactly one final review. There
69
+ is never a second correction or third review. Missing evidence, invalid
70
+ contracts, thrown callbacks, byte mutation, unresolved correction, or a `fail`
71
+ disposition produces `built-needs-review`. Such a run retains available review
72
+ evidence but invokes neither durability nor publication.
@@ -1,5 +1,10 @@
1
+ import { execFile as execFileCallback } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
1
3
  import { access, readFile, realpath } from 'node:fs/promises';
2
- import { basename, join } from 'node:path';
4
+ import { basename, join, relative, resolve, sep } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+
7
+ const execFile = promisify(execFileCallback);
3
8
 
4
9
  const RECIPE_ARTIFACTS = Object.freeze({
5
10
  'project-explainer': ['plan', 'design', 'spec'],
@@ -8,8 +13,10 @@ const RECIPE_ARTIFACTS = Object.freeze({
8
13
 
9
14
  export async function bindProjectSources({
10
15
  projectRoot,
16
+ repoRoot,
11
17
  recipe,
12
18
  suppliedFactBasePath,
19
+ reviewedRepository,
13
20
  }) {
14
21
  if (!projectRoot) {
15
22
  throw new TypeError('projectRoot is required to bind OAT artifacts.');
@@ -20,6 +27,9 @@ export async function bindProjectSources({
20
27
  }
21
28
 
22
29
  const canonicalProjectRoot = await realpath(projectRoot);
30
+ const repository =
31
+ reviewedRepository ??
32
+ (repoRoot ? await resolveReviewedRepository(repoRoot) : null);
23
33
  if (suppliedFactBasePath) {
24
34
  const path = await realpath(suppliedFactBasePath);
25
35
  return {
@@ -30,7 +40,8 @@ export async function bindProjectSources({
30
40
  },
31
41
  reviewedSource: {
32
42
  kind: 'approved-fact-base',
33
- locator: path,
43
+ locator: repository?.repositoryUrl ?? path,
44
+ ...(repository && repository),
34
45
  },
35
46
  };
36
47
  }
@@ -59,6 +70,10 @@ export async function bindProjectSources({
59
70
  `No approved OAT lifecycle artifacts were found for ${recipe}.`,
60
71
  );
61
72
  }
73
+ const reviewedSources =
74
+ repository && repoRoot
75
+ ? await bindReviewedSources(sources, repoRoot, repository)
76
+ : undefined;
62
77
 
63
78
  return {
64
79
  factBase: {
@@ -68,25 +83,172 @@ export async function bindProjectSources({
68
83
  },
69
84
  reviewedSource: {
70
85
  kind: 'approved-oat-artifacts',
71
- locator: canonicalProjectRoot,
86
+ locator: repository?.repositoryUrl ?? canonicalProjectRoot,
87
+ ...(repository && repository),
72
88
  },
73
- sourceLoader: loadOatArtifact,
89
+ ...(reviewedSources && {
90
+ sourceProvenance: reviewedSources.provenance,
91
+ }),
92
+ sourceLoader: reviewedSources
93
+ ? (source) => loadReviewedOatArtifact(source, reviewedSources.documents)
94
+ : loadOatArtifact,
95
+ };
96
+ }
97
+
98
+ export async function resolveReviewedRepository(
99
+ repoRoot,
100
+ { command = execFile } = {},
101
+ ) {
102
+ const cwd = resolve(repoRoot);
103
+ const [{ stdout: revisionOutput }, { stdout: remoteOutput }] =
104
+ await Promise.all([
105
+ command('git', ['rev-parse', 'HEAD'], { cwd }),
106
+ command('git', ['config', '--get', 'remote.origin.url'], { cwd }),
107
+ ]);
108
+ const revision = String(revisionOutput).trim();
109
+ if (!/^[a-f0-9]{40}$/.test(revision)) {
110
+ throw new Error(
111
+ 'Reviewed OAT sources require a full commit SHA, not a moving revision.',
112
+ );
113
+ }
114
+ const repository = githubRepositoryIdentity(String(remoteOutput).trim());
115
+ return {
116
+ repository,
117
+ repositoryUrl: `https://github.com/${repository}`,
118
+ revision,
74
119
  };
75
120
  }
76
121
 
77
122
  async function loadOatArtifact(source) {
78
- const text = (await readFile(source.locator, 'utf8')).trim();
123
+ return oatArtifactDocument(
124
+ source,
125
+ (await readFile(source.locator, 'utf8')).trim(),
126
+ );
127
+ }
128
+
129
+ function loadReviewedOatArtifact(source, documents) {
130
+ const document = documents.get(source.id);
131
+ if (!document) {
132
+ throw new Error(
133
+ `No reviewed Git blob was bound for OAT source ${source.id}.`,
134
+ );
135
+ }
136
+ return structuredClone(document);
137
+ }
138
+
139
+ function oatArtifactDocument(source, text, sourceHash) {
79
140
  return {
80
141
  claims: [
81
142
  {
82
143
  id: source.id,
83
144
  text,
84
145
  locator: source.locator,
146
+ lineRange: lineRangeFor(text),
85
147
  },
86
148
  ],
149
+ ...(sourceHash && { sourceHash }),
87
150
  };
88
151
  }
89
152
 
153
+ async function bindReviewedSources(sources, repoRoot, repository) {
154
+ const canonicalRepoRoot = await realpath(repoRoot);
155
+ const entries = await Promise.all(
156
+ sources.map(async (source) => {
157
+ const repositoryPath = relative(canonicalRepoRoot, source.locator)
158
+ .split(sep)
159
+ .join('/');
160
+ if (
161
+ !repositoryPath ||
162
+ repositoryPath.startsWith('../') ||
163
+ repositoryPath === '..'
164
+ ) {
165
+ throw new Error(
166
+ `OAT source ${source.id} is outside the reviewed repository.`,
167
+ );
168
+ }
169
+ try {
170
+ await execFile(
171
+ 'git',
172
+ ['ls-files', '--error-unmatch', '--', repositoryPath],
173
+ { cwd: canonicalRepoRoot },
174
+ );
175
+ } catch {
176
+ throw new Error(
177
+ `OAT source ${source.id} is not tracked in the reviewed repository.`,
178
+ );
179
+ }
180
+ let reviewedBytes;
181
+ try {
182
+ ({ stdout: reviewedBytes } = await execFile(
183
+ 'git',
184
+ ['show', `${repository.revision}:${repositoryPath}`],
185
+ {
186
+ cwd: canonicalRepoRoot,
187
+ encoding: null,
188
+ maxBuffer: 16 * 1024 * 1024,
189
+ },
190
+ ));
191
+ } catch {
192
+ throw new Error(
193
+ `OAT source ${source.id} is absent from reviewed revision ${repository.revision}.`,
194
+ );
195
+ }
196
+ const workingBytes = await readFile(source.locator);
197
+ const canonicalReviewedBytes = Buffer.from(reviewedBytes);
198
+ if (!workingBytes.equals(canonicalReviewedBytes)) {
199
+ throw new Error(
200
+ `OAT source ${source.id} working tree bytes mismatch reviewed Git blob at revision ${repository.revision}.`,
201
+ );
202
+ }
203
+ const text = canonicalReviewedBytes.toString('utf8').trim();
204
+ return {
205
+ id: source.id,
206
+ provenance: {
207
+ repository: repository.repository,
208
+ revision: repository.revision,
209
+ path: repositoryPath,
210
+ lineRange: lineRangeFor(text),
211
+ },
212
+ document: oatArtifactDocument(
213
+ source,
214
+ text,
215
+ `sha256:${createHash('sha256')
216
+ .update(canonicalReviewedBytes)
217
+ .digest('hex')}`,
218
+ ),
219
+ };
220
+ }),
221
+ );
222
+ return {
223
+ provenance: Object.fromEntries(
224
+ entries.map((entry) => [entry.id, entry.provenance]),
225
+ ),
226
+ documents: new Map(entries.map((entry) => [entry.id, entry.document])),
227
+ };
228
+ }
229
+
230
+ function lineRangeFor(text) {
231
+ return {
232
+ start: 1,
233
+ end: Math.max(1, text.split('\n').length),
234
+ };
235
+ }
236
+
237
+ function githubRepositoryIdentity(remote) {
238
+ const patterns = [
239
+ /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
240
+ /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/,
241
+ /^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
242
+ ];
243
+ for (const pattern of patterns) {
244
+ const match = remote.match(pattern);
245
+ if (match) return `${match[1]}/${match[2]}`;
246
+ }
247
+ throw new Error(
248
+ 'Reviewed OAT sources require a canonical GitHub origin repository.',
249
+ );
250
+ }
251
+
90
252
  function authoritativeTopics(id) {
91
253
  return {
92
254
  plan: ['phases', 'validation-approach'],
@@ -1,9 +1,12 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readFile, realpath } from 'node:fs/promises';
2
- import { relative, resolve, sep } from 'node:path';
3
+ import { join, relative, resolve, sep } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
3
5
 
4
6
  const MODES = new Set(['dedicated', 'completion-bookkeeping']);
5
7
  const ARTIFACT_COMMIT_TOKEN = '$ARTIFACT_COMMIT';
6
8
  const SHA_PATTERN = /^[a-f0-9]{40}$/;
9
+ const PACKAGE_COVERAGE_VERSION = 'explainer-kit.package-coverage/v2';
7
10
 
8
11
  export async function planTrackedRunFinalization(request, context = {}) {
9
12
  assertRequest(request);
@@ -21,10 +24,16 @@ export async function planTrackedRunFinalization(request, context = {}) {
21
24
  'Finalization requires an explainer-kit.manifest/v1 record.',
22
25
  );
23
26
  }
27
+ if (manifest.outcome === 'built-needs-review') {
28
+ throw new Error(
29
+ 'built-needs-review requires a passing visual review before finalization.',
30
+ );
31
+ }
32
+ const packageCoverage = await loadPackageCoverage(context.coreRoot);
24
33
 
25
- const immutablePaths = immutablePackagePaths(manifest).map((path) =>
26
- toRepoPath(repoRoot, resolveRunPath(runRoot, path)),
27
- );
34
+ const immutablePaths = (
35
+ await immutablePackagePaths(manifest, runRoot, packageCoverage)
36
+ ).map((path) => toRepoPath(repoRoot, resolveRunPath(runRoot, path)));
28
37
  const mutablePaths = [
29
38
  toRepoPath(repoRoot, manifestPath),
30
39
  toRepoPath(repoRoot, resolveRunPath(runRoot, manifest.buildRecord?.path)),
@@ -245,7 +254,7 @@ function commitCommands(paths, subject) {
245
254
  ];
246
255
  }
247
256
 
248
- function immutablePackagePaths(manifest) {
257
+ async function immutablePackagePaths(manifest, runRoot, packageCoverage) {
249
258
  if (
250
259
  !manifest.immutableHashes ||
251
260
  typeof manifest.immutableHashes !== 'object' ||
@@ -257,9 +266,87 @@ function immutablePackagePaths(manifest) {
257
266
  if (paths.length === 0) {
258
267
  throw new Error('Manifest does not identify a complete immutable package.');
259
268
  }
269
+ const verifiedBytes = new Map();
270
+ for (const path of paths) {
271
+ const expected = manifest.immutableHashes[path];
272
+ if (!/^sha256:[a-f0-9]{64}$/.test(expected)) {
273
+ throw new Error(`Manifest has an invalid immutable hash for ${path}.`);
274
+ }
275
+ const bytes = await readFile(resolveRunPath(runRoot, path));
276
+ const actual = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
277
+ if (actual !== expected) {
278
+ throw new Error(`Immutable package hash mismatch for ${path}.`);
279
+ }
280
+ verifiedBytes.set(path, bytes);
281
+ }
282
+ const runMode =
283
+ manifest.recipe?.id === 'project-recap'
284
+ ? verifiedRunMode(verifiedBytes.get('run-request.json'))
285
+ : undefined;
286
+ const required = packageCoverage.requiredImmutablePackagePaths(manifest, {
287
+ runMode,
288
+ });
289
+ const missing = required.filter(
290
+ (path) => !(path in manifest.immutableHashes),
291
+ );
292
+ if (missing.length > 0) {
293
+ throw new Error(
294
+ `Manifest immutable hashes do not cover the canonical package: ${missing.join(', ')}.`,
295
+ );
296
+ }
297
+ await packageCoverage.validateImmutablePackageEvidence(manifest, {
298
+ runMode,
299
+ read: (path) => verifiedBytes.get(path),
300
+ });
260
301
  return paths;
261
302
  }
262
303
 
304
+ function verifiedRunMode(bytes) {
305
+ if (!Buffer.isBuffer(bytes)) {
306
+ throw new Error(
307
+ 'Manifest must include hash-verified run-request.json before package coverage is evaluated.',
308
+ );
309
+ }
310
+ let request;
311
+ try {
312
+ request = JSON.parse(bytes.toString('utf8'));
313
+ } catch {
314
+ throw new Error(
315
+ 'Hash-verified run-request.json must contain valid JSON before package coverage is evaluated.',
316
+ );
317
+ }
318
+ if (!['interactive', 'unattended'].includes(request?.mode)) {
319
+ throw new Error(
320
+ 'Hash-verified run-request.json must declare interactive or unattended mode.',
321
+ );
322
+ }
323
+ return request.mode;
324
+ }
325
+
326
+ async function loadPackageCoverage(coreRoot) {
327
+ const root = await realpathRequired(coreRoot, 'coreRoot');
328
+ const modulePath = join(root, 'scripts', 'lib', 'package-coverage.mjs');
329
+ let loaded;
330
+ try {
331
+ loaded = await import(pathToFileURL(modulePath).href);
332
+ } catch (loadError) {
333
+ throw new Error(
334
+ `Compatible explainer package coverage could not be loaded from coreRoot: ${loadError.message}`,
335
+ { cause: loadError },
336
+ );
337
+ }
338
+ if (
339
+ loaded.PACKAGE_COVERAGE_VERSION !== PACKAGE_COVERAGE_VERSION ||
340
+ typeof loaded.requiredImmutablePackagePaths !== 'function' ||
341
+ typeof loaded.validateImmutablePackageEvidence !== 'function'
342
+ ) {
343
+ throw new Error(
344
+ `coreRoot must provide ${PACKAGE_COVERAGE_VERSION} package coverage.`,
345
+ );
346
+ }
347
+ return loaded;
348
+ }
349
+
263
350
  function resolveRunPath(runRoot, path) {
264
351
  const target = resolve(runRoot, path);
265
352
  assertWithin(runRoot, target, `Manifest path ${path}`);