@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,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet-home layout (kernel plan v1, M1-P2 step 1; blueprint section 3 with
|
|
3
|
+
* the SC-002/SC-003 resolutions). The fleet home is a small git repository:
|
|
4
|
+
* durable content (charter/, decisions/, tasks/, backlog.md, package.json,
|
|
5
|
+
* .gitignore) is tracked; state/, worktrees/, and projects/ are ephemeral
|
|
6
|
+
* and gitignored (plan decision D-4, PR-004).
|
|
7
|
+
*/
|
|
8
|
+
export declare const FLEET_DIRS: readonly ["charter", "decisions", "state", "tasks", "worktrees", "projects"];
|
|
9
|
+
/** Files every fleet home carries at its root. */
|
|
10
|
+
export declare const FLEET_FILES: readonly ["backlog.md", "package.json", ".gitignore"];
|
|
11
|
+
/**
|
|
12
|
+
* Exactly these entries are gitignored: clones under projects/ are
|
|
13
|
+
* recoverable from their remotes, worktrees/ are disposable, state/ holds
|
|
14
|
+
* beacons and locks (SC-002, plan decision D-4, PR-004). Nothing else.
|
|
15
|
+
*/
|
|
16
|
+
export declare const FLEET_IGNORED: readonly ["state/", "worktrees/", "projects/"];
|
|
17
|
+
/**
|
|
18
|
+
* Well-known state file names. The lease lock is built by M1-P3 and the
|
|
19
|
+
* watcher beacon by M1-P5 (convention FM-043); doctor reads both as files
|
|
20
|
+
* only, never probing a process (plan constraint C-2). Task currency, when
|
|
21
|
+
* a later phase needs it, comes exclusively from tasks/<id>/meta.json and
|
|
22
|
+
* the turn-end file, never from a log tail (plan constraint C-1).
|
|
23
|
+
*/
|
|
24
|
+
export declare const LOCK_FILE: string;
|
|
25
|
+
export declare const BEACON_FILE: string;
|
|
26
|
+
/** Typed accessors over a validated fleet home. */
|
|
27
|
+
export interface Fleet {
|
|
28
|
+
root: string;
|
|
29
|
+
charterDir: string;
|
|
30
|
+
decisionsDir: string;
|
|
31
|
+
stateDir: string;
|
|
32
|
+
tasksDir: string;
|
|
33
|
+
worktreesDir: string;
|
|
34
|
+
projectsDir: string;
|
|
35
|
+
backlogPath: string;
|
|
36
|
+
packageJsonPath: string;
|
|
37
|
+
gitignorePath: string;
|
|
38
|
+
lockPath: string;
|
|
39
|
+
beaconPath: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Return the layout entries missing from dir, in declaration order.
|
|
43
|
+
* Directories are reported with a trailing slash. An empty result means
|
|
44
|
+
* the layout is complete.
|
|
45
|
+
*/
|
|
46
|
+
export declare function missingLayoutEntries(dir: string): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Validate the layout at dir and return typed accessors. Throws an Error
|
|
49
|
+
* naming every missing entry when the layout is incomplete.
|
|
50
|
+
*/
|
|
51
|
+
export declare function loadFleet(dir: string): Fleet;
|
|
52
|
+
/**
|
|
53
|
+
* A layout entry's type, established BEFORE anything is done with the path.
|
|
54
|
+
* `classifyEntry` in src/task.ts answers "may this be opened as a regular
|
|
55
|
+
* file" and therefore calls a directory irregular, which is the wrong answer
|
|
56
|
+
* for a layout entry: here a directory is the wanted shape. Same discipline,
|
|
57
|
+
* different question, so it is a separate function rather than a flag on that
|
|
58
|
+
* one (plan constraint C-2 is unaffected; nothing here probes a process).
|
|
59
|
+
*/
|
|
60
|
+
export type LayoutEntryClass =
|
|
61
|
+
/** Nothing at the path. */
|
|
62
|
+
{
|
|
63
|
+
kind: "absent";
|
|
64
|
+
}
|
|
65
|
+
/** A directory, or a symlink resolving to one. */
|
|
66
|
+
| {
|
|
67
|
+
kind: "directory";
|
|
68
|
+
}
|
|
69
|
+
/** Present and not a directory, or a symlink resolving to nothing. */
|
|
70
|
+
| {
|
|
71
|
+
kind: "other";
|
|
72
|
+
reason: string;
|
|
73
|
+
}
|
|
74
|
+
/** Neither lstat nor stat could answer the question. */
|
|
75
|
+
| {
|
|
76
|
+
kind: "unexaminable";
|
|
77
|
+
reason: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Classify a layout path without opening it. lstat first, so a symlink is
|
|
81
|
+
* seen as a symlink; then stat, so a symlink to a directory is a directory
|
|
82
|
+
* and a dangling one is reported as such rather than as absent.
|
|
83
|
+
*/
|
|
84
|
+
export declare function classifyLayoutEntry(path: string): LayoutEntryClass;
|
|
85
|
+
/**
|
|
86
|
+
* The EPHEMERAL directories: exactly the gitignored set, with the trailing
|
|
87
|
+
* slash that `.gitignore` needs stripped off. DERIVED from FLEET_IGNORED
|
|
88
|
+
* rather than listed again, because a second list is a second thing to keep
|
|
89
|
+
* in step and the first divergence would be silent: `tiphys resume` would
|
|
90
|
+
* rebuild one set while `.gitignore` ignored another.
|
|
91
|
+
*/
|
|
92
|
+
export declare const EPHEMERAL_DIRS: readonly string[];
|
|
93
|
+
/**
|
|
94
|
+
* The DURABLE directories: every fleet directory that is not ephemeral.
|
|
95
|
+
* A clone of a fleet home carries these and not the ephemeral ones, which
|
|
96
|
+
* is the fact `tiphys resume` exists to act on.
|
|
97
|
+
*/
|
|
98
|
+
export declare const DURABLE_DIRS: readonly string[];
|
|
99
|
+
/**
|
|
100
|
+
* The durable layout entries missing from dir, in declaration order:
|
|
101
|
+
* directories first with a trailing slash, then the root files. An empty
|
|
102
|
+
* result means the directory carries everything a clone of a fleet home
|
|
103
|
+
* carries, which is the precondition `tiphys resume` requires and never
|
|
104
|
+
* fabricates.
|
|
105
|
+
*/
|
|
106
|
+
export declare function missingDurableEntries(dir: string): string[];
|
|
107
|
+
/**
|
|
108
|
+
* THE GUARDED OPEN, AT THE BOTTOM OF THE IMPORT GRAPH.
|
|
109
|
+
*
|
|
110
|
+
* `classifyEntry` in src/task.ts:118 asks exactly this question and its own
|
|
111
|
+
* docblock says where it belongs: "a general filesystem rule and not a task
|
|
112
|
+
* rule, and a dedicated module would be its right home. This module is the
|
|
113
|
+
* lowest one in the import graph that the fix round authorized to touch."
|
|
114
|
+
* This module is LOWER. src/task.ts imports src/lock.ts, src/lock.ts imports
|
|
115
|
+
* src/exclusion.ts, and this file imports nothing from the project at all, so
|
|
116
|
+
* the two modules that hold the kernel's lease and register reads CANNOT
|
|
117
|
+
* reach src/task.ts without making the first import cycle in `src/` (measured
|
|
118
|
+
* at this head: the graph is a strict DAG). They reach these instead.
|
|
119
|
+
*
|
|
120
|
+
* THE DUPLICATION IS REAL AND IT IS NAMED RATHER THAN HIDDEN. Two
|
|
121
|
+
* implementations of one question can drift, and the end state is src/task.ts
|
|
122
|
+
* re-exporting these. That edit is not in this round's declared file set, so
|
|
123
|
+
* it is escalated rather than made, and until it happens the vocabulary, the
|
|
124
|
+
* branch order and the refusal TEXT below are kept identical to src/task.ts's
|
|
125
|
+
* on purpose: doctor already prints "is a named pipe, not a regular file, so
|
|
126
|
+
* it was not opened" for a lease, and a second sentence for the same state
|
|
127
|
+
* would make two true reports read as two different conditions.
|
|
128
|
+
*
|
|
129
|
+
* C-2 is unaffected: lstat and stat are questions about a directory entry,
|
|
130
|
+
* never about a running program.
|
|
131
|
+
*/
|
|
132
|
+
export type PathEntryClass =
|
|
133
|
+
/** Nothing at the path. */
|
|
134
|
+
{
|
|
135
|
+
kind: "absent";
|
|
136
|
+
}
|
|
137
|
+
/** A link is there and resolves to nothing: it exists, and it is empty of evidence. */
|
|
138
|
+
| {
|
|
139
|
+
kind: "dangling";
|
|
140
|
+
}
|
|
141
|
+
/** Safe to open. */
|
|
142
|
+
| {
|
|
143
|
+
kind: "regular";
|
|
144
|
+
}
|
|
145
|
+
/** Present, and opening it is not safe: never opened, always named. */
|
|
146
|
+
| {
|
|
147
|
+
kind: "irregular";
|
|
148
|
+
reason: string;
|
|
149
|
+
}
|
|
150
|
+
/** Neither lstat nor stat could answer the question. */
|
|
151
|
+
| {
|
|
152
|
+
kind: "unexaminable";
|
|
153
|
+
reason: string;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* lstat first, so a symlink is seen as a symlink; then stat, so a symlink to
|
|
157
|
+
* a regular file is regular and a dangling one is reported as such rather
|
|
158
|
+
* than as absent. The path is never opened.
|
|
159
|
+
*/
|
|
160
|
+
export declare function classifyPathEntry(path: string): PathEntryClass;
|
|
161
|
+
/** What a guarded read of a possibly-absent path produced. */
|
|
162
|
+
export type RegularPathRead = {
|
|
163
|
+
kind: "read";
|
|
164
|
+
body: string;
|
|
165
|
+
} | {
|
|
166
|
+
kind: "absent";
|
|
167
|
+
}
|
|
168
|
+
/** Present and not readable, with a reason naming the path. */
|
|
169
|
+
| {
|
|
170
|
+
kind: "refused";
|
|
171
|
+
reason: string;
|
|
172
|
+
};
|
|
173
|
+
/** THE ONE READ of a path that might not be there and might not be a file. */
|
|
174
|
+
export declare function readRegularPathIfPresent(path: string): RegularPathRead;
|
|
175
|
+
/**
|
|
176
|
+
* Refuse an open-for-WRITE of a path that is not a regular file. The hazard
|
|
177
|
+
* is symmetric: open(2) for writing on a FIFO with no reader blocks exactly
|
|
178
|
+
* as reading one with no writer does, so a staged write is as dangerous as a
|
|
179
|
+
* read. Returns the reason, or undefined when the path may be opened (absent
|
|
180
|
+
* included: creating it is the point).
|
|
181
|
+
*/
|
|
182
|
+
export declare function refuseOpenPathForWrite(path: string): string | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* THE KERNEL'S OWN WRITE-THEN-RENAME SCRATCH SUFFIXES.
|
|
185
|
+
*
|
|
186
|
+
* `FLEET_IGNORED` above is a DENYLIST of three directory prefixes, and
|
|
187
|
+
* `tiphys sync` derives "durable" as "not covered by it". That derivation is
|
|
188
|
+
* right for everything the ignore rules were written to cover and blind to a
|
|
189
|
+
* class they were never asked about: a transient artifact the KERNEL ITSELF
|
|
190
|
+
* creates beside a TRACKED target, which git therefore reports as an
|
|
191
|
+
* ordinary new durable path.
|
|
192
|
+
*
|
|
193
|
+
* The enumeration behind this list is every path the kernel writes under a
|
|
194
|
+
* fleet home, classified against `FLEET_IGNORED`. The three that land
|
|
195
|
+
* DURABLE without being fleet content are:
|
|
196
|
+
*
|
|
197
|
+
* status/current.json.tmp src/status.ts:142, a fixed name inside the
|
|
198
|
+
* tracked status/ directory.
|
|
199
|
+
* .cutover.<random>.tmp src/cutover.ts:278, a dot-prefixed random
|
|
200
|
+
* name at the fleet ROOT.
|
|
201
|
+
* tiphys-environment.json src/exclusion.ts:303, durable ON PURPOSE
|
|
202
|
+
* (M4-P21 criterion 3) and therefore NOT in
|
|
203
|
+
* this list.
|
|
204
|
+
*
|
|
205
|
+
* Two members, two directories, two naming shapes, which is why the rule is
|
|
206
|
+
* a SUFFIX and not a filename: a rule naming `current.json.tmp` would close
|
|
207
|
+
* the first and leave the second open.
|
|
208
|
+
*
|
|
209
|
+
* `FLEET_IGNORED` IS DELIBERATELY UNCHANGED. It drives `EPHEMERAL_DIRS`,
|
|
210
|
+
* `DURABLE_DIRS` and the `.gitignore` that `tiphys init` writes, so a glob
|
|
211
|
+
* added there would become a directory name `tiphys resume` tried to rebuild.
|
|
212
|
+
* Nothing stops being synced because of this constant; `tiphys sync` gains a
|
|
213
|
+
* refusal, and only for paths matching a suffix below.
|
|
214
|
+
*/
|
|
215
|
+
export declare const FLEET_SCRATCH_SUFFIXES: readonly string[];
|
|
216
|
+
/**
|
|
217
|
+
* True when a fleet-relative path is a kernel scratch artifact by its name
|
|
218
|
+
* alone. Name-only on purpose: `tiphys sync` asks this about a path git
|
|
219
|
+
* REPORTED, which may already have been renamed away by the time the
|
|
220
|
+
* question is asked, so a stat here would answer about a different world
|
|
221
|
+
* than the one being committed.
|
|
222
|
+
*/
|
|
223
|
+
export declare function isFleetScratchPath(relativePath: string): string | undefined;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Fleet-home layout (kernel plan v1, M1-P2 step 1; blueprint section 3 with
|
|
5
|
+
* the SC-002/SC-003 resolutions). The fleet home is a small git repository:
|
|
6
|
+
* durable content (charter/, decisions/, tasks/, backlog.md, package.json,
|
|
7
|
+
* .gitignore) is tracked; state/, worktrees/, and projects/ are ephemeral
|
|
8
|
+
* and gitignored (plan decision D-4, PR-004).
|
|
9
|
+
*/
|
|
10
|
+
export const FLEET_DIRS = [
|
|
11
|
+
"charter",
|
|
12
|
+
"decisions",
|
|
13
|
+
"state",
|
|
14
|
+
"tasks",
|
|
15
|
+
"worktrees",
|
|
16
|
+
"projects",
|
|
17
|
+
];
|
|
18
|
+
/** Files every fleet home carries at its root. */
|
|
19
|
+
export const FLEET_FILES = ["backlog.md", "package.json", ".gitignore"];
|
|
20
|
+
/**
|
|
21
|
+
* Exactly these entries are gitignored: clones under projects/ are
|
|
22
|
+
* recoverable from their remotes, worktrees/ are disposable, state/ holds
|
|
23
|
+
* beacons and locks (SC-002, plan decision D-4, PR-004). Nothing else.
|
|
24
|
+
*/
|
|
25
|
+
export const FLEET_IGNORED = ["state/", "worktrees/", "projects/"];
|
|
26
|
+
/**
|
|
27
|
+
* Well-known state file names. The lease lock is built by M1-P3 and the
|
|
28
|
+
* watcher beacon by M1-P5 (convention FM-043); doctor reads both as files
|
|
29
|
+
* only, never probing a process (plan constraint C-2). Task currency, when
|
|
30
|
+
* a later phase needs it, comes exclusively from tasks/<id>/meta.json and
|
|
31
|
+
* the turn-end file, never from a log tail (plan constraint C-1).
|
|
32
|
+
*/
|
|
33
|
+
export const LOCK_FILE = join("state", "orchestrator.lock");
|
|
34
|
+
export const BEACON_FILE = join("state", "watcher.beacon");
|
|
35
|
+
/**
|
|
36
|
+
* Return the layout entries missing from dir, in declaration order.
|
|
37
|
+
* Directories are reported with a trailing slash. An empty result means
|
|
38
|
+
* the layout is complete.
|
|
39
|
+
*/
|
|
40
|
+
export function missingLayoutEntries(dir) {
|
|
41
|
+
const missing = [];
|
|
42
|
+
for (const name of FLEET_DIRS) {
|
|
43
|
+
const p = join(dir, name);
|
|
44
|
+
if (!existsSync(p) || !statSync(p).isDirectory()) {
|
|
45
|
+
missing.push(`${name}/`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const name of FLEET_FILES) {
|
|
49
|
+
const p = join(dir, name);
|
|
50
|
+
if (!existsSync(p) || !statSync(p).isFile()) {
|
|
51
|
+
missing.push(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return missing;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate the layout at dir and return typed accessors. Throws an Error
|
|
58
|
+
* naming every missing entry when the layout is incomplete.
|
|
59
|
+
*/
|
|
60
|
+
export function loadFleet(dir) {
|
|
61
|
+
const root = resolve(dir);
|
|
62
|
+
const missing = missingLayoutEntries(root);
|
|
63
|
+
if (missing.length > 0) {
|
|
64
|
+
throw new Error(`not a fleet home: ${root} is missing ${missing.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
root,
|
|
68
|
+
charterDir: join(root, "charter"),
|
|
69
|
+
decisionsDir: join(root, "decisions"),
|
|
70
|
+
stateDir: join(root, "state"),
|
|
71
|
+
tasksDir: join(root, "tasks"),
|
|
72
|
+
worktreesDir: join(root, "worktrees"),
|
|
73
|
+
projectsDir: join(root, "projects"),
|
|
74
|
+
backlogPath: join(root, "backlog.md"),
|
|
75
|
+
packageJsonPath: join(root, "package.json"),
|
|
76
|
+
gitignorePath: join(root, ".gitignore"),
|
|
77
|
+
lockPath: join(root, LOCK_FILE),
|
|
78
|
+
beaconPath: join(root, BEACON_FILE),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Classify a layout path without opening it. lstat first, so a symlink is
|
|
83
|
+
* seen as a symlink; then stat, so a symlink to a directory is a directory
|
|
84
|
+
* and a dangling one is reported as such rather than as absent.
|
|
85
|
+
*/
|
|
86
|
+
export function classifyLayoutEntry(path) {
|
|
87
|
+
try {
|
|
88
|
+
lstatSync(path);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code === "ENOENT") {
|
|
92
|
+
return { kind: "absent" };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
kind: "unexaminable",
|
|
96
|
+
reason: `${path} could not be examined: ${String(error)}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
let stats;
|
|
100
|
+
try {
|
|
101
|
+
stats = statSync(path);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error.code === "ENOENT") {
|
|
105
|
+
return { kind: "other", reason: `${path} is a symlink to nothing` };
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
kind: "unexaminable",
|
|
109
|
+
reason: `${path} could not be examined: ${String(error)}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (stats.isDirectory()) {
|
|
113
|
+
return { kind: "directory" };
|
|
114
|
+
}
|
|
115
|
+
return { kind: "other", reason: `${path} exists and is not a directory` };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The EPHEMERAL directories: exactly the gitignored set, with the trailing
|
|
119
|
+
* slash that `.gitignore` needs stripped off. DERIVED from FLEET_IGNORED
|
|
120
|
+
* rather than listed again, because a second list is a second thing to keep
|
|
121
|
+
* in step and the first divergence would be silent: `tiphys resume` would
|
|
122
|
+
* rebuild one set while `.gitignore` ignored another.
|
|
123
|
+
*/
|
|
124
|
+
export const EPHEMERAL_DIRS = FLEET_IGNORED.map((entry) => entry.endsWith("/") ? entry.slice(0, -1) : entry);
|
|
125
|
+
/**
|
|
126
|
+
* The DURABLE directories: every fleet directory that is not ephemeral.
|
|
127
|
+
* A clone of a fleet home carries these and not the ephemeral ones, which
|
|
128
|
+
* is the fact `tiphys resume` exists to act on.
|
|
129
|
+
*/
|
|
130
|
+
export const DURABLE_DIRS = FLEET_DIRS.filter((name) => !EPHEMERAL_DIRS.includes(name));
|
|
131
|
+
/**
|
|
132
|
+
* The durable layout entries missing from dir, in declaration order:
|
|
133
|
+
* directories first with a trailing slash, then the root files. An empty
|
|
134
|
+
* result means the directory carries everything a clone of a fleet home
|
|
135
|
+
* carries, which is the precondition `tiphys resume` requires and never
|
|
136
|
+
* fabricates.
|
|
137
|
+
*/
|
|
138
|
+
export function missingDurableEntries(dir) {
|
|
139
|
+
const missing = [];
|
|
140
|
+
for (const name of DURABLE_DIRS) {
|
|
141
|
+
if (classifyLayoutEntry(join(dir, name)).kind !== "directory") {
|
|
142
|
+
missing.push(`${name}/`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const name of FLEET_FILES) {
|
|
146
|
+
const p = join(dir, name);
|
|
147
|
+
if (!existsSync(p) || !statSync(p).isFile()) {
|
|
148
|
+
missing.push(name);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return missing;
|
|
152
|
+
}
|
|
153
|
+
function describePathType(stats) {
|
|
154
|
+
if (stats.isDirectory()) {
|
|
155
|
+
return "a directory";
|
|
156
|
+
}
|
|
157
|
+
if (stats.isFIFO()) {
|
|
158
|
+
return "a named pipe";
|
|
159
|
+
}
|
|
160
|
+
if (stats.isSocket()) {
|
|
161
|
+
return "a socket";
|
|
162
|
+
}
|
|
163
|
+
if (stats.isCharacterDevice()) {
|
|
164
|
+
return "a character device";
|
|
165
|
+
}
|
|
166
|
+
if (stats.isBlockDevice()) {
|
|
167
|
+
return "a block device";
|
|
168
|
+
}
|
|
169
|
+
return "an entry of an unrecognized type";
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* lstat first, so a symlink is seen as a symlink; then stat, so a symlink to
|
|
173
|
+
* a regular file is regular and a dangling one is reported as such rather
|
|
174
|
+
* than as absent. The path is never opened.
|
|
175
|
+
*/
|
|
176
|
+
export function classifyPathEntry(path) {
|
|
177
|
+
try {
|
|
178
|
+
lstatSync(path);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error.code === "ENOENT") {
|
|
182
|
+
return { kind: "absent" };
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
kind: "unexaminable",
|
|
186
|
+
reason: `${path} could not be examined: ${String(error)}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
let stats;
|
|
190
|
+
try {
|
|
191
|
+
stats = statSync(path);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error.code === "ENOENT") {
|
|
195
|
+
return { kind: "dangling" };
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
kind: "unexaminable",
|
|
199
|
+
reason: `${path} could not be examined: ${String(error)}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (stats.isFile()) {
|
|
203
|
+
return { kind: "regular" };
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
kind: "irregular",
|
|
207
|
+
reason: `${path} is ${describePathType(stats)}, not a regular file, so it was not opened`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/** THE ONE READ of a path that might not be there and might not be a file. */
|
|
211
|
+
export function readRegularPathIfPresent(path) {
|
|
212
|
+
const entry = classifyPathEntry(path);
|
|
213
|
+
if (entry.kind === "absent" || entry.kind === "dangling") {
|
|
214
|
+
return { kind: "absent" };
|
|
215
|
+
}
|
|
216
|
+
if (entry.kind === "irregular" || entry.kind === "unexaminable") {
|
|
217
|
+
return { kind: "refused", reason: entry.reason };
|
|
218
|
+
}
|
|
219
|
+
let body;
|
|
220
|
+
try {
|
|
221
|
+
body = readFileSync(path, "utf8");
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (error.code === "ENOENT") {
|
|
225
|
+
// Removed between the probe and the read.
|
|
226
|
+
return { kind: "absent" };
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
kind: "refused",
|
|
230
|
+
reason: `${path} could not be read: ${String(error)}`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { kind: "read", body };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Refuse an open-for-WRITE of a path that is not a regular file. The hazard
|
|
237
|
+
* is symmetric: open(2) for writing on a FIFO with no reader blocks exactly
|
|
238
|
+
* as reading one with no writer does, so a staged write is as dangerous as a
|
|
239
|
+
* read. Returns the reason, or undefined when the path may be opened (absent
|
|
240
|
+
* included: creating it is the point).
|
|
241
|
+
*/
|
|
242
|
+
export function refuseOpenPathForWrite(path) {
|
|
243
|
+
const entry = classifyPathEntry(path);
|
|
244
|
+
if (entry.kind === "irregular" || entry.kind === "unexaminable") {
|
|
245
|
+
return entry.reason;
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
/* ------------------------------------------------------------------ */
|
|
250
|
+
/* The fleet home's SYNC CLASSES */
|
|
251
|
+
/* ------------------------------------------------------------------ */
|
|
252
|
+
/**
|
|
253
|
+
* THE KERNEL'S OWN WRITE-THEN-RENAME SCRATCH SUFFIXES.
|
|
254
|
+
*
|
|
255
|
+
* `FLEET_IGNORED` above is a DENYLIST of three directory prefixes, and
|
|
256
|
+
* `tiphys sync` derives "durable" as "not covered by it". That derivation is
|
|
257
|
+
* right for everything the ignore rules were written to cover and blind to a
|
|
258
|
+
* class they were never asked about: a transient artifact the KERNEL ITSELF
|
|
259
|
+
* creates beside a TRACKED target, which git therefore reports as an
|
|
260
|
+
* ordinary new durable path.
|
|
261
|
+
*
|
|
262
|
+
* The enumeration behind this list is every path the kernel writes under a
|
|
263
|
+
* fleet home, classified against `FLEET_IGNORED`. The three that land
|
|
264
|
+
* DURABLE without being fleet content are:
|
|
265
|
+
*
|
|
266
|
+
* status/current.json.tmp src/status.ts:142, a fixed name inside the
|
|
267
|
+
* tracked status/ directory.
|
|
268
|
+
* .cutover.<random>.tmp src/cutover.ts:278, a dot-prefixed random
|
|
269
|
+
* name at the fleet ROOT.
|
|
270
|
+
* tiphys-environment.json src/exclusion.ts:303, durable ON PURPOSE
|
|
271
|
+
* (M4-P21 criterion 3) and therefore NOT in
|
|
272
|
+
* this list.
|
|
273
|
+
*
|
|
274
|
+
* Two members, two directories, two naming shapes, which is why the rule is
|
|
275
|
+
* a SUFFIX and not a filename: a rule naming `current.json.tmp` would close
|
|
276
|
+
* the first and leave the second open.
|
|
277
|
+
*
|
|
278
|
+
* `FLEET_IGNORED` IS DELIBERATELY UNCHANGED. It drives `EPHEMERAL_DIRS`,
|
|
279
|
+
* `DURABLE_DIRS` and the `.gitignore` that `tiphys init` writes, so a glob
|
|
280
|
+
* added there would become a directory name `tiphys resume` tried to rebuild.
|
|
281
|
+
* Nothing stops being synced because of this constant; `tiphys sync` gains a
|
|
282
|
+
* refusal, and only for paths matching a suffix below.
|
|
283
|
+
*/
|
|
284
|
+
export const FLEET_SCRATCH_SUFFIXES = [
|
|
285
|
+
".tmp",
|
|
286
|
+
".stage",
|
|
287
|
+
".mutex",
|
|
288
|
+
];
|
|
289
|
+
/**
|
|
290
|
+
* True when a fleet-relative path is a kernel scratch artifact by its name
|
|
291
|
+
* alone. Name-only on purpose: `tiphys sync` asks this about a path git
|
|
292
|
+
* REPORTED, which may already have been renamed away by the time the
|
|
293
|
+
* question is asked, so a stat here would answer about a different world
|
|
294
|
+
* than the one being committed.
|
|
295
|
+
*/
|
|
296
|
+
export function isFleetScratchPath(relativePath) {
|
|
297
|
+
return FLEET_SCRATCH_SUFFIXES.find((suffix) => relativePath.endsWith(suffix));
|
|
298
|
+
}
|