@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,47 @@
|
|
|
1
|
+
/** The remote a fleet home pushes to unless told otherwise. */
|
|
2
|
+
export declare const DEFAULT_REMOTE = "origin";
|
|
3
|
+
/**
|
|
4
|
+
* The commit subject, and it carries NO PATHS ON PURPOSE (criterion 6,
|
|
5
|
+
* CLAUDE.md binding convention 7: commit messages carry no AI model or tool
|
|
6
|
+
* names). A message composed from the paths it commits inherits whatever
|
|
7
|
+
* those paths are named, and a fleet's decision records and task directories
|
|
8
|
+
* are named after the things they decide, which routinely includes the
|
|
9
|
+
* harness or the model a decision is about. A count cannot carry a name.
|
|
10
|
+
*/
|
|
11
|
+
export declare function syncCommitMessage(count: number): string;
|
|
12
|
+
/** One path git reports as changed, with its two status columns. */
|
|
13
|
+
export interface ChangedPath {
|
|
14
|
+
path: string;
|
|
15
|
+
/** The index column (X). A space means unstaged; `?` means untracked. */
|
|
16
|
+
index: string;
|
|
17
|
+
/** The worktree column (Y). */
|
|
18
|
+
worktree: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse `git status --porcelain=v1 -z --untracked-files=all`.
|
|
22
|
+
*
|
|
23
|
+
* The NUL form is the only safe one: a path holding a space, a quote or a
|
|
24
|
+
* newline is printed raw here and is C-quoted in the newline form, so the
|
|
25
|
+
* newline form would need an unquoting pass that is its own defect surface.
|
|
26
|
+
* A rename or copy record carries TWO paths, the new one first and the
|
|
27
|
+
* original second, and both are returned: a rename out of the durable half
|
|
28
|
+
* into the ephemeral half must be visible as a change to both names.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parsePorcelainStatus(payload: string): ChangedPath[];
|
|
31
|
+
/** The `.gitignore` rule that makes one path ephemeral. */
|
|
32
|
+
export interface IgnoreRule {
|
|
33
|
+
source: string;
|
|
34
|
+
line: string;
|
|
35
|
+
pattern: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse `git check-ignore --no-index -v -z --stdin`, whose output is a flat
|
|
39
|
+
* NUL-separated stream of four fields per MATCHING path: source, line
|
|
40
|
+
* number, pattern, pathname. Paths that match no rule are absent from the
|
|
41
|
+
* output entirely, which is what makes the result a set of the ephemeral
|
|
42
|
+
* ones rather than a verdict per input.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseCheckIgnore(payload: string): Map<string, IgnoreRule>;
|
|
45
|
+
/** How an ignore rule is named in every line this command prints. */
|
|
46
|
+
export declare function renderRule(rule: IgnoreRule): string;
|
|
47
|
+
export declare function cmdSync(argv: string[]): number;
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { EX_USAGE } from "../cli.js";
|
|
3
|
+
import { FLEET_SCRATCH_SUFFIXES, isFleetScratchPath, loadFleet } from "../fleet.js";
|
|
4
|
+
import { MACHINE_IDENTITY_EMAIL, MACHINE_IDENTITY_NAME } from "./init.js";
|
|
5
|
+
/**
|
|
6
|
+
* `tiphys sync`: commit the durable half of a fleet home and push it
|
|
7
|
+
* (kernel plan M4, M4-P18).
|
|
8
|
+
*
|
|
9
|
+
* WHAT THIS IS THE EXECUTABLE HALF OF. AGENTS.md's
|
|
10
|
+
* `fleet-state-commit-discipline` clause says durable state is committed and
|
|
11
|
+
* pushed at the moment it changes, and that the PUSH is the half that gets
|
|
12
|
+
* dropped. Until this phase the whole clause was discharged by an agent
|
|
13
|
+
* remembering to run git, which is the shape this repository has recorded
|
|
14
|
+
* three times: a rule that depends on remembering does not survive a busy
|
|
15
|
+
* session, and the answer is a mechanism.
|
|
16
|
+
*
|
|
17
|
+
* THE DURABLE SET IS DERIVED, NEVER LISTED HERE (criterion 2). A second list
|
|
18
|
+
* of ephemeral prefixes inside this file would be a second thing to keep in
|
|
19
|
+
* step with the fleet's own `.gitignore`, and the first divergence would be
|
|
20
|
+
* silent: a prefix added to the ignore set would keep being committed by a
|
|
21
|
+
* command that had never heard of it. So the question "is this path
|
|
22
|
+
* ephemeral" is answered by GIT, against the fleet `.gitignore` that
|
|
23
|
+
* `tiphys init` wrote, and this module holds no prefix list at all.
|
|
24
|
+
*
|
|
25
|
+
* `--no-index` IS THE LOAD-BEARING FLAG AND THE REASON IS THE HAZARD ITSELF.
|
|
26
|
+
* `git check-ignore` without it answers "would git ignore this path", and a
|
|
27
|
+
* TRACKED file is never ignored whatever `.gitignore` says, so it answers 1
|
|
28
|
+
* for exactly the file this command must exclude. The dangerous state is a
|
|
29
|
+
* lease or a scratch file that got tracked once (by a `git add -f`, or by
|
|
30
|
+
* being committed before the prefix was ignored): from then on every `git
|
|
31
|
+
* add -A` re-commits it, and a fleet's lease travels through the remote to
|
|
32
|
+
* environments that must rebuild it rather than restore it. With
|
|
33
|
+
* `--no-index` the answer is about the RULES, which is the question being
|
|
34
|
+
* asked. Measured contract in witness/captures/m4-p18-git-contracts.txt.
|
|
35
|
+
*
|
|
36
|
+
* WHY A STAGED EPHEMERAL PATH IS A REFUSAL AND NOT A SKIP (criterion 4).
|
|
37
|
+
* This command stages the durable paths it enumerated and then commits, and
|
|
38
|
+
* `git commit` commits the INDEX, not the pathspec it was handed. An
|
|
39
|
+
* operator who ran `git add -A` first has already put the lease in the
|
|
40
|
+
* index, so committing at all would commit it. Unstaging on the operator's
|
|
41
|
+
* behalf is a destructive act on work this command did not create, so the
|
|
42
|
+
* refusal names the path and the rule that makes it ephemeral, and commits
|
|
43
|
+
* nothing.
|
|
44
|
+
*
|
|
45
|
+
* SUBSTRATE-NEUTRAL (DR-0007) and C-2 clean: pure git and filesystem, no
|
|
46
|
+
* process probing, no pid, no signal.
|
|
47
|
+
*/
|
|
48
|
+
const USAGE = "usage: tiphys sync [--remote <name>]";
|
|
49
|
+
/** The remote a fleet home pushes to unless told otherwise. */
|
|
50
|
+
export const DEFAULT_REMOTE = "origin";
|
|
51
|
+
/**
|
|
52
|
+
* The commit subject, and it carries NO PATHS ON PURPOSE (criterion 6,
|
|
53
|
+
* CLAUDE.md binding convention 7: commit messages carry no AI model or tool
|
|
54
|
+
* names). A message composed from the paths it commits inherits whatever
|
|
55
|
+
* those paths are named, and a fleet's decision records and task directories
|
|
56
|
+
* are named after the things they decide, which routinely includes the
|
|
57
|
+
* harness or the model a decision is about. A count cannot carry a name.
|
|
58
|
+
*/
|
|
59
|
+
export function syncCommitMessage(count) {
|
|
60
|
+
return `tiphys sync: ${String(count)} durable path(s)`;
|
|
61
|
+
}
|
|
62
|
+
function runGit(root, args, options = {}) {
|
|
63
|
+
const run = spawnSync("git", ["-C", root, ...args], {
|
|
64
|
+
encoding: "utf8",
|
|
65
|
+
input: options.input,
|
|
66
|
+
env: options.extraEnv === undefined
|
|
67
|
+
? process.env
|
|
68
|
+
: { ...process.env, ...options.extraEnv },
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
status: run.status,
|
|
72
|
+
stdout: run.stdout ?? "",
|
|
73
|
+
stderr: run.stderr ?? "",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parse `git status --porcelain=v1 -z --untracked-files=all`.
|
|
78
|
+
*
|
|
79
|
+
* The NUL form is the only safe one: a path holding a space, a quote or a
|
|
80
|
+
* newline is printed raw here and is C-quoted in the newline form, so the
|
|
81
|
+
* newline form would need an unquoting pass that is its own defect surface.
|
|
82
|
+
* A rename or copy record carries TWO paths, the new one first and the
|
|
83
|
+
* original second, and both are returned: a rename out of the durable half
|
|
84
|
+
* into the ephemeral half must be visible as a change to both names.
|
|
85
|
+
*/
|
|
86
|
+
export function parsePorcelainStatus(payload) {
|
|
87
|
+
const fields = payload.split("\0");
|
|
88
|
+
const found = [];
|
|
89
|
+
for (let index = 0; index < fields.length; index += 1) {
|
|
90
|
+
const record = fields[index];
|
|
91
|
+
if (record === "") {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const x = record.slice(0, 1);
|
|
95
|
+
const y = record.slice(1, 2);
|
|
96
|
+
const path = record.slice(3);
|
|
97
|
+
found.push({ path, index: x, worktree: y });
|
|
98
|
+
if (x === "R" || x === "C" || y === "R" || y === "C") {
|
|
99
|
+
const original = fields[index + 1];
|
|
100
|
+
index += 1;
|
|
101
|
+
if (original !== undefined && original !== "") {
|
|
102
|
+
found.push({ path: original, index: x, worktree: y });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return found;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Parse `git check-ignore --no-index -v -z --stdin`, whose output is a flat
|
|
110
|
+
* NUL-separated stream of four fields per MATCHING path: source, line
|
|
111
|
+
* number, pattern, pathname. Paths that match no rule are absent from the
|
|
112
|
+
* output entirely, which is what makes the result a set of the ephemeral
|
|
113
|
+
* ones rather than a verdict per input.
|
|
114
|
+
*/
|
|
115
|
+
export function parseCheckIgnore(payload) {
|
|
116
|
+
const fields = payload.split("\0");
|
|
117
|
+
const found = new Map();
|
|
118
|
+
for (let index = 0; index + 3 < fields.length; index += 4) {
|
|
119
|
+
const source = fields[index];
|
|
120
|
+
const line = fields[index + 1];
|
|
121
|
+
const pattern = fields[index + 2];
|
|
122
|
+
const path = fields[index + 3];
|
|
123
|
+
if (path === "") {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
found.set(path, { source, line, pattern });
|
|
127
|
+
}
|
|
128
|
+
return found;
|
|
129
|
+
}
|
|
130
|
+
/** How an ignore rule is named in every line this command prints. */
|
|
131
|
+
export function renderRule(rule) {
|
|
132
|
+
return `${rule.source}:${rule.line} ${rule.pattern}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Ask git which of these paths the fleet `.gitignore` covers.
|
|
136
|
+
*
|
|
137
|
+
* THE THREE EXIT CODES ARE NOT TWO. 0 means at least one path matched, 1
|
|
138
|
+
* means none did, and 128 means git could not answer at all. Folding 128
|
|
139
|
+
* into "nothing is ephemeral" is how a command reports a clean sync while
|
|
140
|
+
* committing a lease, so it is a refusal here and the exit code is named.
|
|
141
|
+
*/
|
|
142
|
+
function classify(root, paths) {
|
|
143
|
+
if (paths.length === 0) {
|
|
144
|
+
return { ok: true, ephemeral: new Map() };
|
|
145
|
+
}
|
|
146
|
+
const run = runGit(root, ["check-ignore", "--no-index", "-v", "-z", "--stdin"], {
|
|
147
|
+
input: `${paths.join("\0")}\0`,
|
|
148
|
+
});
|
|
149
|
+
if (run.status === 0) {
|
|
150
|
+
return { ok: true, ephemeral: parseCheckIgnore(run.stdout) };
|
|
151
|
+
}
|
|
152
|
+
if (run.status === 1) {
|
|
153
|
+
return { ok: true, ephemeral: new Map() };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
reason: `git check-ignore exited ${String(run.status)} and could not say which paths are ephemeral, ` +
|
|
158
|
+
`so nothing was committed: ${run.stderr.split("\n")[0] ?? ""}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function parseArgs(argv) {
|
|
162
|
+
let remote = DEFAULT_REMOTE;
|
|
163
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
164
|
+
const flag = argv[index];
|
|
165
|
+
if (flag !== "--remote") {
|
|
166
|
+
return { usageError: `unknown option ${flag}` };
|
|
167
|
+
}
|
|
168
|
+
const value = argv[index + 1];
|
|
169
|
+
if (value === undefined) {
|
|
170
|
+
return { usageError: "--remote requires a value" };
|
|
171
|
+
}
|
|
172
|
+
remote = value;
|
|
173
|
+
index += 1;
|
|
174
|
+
}
|
|
175
|
+
return { remote };
|
|
176
|
+
}
|
|
177
|
+
export function cmdSync(argv) {
|
|
178
|
+
const parsed = parseArgs(argv);
|
|
179
|
+
if (parsed.remote === undefined) {
|
|
180
|
+
process.stderr.write(`tiphys sync: ${parsed.usageError ?? "usage error"}\n${USAGE}\n`);
|
|
181
|
+
return EX_USAGE;
|
|
182
|
+
}
|
|
183
|
+
const remote = parsed.remote;
|
|
184
|
+
const fleet = loadFleet(process.cwd());
|
|
185
|
+
/* THE REMOTE IS ESTABLISHED BEFORE ANYTHING IS COMMITTED. The discipline
|
|
186
|
+
is commit AND push; a sync that commits and then discovers there is
|
|
187
|
+
nowhere to push has done the half that gets dropped and reported the
|
|
188
|
+
half that does not. */
|
|
189
|
+
const remotes = runGit(fleet.root, ["remote"]);
|
|
190
|
+
if (remotes.status !== 0) {
|
|
191
|
+
process.stderr.write(`tiphys sync: git remote failed in ${fleet.root}: ${remotes.stderr.split("\n")[0] ?? ""}\n`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
const known = remotes.stdout.split("\n").map((line) => line.trim());
|
|
195
|
+
if (!known.includes(remote)) {
|
|
196
|
+
process.stderr.write(`tiphys sync: ${fleet.root} has no remote named ${remote}, so durable state cannot be pushed; ` +
|
|
197
|
+
`add one with git remote add ${remote} <url>, nothing was committed\n`);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
const status = runGit(fleet.root, [
|
|
201
|
+
"status",
|
|
202
|
+
"--porcelain=v1",
|
|
203
|
+
"-z",
|
|
204
|
+
"--untracked-files=all",
|
|
205
|
+
]);
|
|
206
|
+
if (status.status !== 0) {
|
|
207
|
+
process.stderr.write(`tiphys sync: git status failed in ${fleet.root}: ${status.stderr.split("\n")[0] ?? ""}\n`);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
const changed = parsePorcelainStatus(status.stdout);
|
|
211
|
+
const classified = classify(fleet.root, [...new Set(changed.map((entry) => entry.path))]);
|
|
212
|
+
if (!classified.ok) {
|
|
213
|
+
process.stderr.write(`tiphys sync: ${classified.reason}\n`);
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
const ephemeral = classified.ephemeral;
|
|
217
|
+
/* THE SCRATCH REFUSAL RUNS BEFORE THE STAGED-EPHEMERAL ONE, and the order
|
|
218
|
+
is stated because it is a choice. A fleet carrying both a stray scratch
|
|
219
|
+
file and a staged ephemeral path reports the scratch file and stops; both
|
|
220
|
+
arms refuse and commit nothing, so no path is committed either way, and
|
|
221
|
+
the operator sees one refusal at a time rather than two. It also keeps the
|
|
222
|
+
staged-ephemeral block the last `return 1` before `const durable`, which
|
|
223
|
+
is where witness/sync-staged-lease-refused.json aims its second member. */
|
|
224
|
+
/* THE DENYLIST IS THREE DIRECTORY PREFIXES AND THE QUESTION IT ANSWERS IS
|
|
225
|
+
NARROWER THAN THE ONE THIS COMMAND ASKS.
|
|
226
|
+
|
|
227
|
+
`git check-ignore --no-index` answers "is this path covered by a rule
|
|
228
|
+
someone wrote down". This command reads that as "is this path meant to
|
|
229
|
+
be committed". Those are the same question only for paths someone
|
|
230
|
+
thought to declare, and the fleet ignore set (src/fleet.ts:29) declares
|
|
231
|
+
three directory prefixes and nothing else. Anything the kernel writes
|
|
232
|
+
outside them is DURABLE BY DEFAULT.
|
|
233
|
+
|
|
234
|
+
Two paths the kernel itself creates land there, and neither is fleet
|
|
235
|
+
content: the atomic-rename scratch file src/status.ts:142 writes beside
|
|
236
|
+
its TRACKED target, and the one src/cutover.ts:278 writes at the fleet
|
|
237
|
+
ROOT under a random name. Both are write-then-rename scratch, both are
|
|
238
|
+
left behind by the failure this project's own tuition calls ordinary
|
|
239
|
+
rather than exceptional (a process killed mid-write), and once either is
|
|
240
|
+
committed it is tracked forever by ordinary git semantics.
|
|
241
|
+
|
|
242
|
+
A REFUSAL AND NOT A SILENT EXCLUSION, for the reason criterion 4 already
|
|
243
|
+
gives about a staged ephemeral path: this command must not quietly drop
|
|
244
|
+
a path an operator can see in `git status`. The cost is stated rather
|
|
245
|
+
than discovered: a `tiphys sync` that races a live `tiphys status emit`
|
|
246
|
+
now fails with a named path instead of committing a temp file, and the
|
|
247
|
+
operator re-runs.
|
|
248
|
+
|
|
249
|
+
THE FLEET IGNORE SET IS NOT WIDENED TO DO THIS, and this module still
|
|
250
|
+
holds no copy of it. That constant also drives `EPHEMERAL_DIRS`,
|
|
251
|
+
`DURABLE_DIRS` and the `.gitignore` `tiphys init` writes, so a glob
|
|
252
|
+
added there would become a directory name `tiphys resume` tried to
|
|
253
|
+
rebuild. Nothing stops being synced; this is a second, separately named
|
|
254
|
+
rule about SUFFIXES, declared once in src/fleet.ts and read from there,
|
|
255
|
+
exactly as the ignore question is asked of git rather than answered
|
|
256
|
+
here. */
|
|
257
|
+
const scratch = [...new Set(changed.map((entry) => entry.path))]
|
|
258
|
+
.filter((path) => !ephemeral.has(path))
|
|
259
|
+
.map((path) => ({ path, suffix: isFleetScratchPath(path) }))
|
|
260
|
+
.filter((candidate) => candidate.suffix !== undefined)
|
|
261
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
262
|
+
if (scratch.length > 0) {
|
|
263
|
+
for (const candidate of scratch) {
|
|
264
|
+
process.stderr.write(`tiphys sync: ${candidate.path} ends in ${candidate.suffix}, which is a ` +
|
|
265
|
+
`write-then-rename scratch suffix this kernel uses (${FLEET_SCRATCH_SUFFIXES.join(", ")}), ` +
|
|
266
|
+
`and no fleet .gitignore rule covers it, so committing it would track it forever; ` +
|
|
267
|
+
`remove it and re-run, nothing was committed\n`);
|
|
268
|
+
}
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
/* CRITERION 4, and the order matters: every staged ephemeral path is
|
|
272
|
+
reported before anything is staged or committed, so the refusal is a
|
|
273
|
+
statement about the tree as the operator left it. */
|
|
274
|
+
const stagedEphemeral = changed.filter((entry) => ephemeral.has(entry.path) && entry.index !== " " && entry.index !== "?");
|
|
275
|
+
if (stagedEphemeral.length > 0) {
|
|
276
|
+
for (const entry of stagedEphemeral) {
|
|
277
|
+
const rule = ephemeral.get(entry.path);
|
|
278
|
+
process.stderr.write(`tiphys sync: ${entry.path} is staged and is ephemeral by ${renderRule(rule)}; ` +
|
|
279
|
+
`unstage it with git restore --staged -- ${entry.path} and re-run, nothing was committed\n`);
|
|
280
|
+
}
|
|
281
|
+
return 1;
|
|
282
|
+
}
|
|
283
|
+
const durable = [
|
|
284
|
+
...new Set(changed
|
|
285
|
+
.map((entry) => entry.path)
|
|
286
|
+
.filter((path) => !ephemeral.has(path))),
|
|
287
|
+
].sort();
|
|
288
|
+
const excluded = [
|
|
289
|
+
...new Set(changed.map((entry) => entry.path).filter((path) => ephemeral.has(path))),
|
|
290
|
+
].sort();
|
|
291
|
+
/* The excluded paths are PRINTED WITH THE RULE THAT EXCLUDED THEM. A
|
|
292
|
+
command that silently drops paths is indistinguishable from one that
|
|
293
|
+
never saw them, and the rule reference is what makes the derivation
|
|
294
|
+
observable rather than asserted. */
|
|
295
|
+
for (const path of excluded) {
|
|
296
|
+
process.stdout.write(`EXCLUDED ${path} ${renderRule(ephemeral.get(path))}\n`);
|
|
297
|
+
}
|
|
298
|
+
if (durable.length === 0) {
|
|
299
|
+
process.stdout.write("NOTHING TO COMMIT\n");
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const added = runGit(fleet.root, ["add", "--", ...durable]);
|
|
303
|
+
if (added.status !== 0) {
|
|
304
|
+
process.stderr.write(`tiphys sync: git add failed in ${fleet.root}: ${added.stderr.split("\n")[0] ?? ""}\n`);
|
|
305
|
+
return 1;
|
|
306
|
+
}
|
|
307
|
+
const committed = runGit(fleet.root, ["commit", "-m", syncCommitMessage(durable.length)], {
|
|
308
|
+
/* The documented deterministic machine identity, command-scoped,
|
|
309
|
+
exactly as the bootstrap commit does it: CI runners have no git
|
|
310
|
+
identity and this must never touch user or global config
|
|
311
|
+
(CLAUDE.md standing warning 5). */
|
|
312
|
+
extraEnv: {
|
|
313
|
+
GIT_AUTHOR_NAME: MACHINE_IDENTITY_NAME,
|
|
314
|
+
GIT_AUTHOR_EMAIL: MACHINE_IDENTITY_EMAIL,
|
|
315
|
+
GIT_COMMITTER_NAME: MACHINE_IDENTITY_NAME,
|
|
316
|
+
GIT_COMMITTER_EMAIL: MACHINE_IDENTITY_EMAIL,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
if (committed.status !== 0) {
|
|
320
|
+
process.stderr.write(`tiphys sync: git commit failed in ${fleet.root}: ${committed.stderr.split("\n")[0] ?? ""}\n`);
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
for (const path of durable) {
|
|
324
|
+
process.stdout.write(`COMMITTED ${path}\n`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/* THE PUSH ARM IS NOT OPTIONAL AND ITS FAILURE IS NOT A WARNING. The
|
|
328
|
+
failure carries GIT'S OWN first stderr line rather than a message
|
|
329
|
+
composed here: a retry signature or a diagnosis derived from a
|
|
330
|
+
hand-written example is the failure T-003 and CLAUDE.md standing warning
|
|
331
|
+
10 both record. */
|
|
332
|
+
const pushed = runGit(fleet.root, ["push", remote, "HEAD"]);
|
|
333
|
+
if (pushed.status !== 0) {
|
|
334
|
+
const first = pushed.stderr.split("\n")[0] ?? "";
|
|
335
|
+
process.stderr.write(`tiphys sync: git push to ${remote} exited ${String(pushed.status)}, ` +
|
|
336
|
+
`so the durable state is committed locally and NOT pushed: ${first}\n`);
|
|
337
|
+
return 1;
|
|
338
|
+
}
|
|
339
|
+
process.stdout.write(`PUSHED ${remote}\n`);
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cmdTeardown(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { EX_USAGE } from "../cli.js";
|
|
2
|
+
import { loadFleet } from "../fleet.js";
|
|
3
|
+
import { warnIfWatcherStale } from "../liveness.js";
|
|
4
|
+
import { singleLine } from "../task.js";
|
|
5
|
+
import { teardownTask } from "../teardown.js";
|
|
6
|
+
/**
|
|
7
|
+
* tiphys teardown --task <id> [--salvage] (kernel plan v1, M1-P4
|
|
8
|
+
* step 5). Runs in a fleet home (cwd).
|
|
9
|
+
*
|
|
10
|
+
* Every refusal is exit nonzero plus a SINGLE reason line naming the
|
|
11
|
+
* blocking condition. A failure of the destroy itself is passed through
|
|
12
|
+
* with the pool's own wording, which distinguishes a refusal (a true
|
|
13
|
+
* no-op) from a partial failure (the worktree is already gone and the
|
|
14
|
+
* survivors are enumerated); this command adds no framing of its own to
|
|
15
|
+
* either, because the two must never be described alike.
|
|
16
|
+
*/
|
|
17
|
+
const USAGE = "usage: tiphys teardown --task <id> [--salvage] [--from-reconstructed]";
|
|
18
|
+
function usageError(message) {
|
|
19
|
+
if (message !== undefined) {
|
|
20
|
+
process.stderr.write(`tiphys teardown: ${message}\n`);
|
|
21
|
+
}
|
|
22
|
+
process.stderr.write(`${USAGE}\n`);
|
|
23
|
+
return EX_USAGE;
|
|
24
|
+
}
|
|
25
|
+
function parseFlags(args) {
|
|
26
|
+
const parsed = {
|
|
27
|
+
task: undefined,
|
|
28
|
+
salvage: false,
|
|
29
|
+
fromReconstructed: false,
|
|
30
|
+
};
|
|
31
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
32
|
+
const flag = args[i];
|
|
33
|
+
const value = args[i + 1];
|
|
34
|
+
if (flag === "--task" && value !== undefined) {
|
|
35
|
+
parsed.task = value;
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
else if (flag === "--salvage") {
|
|
39
|
+
parsed.salvage = true;
|
|
40
|
+
}
|
|
41
|
+
else if (flag === "--from-reconstructed") {
|
|
42
|
+
parsed.fromReconstructed = true;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
export async function cmdTeardown(args) {
|
|
51
|
+
const flags = parseFlags(args);
|
|
52
|
+
if (flags === undefined) {
|
|
53
|
+
return usageError();
|
|
54
|
+
}
|
|
55
|
+
if (flags.task === undefined) {
|
|
56
|
+
return usageError("teardown requires --task <id>");
|
|
57
|
+
}
|
|
58
|
+
let fleet;
|
|
59
|
+
try {
|
|
60
|
+
fleet = loadFleet(process.cwd());
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
process.stderr.write(`tiphys teardown: ${singleLine(error.message)}\n`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
// Liveness guard (M1-P5 step 2): one stderr warning line when work is
|
|
67
|
+
// in flight and supervision has gone quiet, and then the teardown
|
|
68
|
+
// proceeds exactly as it would have. It never blocks and never changes
|
|
69
|
+
// an exit code (criteria 10 and 11).
|
|
70
|
+
warnIfWatcherStale(fleet);
|
|
71
|
+
const result = await teardownTask(fleet, {
|
|
72
|
+
taskId: flags.task,
|
|
73
|
+
salvage: flags.salvage,
|
|
74
|
+
fromReconstructed: flags.fromReconstructed,
|
|
75
|
+
});
|
|
76
|
+
if (!result.ok) {
|
|
77
|
+
// Plan step 5: "every refusal is exit nonzero plus a single reason
|
|
78
|
+
// line". Enforced here structurally (CR-303) rather than trusted of
|
|
79
|
+
// every reason string, because reasons interpolate git output and a
|
|
80
|
+
// raised error message, neither of which is guaranteed to be short.
|
|
81
|
+
process.stderr.write(`tiphys teardown: ${singleLine(result.reason)}\n`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
const suffix = result.value.salvaged ? " (leavings salvaged and pushed)" : "";
|
|
85
|
+
process.stdout.write(`torn down ${result.value.taskId}${suffix}\n`);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys tuition add|list|index` (kernel plan M3, M3-P8 steps 2 and 6;
|
|
3
|
+
* R-091).
|
|
4
|
+
*
|
|
5
|
+
* add --file <entry> [--into <dir>] validate an entry and file it
|
|
6
|
+
* list [--kernel-relevant] [--dir <d>] one line per entry
|
|
7
|
+
* index [--out <f>] [--check] [--dir <d>] project the mechanism index
|
|
8
|
+
*
|
|
9
|
+
* PROMOTION IS NOT A SUBCOMMAND, and that is a decision rather than an
|
|
10
|
+
* omission. R-091 says kernel-relevant tuition ships upstream as a kernel pull
|
|
11
|
+
* request; the kernel never opens pull requests, so promotion is a documented
|
|
12
|
+
* orchestrator procedure (M3-P9's `AGENTS.md`) and building a promoter that M3
|
|
13
|
+
* would use once is the M1-P3 mistake.
|
|
14
|
+
*
|
|
15
|
+
* `add` WRITES WITH AN EXCLUSIVE CREATE, so a second entry claiming an id that
|
|
16
|
+
* already exists fails loudly and names the file rather than overwriting a
|
|
17
|
+
* record. A `T-nnn` id is never renumbered and never reused after retirement,
|
|
18
|
+
* which makes an overwrite a silent loss of exactly the artifact this feed
|
|
19
|
+
* exists to keep.
|
|
20
|
+
*/
|
|
21
|
+
export declare function cmdTuition(argv: string[]): number;
|