vigiles 29.1.0 → 30.0.1

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 (122) hide show
  1. package/dist/adapter-conformance.d.ts +1 -1
  2. package/dist/adapter-conformance.js +106 -25
  3. package/dist/adapter-registry.d.ts +61 -14
  4. package/dist/adapter-registry.js +78 -10
  5. package/dist/adapter.d.ts +23 -2
  6. package/dist/adapter.js +13 -1
  7. package/dist/adapters/claude-code/adapter.d.ts +32 -2
  8. package/dist/adapters/claude-code/adapter.js +44 -23
  9. package/dist/adapters/claude-code/agent-runtime.js +3 -1
  10. package/dist/adapters/claude-code/dialect.js +87 -21
  11. package/dist/adapters/claude-code/effect-region.js +3 -1
  12. package/dist/adapters/claude-code/hook-protocol.js +16 -0
  13. package/dist/adapters/claude-code/instruction-chain.d.ts +25 -0
  14. package/dist/adapters/claude-code/instruction-chain.js +626 -0
  15. package/dist/adapters/claude-code/layout.d.ts +2 -2
  16. package/dist/adapters/claude-code/layout.js +42 -8
  17. package/dist/adapters/claude-code/model-access.d.ts +41 -0
  18. package/dist/adapters/claude-code/model-access.js +46 -0
  19. package/dist/adapters/claude-code/skill-reachability.d.ts +125 -0
  20. package/dist/adapters/claude-code/skill-reachability.js +111 -0
  21. package/dist/adapters/claude-code/skill-runtime.js +3 -1
  22. package/dist/adapters/codex/adapter.d.ts +39 -2
  23. package/dist/adapters/codex/adapter.js +29 -29
  24. package/dist/adapters/codex/dialect.js +11 -6
  25. package/dist/adapters/codex/eval.d.ts +10 -0
  26. package/dist/adapters/codex/eval.js +48 -1
  27. package/dist/adapters/codex/hook-protocol.d.ts +2 -1
  28. package/dist/adapters/codex/hook-protocol.js +10 -0
  29. package/dist/adapters/codex/instruction-chain.d.ts +40 -0
  30. package/dist/adapters/codex/instruction-chain.js +105 -0
  31. package/dist/adapters/codex/layout.d.ts +1 -1
  32. package/dist/adapters/codex/layout.js +41 -14
  33. package/dist/adapters/opencode/adapter.d.ts +33 -2
  34. package/dist/adapters/opencode/adapter.js +36 -36
  35. package/dist/adapters/opencode/dialect.js +2 -2
  36. package/dist/adapters/opencode/instruction-chain.d.ts +37 -0
  37. package/dist/adapters/opencode/instruction-chain.js +70 -0
  38. package/dist/adapters/opencode/layout.d.ts +19 -0
  39. package/dist/adapters/opencode/layout.js +34 -15
  40. package/dist/adoptability.d.ts +31 -1
  41. package/dist/adoptability.js +57 -0
  42. package/dist/cli-main.js +185 -102
  43. package/dist/core/adapter.d.ts +213 -61
  44. package/dist/core/compile.d.ts +2 -2
  45. package/dist/core/compile.js +57 -46
  46. package/dist/core/compose.d.ts +5 -3
  47. package/dist/core/compose.js +5 -3
  48. package/dist/core/config-schema.d.ts +14 -2
  49. package/dist/core/config-schema.js +20 -7
  50. package/dist/core/dialect.d.ts +54 -12
  51. package/dist/core/dialect.js +56 -0
  52. package/dist/core/eval-driver.d.ts +194 -0
  53. package/dist/core/eval-driver.js +3 -0
  54. package/dist/core/frontmatter-read.d.ts +10 -0
  55. package/dist/core/frontmatter-read.js +30 -3
  56. package/dist/core/guards.js +3 -1
  57. package/dist/core/hook-program.d.ts +27 -2
  58. package/dist/core/hook-program.js +29 -24
  59. package/dist/core/hook-protocol.d.ts +54 -0
  60. package/dist/core/install-reader.d.ts +18 -0
  61. package/dist/core/install-reader.js +88 -0
  62. package/dist/core/instruction-chain.d.ts +444 -0
  63. package/dist/core/instruction-chain.js +292 -0
  64. package/dist/core/instruction-weight.d.ts +96 -14
  65. package/dist/core/instruction-weight.js +65 -30
  66. package/dist/core/layout.d.ts +220 -33
  67. package/dist/core/layout.js +115 -1
  68. package/dist/core/lethal-trifecta.d.ts +12 -7
  69. package/dist/core/lethal-trifecta.js +13 -13
  70. package/dist/core/live-driver.d.ts +137 -0
  71. package/dist/core/live-driver.js +14 -0
  72. package/dist/core/markdown.d.ts +23 -0
  73. package/dist/core/markdown.js +77 -28
  74. package/dist/core/orphans.js +9 -7
  75. package/dist/core/settings-codec.d.ts +17 -0
  76. package/dist/core/settings-codec.js +56 -0
  77. package/dist/core/surface-discovery.d.ts +2 -2
  78. package/dist/core/surface-discovery.js +24 -12
  79. package/dist/core/surface-scopes.d.ts +26 -6
  80. package/dist/core/surface-scopes.js +52 -11
  81. package/dist/core/validate.js +16 -3
  82. package/dist/coverage-artifact.d.ts +3 -2
  83. package/dist/coverage-artifact.js +6 -5
  84. package/dist/eval-cache.d.ts +6 -1
  85. package/dist/eval-cache.js +11 -1
  86. package/dist/eval.d.ts +16 -108
  87. package/dist/eval.js +36 -2
  88. package/dist/harness-test.d.ts +3 -63
  89. package/dist/hook-install.d.ts +12 -1
  90. package/dist/hook-install.js +12 -1
  91. package/dist/hook-runtime.js +4 -2
  92. package/dist/hook-state-store.js +3 -1
  93. package/dist/local-files-tracked.d.ts +17 -0
  94. package/dist/local-files-tracked.js +70 -0
  95. package/dist/local-files.d.ts +62 -0
  96. package/dist/local-files.js +183 -0
  97. package/dist/observe.d.ts +3 -2
  98. package/dist/observe.js +7 -6
  99. package/dist/plugin-loader.d.ts +1 -1
  100. package/dist/plugin-loader.js +43 -36
  101. package/dist/scan-behavioral.d.ts +34 -25
  102. package/dist/scan-behavioral.js +122 -58
  103. package/dist/scan-core.js +37 -18
  104. package/dist/scan-files.d.ts +1 -1
  105. package/dist/scan-files.js +53 -33
  106. package/dist/scan-trigger-suggest.d.ts +0 -21
  107. package/dist/scan-trigger-suggest.js +0 -23
  108. package/dist/scan.d.ts +4 -4
  109. package/dist/scan.js +120 -84
  110. package/dist/skill-harness.d.ts +21 -5
  111. package/dist/skill-harness.js +29 -11
  112. package/dist/surface-discovery-fs.d.ts +2 -0
  113. package/dist/surface-discovery-fs.js +108 -6
  114. package/dist/test-coverage-files.js +24 -17
  115. package/dist/test-coverage.d.ts +9 -3
  116. package/dist/test-coverage.js +32 -22
  117. package/dist/verify-plugin-guards.js +1 -1
  118. package/package.json +1 -1
  119. package/dist/skill-reachability.d.ts +0 -68
  120. package/dist/skill-reachability.js +0 -205
  121. /package/dist/{dialect-drift.d.ts → adapters/claude-code/dialect-drift.d.ts} +0 -0
  122. /package/dist/{dialect-drift.js → adapters/claude-code/dialect-drift.js} +0 -0
