@tiphys/kernel 0.1.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 +56 -4
- package/assurance-modes.yaml +23 -2
- package/dist/bin/tiphys.js +86 -8
- package/dist/src/adapters/load.d.ts +202 -0
- package/dist/src/adapters/load.js +440 -0
- package/dist/src/brief.js +27 -20
- package/dist/src/checks.d.ts +720 -9
- package/dist/src/checks.js +1874 -163
- package/dist/src/cli.js +11 -0
- package/dist/src/commands/brief.js +27 -4
- package/dist/src/commands/cutover.d.ts +35 -0
- package/dist/src/commands/cutover.js +448 -0
- package/dist/src/commands/doctor.d.ts +229 -0
- package/dist/src/commands/doctor.js +968 -27
- package/dist/src/commands/init.d.ts +3 -3
- package/dist/src/commands/init.js +57 -8
- package/dist/src/commands/lock.d.ts +33 -0
- package/dist/src/commands/lock.js +117 -6
- package/dist/src/commands/next.d.ts +130 -0
- package/dist/src/commands/next.js +597 -0
- package/dist/src/commands/pool.js +12 -1
- package/dist/src/commands/resume.d.ts +1 -0
- package/dist/src/commands/resume.js +88 -0
- package/dist/src/commands/spawn.js +51 -2
- package/dist/src/commands/status.d.ts +6 -4
- package/dist/src/commands/status.js +6 -4
- package/dist/src/commands/sync.d.ts +47 -0
- package/dist/src/commands/sync.js +341 -0
- package/dist/src/commands/teardown.js +10 -2
- package/dist/src/commands/validate.js +70 -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 +152 -2
- package/dist/src/exec/env.js +146 -2
- package/dist/src/fleet.d.ts +172 -0
- package/dist/src/fleet.js +219 -1
- package/dist/src/gates/citations.js +7 -1
- package/dist/src/gates/coverage.d.ts +113 -22
- package/dist/src/gates/coverage.js +166 -31
- package/dist/src/gates/credentials.d.ts +159 -0
- package/dist/src/gates/credentials.js +221 -2
- package/dist/src/gates/gate-classes.d.ts +56 -0
- package/dist/src/gates/gate-classes.js +633 -0
- package/dist/src/gates/merge-preconditions.d.ts +319 -0
- package/dist/src/gates/merge-preconditions.js +932 -0
- package/dist/src/gates/red-witness.js +105 -13
- package/dist/src/gates/run.d.ts +49 -1
- package/dist/src/gates/run.js +83 -5
- package/dist/src/gates/schemas/phase-declaration.schema.json +45 -0
- package/dist/src/gates/suite.js +48 -7
- package/dist/src/hooks.d.ts +55 -3
- package/dist/src/hooks.js +69 -6
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.js +30 -0
- package/dist/src/lock.d.ts +82 -4
- package/dist/src/lock.js +314 -22
- package/dist/src/model-resolution.d.ts +159 -0
- package/dist/src/model-resolution.js +307 -0
- package/dist/src/path-identity.d.ts +32 -0
- package/dist/src/path-identity.js +38 -0
- package/dist/src/pool.d.ts +197 -1
- package/dist/src/pool.js +289 -22
- package/dist/src/roles.d.ts +31 -0
- package/dist/src/roles.js +42 -0
- package/dist/src/spawn.d.ts +307 -2
- package/dist/src/spawn.js +690 -19
- package/dist/src/status.d.ts +27 -2
- package/dist/src/status.js +34 -5
- package/dist/src/task.d.ts +295 -55
- package/dist/src/task.js +125 -123
- package/dist/src/teardown.d.ts +7 -0
- package/dist/src/teardown.js +120 -12
- package/dist/src/validate.d.ts +44 -11
- package/dist/src/validate.js +44 -34
- package/dist/src/watcher.js +1 -11
- package/dist/src/witness/run.d.ts +32 -7
- package/dist/src/witness/run.js +76 -30
- package/dist/src/witness/spec.d.ts +168 -0
- package/dist/src/witness/spec.js +240 -18
- package/dist/tsconfig.src.tsbuildinfo +1 -1
- package/gate-registry.yaml +136 -0
- package/gates.manifest.json +63 -1
- package/package.json +18 -3
- package/roles/implementer.md +3 -0
- package/schemas/README.md +1 -0
- package/schemas/assurance-modes.schema.json +1 -1
- package/schemas/charter.schema.json +19 -0
- package/schemas/cutover-state.schema.json +64 -0
- package/schemas/executor-record.schema.json +36 -0
- package/schemas/model-resolution.schema.json +362 -0
- package/schemas/verdict.schema.json +9 -3
- package/schemas/write-bypass.schema.json +69 -0
package/dist/src/status.d.ts
CHANGED
|
@@ -11,18 +11,43 @@
|
|
|
11
11
|
* CONSTRAINT C-1, NAMED BECAUSE IT DECIDES THE SHAPE OF THIS MODULE.
|
|
12
12
|
* "Never read current state from the tail of an append-only log." The stream
|
|
13
13
|
* at `state/status/stream.jsonl` is the HISTORY and is append-only. The
|
|
14
|
-
* current state is `
|
|
14
|
+
* current state is `status/current.json`, a whole document rewritten
|
|
15
15
|
* atomically on every emit. `readCurrent` opens `current.json` and nothing
|
|
16
16
|
* else; it does not know the stream's path. A truncated, corrupted or
|
|
17
17
|
* half-written stream therefore cannot change what `tiphys status show`
|
|
18
18
|
* reports, and that is the property criterion 7 witnesses in both directions.
|
|
19
19
|
*
|
|
20
|
+
* THE SPLIT (M4-D-13, decided in the M4-P18 plan section). The two documents
|
|
21
|
+
* used to sit side by side under the gitignored `state/` prefix, so the one
|
|
22
|
+
* sentence that says where the pipeline stands did not survive a fleet being
|
|
23
|
+
* reclaimed: AGENTS.md's `fleet-state-commit-discipline` clause names "the
|
|
24
|
+
* state file that says where the pipeline stands" as DURABLE, and an ignored
|
|
25
|
+
* path can be neither committed nor pushed. So the pointer moved OUT of
|
|
26
|
+
* `state/` to a tracked path and the stream stayed, because the stream is
|
|
27
|
+
* history that a restart rebuilds.
|
|
28
|
+
*
|
|
29
|
+
* C-1 IS THE REASON FOR THE SPLIT AND NOT A CONSTRAINT IT HAS TO DODGE.
|
|
30
|
+
* `readCurrent` opens the whole document and has no code path that reaches
|
|
31
|
+
* the stream, so moving the document changes WHERE it lives and changes
|
|
32
|
+
* NOTHING about how current state is read. The pair of witnesses in
|
|
33
|
+
* test/status.test.ts is re-pointed at the new layout and still reddens
|
|
34
|
+
* under the same mutation: `readCurrent` aimed at STREAM_FILE.
|
|
35
|
+
*
|
|
20
36
|
* ATOMIC REWRITE means write a temp file beside the target and rename. A
|
|
21
37
|
* rename within one directory is atomic on POSIX, so a reader either sees the
|
|
22
38
|
* whole previous document or the whole new one and never a partial write.
|
|
39
|
+
* The temp file is created beside the DURABLE document, so the two paths in
|
|
40
|
+
* the rename stay inside one directory after the split.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Where the status files live inside a fleet home, and the two directories
|
|
44
|
+
* are on OPPOSITE sides of the fleet `.gitignore` by design (M4-D-13).
|
|
45
|
+
* `STATUS_DIR` is under the ignored `state/` prefix and holds the rebuilt
|
|
46
|
+
* history; `DURABLE_STATUS_DIR` is tracked and holds the one document a
|
|
47
|
+
* restart must not lose.
|
|
23
48
|
*/
|
|
24
|
-
/** Where the status files live inside a fleet home. */
|
|
25
49
|
export declare const STATUS_DIR: string;
|
|
50
|
+
export declare const DURABLE_STATUS_DIR = "status";
|
|
26
51
|
export declare const STREAM_FILE: string;
|
|
27
52
|
export declare const CURRENT_FILE: string;
|
|
28
53
|
/**
|
package/dist/src/status.js
CHANGED
|
@@ -11,23 +11,48 @@
|
|
|
11
11
|
* CONSTRAINT C-1, NAMED BECAUSE IT DECIDES THE SHAPE OF THIS MODULE.
|
|
12
12
|
* "Never read current state from the tail of an append-only log." The stream
|
|
13
13
|
* at `state/status/stream.jsonl` is the HISTORY and is append-only. The
|
|
14
|
-
* current state is `
|
|
14
|
+
* current state is `status/current.json`, a whole document rewritten
|
|
15
15
|
* atomically on every emit. `readCurrent` opens `current.json` and nothing
|
|
16
16
|
* else; it does not know the stream's path. A truncated, corrupted or
|
|
17
17
|
* half-written stream therefore cannot change what `tiphys status show`
|
|
18
18
|
* reports, and that is the property criterion 7 witnesses in both directions.
|
|
19
19
|
*
|
|
20
|
+
* THE SPLIT (M4-D-13, decided in the M4-P18 plan section). The two documents
|
|
21
|
+
* used to sit side by side under the gitignored `state/` prefix, so the one
|
|
22
|
+
* sentence that says where the pipeline stands did not survive a fleet being
|
|
23
|
+
* reclaimed: AGENTS.md's `fleet-state-commit-discipline` clause names "the
|
|
24
|
+
* state file that says where the pipeline stands" as DURABLE, and an ignored
|
|
25
|
+
* path can be neither committed nor pushed. So the pointer moved OUT of
|
|
26
|
+
* `state/` to a tracked path and the stream stayed, because the stream is
|
|
27
|
+
* history that a restart rebuilds.
|
|
28
|
+
*
|
|
29
|
+
* C-1 IS THE REASON FOR THE SPLIT AND NOT A CONSTRAINT IT HAS TO DODGE.
|
|
30
|
+
* `readCurrent` opens the whole document and has no code path that reaches
|
|
31
|
+
* the stream, so moving the document changes WHERE it lives and changes
|
|
32
|
+
* NOTHING about how current state is read. The pair of witnesses in
|
|
33
|
+
* test/status.test.ts is re-pointed at the new layout and still reddens
|
|
34
|
+
* under the same mutation: `readCurrent` aimed at STREAM_FILE.
|
|
35
|
+
*
|
|
20
36
|
* ATOMIC REWRITE means write a temp file beside the target and rename. A
|
|
21
37
|
* rename within one directory is atomic on POSIX, so a reader either sees the
|
|
22
38
|
* whole previous document or the whole new one and never a partial write.
|
|
39
|
+
* The temp file is created beside the DURABLE document, so the two paths in
|
|
40
|
+
* the rename stay inside one directory after the split.
|
|
23
41
|
*/
|
|
24
42
|
import { mkdirSync, renameSync, writeFileSync, appendFileSync } from "node:fs";
|
|
25
43
|
import { join } from "node:path";
|
|
26
44
|
import { refuseOpenForWrite, readRegularFileIfPresent } from "./task.js";
|
|
27
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Where the status files live inside a fleet home, and the two directories
|
|
47
|
+
* are on OPPOSITE sides of the fleet `.gitignore` by design (M4-D-13).
|
|
48
|
+
* `STATUS_DIR` is under the ignored `state/` prefix and holds the rebuilt
|
|
49
|
+
* history; `DURABLE_STATUS_DIR` is tracked and holds the one document a
|
|
50
|
+
* restart must not lose.
|
|
51
|
+
*/
|
|
28
52
|
export const STATUS_DIR = join("state", "status");
|
|
53
|
+
export const DURABLE_STATUS_DIR = "status";
|
|
29
54
|
export const STREAM_FILE = join(STATUS_DIR, "stream.jsonl");
|
|
30
|
-
export const CURRENT_FILE = join(
|
|
55
|
+
export const CURRENT_FILE = join(DURABLE_STATUS_DIR, "current.json");
|
|
31
56
|
/**
|
|
32
57
|
* The five supervisor-actionable states, blueprint section 5. Duplicated
|
|
33
58
|
* from `schemas/status-line.schema.json` so the CLI can reject a bad state
|
|
@@ -66,8 +91,12 @@ export function makeStatusRecord(input) {
|
|
|
66
91
|
* names an event no record supports.
|
|
67
92
|
*/
|
|
68
93
|
export function emitStatus(fleetRoot, record) {
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
/* TWO DIRECTORIES SINCE THE SPLIT, and both are created before either is
|
|
95
|
+
written: the ephemeral one is rebuilt by `tiphys resume` after a reclaim
|
|
96
|
+
and the durable one comes across in a clone, so on any given fleet home
|
|
97
|
+
either can be the one that is absent. */
|
|
98
|
+
mkdirSync(join(fleetRoot, STATUS_DIR), { recursive: true });
|
|
99
|
+
mkdirSync(join(fleetRoot, DURABLE_STATUS_DIR), { recursive: true });
|
|
71
100
|
const streamPath = join(fleetRoot, STREAM_FILE);
|
|
72
101
|
const currentPath = join(fleetRoot, CURRENT_FILE);
|
|
73
102
|
const temporaryPath = `${currentPath}.tmp`;
|
package/dist/src/task.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Fleet } from "./fleet.ts";
|
|
2
|
+
import { classifyPathEntry as classifyEntry, readRegularPathIfPresent as readRegularFileIfPresent, refuseOpenPathForWrite as refuseOpenForWrite } from "./fleet.ts";
|
|
2
3
|
/**
|
|
3
4
|
* Task state (kernel plan v1, M1-P4 step 1). A task's durable state is
|
|
4
5
|
* <fleet>/tasks/<id>/, holding plain JSON per plan decision D-3:
|
|
@@ -65,65 +66,210 @@ import type { Fleet } from "./fleet.ts";
|
|
|
65
66
|
* the window is now the only way to reach the block rather than the
|
|
66
67
|
* default path to it.
|
|
67
68
|
*/
|
|
68
|
-
export type EntryClass =
|
|
69
|
-
/** Nothing at the path. */
|
|
70
|
-
{
|
|
71
|
-
kind: "absent";
|
|
72
|
-
}
|
|
73
|
-
/** A link is there and resolves to nothing: it exists, and it is empty of evidence. */
|
|
74
|
-
| {
|
|
75
|
-
kind: "dangling";
|
|
76
|
-
}
|
|
77
|
-
/** Safe to open. */
|
|
78
|
-
| {
|
|
79
|
-
kind: "regular";
|
|
80
|
-
}
|
|
81
|
-
/** Present, and opening it is not safe: never opened, always named. */
|
|
82
|
-
| {
|
|
83
|
-
kind: "irregular";
|
|
84
|
-
reason: string;
|
|
85
|
-
}
|
|
86
|
-
/** Neither lstat nor stat could answer the question. */
|
|
87
|
-
| {
|
|
88
|
-
kind: "unexaminable";
|
|
89
|
-
reason: string;
|
|
90
|
-
};
|
|
91
69
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
70
|
+
* ONE IMPLEMENTATION, RE-EXPORTED, AND THAT IS THE DR-0047 SWEEP ROUND 2
|
|
71
|
+
* CONSOLIDATION.
|
|
72
|
+
*
|
|
73
|
+
* The paragraphs above explain WHY the probe exists and they still hold. What
|
|
74
|
+
* changed is where it lives. Until this round there were TWO byte-equivalent
|
|
75
|
+
* copies of it: `classifyEntry`/`readRegularFileIfPresent`/`refuseOpenForWrite`
|
|
76
|
+
* here, and `classifyPathEntry`/`readRegularPathIfPresent`/
|
|
77
|
+
* `refuseOpenPathForWrite` in src/fleet.ts, with identical types, identical
|
|
78
|
+
* bodies and identical sentences. Round 1 created the second deliberately and
|
|
79
|
+
* both implementers flagged it: src/lock.ts and src/exclusion.ts sit BELOW this
|
|
80
|
+
* module in the import graph, so importing from here would have made a cycle.
|
|
81
|
+
*
|
|
82
|
+
* THE DIRECTION THAT REMOVES THE COPY WITHOUT MAKING A CYCLE IS DOWNWARD, AND
|
|
83
|
+
* IT IS THE ONE THE COMMENT ABOVE ALREADY ASKED FOR. src/fleet.ts imports NO
|
|
84
|
+
* local module at all, so it is the leaf; this module already imports
|
|
85
|
+
* `Fleet` from it. The chain is task -> fleet, and lock -> fleet, and nothing
|
|
86
|
+
* points back. The alternative, making lock and exclusion call into here, is
|
|
87
|
+
* the cycle task -> lock -> task and is what round 1 correctly refused.
|
|
88
|
+
*
|
|
89
|
+
* THE NAMES HERE ARE KEPT AS ALIASES rather than the 88 call sites in 31 files
|
|
90
|
+
* being rewritten. The names are the whole reason the rewrite would be
|
|
91
|
+
* expensive and none of the reason the duplication was a defect: what mattered
|
|
92
|
+
* was two bodies that could drift, and there is now one body.
|
|
96
93
|
*/
|
|
97
|
-
export
|
|
94
|
+
export type { PathEntryClass as EntryClass, RegularPathRead as RegularRead } from "./fleet.ts";
|
|
95
|
+
export { classifyEntry, readRegularFileIfPresent, refuseOpenForWrite };
|
|
96
|
+
export type TaskShape = "ship" | "scout";
|
|
97
|
+
export type TaskStatus = "open" | "closed";
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
99
|
+
* WHOSE AUTHORITY A PAYLOAD RUNS UNDER (M4-P8 step 2).
|
|
100
|
+
*
|
|
101
|
+
* `orchestrator` is the process that plans, reviews and merges, and is the
|
|
102
|
+
* only payload class the declared credential escape hatch was ever written
|
|
103
|
+
* for (SpawnOptions.allowPrCredentials, M2-P8 criterion 1: "for the
|
|
104
|
+
* orchestrator's own spawns, never for an implementer payload"). `project`
|
|
105
|
+
* is every payload that works inside a project clone.
|
|
106
|
+
*
|
|
107
|
+
* It is DECLARED, never derived. The kernel cannot infer it from a command,
|
|
108
|
+
* a role or a branch without holding a vocabulary for one of those, and a
|
|
109
|
+
* derived answer would be wrong in the one direction that matters.
|
|
104
110
|
*/
|
|
105
|
-
export
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
export type PayloadClass = "orchestrator" | "project";
|
|
112
|
+
/**
|
|
113
|
+
* One granted allowlist extension, as it is recorded in meta.json.
|
|
114
|
+
*
|
|
115
|
+
* BUILT THROUGH THE ACCESSORS, NEVER BY READING `entry.name` DIRECTLY
|
|
116
|
+
* (CR-B-002, the half of it that is about the record). A bare-string entry
|
|
117
|
+
* has no `name` property, so `{name: entry.name, reason: entry.reason}`
|
|
118
|
+
* produced the literal record `{}`: an operator opening the task directory
|
|
119
|
+
* could see THAT a widening happened and not WHICH name was widened. The
|
|
120
|
+
* record is now built with `extensionName` / `extensionReason`
|
|
121
|
+
* (src/exec/env.ts), which read both forms.
|
|
122
|
+
*
|
|
123
|
+
* `reason` is OPTIONAL HERE AND REQUIRED ON THE AUDITED ROUTE, and the
|
|
124
|
+
* difference is the point rather than an inconsistency. This type describes
|
|
125
|
+
* what was actually recorded; the audited route refuses an entry with no
|
|
126
|
+
* usable reason before this record is built, so an entry reaching meta.json
|
|
127
|
+
* without one came through a seam that does not demand one. Recording the
|
|
128
|
+
* absence AS an absence (the key is simply not present in the JSON) is what
|
|
129
|
+
* keeps it distinguishable from a blank reason. Defaulting it to `""` would
|
|
130
|
+
* substitute a valid-looking value for a missing one, which is the same
|
|
131
|
+
* mechanism as CR-B-002 wearing a record's clothes.
|
|
132
|
+
*/
|
|
133
|
+
export interface CredentialExtensionRecord {
|
|
134
|
+
name: string;
|
|
135
|
+
reason?: string;
|
|
112
136
|
}
|
|
113
|
-
/** Present and not readable, with a reason naming the path. */
|
|
114
|
-
| {
|
|
115
|
-
kind: "refused";
|
|
116
|
-
reason: string;
|
|
117
|
-
};
|
|
118
137
|
/**
|
|
119
|
-
* THE
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
138
|
+
* THE HANDOVER COMPARISON (M4-P8 criterion 6): what the kernel handed the
|
|
139
|
+
* adapter against what actually reached the child.
|
|
140
|
+
*
|
|
141
|
+
* TWO PROPERTIES, AND THE STATUS WORD NAMES WHICH OF THEM WERE CHECKED.
|
|
142
|
+
* That is finding CR-B-001 (clean-room-retro-B-criteria, HIGH) and it is
|
|
143
|
+
* worth stating as a mechanism rather than as one word: `status: "compared"`
|
|
144
|
+
* used to be written after comparing NAME SETS ONLY, while an operator reads
|
|
145
|
+
* "compared" as "the handover was verified". An adapter that keeps the name
|
|
146
|
+
* set byte-identical and merely restores `HOME` and `XDG_CONFIG_HOME` to
|
|
147
|
+
* their real paths defeats the whole M2R-004 defense, because that defense
|
|
148
|
+
* works by REDIRECTING those names rather than by dropping them, and the
|
|
149
|
+
* record then POSITIVELY ASSERTED a clean handover while the child read the
|
|
150
|
+
* real gh credential store. The general shape is a record whose status word
|
|
151
|
+
* is stronger than the check behind it.
|
|
152
|
+
*
|
|
153
|
+
* Both halves of the repair are here. The word is weakened so it can only
|
|
154
|
+
* say what was checked, and the check is strengthened so the pointer VALUES
|
|
155
|
+
* are compared too.
|
|
156
|
+
*
|
|
157
|
+
* compared names AND the five CREDENTIAL_STORE_REDIRECTIONS
|
|
158
|
+
* pointers were both in hand and both compared.
|
|
159
|
+
* names-compared the name sets were compared and NO pointer evidence
|
|
160
|
+
* was available. This is the value the old `compared`
|
|
161
|
+
* should have carried: it is a true statement about one
|
|
162
|
+
* property of two, and it never reads as a clean bill.
|
|
163
|
+
* pointers-compared the pointers were observed in the child and compared,
|
|
164
|
+
* and the adapter reported no name set. Reachable with
|
|
165
|
+
* any adapter written before M4-P8, which reports
|
|
166
|
+
* nothing, running under the current turn-end hook.
|
|
167
|
+
* unreported neither property could be checked: no reported name
|
|
168
|
+
* set and no pointer evidence.
|
|
169
|
+
* not-applicable the spawn ran under the declared escape hatch, so the
|
|
170
|
+
* kernel handed over no environment at all and there is
|
|
171
|
+
* nothing a difference could be measured against.
|
|
172
|
+
*
|
|
173
|
+
* NO VALUE OF ANY VARIABLE IS RECORDED HERE, and the original design's
|
|
174
|
+
* reason for that is untouched and right: a value comparison that WROTE the
|
|
175
|
+
* values would put credential material into a record an operator reads.
|
|
176
|
+
* `changedRedirections` carries NAMES only. The five redirection targets are
|
|
177
|
+
* harness-owned paths inside the task directory and carry no credential
|
|
178
|
+
* material, but recording only the names that differ is strictly less and is
|
|
179
|
+
* enough to act on.
|
|
123
180
|
*/
|
|
124
|
-
export
|
|
125
|
-
|
|
126
|
-
|
|
181
|
+
export interface CredentialHandoverRecord {
|
|
182
|
+
status: "compared" | "names-compared" | "pointers-compared" | "unreported" | "not-applicable";
|
|
183
|
+
/** Names the adapter reported that the kernel did not hand over. */
|
|
184
|
+
added: string[];
|
|
185
|
+
/** Names the kernel handed over that the adapter did not report. */
|
|
186
|
+
removed: string[];
|
|
187
|
+
/**
|
|
188
|
+
* Credential-store pointers whose value where the payload ran is not the
|
|
189
|
+
* harness-owned path the kernel handed over. Names only, never values.
|
|
190
|
+
* Empty when the pointers were compared and agreed, and also empty when
|
|
191
|
+
* there was no pointer evidence, which is why `status` and not this array
|
|
192
|
+
* is what says whether the comparison happened.
|
|
193
|
+
*/
|
|
194
|
+
changedRedirections: string[];
|
|
195
|
+
/**
|
|
196
|
+
* WHICH ARTIFACT THE POINTER EVIDENCE WAS READ FROM, absent when there was
|
|
197
|
+
* none. It is a statement about a FILE, not a provenance the kernel
|
|
198
|
+
* verified, and the value used to say otherwise.
|
|
199
|
+
*
|
|
200
|
+
* turn-end-record read from tasks/<id>/turn-end. The kernel generates the
|
|
201
|
+
* hook that normally writes it, and for an adapter that
|
|
202
|
+
* honours the contract those values come from inside the
|
|
203
|
+
* child environment. THE FILE IS ALSO ADAPTER-REACHABLE:
|
|
204
|
+
* the adapter is handed `hookPath` and the record sits
|
|
205
|
+
* beside it, so an adapter that skips the hook and writes
|
|
206
|
+
* the record itself produces a record the kernel cannot
|
|
207
|
+
* tell from the hook's.
|
|
208
|
+
* adapter read from the adapter's launch outcome. The adapter's
|
|
209
|
+
* word about its own behaviour, and always was.
|
|
210
|
+
*
|
|
211
|
+
* THE VALUE WAS `child` UNTIL THE DR-0047 SWEEP FIX ROUND (CR-F-CRED-001,
|
|
212
|
+
* MEDIUM). Measured: an adapter that reverted HOME and XDG_CONFIG_HOME for
|
|
213
|
+
* its payload and wrote the turn-end record itself produced
|
|
214
|
+
* `{"status":"compared","changedRedirections":[],"redirectionSource":"child"}`
|
|
215
|
+
* while the payload could reach a real gh credential store. The word `child`
|
|
216
|
+
* asserted an observation no child had made. Neither value is evidence that
|
|
217
|
+
* the adapter was honest, and the record now says only what it can support.
|
|
218
|
+
* This is CR-B-001's own mechanism, a record whose status word is stronger
|
|
219
|
+
* than the check behind it, recurring one level up inside the same record;
|
|
220
|
+
* see src/hooks.ts for why a nonce does not close it and a rename does.
|
|
221
|
+
*/
|
|
222
|
+
redirectionSource?: "turn-end-record" | "adapter";
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* THE CREDENTIAL DECISION FOR ONE SPAWN (M4-P8 step 6), recorded in the
|
|
226
|
+
* file the kernel already owns rather than in a new one across a new seam.
|
|
227
|
+
*
|
|
228
|
+
* The hazard this phase names is "a credential reaches a project payload
|
|
229
|
+
* and no artifact says so". Every arm of the decision is therefore written
|
|
230
|
+
* down even when it is the boring one: the payload class, whether the
|
|
231
|
+
* scrub ran, every widening with the reason it was granted, the handover
|
|
232
|
+
* comparison, and the refusal if one fired after this record existed.
|
|
233
|
+
*/
|
|
234
|
+
export interface TaskCredentialRecord {
|
|
235
|
+
payloadClass: PayloadClass;
|
|
236
|
+
/**
|
|
237
|
+
* `scrubbed` when buildChildEnv constructed the environment, `inherited`
|
|
238
|
+
* when allowPrCredentials handed the parent's environment over unchanged.
|
|
239
|
+
*
|
|
240
|
+
* THIS FIELD RECORDS THE KERNEL'S DECISION AND ITS OWN CONSTRUCTION, NOT
|
|
241
|
+
* WHAT THE CHILD RECEIVED, and it is spelled out because it is the second
|
|
242
|
+
* member of CR-B-001's class found in this record (the derivation is in
|
|
243
|
+
* delivery/work-history/credential-route-fixes.md). "scrubbed" is a strong
|
|
244
|
+
* word: it says buildChildEnv ran, staged the five empty redirect targets
|
|
245
|
+
* and returned an environment, because a failure there is a rollback. It
|
|
246
|
+
* does NOT say the payload ran with that environment, because between this
|
|
247
|
+
* field being written and the payload starting there is an adapter. The
|
|
248
|
+
* field that speaks to what reached the child is `handover`, and it says
|
|
249
|
+
* which of its two properties it checked.
|
|
250
|
+
*/
|
|
251
|
+
scrubMode: "scrubbed" | "inherited";
|
|
252
|
+
/** Every granted extension, in the order the caller declared them. */
|
|
253
|
+
extensions: CredentialExtensionRecord[];
|
|
254
|
+
/**
|
|
255
|
+
* ABSENT UNTIL THE LAUNCH HAS REPORTED, and the absence is a fact rather
|
|
256
|
+
* than an omission: this record is written BEFORE the payload starts,
|
|
257
|
+
* which is what makes a failure to write it safe to roll back, and at
|
|
258
|
+
* that moment no adapter has launched anything to compare. It is the same
|
|
259
|
+
* reason `ExecutorRecord` carries no resolved model.
|
|
260
|
+
*/
|
|
261
|
+
handover?: CredentialHandoverRecord;
|
|
262
|
+
/**
|
|
263
|
+
* The refusal that fired AFTER this record was written, if one did.
|
|
264
|
+
*
|
|
265
|
+
* The step-5 refusal (the escape hatch asked for on a project payload) is
|
|
266
|
+
* deliberately absent from this field and cannot appear in it: that one
|
|
267
|
+
* refuses before the task directory exists, so there is no meta.json to
|
|
268
|
+
* record it in, and creating one would undo the property that a refused
|
|
269
|
+
* spawn creates nothing.
|
|
270
|
+
*/
|
|
271
|
+
refusal?: string;
|
|
272
|
+
}
|
|
127
273
|
export declare const TASK_SHAPES: readonly TaskShape[];
|
|
128
274
|
/**
|
|
129
275
|
* Task meta (the plan's field set, M1-P4 step 1).
|
|
@@ -146,6 +292,12 @@ export declare const TASK_SHAPES: readonly TaskShape[];
|
|
|
146
292
|
* produced in M1-P3.
|
|
147
293
|
* - status: open at spawn, closed by a successful teardown.
|
|
148
294
|
* - createdAt: ISO-8601 timestamp of the spawn.
|
|
295
|
+
* - credentials: the M4-P8 credential decision for this spawn. OPTIONAL in
|
|
296
|
+
* the type, and the optionality is about READING rather than writing:
|
|
297
|
+
* every spawn from M4-P8 on writes it, and `readTaskMeta` deliberately
|
|
298
|
+
* does not require it, so a record written by an earlier kernel still
|
|
299
|
+
* reads instead of becoming an unreadable task whose worktree nobody can
|
|
300
|
+
* tear down. Consumers that need it check for it.
|
|
149
301
|
*/
|
|
150
302
|
export interface TaskMeta {
|
|
151
303
|
id: string;
|
|
@@ -157,6 +309,7 @@ export interface TaskMeta {
|
|
|
157
309
|
baseOffline: boolean;
|
|
158
310
|
status: TaskStatus;
|
|
159
311
|
createdAt: string;
|
|
312
|
+
credentials?: TaskCredentialRecord;
|
|
160
313
|
}
|
|
161
314
|
export declare function taskDir(fleet: Fleet, taskId: string): string;
|
|
162
315
|
export declare function metaPath(fleet: Fleet, taskId: string): string;
|
|
@@ -168,9 +321,45 @@ export declare function reportPath(fleet: Fleet, taskId: string): string;
|
|
|
168
321
|
export declare function renderTaskMeta(meta: TaskMeta): string;
|
|
169
322
|
export declare function writeTaskMeta(fleet: Fleet, meta: TaskMeta): void;
|
|
170
323
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
324
|
+
* THE FOUR WAYS A TASK RECORD FAILS TO READ, KEPT APART (CR-F02, MEDIUM;
|
|
325
|
+
* T-036's mechanism).
|
|
326
|
+
*
|
|
327
|
+
* `absent` is a category that is empty BY OBSERVATION: the kernel looked and
|
|
328
|
+
* there is no record. Every other member is a category that is empty BY
|
|
329
|
+
* CONSTRUCTION: there IS something there and this read could not turn it into
|
|
330
|
+
* a record. Collapsing them makes "nothing here" and "something here I could
|
|
331
|
+
* not read" the same answer, and a task killed mid-write is the ordinary
|
|
332
|
+
* failure this repository keeps meeting, so the two are not the same answer at
|
|
333
|
+
* any caller that decides whether work is in flight.
|
|
334
|
+
*/
|
|
335
|
+
export type TaskMetaRead =
|
|
336
|
+
/** A record that read and passed the field check. */
|
|
337
|
+
{
|
|
338
|
+
kind: "read";
|
|
339
|
+
meta: TaskMeta;
|
|
340
|
+
}
|
|
341
|
+
/** Nothing at this path. Empty by observation. */
|
|
342
|
+
| {
|
|
343
|
+
kind: "absent";
|
|
344
|
+
}
|
|
345
|
+
/** Present and not openable as a regular file, with the probe's reason. */
|
|
346
|
+
| {
|
|
347
|
+
kind: "unreadable";
|
|
348
|
+
reason: string;
|
|
349
|
+
}
|
|
350
|
+
/** Present, opened, and not JSON. The truncated-mid-write shape. */
|
|
351
|
+
| {
|
|
352
|
+
kind: "unparsable";
|
|
353
|
+
reason: string;
|
|
354
|
+
}
|
|
355
|
+
/** Present, parsed, and not a task record. Names the first bad field. */
|
|
356
|
+
| {
|
|
357
|
+
kind: "malformed";
|
|
358
|
+
reason: string;
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* THE ONE READ of a task record, and the one that says WHICH of the four
|
|
362
|
+
* outcomes happened.
|
|
174
363
|
*
|
|
175
364
|
* The type probe is INSIDE this function and not in front of one of its
|
|
176
365
|
* callers (CR-520, CR-521). There is exactly one implementation of "read a
|
|
@@ -178,6 +367,29 @@ export declare function writeTaskMeta(fleet: Fleet, meta: TaskMeta): void;
|
|
|
178
367
|
* cannot reopen the hole: src/teardown.ts reaches this directly, without
|
|
179
368
|
* going through the liveness classifier, and a named pipe here used to
|
|
180
369
|
* hang it forever.
|
|
370
|
+
*
|
|
371
|
+
* WHY THIS IS A SEPARATE FUNCTION FROM `readTaskMeta` RATHER THAN ITS NEW
|
|
372
|
+
* SIGNATURE, declared rather than left to be inferred. Widening the return
|
|
373
|
+
* type of `readTaskMeta` is the shape this repair wants, and it is a
|
|
374
|
+
* compile-time break in four modules that this fix round's file list does not
|
|
375
|
+
* own (src/pool.ts, src/liveness.ts, src/teardown.ts, src/commands/next.ts).
|
|
376
|
+
* The distinction is therefore made AVAILABLE upstream of all of them here,
|
|
377
|
+
* `readTaskMeta` is documented as the deliberate NARROWING of it, and the
|
|
378
|
+
* sites that still collapse are named in the fix round's work history rather
|
|
379
|
+
* than quietly left.
|
|
380
|
+
*/
|
|
381
|
+
export declare function classifyTaskMeta(fleet: Fleet, taskId: string): TaskMetaRead;
|
|
382
|
+
/**
|
|
383
|
+
* Read meta.json, or undefined when it is absent, is not a regular file,
|
|
384
|
+
* does not parse, or parses and is not a task record.
|
|
385
|
+
*
|
|
386
|
+
* THIS IS A DELIBERATE NARROWING OF `classifyTaskMeta` AND THE COLLAPSE IS THE
|
|
387
|
+
* WHOLE OF CR-F02. `undefined` answers "is there a readable record" and it
|
|
388
|
+
* cannot answer "is there a task here", because it is returned both when the
|
|
389
|
+
* kernel looked and found nothing and when it found something it could not
|
|
390
|
+
* read. A caller that reports an absence, skips an entry, or decides nothing
|
|
391
|
+
* is in flight must call `classifyTaskMeta` instead; a caller that REFUSES on
|
|
392
|
+
* every one of the four (teardown does) loses nothing by using this.
|
|
181
393
|
*/
|
|
182
394
|
export declare function readTaskMeta(fleet: Fleet, taskId: string): TaskMeta | undefined;
|
|
183
395
|
/** Set meta.json status (teardown's last step; C-1's state authority). */
|
|
@@ -262,3 +474,31 @@ export type StepResult<T> = {
|
|
|
262
474
|
* still decides whether to roll back, report a partial failure, or refuse.
|
|
263
475
|
*/
|
|
264
476
|
export declare function runStep<T>(what: string, step: () => T): StepResult<T>;
|
|
477
|
+
/**
|
|
478
|
+
* runStep's shape for a step that returns a PROMISE (M4-P2 step 2).
|
|
479
|
+
*
|
|
480
|
+
* This is a PROMOTION, not a new function. It lived module-private in
|
|
481
|
+
* src/watcher.ts, where its own comment said "src/task.ts covers the sync
|
|
482
|
+
* one"; spawn's launch call site needs the same shape now that
|
|
483
|
+
* `ExecutorAdapter.launch` returns a promise, and T-005's one-mechanism
|
|
484
|
+
* rule makes copying it a third time the wrong answer. The promotion was
|
|
485
|
+
* measured before it was made: the two functions were run side by side over
|
|
486
|
+
* six `what`/thrown-error pairs covering an ordinary Error, an Error with an
|
|
487
|
+
* empty message, a thrown string, a thrown object with a custom toString, an
|
|
488
|
+
* Error subclass and a multi-line pair, and their reason strings were
|
|
489
|
+
* compared as buffers. All six were byte-identical, so no watcher message
|
|
490
|
+
* changes on any of those six. The capture is in the M4-P2 work history
|
|
491
|
+
* under `delivery/work-history/m4-p2.md`.
|
|
492
|
+
*
|
|
493
|
+
* WHY A SEPARATE FUNCTION RATHER THAN WIDENING runStep. `runStep` returns
|
|
494
|
+
* `{ok: true, value}` the instant its callback returns, and a callback that
|
|
495
|
+
* returns a promise returns one IMMEDIATELY. So `runStep` over an async
|
|
496
|
+
* step is not merely imprecise: its `ok: true` is a claim that the step
|
|
497
|
+
* SUCCEEDED when nothing has run yet, and a later rejection escapes the
|
|
498
|
+
* result type entirely as an unhandled rejection. That is the exact defect
|
|
499
|
+
* this phase's criterion 4 witness reddens against.
|
|
500
|
+
*
|
|
501
|
+
* It never swallows, on the same terms as the sync one: the caller gets a
|
|
502
|
+
* reason naming the step and still decides what to do about it.
|
|
503
|
+
*/
|
|
504
|
+
export declare function runStepAsync<T>(what: string, step: () => Promise<T>): Promise<StepResult<T>>;
|