@tiphys/kernel 0.0.0 → 0.2.0
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.
- package/AGENTS.md +663 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +299 -0
- package/checklists/clean-room.yaml +325 -0
- package/checklists/env-failure-diagnosis.yaml +68 -0
- package/checklists/flake-playbook.yaml +68 -0
- package/checklists/hazard-review.yaml +144 -0
- package/checklists/plan-review.yaml +103 -0
- package/dist/bin/tiphys.d.ts +2 -0
- package/dist/bin/tiphys.js +92 -0
- package/dist/src/adapters/load.d.ts +202 -0
- package/dist/src/adapters/load.js +440 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +66 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +1539 -0
- package/dist/src/checks.js +5025 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +68 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +343 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/cutover.d.ts +35 -0
- package/dist/src/commands/cutover.js +448 -0
- package/dist/src/commands/doctor.d.ts +264 -0
- package/dist/src/commands/doctor.js +1546 -0
- package/dist/src/commands/gates.d.ts +9 -0
- package/dist/src/commands/gates.js +360 -0
- package/dist/src/commands/init.d.ts +30 -0
- package/dist/src/commands/init.js +177 -0
- package/dist/src/commands/lock.d.ts +34 -0
- package/dist/src/commands/lock.js +340 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -0
- package/dist/src/commands/next.d.ts +130 -0
- package/dist/src/commands/next.js +597 -0
- package/dist/src/commands/plan.d.ts +20 -0
- package/dist/src/commands/plan.js +105 -0
- package/dist/src/commands/pool.d.ts +1 -0
- package/dist/src/commands/pool.js +139 -0
- package/dist/src/commands/resume.d.ts +1 -0
- package/dist/src/commands/resume.js +88 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +195 -0
- package/dist/src/commands/status.d.ts +21 -0
- package/dist/src/commands/status.js +122 -0
- package/dist/src/commands/sync.d.ts +47 -0
- package/dist/src/commands/sync.js +341 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +87 -0
- package/dist/src/commands/tuition.d.ts +21 -0
- package/dist/src/commands/tuition.js +218 -0
- package/dist/src/commands/validate.d.ts +78 -0
- package/dist/src/commands/validate.js +430 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/cutover.d.ts +584 -0
- package/dist/src/cutover.js +1444 -0
- package/dist/src/exclusion.d.ts +389 -0
- package/dist/src/exclusion.js +843 -0
- package/dist/src/exec/env.d.ts +278 -0
- package/dist/src/exec/env.js +334 -0
- package/dist/src/fleet.d.ts +223 -0
- package/dist/src/fleet.js +298 -0
- package/dist/src/gates/adapters/http-json.d.ts +5 -0
- package/dist/src/gates/adapters/http-json.js +283 -0
- package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
- package/dist/src/gates/adapters/migrations-command.js +373 -0
- package/dist/src/gates/citations.d.ts +408 -0
- package/dist/src/gates/citations.js +1169 -0
- package/dist/src/gates/coverage.d.ts +375 -0
- package/dist/src/gates/coverage.js +836 -0
- package/dist/src/gates/credentials.d.ts +233 -0
- package/dist/src/gates/credentials.js +752 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/gate-classes.d.ts +56 -0
- package/dist/src/gates/gate-classes.js +633 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -0
- package/dist/src/gates/merge-preconditions.d.ts +319 -0
- package/dist/src/gates/merge-preconditions.js +932 -0
- package/dist/src/gates/migrations.d.ts +1 -0
- package/dist/src/gates/migrations.js +36 -0
- package/dist/src/gates/pin.d.ts +114 -0
- package/dist/src/gates/pin.js +154 -0
- package/dist/src/gates/red-witness.d.ts +22 -0
- package/dist/src/gates/red-witness.js +482 -0
- package/dist/src/gates/release.d.ts +283 -0
- package/dist/src/gates/release.js +820 -0
- package/dist/src/gates/result.d.ts +116 -0
- package/dist/src/gates/result.js +91 -0
- package/dist/src/gates/run.d.ts +614 -0
- package/dist/src/gates/run.js +1614 -0
- package/dist/src/gates/schemas/citation-config.schema.json +59 -0
- package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
- package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
- package/dist/src/gates/schemas/gate-result.schema.json +160 -0
- package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
- package/dist/src/gates/schemas/release-record.schema.json +119 -0
- package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
- package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
- package/dist/src/gates/scope.d.ts +131 -0
- package/dist/src/gates/scope.js +1018 -0
- package/dist/src/gates/suite.d.ts +217 -0
- package/dist/src/gates/suite.js +968 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +84 -0
- package/dist/src/hooks.js +125 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.js +30 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +256 -0
- package/dist/src/lock.js +792 -0
- package/dist/src/model-resolution.d.ts +159 -0
- package/dist/src/model-resolution.js +307 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +34 -0
- package/dist/src/path-identity.js +48 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +326 -0
- package/dist/src/pool.js +988 -0
- package/dist/src/roles.d.ts +461 -0
- package/dist/src/roles.js +776 -0
- package/dist/src/spawn.d.ts +482 -0
- package/dist/src/spawn.js +1003 -0
- package/dist/src/status.d.ts +116 -0
- package/dist/src/status.js +148 -0
- package/dist/src/task.d.ts +504 -0
- package/dist/src/task.js +307 -0
- package/dist/src/teardown.d.ts +39 -0
- package/dist/src/teardown.js +422 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +263 -0
- package/dist/src/validate.js +742 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.js +38 -0
- package/dist/src/watcher.d.ts +275 -0
- package/dist/src/watcher.js +849 -0
- package/dist/src/witness/run.d.ts +299 -0
- package/dist/src/witness/run.js +1373 -0
- package/dist/src/witness/spec.d.ts +270 -0
- package/dist/src/witness/spec.js +475 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +526 -0
- package/gates.manifest.json +257 -0
- package/package.json +72 -3
- package/role-model-config.yaml +88 -0
- package/roles/README.md +128 -0
- package/roles/_shared-dispatch-contract.md +87 -0
- package/roles/adversarial-plan-reviewer.md +80 -0
- package/roles/clean-room-reviewer.md +140 -0
- package/roles/implementer.md +463 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +82 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +185 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/cutover-state.schema.json +64 -0
- package/schemas/decision-record.schema.json +88 -0
- package/schemas/executor-record.schema.json +36 -0
- package/schemas/final-report.schema.json +90 -0
- package/schemas/finding.schema.json +106 -0
- package/schemas/gate-registry.schema.json +260 -0
- package/schemas/mechanism-index.schema.json +94 -0
- package/schemas/model-resolution.schema.json +362 -0
- package/schemas/plan.schema.json +300 -0
- package/schemas/report.schema.json +579 -0
- package/schemas/role-brief.schema.json +105 -0
- package/schemas/role-model-config.schema.json +90 -0
- package/schemas/status-line.schema.json +40 -0
- package/schemas/tuition.schema.json +191 -0
- package/schemas/verdict.schema.json +295 -0
- package/schemas/work-history.schema.json +183 -0
- package/schemas/write-bypass.schema.json +69 -0
- package/templates/charter.example.yaml +54 -0
- package/templates/decision-record.example.yaml +27 -0
- package/templates/final-report.example.yaml +80 -0
- package/templates/plan.example.yaml +87 -0
- package/templates/report.example.yaml +236 -0
- package/templates/warnings.md +74 -0
- package/templates/work-history.example.yaml +185 -0
- package/tuition/README.md +76 -0
- package/tuition/T-001.yaml +48 -0
- package/tuition/T-002.yaml +51 -0
- package/tuition/T-003.yaml +100 -0
- package/tuition/T-004.yaml +52 -0
- package/tuition/T-005.yaml +72 -0
- package/tuition/T-006.yaml +81 -0
- package/tuition/T-007.yaml +56 -0
- package/tuition/T-008.yaml +111 -0
- package/tuition/T-009.yaml +50 -0
- package/tuition/T-015.yaml +36 -0
- package/tuition/T-016.yaml +36 -0
- package/tuition/T-017.yaml +46 -0
- package/tuition/T-018.yaml +84 -0
- package/tuition/T-021.yaml +40 -0
- package/tuition/T-022.yaml +36 -0
- package/tuition/mechanism-index.yaml +256 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { realpathSync } from "node:fs";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { readRegularFileIfPresent, singleLine } from "../task.js";
|
|
14
|
+
/**
|
|
15
|
+
* ADAPTER SELECTION AND LOADING (kernel plan M4, M4-P4; M4-D-03 at
|
|
16
|
+
* delivery/plan/m4-intake.md:729).
|
|
17
|
+
*
|
|
18
|
+
* THE RESOLUTION ROOT IS THE WHOLE SECURITY PROPERTY, and it is the reason
|
|
19
|
+
* this module exists rather than a one-line `await import(specifier)` at the
|
|
20
|
+
* spawn call site. Loading an adapter executes third-party code INSIDE the
|
|
21
|
+
* orchestrator process, the same process that holds delegated merge
|
|
22
|
+
* authority under DR-0012. DR-0029 Part 2c's untrusted-project-content
|
|
23
|
+
* boundary does not exist and M4-D-23 recommends it is not built in M4, so
|
|
24
|
+
* the only mitigation available is WHERE the code is resolved from:
|
|
25
|
+
*
|
|
26
|
+
* - the FLEET HOME is owner-controlled and already pins `@tiphys/kernel`
|
|
27
|
+
* exactly, which is what makes it a boundary at all;
|
|
28
|
+
* - the PROJECT CLONE is the thing under review and may contain anything
|
|
29
|
+
* a contributor pushed, including a `node_modules/` directory.
|
|
30
|
+
*
|
|
31
|
+
* So resolution is rooted at `<fleet home>/package.json` rather than at this
|
|
32
|
+
* module or at `process.cwd()`. A plain `await import(specifier)` resolves
|
|
33
|
+
* relative to THIS MODULE's URL, which is the kernel checkout, and
|
|
34
|
+
* `import.meta.resolve(specifier, parent)` silently ignores its second
|
|
35
|
+
* argument unless Node is started with `--experimental-import-meta-resolve`
|
|
36
|
+
* (measured on v26.6.0, 2026-09-17: the parent was ignored and the specifier
|
|
37
|
+
* resolved from the CALLER's file). Both of those are the shape this module
|
|
38
|
+
* refuses to be.
|
|
39
|
+
*
|
|
40
|
+
* THE ROOTING IS NOT BY ITSELF THE PROPERTY, AND THE SENTENCE THAT STOOD HERE
|
|
41
|
+
* SAID IT WAS (CR-B-004, re-raised as CR-F-CRED-002 and CH-002).
|
|
42
|
+
*
|
|
43
|
+
* Until this round this paragraph asserted, without qualification, that
|
|
44
|
+
* "NOTHING here ever consults the project clone". That is true of BARE
|
|
45
|
+
* specifiers, which is the only shape M4-P4 criterion 2's witness exercises:
|
|
46
|
+
* `createRequire` walks the fleet home and its PARENTS looking for
|
|
47
|
+
* `node_modules`, and the project clone sits BELOW the fleet home at
|
|
48
|
+
* `<fleet>/projects/<name>`, so no such walk reaches it. It was FALSE of the
|
|
49
|
+
* module. Measured at `ad2428b`, with a module in the project clone that drops
|
|
50
|
+
* a sentinel on import: a bare specifier did not evaluate it, an ABSOLUTE
|
|
51
|
+
* specifier did, and the fleet-relative `./projects/demo/evil.mjs` did,
|
|
52
|
+
* because a relative path does not walk up looking for `node_modules`, it
|
|
53
|
+
* walks DOWN the tree the rooting chose.
|
|
54
|
+
*
|
|
55
|
+
* The mechanism is a constraint that holds for one INPUT SHAPE, documented as
|
|
56
|
+
* holding for the module. Both halves are now checked against the RESOLVED
|
|
57
|
+
* REAL PATH, which is the only thing that describes where the code actually
|
|
58
|
+
* comes from, rather than against the specifier's spelling:
|
|
59
|
+
*
|
|
60
|
+
* - a resolved path inside `<fleet>/projects/` is refused WHATEVER the
|
|
61
|
+
* specifier's shape, because the project tree is the thing under review;
|
|
62
|
+
* - a PATH-SHAPED specifier (absolute, or beginning `./` or `../`) whose
|
|
63
|
+
* resolved path is outside the fleet home is refused, because an absolute
|
|
64
|
+
* specifier can point anywhere and a relative one can climb out with
|
|
65
|
+
* `../`. A BARE specifier is exempt from the second check only, because
|
|
66
|
+
* `createRequire`'s parent walk legitimately finds a hoisted
|
|
67
|
+
* `node_modules` above the fleet home, and refusing that would break an
|
|
68
|
+
* ordinary install rather than a hazard.
|
|
69
|
+
*
|
|
70
|
+
* `realpathSync` is used for both, so a symlink planted inside the fleet home
|
|
71
|
+
* pointing into the project clone is refused by the same check rather than by
|
|
72
|
+
* a second one.
|
|
73
|
+
*
|
|
74
|
+
* WHAT IS STILL NOT DEFENDED, said here so the next reader does not re-derive
|
|
75
|
+
* it: an operator who types an absolute path INTO the fleet home gets what
|
|
76
|
+
* they typed, and the fleet home is owner-controlled by assumption. This moves
|
|
77
|
+
* the boundary to the fleet home for every specifier shape; it does not defend
|
|
78
|
+
* inside it.
|
|
79
|
+
*
|
|
80
|
+
* WHAT THIS MODULE DOES NOT DEFEND. Once a specifier resolves inside the
|
|
81
|
+
* fleet home, its code runs. A legitimate fleet-home adapter that is later
|
|
82
|
+
* compromised is not covered by anything here, and no criterion of this
|
|
83
|
+
* phase claims otherwise: this phase MOVES the boundary to the fleet home,
|
|
84
|
+
* it does not defend inside it. That residue is M4-D-23's, stated rather
|
|
85
|
+
* than implied.
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* The built-in adapter's name, and the one name a loaded adapter may not
|
|
89
|
+
* claim (M4-P4 criterion 5).
|
|
90
|
+
*
|
|
91
|
+
* IT LIVES HERE RATHER THAN IN `src/spawn.ts` so that the runtime import
|
|
92
|
+
* edge runs one way only: `src/spawn.ts` imports this value, and this module
|
|
93
|
+
* imports nothing but a TYPE back. A value-level cycle between the launch
|
|
94
|
+
* module and the loader would evaluate one of them inside the other's
|
|
95
|
+
* temporal dead zone the first time an import order changed, which is a
|
|
96
|
+
* failure nothing in the suite would predict.
|
|
97
|
+
*
|
|
98
|
+
* The launch record's `adapter` field (src/spawn.ts:113) is the only thing
|
|
99
|
+
* that ever says what ran. A loaded adapter naming itself after the built-in
|
|
100
|
+
* one makes every later record ambiguous, and the ambiguity is unresolvable
|
|
101
|
+
* after the fact because the record is the only witness. This is the
|
|
102
|
+
* misattribution guard src/gates/schemas/release-record.schema.json:26
|
|
103
|
+
* already instantiates for release records, applied one layer down.
|
|
104
|
+
*/
|
|
105
|
+
export const BUILT_IN_ADAPTER_NAME = "subprocess";
|
|
106
|
+
/**
|
|
107
|
+
* The fleet-home `package.json` field that names a default adapter, as a
|
|
108
|
+
* JSON pointer written out in the one place that reads it.
|
|
109
|
+
*
|
|
110
|
+
* `tiphys.adapter` rather than a top-level key: the kernel's own
|
|
111
|
+
* `package.json` already namespaces its configuration under `tiphys`
|
|
112
|
+
* (package.json:52), so a fleet home carrying kernel configuration uses the
|
|
113
|
+
* same namespace rather than inventing a second convention.
|
|
114
|
+
*/
|
|
115
|
+
export const FLEET_ADAPTER_FIELD = "tiphys.adapter";
|
|
116
|
+
/**
|
|
117
|
+
* The specifier the fleet home declares as its default, if any.
|
|
118
|
+
*
|
|
119
|
+
* READ THROUGH `readRegularFileIfPresent`, never `readFileSync`: the fleet
|
|
120
|
+
* home is a directory this process does not own the contents of, and a FIFO
|
|
121
|
+
* at `package.json` would hang the spawn rather than refuse it. That is the
|
|
122
|
+
* hazard CR-520 records for `meta.json`, one path along.
|
|
123
|
+
*
|
|
124
|
+
* A fleet home with no `package.json`, no `tiphys` object, or no `adapter`
|
|
125
|
+
* key declares NOTHING, which is a different fact from declaring something
|
|
126
|
+
* unusable: the first yields `undefined` and the built-in default, the
|
|
127
|
+
* second is a refusal naming the field.
|
|
128
|
+
*/
|
|
129
|
+
export function fleetAdapterSpecifier(fleet) {
|
|
130
|
+
const read = readRegularFileIfPresent(fleet.packageJsonPath);
|
|
131
|
+
if (read.kind === "absent") {
|
|
132
|
+
return { ok: true, specifier: undefined };
|
|
133
|
+
}
|
|
134
|
+
if (read.kind === "refused") {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
reason: `the fleet home package.json could not be read to find a default ` +
|
|
138
|
+
`adapter (${read.reason})`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
let parsed;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(read.body);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: `the fleet home package.json ${fleet.packageJsonPath} does not parse as ` +
|
|
149
|
+
`JSON (${singleLine(error.message)}), so the ` +
|
|
150
|
+
`${FLEET_ADAPTER_FIELD} field cannot be read`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
reason: `the fleet home package.json ${fleet.packageJsonPath} does not parse as ` +
|
|
157
|
+
`an object, so the ${FLEET_ADAPTER_FIELD} field cannot be read`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const tiphys = parsed.tiphys;
|
|
161
|
+
if (tiphys === undefined) {
|
|
162
|
+
return { ok: true, specifier: undefined };
|
|
163
|
+
}
|
|
164
|
+
if (typeof tiphys !== "object" || tiphys === null || Array.isArray(tiphys)) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
reason: `the fleet home package.json ${fleet.packageJsonPath} declares tiphys as ` +
|
|
168
|
+
`${Array.isArray(tiphys) ? "an array" : typeof tiphys}, and ` +
|
|
169
|
+
`${FLEET_ADAPTER_FIELD} can only be read from an object`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const adapter = tiphys.adapter;
|
|
173
|
+
if (adapter === undefined) {
|
|
174
|
+
return { ok: true, specifier: undefined };
|
|
175
|
+
}
|
|
176
|
+
if (typeof adapter !== "string" || adapter.trim() === "") {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
reason: `the fleet home package.json ${fleet.packageJsonPath} declares ` +
|
|
180
|
+
`${FLEET_ADAPTER_FIELD} as ${typeof adapter === "string" ? "an empty string" : typeof adapter}, ` +
|
|
181
|
+
`and a default adapter must be a non-empty module specifier`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, specifier: adapter };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Resolve `specifier` with Node module resolution ROOTED AT THE FLEET HOME,
|
|
188
|
+
* then evaluate it and check that what came back is an adapter.
|
|
189
|
+
*
|
|
190
|
+
* `createRequire(<fleet home>/package.json)` is the rooting. Every lookup it
|
|
191
|
+
* performs starts at the fleet home and walks its PARENTS, which is ordinary
|
|
192
|
+
* Node resolution with a base this process chose; the project clone lives
|
|
193
|
+
* BELOW the fleet home (`<fleet>/projects/<name>`), so no walk from the
|
|
194
|
+
* fleet home can ever reach its `node_modules`. A relative specifier
|
|
195
|
+
* (`./adapters/mine.js`) is likewise resolved against the fleet home rather
|
|
196
|
+
* than against the current working directory.
|
|
197
|
+
*
|
|
198
|
+
* THE RESOLVED PATH IS THEN CHECKED FOR CONTAINMENT (CR-F-CRED-002, CH-002).
|
|
199
|
+
* `requireFromFleet.resolve` is where the rooting happens and it is NOT where
|
|
200
|
+
* the boundary is enforced, because a path-shaped specifier is not subject to
|
|
201
|
+
* the walk the rooting performs. `refuseResolvedAdapterPath` below is the
|
|
202
|
+
* enforcement and it runs BEFORE the `import`, so a refused module is never
|
|
203
|
+
* evaluated; see the module comment for the two rules and for what they do
|
|
204
|
+
* not cover.
|
|
205
|
+
*
|
|
206
|
+
* THE CONDITION SET IS `require`, AND THAT IS A REAL LIMITATION RATHER THAN
|
|
207
|
+
* AN OVERSIGHT. `require.resolve` applies an `exports` map under the
|
|
208
|
+
* `require` condition, so a package whose map offers ONLY an `import`
|
|
209
|
+
* condition fails to resolve here with ERR_PACKAGE_PATH_NOT_EXPORTED
|
|
210
|
+
* (measured, node v26.6.0, 2026-09-17). Resolution succeeding says nothing
|
|
211
|
+
* about the module SYSTEM: the resolved path is then imported as an ES
|
|
212
|
+
* module through a file URL, so an ESM adapter loads correctly as long as its
|
|
213
|
+
* `exports` map offers a `require` or `default` condition, or a bare string.
|
|
214
|
+
* The refusal below says so, because an adapter author reading
|
|
215
|
+
* "cannot be resolved" with no further detail would go looking in the wrong
|
|
216
|
+
* place. The alternative, a parameterised ESM resolver, does not exist in
|
|
217
|
+
* stable Node; see the module docs above for the measurement.
|
|
218
|
+
*/
|
|
219
|
+
/**
|
|
220
|
+
* Is `specifier` a PATH rather than a package name? Node's own rule, and it is
|
|
221
|
+
* spelled out rather than approximated: a specifier is relative when it begins
|
|
222
|
+
* `./` or `../` (or their platform separator forms), and absolute when
|
|
223
|
+
* `isAbsolute` says so. Everything else is bare and reaches the `node_modules`
|
|
224
|
+
* walk.
|
|
225
|
+
*/
|
|
226
|
+
function isPathShapedSpecifier(specifier) {
|
|
227
|
+
if (isAbsolute(specifier)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
for (const prefix of ["./", "../", `.${sep}`, `..${sep}`]) {
|
|
231
|
+
if (specifier.startsWith(prefix)) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Resolve a path to its real location, or return it unchanged when it cannot
|
|
239
|
+
* be resolved. An unresolvable path is NOT treated as safe: it is returned as
|
|
240
|
+
* it stands and the containment rules below judge it, so the failure mode of
|
|
241
|
+
* this helper is a refusal rather than a pass.
|
|
242
|
+
*/
|
|
243
|
+
function realPathOrItself(path) {
|
|
244
|
+
try {
|
|
245
|
+
return realpathSync(path);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return path;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Whether `path` is `root` itself or sits underneath it. */
|
|
252
|
+
function isInside(root, path) {
|
|
253
|
+
if (path === root) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
const rel = relative(root, path);
|
|
257
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* THE CONTAINMENT RULES, applied to the RESOLVED REAL PATH (CR-F-CRED-002,
|
|
261
|
+
* CH-002). Exported so a test can drive both rules directly without staging a
|
|
262
|
+
* module on disk for every member; `loadAdapter` is the only caller in `src/`.
|
|
263
|
+
*
|
|
264
|
+
* Returns a refusal reason, or undefined to allow. Both rules name the
|
|
265
|
+
* resolved path, because an operator reading the refusal has to be able to see
|
|
266
|
+
* WHERE the specifier landed, which is precisely the thing the specifier's own
|
|
267
|
+
* spelling hides.
|
|
268
|
+
*/
|
|
269
|
+
export function refuseResolvedAdapterPath(fleet, specifier, origin, resolved) {
|
|
270
|
+
const realResolved = realPathOrItself(resolved);
|
|
271
|
+
const realFleetRoot = realPathOrItself(fleet.root);
|
|
272
|
+
const realProjectsDir = realPathOrItself(fleet.projectsDir === "" ? join(realFleetRoot, "projects") : fleet.projectsDir);
|
|
273
|
+
if (isInside(realProjectsDir, realResolved)) {
|
|
274
|
+
return (`the adapter ${specifier} (${origin}) resolves to ${realResolved}, which is ` +
|
|
275
|
+
`inside the project tree ${realProjectsDir}; a project clone is the code ` +
|
|
276
|
+
`under review and loading an adapter runs it inside the orchestrator ` +
|
|
277
|
+
`process, so no specifier of any shape may resolve there`);
|
|
278
|
+
}
|
|
279
|
+
if (isPathShapedSpecifier(specifier) && !isInside(realFleetRoot, realResolved)) {
|
|
280
|
+
return (`the adapter ${specifier} (${origin}) is a path-shaped specifier resolving ` +
|
|
281
|
+
`to ${realResolved}, which is outside the fleet home ${realFleetRoot}; the ` +
|
|
282
|
+
`fleet home is the only resolution root the kernel trusts for adapters, and ` +
|
|
283
|
+
`a path specifier is not constrained by the rooting the way a package name is`);
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
export async function loadAdapter(fleet, specifier, origin) {
|
|
288
|
+
const requireFromFleet = createRequire(fleet.packageJsonPath);
|
|
289
|
+
let resolved;
|
|
290
|
+
try {
|
|
291
|
+
resolved = requireFromFleet.resolve(specifier);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
const code = error.code ?? "";
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
reason: `the adapter ${specifier} (${origin}) could not be resolved from the ` +
|
|
298
|
+
`fleet home ${fleet.root}, which is the ONLY resolution root the kernel ` +
|
|
299
|
+
`uses for adapters: install it in the fleet home, and note that an ` +
|
|
300
|
+
`exports map offering only an import condition does not resolve here ` +
|
|
301
|
+
`(${code === "" ? singleLine(String(error)) : code})`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const containmentRefusal = refuseResolvedAdapterPath(fleet, specifier, origin, resolved);
|
|
305
|
+
if (containmentRefusal !== undefined) {
|
|
306
|
+
return { ok: false, reason: containmentRefusal };
|
|
307
|
+
}
|
|
308
|
+
let module;
|
|
309
|
+
try {
|
|
310
|
+
module = await import(__rewriteRelativeImportExtension(pathToFileURL(resolved).href));
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
reason: `the adapter ${specifier} (${origin}) resolved to ${resolved} but could ` +
|
|
316
|
+
`not be evaluated: ${singleLine(String(error))}`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return checkAdapterShape(module, specifier, origin, resolved);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* IS THIS AN ADAPTER (M4-P4 criterion 4)?
|
|
323
|
+
*
|
|
324
|
+
* The three shapes the criterion names are three ways a module can be
|
|
325
|
+
* IMPORTABLE and not be an adapter, and each of them is silent at the import
|
|
326
|
+
* itself: a module with no default export, a default export with no `launch`,
|
|
327
|
+
* and a `launch` that is not callable. Left unchecked, the first two surface
|
|
328
|
+
* as `TypeError: adapter.launch is not a function` thrown out of `spawnTask`
|
|
329
|
+
* AFTER pool create has made a worktree, a branch and a pool record, which is
|
|
330
|
+
* the same "refusal that arrives as a crash, after creation" shape M4-P3's
|
|
331
|
+
* requirement checks exist to prevent (src/spawn.ts's checkAdapterRequirements).
|
|
332
|
+
*
|
|
333
|
+
* A FOURTH CHECK IS HERE THAT THE CRITERION DOES NOT NAME, and it is declared
|
|
334
|
+
* rather than smuggled: the adapter's `name` must be a non-empty string.
|
|
335
|
+
* Criterion 5 refuses one particular name, and `undefined` written into the
|
|
336
|
+
* launch record's `adapter` field would defeat the whole point of that
|
|
337
|
+
* criterion by making the record say nothing at all. A guard on the value of
|
|
338
|
+
* a field that may be absent is a guard that cannot go red for the absent
|
|
339
|
+
* case.
|
|
340
|
+
*
|
|
341
|
+
* Every refusal names the SPECIFIER and the member, because an operator
|
|
342
|
+
* reading it has a module to go and fix and needs to know which one.
|
|
343
|
+
*/
|
|
344
|
+
export function checkAdapterShape(module, specifier, origin, resolved) {
|
|
345
|
+
const where = `the adapter ${specifier} (${origin}), resolved to ${resolved},`;
|
|
346
|
+
if (typeof module !== "object" || module === null) {
|
|
347
|
+
return { ok: false, reason: `${where} did not evaluate to a module object` };
|
|
348
|
+
}
|
|
349
|
+
if (!Object.hasOwn(module, "default")) {
|
|
350
|
+
return {
|
|
351
|
+
ok: false,
|
|
352
|
+
reason: `${where} has no default export; an adapter module exports its ` +
|
|
353
|
+
`ExecutorAdapter as the default export`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const candidate = module.default;
|
|
357
|
+
if (typeof candidate !== "object" || candidate === null) {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
reason: `${where} exports a default that is ${candidate === null ? "null" : typeof candidate}, ` +
|
|
361
|
+
`not an object with a launch member`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const launch = candidate.launch;
|
|
365
|
+
if (launch === undefined) {
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
reason: `${where} exports a default with no launch member; ExecutorAdapter ` +
|
|
369
|
+
`requires launch(request)`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (typeof launch !== "function") {
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
reason: `${where} exports a default whose launch member is ${typeof launch}, ` +
|
|
376
|
+
`not a function; ExecutorAdapter requires launch(request)`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const name = candidate.name;
|
|
380
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
reason: `${where} exports a default with no usable name member; the launch ` +
|
|
384
|
+
`record's adapter field is the only thing that says what ran, so an ` +
|
|
385
|
+
`adapter must name itself with a non-empty string`,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
if (name === BUILT_IN_ADAPTER_NAME) {
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
reason: `${where} names itself ${BUILT_IN_ADAPTER_NAME}, which is the built-in ` +
|
|
392
|
+
`adapter's name; a loaded adapter may not claim it, because the launch ` +
|
|
393
|
+
`record's adapter field would then no longer say which one ran`,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return { ok: true, adapter: candidate };
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* WHICH ADAPTER THIS SPAWN USES, AND WHY (M4-P4 criteria 1 and 6).
|
|
400
|
+
*
|
|
401
|
+
* WHICH SOURCE WINS, and it is explicit in the record rather than silent:
|
|
402
|
+
* the `--adapter` FLAG outranks the fleet-home default, and the fleet-home
|
|
403
|
+
* default outranks the built-in one.
|
|
404
|
+
*
|
|
405
|
+
* THE WORDING IS DELIBERATE AND IS NOT A STYLE CHOICE. The word this sentence
|
|
406
|
+
* would naturally use is claimed by four rows of
|
|
407
|
+
* delivery/plan/cutover/retirement-inventory.json, which assert that no kernel
|
|
408
|
+
* artifact states an authority order among charter, plan and decision records
|
|
409
|
+
* and enumerate BY NAME the `src/` files allowed to carry the token. A new
|
|
410
|
+
* file carrying it reddens `scripts/check-retirement-inventory.mjs`, correctly,
|
|
411
|
+
* because the checker cannot tell a benign use from a refuting one without a
|
|
412
|
+
* human reading it. This use is benign and incidental, so the token is not
|
|
413
|
+
* used here rather than the rows being widened from a phase that does not own
|
|
414
|
+
* that document. A flag is a per-spawn instruction and
|
|
415
|
+
* a fleet-home field is a standing one, so the narrower statement wins; the
|
|
416
|
+
* opposite order would make the flag unusable on any fleet that declared a
|
|
417
|
+
* default.
|
|
418
|
+
*
|
|
419
|
+
* `undefined` for the adapter means USE THE BUILT-IN ONE, and it is returned
|
|
420
|
+
* as `undefined` rather than as `subprocessAdapter` so that this module
|
|
421
|
+
* imports nothing but a type from `src/spawn.ts`. See BUILT_IN_ADAPTER_NAME
|
|
422
|
+
* above for why the import edge runs one way.
|
|
423
|
+
*/
|
|
424
|
+
export async function selectAdapter(fleet, flagSpecifier) {
|
|
425
|
+
if (flagSpecifier !== undefined) {
|
|
426
|
+
const origin = "--adapter";
|
|
427
|
+
const loaded = await loadAdapter(fleet, flagSpecifier, origin);
|
|
428
|
+
return loaded.ok ? { ok: true, adapter: loaded.adapter, origin } : loaded;
|
|
429
|
+
}
|
|
430
|
+
const declared = fleetAdapterSpecifier(fleet);
|
|
431
|
+
if (!declared.ok) {
|
|
432
|
+
return declared;
|
|
433
|
+
}
|
|
434
|
+
if (declared.specifier === undefined) {
|
|
435
|
+
return { ok: true, adapter: undefined, origin: "the built-in default" };
|
|
436
|
+
}
|
|
437
|
+
const origin = `the fleet home ${FLEET_ADAPTER_FIELD} field`;
|
|
438
|
+
const loaded = await loadAdapter(fleet, declared.specifier, origin);
|
|
439
|
+
return loaded.ok ? { ok: true, adapter: loaded.adapter, origin } : loaded;
|
|
440
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Fleet } from "./fleet.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Brief assembly (kernel plan v1, M1-P4 step 2; R-033b, R-083b).
|
|
4
|
+
*
|
|
5
|
+
* The brief for a task is written once, at spawn, to tasks/<id>/brief.md:
|
|
6
|
+
* the operator's brief file verbatim, followed by the fleet's
|
|
7
|
+
* environment-warnings file verbatim when the fleet has one. The
|
|
8
|
+
* warnings file is <fleet>/warnings.md (fleet root, tracked content, not
|
|
9
|
+
* the ignored state/ area). When no warnings file exists, brief.md is
|
|
10
|
+
* byte-identical to the brief file.
|
|
11
|
+
*
|
|
12
|
+
* Verbatim means verbatim: no heading, no banner, no rewriting. A brief
|
|
13
|
+
* is the durable instruction an implementer is judged against, and a
|
|
14
|
+
* kernel that edits it makes the audit trail lie. The only byte this
|
|
15
|
+
* module may add is a single newline between the two documents when the
|
|
16
|
+
* brief does not already end in one.
|
|
17
|
+
*/
|
|
18
|
+
/** The fleet's environment-warnings file, appended to every brief. */
|
|
19
|
+
export declare const WARNINGS_FILE = "warnings.md";
|
|
20
|
+
export declare function warningsPath(fleet: Fleet): string;
|
|
21
|
+
export type BriefResult = {
|
|
22
|
+
ok: true;
|
|
23
|
+
value: string;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
reason: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Assemble tasks/<id>/brief.md from briefFile. Returns the written path.
|
|
30
|
+
* The task directory must already exist.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assembleBrief(fleet: Fleet, taskId: string, briefFile: string): BriefResult;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readRegularPathIfPresent, refuseOpenPathForWrite, } from "./fleet.js";
|
|
4
|
+
import { briefPath } from "./task.js";
|
|
5
|
+
/**
|
|
6
|
+
* Brief assembly (kernel plan v1, M1-P4 step 2; R-033b, R-083b).
|
|
7
|
+
*
|
|
8
|
+
* The brief for a task is written once, at spawn, to tasks/<id>/brief.md:
|
|
9
|
+
* the operator's brief file verbatim, followed by the fleet's
|
|
10
|
+
* environment-warnings file verbatim when the fleet has one. The
|
|
11
|
+
* warnings file is <fleet>/warnings.md (fleet root, tracked content, not
|
|
12
|
+
* the ignored state/ area). When no warnings file exists, brief.md is
|
|
13
|
+
* byte-identical to the brief file.
|
|
14
|
+
*
|
|
15
|
+
* Verbatim means verbatim: no heading, no banner, no rewriting. A brief
|
|
16
|
+
* is the durable instruction an implementer is judged against, and a
|
|
17
|
+
* kernel that edits it makes the audit trail lie. The only byte this
|
|
18
|
+
* module may add is a single newline between the two documents when the
|
|
19
|
+
* brief does not already end in one.
|
|
20
|
+
*/
|
|
21
|
+
/** The fleet's environment-warnings file, appended to every brief. */
|
|
22
|
+
export const WARNINGS_FILE = "warnings.md";
|
|
23
|
+
export function warningsPath(fleet) {
|
|
24
|
+
return join(fleet.root, WARNINGS_FILE);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Assemble tasks/<id>/brief.md from briefFile. Returns the written path.
|
|
28
|
+
* The task directory must already exist.
|
|
29
|
+
*/
|
|
30
|
+
export function assembleBrief(fleet, taskId, briefFile) {
|
|
31
|
+
/* THREE PATHS, NONE OF THEM THIS MODULE'S OWN, AND THE TYPE OF EACH IS
|
|
32
|
+
ESTABLISHED BEFORE IT IS OPENED (T-008's shape in shipped code). Until
|
|
33
|
+
this round all three were bare: `--brief` is named by the CALLER,
|
|
34
|
+
`warnings.md` is fleet content the kernel did not create, and the target
|
|
35
|
+
is a path this module CREATES, which is the write direction and blocks
|
|
36
|
+
on a FIFO exactly as a read does. A named pipe at any of them took
|
|
37
|
+
`tiphys spawn` down forever with zero output; `existsSync` did not help,
|
|
38
|
+
because a FIFO exists. */
|
|
39
|
+
const briefRead = readRegularPathIfPresent(briefFile);
|
|
40
|
+
if (briefRead.kind === "absent") {
|
|
41
|
+
return { ok: false, reason: `cannot read brief file ${briefFile}: it is absent` };
|
|
42
|
+
}
|
|
43
|
+
if (briefRead.kind === "refused") {
|
|
44
|
+
return { ok: false, reason: `cannot read brief file ${briefFile}: ${briefRead.reason}` };
|
|
45
|
+
}
|
|
46
|
+
let content = briefRead.body;
|
|
47
|
+
const warnings = warningsPath(fleet);
|
|
48
|
+
const warningsRead = readRegularPathIfPresent(warnings);
|
|
49
|
+
if (warningsRead.kind === "refused") {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: `cannot read fleet warnings file ${warnings}: ${warningsRead.reason}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (warningsRead.kind === "read") {
|
|
56
|
+
const separator = content === "" || content.endsWith("\n") ? "" : "\n";
|
|
57
|
+
content = `${content}${separator}${warningsRead.body}`;
|
|
58
|
+
}
|
|
59
|
+
const target = briefPath(fleet, taskId);
|
|
60
|
+
const refusal = refuseOpenPathForWrite(target);
|
|
61
|
+
if (refusal !== undefined) {
|
|
62
|
+
return { ok: false, reason: `cannot write brief ${target}: ${refusal}` };
|
|
63
|
+
}
|
|
64
|
+
writeFileSync(target, content);
|
|
65
|
+
return { ok: true, value: target };
|
|
66
|
+
}
|