@@ -27,7 +27,7 @@
27
27
  * not be re-read); pair with a behavioral/judged check for certainty.
28
28
  */
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.codexEvalDriver = exports.CODEX_TRIGGER_RATE_EXPERIMENTAL = void 0;
30
+ exports.codexLiveDriver = exports.codexEvalDriver = exports.CODEX_TRIGGER_RATE_EXPERIMENTAL = void 0;
31
31
  exports.parseCodexEvalRun = parseCodexEvalRun;
32
32
  exports.codexRunError = codexRunError;
33
33
  exports.codexSkillFired = codexSkillFired;
@@ -244,6 +244,53 @@ exports.codexEvalDriver = {
244
244
  // Codex-only: the trigger-rate number is not validated (see the constant above).
245
245
  experimental: exports.CODEX_TRIGGER_RATE_EXPERIMENTAL,
246
246
  };
247
+ /**
248
+ * The Codex {@link HarnessLiveDriver} — the EXECUTING tiers' side of the
249
+ * adapter, reached through `codexAdapter.liveDriver()`.
250
+ *
251
+ * It is the object `scan-behavioral.ts:buildProbe` used to build from
252
+ * `harness === "codex"`: the same four answers, now carried by the adapter that
253
+ * knows them instead of switched on by a name in the application layer.
254
+ */
255
+ exports.codexLiveDriver = {
256
+ evalDriver: exports.codexEvalDriver,
257
+ // 🔴 ANSWERED WITHOUT TOUCHING THE MACHINE, and that is a CONSTRAINT of the
258
+ // caller rather than a property of this harness. `access` is read on the
259
+ // AUDIT path, before `decideExecute` and `resolveExecution` have established
260
+ // consent — including `--json`, `--no-interactive` and a remembered "no" —
261
+ // and that path promises to execute nothing. A first draft probed the binary
262
+ // with `codexDriver.available()`, which spawns `codex --version`; harmless in
263
+ // itself, and still a process this run had no permission to start.
264
+ //
265
+ // Claude Code's `access` reads env only, so with this one the guarantee stops
266
+ // being a property of whichever adapter happens to be driving and becomes
267
+ // structural: NO adapter executes anything to answer it.
268
+ //
269
+ // What that costs, stated rather than hidden: a machine with no codex binary
270
+ // is told the tier is reachable and finds out at RUN time instead, where the
271
+ // probe self-reports unavailable and the tier reports a miss. That is exactly
272
+ // what shipped before the port existed (`adapter.name === "codex"` was true
273
+ // with no probe at all), so this is not a regression — it is the old answer
274
+ // with the reason written down. The remedy string that used to ride on
275
+ // `{ kind: "none", fix }` is deleted rather than parked: it had no reader
276
+ // left, and an exported constant nothing prints is a claim, not a feature.
277
+ //
278
+ // "Subscription" rather than "metered": the codex CLI carries its own auth
279
+ // (a ChatGPT plan or an API key) and nothing outside a run can tell which,
280
+ // so the CLI words this harness $0 metered, as it always has.
281
+ access: () => ({ kind: "subscription" }),
282
+ // NO skill-selection event exists here, so firing is INFERRED from the
283
+ // SKILL.md read — wrong in both directions (see the constant above). The
284
+ // caveat travels with the signal so a report can never print the number bare,
285
+ // and the selection-collision matrix refuses this driver at the type level.
286
+ firing: { kind: "inferred", caveat: exports.CODEX_TRIGGER_RATE_EXPERIMENTAL },
287
+ // No namespace: firing is the SKILL.md read, which carries the bare name.
288
+ firedFor: (skill) => (t) => codexSkillFired(t, skill),
289
+ // A MEASURED LIMITATION, not a capability: stubbing a Claude-shaped plugin
290
+ // for Codex is unvalidated, so the real skills are installed and firing is
291
+ // detected regardless of body.
292
+ installsStubs: false,
293
+ };
247
294
  /**
248
295
  * Spawn real `codex exec --json` for the eval tier (real model, the user's codex
249
296
  * auth — NOT the mock). CONFIRMED flags (codex 0.139.0): `--json` for the event
@@ -3,7 +3,8 @@
3
3
  * Finding: it is essentially IDENTICAL to Claude Code's (exit 2 / `decision:block`
4
4
  * / `permissionDecision:deny`) — the thin `HookProtocol` port was the right call.
5
5
  * The genuine deltas are the env vars a hook receives + the TOML config format
6
- * (the latter lives in PluginLayout.settingsFormat, not here).
6
+ * (the ENCODING lives in PluginLayout.settings, a codec; the entry SHAPE is
7
+ * `registration`/`mergeRegistrations` below).
7
8
  *
8
9
  * Context injection (`hookSpecificOutput.additionalContext`) is ALSO shared — same
9
10
  * shape, confirmed against the official Codex hooks docs
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.codexHookProtocol = void 0;
4
+ const hook_install_js_1 = require("../../hook-install.js");
4
5
  exports.codexHookProtocol = {
5
6
  name: "codex",
6
7
  blockExitCode: 2,
@@ -27,5 +28,14 @@ exports.codexHookProtocol = {
27
28
  "permission_mode",
28
29
  "PLUGIN_ROOT",
29
30
  ],
31
+ // Codex is FLAT: `[[hooks.<event>]]` carries one `{matcher?, command}` per
32
+ // entry. Same fact `toTomlEntries` encodes on the merge side.
33
+ registration(on, matcher, command) {
34
+ const entry = matcher === undefined ? { command } : { matcher, command };
35
+ return { hooks: { [on]: [entry] } };
36
+ },
37
+ mergeRegistrations(existing, compiled, managedBy) {
38
+ return (0, hook_install_js_1.mergeHooksToml)(existing, compiled, managedBy);
39
+ },
30
40
  };
31
41
  //# sourceMappingURL=hook-protocol.js.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Codex's instruction chain — the one file a repo-root session loads, and why
3
+ * every other `AGENTS.md`-shaped file in the map does not.
4
+ *
5
+ * Verbatim from the vendor page (`https://developers.openai.com/codex/guides/agents-md`,
6
+ * fetched 2026-09-21 and recorded in zernie/vigiles#262): starting at the
7
+ * project root Codex walks DOWN to the current working directory, taking **at
8
+ * most one file per directory** along that path, checking `AGENTS.override.md`
9
+ * first, then `AGENTS.md`, then fallback names the repo itself declares via
10
+ * `project_doc_fallback_filenames` in `config.toml`. The files are concatenated
11
+ * root-down and truncated at a byte cap on file boundaries.
12
+ *
13
+ * 🔴 SO `"**\/AGENTS.md"` WAS NOT MERELY UNBOUNDED, IT WAS WRONG. At a repo-root
14
+ * session the chain is the ROOT DIRECTORY ALONE — cwd and root are the same
15
+ * directory, so there is no path to walk down. Summing every nested `AGENTS.md`
16
+ * added up files that never load together: a monorepo with twelve package-level
17
+ * files was told it was 12× over a budget no session ever approaches. Codex
18
+ * TRUNCATES silently over its budget, so this number is the only warning a user
19
+ * gets, and it was crying wolf on the harness where wolf means "your rules do
20
+ * not exist".
21
+ *
22
+ * And `AGENTS.override.md` is the mirror image of Claude Code's local file: it
23
+ * REPLACES the committed file rather than being appended after it. That is the
24
+ * difference the `replaced` reason exists to carry — the combination rule is the
25
+ * harness's, the fact that a file was hidden is reported to the core.
26
+ */
27
+ import type { InstructionChain } from "../../core/instruction-chain.js";
28
+ /** `AGENTS.md` → `AGENTS.override.md`: the per-machine file that REPLACES it. */
29
+ export declare function overrideSiblingOf(instructionFile: string): string;
30
+ /** Inputs the layout supplies so this module names no path of its own. */
31
+ export interface CodexChainInput {
32
+ /** `AGENTS.md`. */
33
+ readonly instructionFile: string;
34
+ /** The settings files, in precedence order (repo, then the local sibling). */
35
+ readonly settingsPaths: readonly string[];
36
+ /** The layout's own codec — this module does not know the encoding. */
37
+ readonly parseSettings: (text: string) => Record<string, unknown>;
38
+ }
39
+ export declare function codexInstructionChain(files: Readonly<Record<string, string>>, input: CodexChainInput): InstructionChain;
40
+ //# sourceMappingURL=instruction-chain.d.ts.map
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.overrideSiblingOf = overrideSiblingOf;
4
+ exports.codexInstructionChain = codexInstructionChain;
5
+ const instruction_chain_js_1 = require("../../core/instruction-chain.js");
6
+ /** The `config.toml` key by which a repo declares its own instruction names. */
7
+ const FALLBACK_NAMES_KEY = "project_doc_fallback_filenames";
8
+ /** `AGENTS.md` → `AGENTS.override.md`: the per-machine file that REPLACES it. */
9
+ function overrideSiblingOf(instructionFile) {
10
+ return (0, instruction_chain_js_1.siblingNamed)(instructionFile, "override");
11
+ }
12
+ /** `project_doc_fallback_filenames`, in the order the repo declared them. */
13
+ function fallbackNames(files, input) {
14
+ const out = [];
15
+ for (const path of input.settingsPaths) {
16
+ const text = files[path];
17
+ if (text === undefined)
18
+ continue;
19
+ let value;
20
+ try {
21
+ value = input.parseSettings(text);
22
+ }
23
+ catch {
24
+ // A config mid-merge must not decide which instructions load; falling
25
+ // back to "the documented names only" is the conservative reading.
26
+ continue;
27
+ }
28
+ const raw = value[FALLBACK_NAMES_KEY];
29
+ if (!Array.isArray(raw))
30
+ continue;
31
+ for (const name of raw) {
32
+ if (typeof name === "string" && name !== "" && !out.includes(name)) {
33
+ out.push(name);
34
+ }
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+ function codexInstructionChain(files, input) {
40
+ const override = overrideSiblingOf(input.instructionFile);
41
+ // The root directory's ONE slot, in the vendor's precedence order.
42
+ //
43
+ // 🔴 THE OVERRIDE IS A REPOSITORY FILE, NOT A PER-MACHINE ONE. It used to be
44
+ // `scope: "local"` on the reasoning "like Claude Code's, it is a per-machine
45
+ // file" — an analogy, and the vendor does not support it. Codex's guide:
46
+ // "In each directory along the path, it checks for `AGENTS.override.md`,
47
+ // then `AGENTS.md`". The one override it calls temporary is the GLOBAL
48
+ // `~/.codex/AGENTS.override.md`; the guide never mentions `.gitignore`.
49
+ // Measured on the browser engine, where every file is committed by
50
+ // construction: a repository holding both files published `committed 0`,
51
+ // while Codex loads the override's bytes. Same class as the invented
52
+ // `.codex/config.local.toml` earlier in this PR — a sibling modelled by
53
+ // analogy instead of by observation.
54
+ const candidates = [
55
+ { path: override, role: "root", scope: "repo" },
56
+ { path: input.instructionFile, role: "root", scope: "repo" },
57
+ ...fallbackNames(files, input).map((name) => ({
58
+ path: name,
59
+ role: "fallback",
60
+ scope: "repo",
61
+ })),
62
+ ];
63
+ const loaded = [];
64
+ const unloaded = [];
65
+ for (const candidate of candidates) {
66
+ if (files[candidate.path] === undefined)
67
+ continue;
68
+ const winner = loaded[0];
69
+ if (winner === undefined) {
70
+ loaded.push(candidate);
71
+ continue;
72
+ }
73
+ // AT MOST ONE FILE PER DIRECTORY. Everything after the winner is present
74
+ // and does not load, and says by whom it was replaced — which is the whole
75
+ // content of the field #262 called `shadows`.
76
+ unloaded.push({
77
+ ...candidate,
78
+ reason: { kind: "replaced", by: winner.path },
79
+ });
80
+ }
81
+ // A nested `AGENTS.md` is NOT loaded at a repo-root session: the walk goes
82
+ // root→cwd, and at the root those are the same directory. The domain's bound
83
+ // never enumerates one, so this branch is reached only by a caller holding a
84
+ // wider map — and then the honest answer is "only when the session runs in
85
+ // that directory", not "always".
86
+ const named = new Set([...loaded, ...unloaded].map((e) => e.path));
87
+ const leaves = new Set([input.instructionFile, override]);
88
+ for (const path of Object.keys(files).sort()) {
89
+ if (named.has(path) || !path.includes("/"))
90
+ continue;
91
+ if (!leaves.has(path.slice(path.lastIndexOf("/") + 1)))
92
+ continue;
93
+ unloaded.push({
94
+ path,
95
+ role: "root",
96
+ scope: "repo",
97
+ reason: { kind: "on-demand", when: "subdirectory" },
98
+ });
99
+ }
100
+ // No imports and no patterns: Codex's project doc has no include mechanism —
101
+ // an empty array here is a STATEMENT, not a stub, and the property tests hold
102
+ // over it the same way they hold over a populated one.
103
+ return { loaded, unloaded, imports: [], patterns: [], redirects: [] };
104
+ }
105
+ //# sourceMappingURL=instruction-chain.js.map
@@ -52,7 +52,7 @@
52
52
  * - MCP detection (`mcpConfigFile`/`mcpManifestKey`) is JSON-shaped, so it won't
53
53
  * see Codex's `[mcp_servers]` TOML table — a known layout-port gap.
54
54
  * 🔴 THE PATHS BELOW ARE DOCUMENTED IN `docs/configuration.md`. Change any of
55
- * them — `instructionFile`, `surfaceDirs`, `userSurfaceRoot`, `rulesDir` — and
55
+ * them — `instructionFile`, `surfaces`, `userSurfaceRoot`, `rulesDir` — and
56
56
  * that page is wrong until you edit it too. The page marks this symbol with
57
57
  * `vigiles:symbol`, so RENAMING it turns `vigiles lint` red and forces the
58
58
  * edit; changing a VALUE in place does not, and nothing today catches that.
@@ -1,30 +1,57 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.codexLayout = void 0;
4
+ const instruction_chain_js_1 = require("../../core/instruction-chain.js");
5
+ const settings_codec_js_1 = require("../../core/settings-codec.js");
6
+ const instruction_chain_js_2 = require("./instruction-chain.js");
4
7
  exports.codexLayout = {
5
8
  name: "codex",
6
9
  manifestPath: ".codex/config.toml",
7
10
  hooksConventionPath: ".codex/hooks.json",
8
11
  settingsPath: ".codex/config.toml",
9
- settingsFormat: "toml",
12
+ settings: settings_codec_js_1.tomlSettingsCodec,
10
13
  instructionFile: "AGENTS.md",
11
- // Surfaces carry their OWN prefix and `materializeRoot` is "" — the OpenCode
12
- // style, not the Claude Code one. Codex's skills and its prompts do NOT share a
13
- // parent (`.agents/` vs `.codex/`), so no single `materializeRoot` can name
14
+ // Surfaces carry their OWN prefix and the materialize prefix is "" — the
15
+ // OpenCode style, not the Claude Code one. Codex's skills and its prompts do
16
+ // NOT share a parent (`.agents/` vs `.codex/`), so no single root can name
14
17
  // both; spelling each dir in full is the only shape that keeps the reported key
15
18
  // equal to the real on-disk path.
16
- surfaceDirs: [".agents/skills", "prompts"],
17
- skillDir: ".agents/skills",
18
- agentDir: "", // Codex `[agents]` is a TOML concurrency table, not a subagent dir
19
- // Custom prompts are documented ONLY at `~/.codex/prompts` (user-global,
20
- // top-level `.md`, and marked deprecated in favour of skills). No repo-level
21
- // location is documented, so this prototype's root-level `prompts/` is left as
22
- // it was rather than moved on a guess.
23
- commandDir: "prompts",
24
- materializeRoot: "",
19
+ surfaces: {
20
+ skill: ".agents/skills",
21
+ // No `agent` key: Codex `[agents]` is a TOML concurrency table, not a
22
+ // subagent dir. An ABSENT key is the only spelling of "this harness has no
23
+ // such surface" — it used to be `agentDir: ""`, a second spelling that every
24
+ // reader had to remember to test for.
25
+ //
26
+ // Custom prompts are documented ONLY at `~/.codex/prompts` (user-global,
27
+ // top-level `.md`, and marked deprecated in favour of skills). No repo-level
28
+ // location is documented, so this prototype's root-level `prompts/` is left
29
+ // as it was rather than moved on a guess.
30
+ command: "prompts",
31
+ },
32
+ // No `userSurfaceRoot`: the surfaces carry their own prefix, so the
33
+ // materialize prefix is "" and a file-map key equals the on-disk path. That
34
+ // used to be spelled twice, as `materializeRoot: ""` beside an absent
35
+ // `userSurfaceRoot`.
36
+ //
37
+ // ⚠️ `hookScriptsDir: "hooks"` carries over the value the hand-written
38
+ // `intraRefDirs` had. NOTHING in the vendor pages confirms a repo-level
39
+ // `hooks/` directory for Codex; it is kept as it was rather than "aligned"
40
+ // from a guess, the same stance `installCodexSkills` takes above.
41
+ hookScriptsDir: "hooks",
25
42
  pluginRootToken: "${PLUGIN_ROOT}",
26
43
  mcpConfigFile: ".mcp.json",
27
44
  mcpManifestKey: "mcp_servers",
28
- intraRefDirs: [".agents/skills", "prompts", "hooks"],
45
+ // The root directory's ONE slot — override, then the committed file, then the
46
+ // names the repo itself declares in `config.toml`. See ./instruction-chain.ts
47
+ // for the vendor wording and for why `"**/AGENTS.md"` was not merely unbounded
48
+ // but wrong about what a root session loads.
49
+ instructionChain(files) {
50
+ return (0, instruction_chain_js_2.codexInstructionChain)(files, {
51
+ instructionFile: exports.codexLayout.instructionFile,
52
+ settingsPaths: (0, instruction_chain_js_1.settingsSourcePaths)(exports.codexLayout),
53
+ parseSettings: (text) => exports.codexLayout.settings.parse(text),
54
+ });
55
+ },
29
56
  };
30
57
  //# sourceMappingURL=layout.js.map
@@ -1,3 +1,34 @@
1
- import type { HarnessAdapter } from "../../core/adapter.js";
2
- export declare const opencodeAdapter: HarnessAdapter;
1
+ /**
2
+ * opencodeAdapter — EXPERIMENTAL, internal-only prototype `HarnessAdapter`. It
3
+ * exists to PROVE the kit generalizes to the optional-transport-port shape: a
4
+ * harness that does pillar 1 AND is mockable (openai-compatible) BUT whose hooks
5
+ * are in-process JS/TS plugin modules, not shell processes. So it declares
6
+ * `shellHooks: false`, ships NO `hookProtocol`, and the conformance kit must
7
+ * accept it without demanding a fake one. This is the pillar-1-only,
8
+ * no-shell-hooks shape the capability gating was built for.
9
+ *
10
+ * 🔴 AND IT IS THE PORT'S THIRD IMPLEMENTATION, which is why two of the port's
11
+ * illegal states were live HERE and nowhere else: a port validated against two
12
+ * implementations cannot see a state only the third can reach. The other one
13
+ * was in its layout (a skill dir outside `surfaceDirs`).
14
+ *
15
+ * It is deliberately NOT registered in `src/adapter-registry.ts` and NOT exported
16
+ * from any `vigiles/*` subpath, so the CLI never auto-detects OpenCode and
17
+ * consumers can't import it. Promote it (register + `vigiles/opencode` export +
18
+ * the deferred transport renderers) only when OpenCode support actually ships.
19
+ */
20
+ import type { DetectSignal } from "../../core/adapter.js";
21
+ export declare const opencodeAdapter: {
22
+ readonly name: "opencode";
23
+ readonly harnessTesting: false;
24
+ readonly shellHooks: false;
25
+ readonly subagents: true;
26
+ readonly dialect: import("../claude-code/dialect.js").HarnessDialect;
27
+ readonly layout: import("../../adapter.js").PluginLayout;
28
+ readonly claims: (path: string) => boolean;
29
+ readonly detect: (exists: (repoRelative: string) => boolean) => DetectSignal;
30
+ /** Nothing to say about an opencode install — see the Codex adapter for why
31
+ * `[]` is the answer rather than an absent capability. */
32
+ readonly advisories: () => readonly string[];
33
+ };
3
34
  //# sourceMappingURL=adapter.d.ts.map
@@ -1,55 +1,55 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.opencodeAdapter = void 0;
4
- /**
5
- * opencodeAdapter — EXPERIMENTAL, internal-only prototype `HarnessAdapter`. It
6
- * exists to PROVE the kit generalizes to the optional-transport-port shape: a
7
- * harness that does pillar 1 AND is mockable (openai-compatible) BUT whose hooks
8
- * are in-process JS/TS plugin modules, not shell processes. So it declares
9
- * `shellHooks: false`, ships NO `hookProtocol`, and the conformance kit must
10
- * accept it without demanding a fake one. This is the pillar-1-+-mockable-but-
11
- * no-shell-hooks shape the capability gating was built for.
12
- *
13
- * It is deliberately NOT registered in `src/adapter-registry.ts` and NOT exported
14
- * from any `vigiles/*` subpath, so the CLI never auto-detects OpenCode and
15
- * consumers can't import it. Promote it (register + `vigiles/opencode` export +
16
- * the deferred transport renderers) only when OpenCode support actually ships.
17
- */
18
- const node_fs_1 = require("node:fs");
19
- const node_path_1 = require("node:path");
20
4
  const dialect_js_1 = require("./dialect.js");
21
5
  const layout_js_1 = require("./layout.js");
22
6
  const surface_discovery_js_1 = require("../../core/surface-discovery.js");
23
- const runtime_js_1 = require("./runtime.js");
24
- const model_mock_js_1 = require("./model-mock.js");
7
+ // `opencodeRuntime` / `opencodeModelMock` are NOT imported: with
8
+ // `harnessTesting: false` the type gives those fields `?: never`, so carrying
9
+ // them would be a compile error. The modules stay on disk for the commit that
10
+ // adds a driver and flips the flag back.
25
11
  exports.opencodeAdapter = {
26
12
  name: "opencode",
27
- // Pillar 1 + mockable (openai-chat SSE), but hooks are in-process JS/TS plugin
28
- // modules — no shell-hook tier, hence shellHooks:false and NO hookProtocol.
29
- capabilities: {
30
- referenceVerification: true,
31
- harnessTesting: true,
32
- shellHooks: false,
33
- subagents: true,
34
- },
13
+ // 🔴 `harnessTesting: false`, AND IT USED TO SAY `true`. That declaration was
14
+ // the port's second live illegal state: the flag was set, `runtime` and
15
+ // `modelMock` were present, and there was NO `harnessTestDriver` behind them
16
+ // — so `runHarnessTest` threw "declares harnessTesting but carries no
17
+ // harnessTestDriver" at run time, and the conformance kit missed it because
18
+ // it checked the two ports it knew about and not the thunk.
19
+ //
20
+ // The declaration was also simply untrue, and the repo said so elsewhere:
21
+ // `docs/harnesses.md` records OpenCode's mockable tier as "declared but not
22
+ // yet built". So this is not a downgrade — it is the flag catching up with
23
+ // the tier, and `runtime`/`modelMock` come off with it because the `false`
24
+ // arm of `TestingPorts` types them `?: never`. Set it back to `true` in the
25
+ // same commit that adds a driver; the type will refuse anything else.
26
+ harnessTesting: false,
27
+ // Hooks are in-process JS/TS plugin modules — no shell-hook tier, hence
28
+ // shellHooks:false and NO hookProtocol (the `false` arm types it `?: never`,
29
+ // so shipping one is now an error rather than dead weight).
30
+ shellHooks: false,
31
+ subagents: true,
35
32
  dialect: dialect_js_1.opencodeDialect,
36
33
  layout: layout_js_1.opencodeLayout,
37
- runtime: runtime_js_1.opencodeRuntime,
38
- modelMock: model_mock_js_1.opencodeModelMock,
39
- // No hookProtocol: OpenCode hooks are code modules, not shell processes.
40
34
  // Derived from the layout, never listed again here — see `claims` on
41
35
  // `HarnessAdapter` for why this method takes a PATH and not a root.
42
36
  claims(path) {
43
37
  return (0, surface_discovery_js_1.layoutClaims)(layout_js_1.opencodeLayout, path);
44
38
  },
45
- detect(root) {
39
+ detect(exists) {
46
40
  // An `opencode.json` is a strong signal; a bare AGENTS.md is weak (many
47
- // harnesses read it). (Unused while unregistered — kept for symmetry.)
48
- if ((0, node_fs_1.existsSync)((0, node_path_1.join)(root, "opencode.json")))
49
- return 3;
50
- if ((0, node_fs_1.existsSync)((0, node_path_1.join)(root, layout_js_1.opencodeLayout.instructionFile)))
51
- return 1;
52
- return 0;
41
+ // harnesses read it). (Unused while unregistered — kept for symmetry, and
42
+ // so the property tests have a THIRD implementation to run against.)
43
+ if (exists(layout_js_1.opencodeLayout.manifestPath))
44
+ return { specificity: 3, via: "manifest" };
45
+ if (exists(layout_js_1.opencodeLayout.instructionFile))
46
+ return { specificity: 1, via: "instruction-file" };
47
+ return { specificity: 0, via: "instruction-file" };
48
+ },
49
+ /** Nothing to say about an opencode install — see the Codex adapter for why
50
+ * `[]` is the answer rather than an absent capability. */
51
+ advisories() {
52
+ return [];
53
53
  },
54
54
  };
55
55
  //# sourceMappingURL=adapter.js.map
@@ -30,7 +30,7 @@ exports.opencodeDialect = {
30
30
  instructionTargets: ["AGENTS.md", "CLAUDE.md"],
31
31
  pluginRootToken: "${OPENCODE_PLUGIN_ROOT}",
32
32
  // OpenCode reads the minimal cross-tool SKILL.md frontmatter (name +
33
- // description); the CC-only keys are not part of its format.
34
- skillFrontmatter: "minimal",
33
+ // description); the richer keys are not part of its format.
34
+ skillFrontmatterKeys: ["name", "description"],
35
35
  };
36
36
  //# sourceMappingURL=dialect.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * OpenCode's instruction chain — EXPERIMENTAL, like the rest of this adapter.
3
+ *
4
+ * It exists because it is the port's THIRD implementation, and the third one is
5
+ * where a shape has room to go wrong: both of the layout port's live illegal
6
+ * states were here, in the implementation the contract suite did not range over.
7
+ * For `instructionChain` it earns its keep a second way — it is the only
8
+ * implementation in this repo that populates {@link InstructionChain.patterns},
9
+ * so without it that half of the interface would be typed, documented and
10
+ * exercised by nothing.
11
+ *
12
+ * ⚠️ SOURCE OF THE `instructions` FACT, NAMED RATHER THAN IMPLIED. The key and
13
+ * its glob shape (`instructions: ["packages/*\/AGENTS.md"]`) are taken from
14
+ * `docs/design/port-redesign-round-2-2026-09-21.md` §1, which is where this
15
+ * adapter's other shapes come from too. It is NOT a vendor page this session
16
+ * fetched, and the adapter is unregistered and unexported, so nothing a user
17
+ * runs depends on it being exactly right. Do not promote it to a registered
18
+ * adapter without re-reading the vendor.
19
+ *
20
+ * What it does NOT do is expand those globs. A pattern is reported so the weight
21
+ * can say "plus N pattern(s) not weighed" instead of printing a number that is
22
+ * quietly missing them — the same stance the Codex chain takes towards a nested
23
+ * file, and the opposite of what the glob list this replaced did, which was to
24
+ * expand an adapter's pattern by walking the user's repository.
25
+ */
26
+ import type { InstructionChain } from "../../core/instruction-chain.js";
27
+ /** Inputs the layout supplies so this module names no path of its own. */
28
+ export interface OpencodeChainInput {
29
+ /** `AGENTS.md`. */
30
+ readonly instructionFile: string;
31
+ /** `opencode.json` — both the manifest and the settings file here. */
32
+ readonly settingsPaths: readonly string[];
33
+ /** The layout's own codec — this module does not know the encoding. */
34
+ readonly parseSettings: (text: string) => Record<string, unknown>;
35
+ }
36
+ export declare function opencodeInstructionChain(files: Readonly<Record<string, string>>, input: OpencodeChainInput): InstructionChain;
37
+ //# sourceMappingURL=instruction-chain.d.ts.map
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.opencodeInstructionChain = opencodeInstructionChain;
4
+ const instruction_chain_js_1 = require("../../core/instruction-chain.js");
5
+ /** The manifest key listing extra instruction files, as the design records it. */
6
+ const INSTRUCTIONS_KEY = "instructions";
7
+ /** A value the domain would have to WALK to expand, rather than look up. */
8
+ function isPattern(value) {
9
+ return /[*?[\]{}]/.test(value) || value.includes("://");
10
+ }
11
+ /** The `instructions` entries one settings file declares, in declared order. */
12
+ function declaredInstructions(files, input) {
13
+ const out = [];
14
+ for (const from of input.settingsPaths) {
15
+ const text = files[from];
16
+ if (text === undefined)
17
+ continue;
18
+ let value;
19
+ try {
20
+ value = input.parseSettings(text);
21
+ }
22
+ catch {
23
+ continue;
24
+ }
25
+ const raw = value[INSTRUCTIONS_KEY];
26
+ if (!Array.isArray(raw))
27
+ continue;
28
+ for (const entry of raw) {
29
+ if (typeof entry === "string" && entry !== "") {
30
+ // The token AS WRITTEN is the manifest string itself — OpenCode names
31
+ // a path directly rather than prefixing it, so token === path here.
32
+ out.push({ path: entry, token: entry, from });
33
+ }
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+ function opencodeInstructionChain(files, input) {
39
+ const loaded = [];
40
+ const imports = [];
41
+ const patterns = [];
42
+ if (files[input.instructionFile] !== undefined) {
43
+ loaded.push({ path: input.instructionFile, role: "root", scope: "repo" });
44
+ }
45
+ for (const entry of declaredInstructions(files, input)) {
46
+ if (isPattern(entry.path)) {
47
+ patterns.push({ pattern: entry.path, from: entry.from });
48
+ continue;
49
+ }
50
+ if (!(0, instruction_chain_js_1.isRepoRootedImport)(entry.path))
51
+ continue;
52
+ imports.push(entry);
53
+ if (files[entry.path] === undefined)
54
+ continue;
55
+ if (loaded.some((e) => e.path === entry.path))
56
+ continue;
57
+ loaded.push({
58
+ path: entry.path,
59
+ role: "import",
60
+ scope: "repo",
61
+ via: { from: entry.from, token: entry.token },
62
+ });
63
+ }
64
+ // No `unloaded`, and no `redirects`: nothing in OpenCode's documented shape
65
+ // hides one file behind another, and its extra instruction files are named in
66
+ // the MANIFEST rather than inside the root file, so the root file cannot be a
67
+ // pure pointer. Empty arrays are the statement, not a gap.
68
+ return { loaded, unloaded: [], imports, patterns, redirects: [] };
69
+ }
70
+ //# sourceMappingURL=instruction-chain.js.map
@@ -3,6 +3,25 @@
3
3
  * OpenCode: `opencode.json` manifest/settings, `AGENTS.md`, `.opencode/`
4
4
  * surfaces, `${OPENCODE_PLUGIN_ROOT}`. Validates the layout port against real
5
5
  * OpenCode shapes. NOT exported / NOT registered.
6
+ *
7
+ * 🔴 IT IS THE PORT'S THIRD IMPLEMENTATION, AND IT SHIPPED BROKEN — which is
8
+ * exactly the job a third implementation has. Until 2026-09-21 this descriptor
9
+ * read:
10
+ *
11
+ * surfaceDirs: [".opencode/agent", ".opencode/command"],
12
+ * skillDir: ".opencode/skill",
13
+ *
14
+ * Two fields naming the same set of directories, disagreeing. Everything that
15
+ * materializes, counts or scans a surface ranged over `surfaceDirs`, so
16
+ * OpenCode's skills were NAMED by the port and READ by nothing: a repo with
17
+ * skills under `.opencode/skill` graded as having none. Nothing caught it,
18
+ * because a port validated against two implementations cannot see a state only
19
+ * the third can reach.
20
+ *
21
+ * It cannot be written now: `surfaces` is one record keyed by kind, so there is
22
+ * no second field to disagree with, and `surfaceDirs()` is derived from it.
23
+ * Listing the skill dir here is therefore also a BEHAVIOUR change for this
24
+ * prototype — its skills become readable for the first time.
6
25
  */
7
26
  import type { PluginLayout } from "../../core/layout.js";
8
27
  export declare const opencodeLayout: PluginLayout;