@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.
Files changed (207) hide show
  1. package/AGENTS.md +663 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +299 -0
  4. package/checklists/clean-room.yaml +325 -0
  5. package/checklists/env-failure-diagnosis.yaml +68 -0
  6. package/checklists/flake-playbook.yaml +68 -0
  7. package/checklists/hazard-review.yaml +144 -0
  8. package/checklists/plan-review.yaml +103 -0
  9. package/dist/bin/tiphys.d.ts +2 -0
  10. package/dist/bin/tiphys.js +92 -0
  11. package/dist/src/adapters/load.d.ts +202 -0
  12. package/dist/src/adapters/load.js +440 -0
  13. package/dist/src/brief.d.ts +32 -0
  14. package/dist/src/brief.js +66 -0
  15. package/dist/src/checklists.d.ts +169 -0
  16. package/dist/src/checklists.js +310 -0
  17. package/dist/src/checks.d.ts +1539 -0
  18. package/dist/src/checks.js +5025 -0
  19. package/dist/src/cli.d.ts +9 -0
  20. package/dist/src/cli.js +68 -0
  21. package/dist/src/commands/brief.d.ts +92 -0
  22. package/dist/src/commands/brief.js +343 -0
  23. package/dist/src/commands/checklist.d.ts +42 -0
  24. package/dist/src/commands/checklist.js +168 -0
  25. package/dist/src/commands/cutover.d.ts +35 -0
  26. package/dist/src/commands/cutover.js +448 -0
  27. package/dist/src/commands/doctor.d.ts +264 -0
  28. package/dist/src/commands/doctor.js +1546 -0
  29. package/dist/src/commands/gates.d.ts +9 -0
  30. package/dist/src/commands/gates.js +360 -0
  31. package/dist/src/commands/init.d.ts +30 -0
  32. package/dist/src/commands/init.js +177 -0
  33. package/dist/src/commands/lock.d.ts +34 -0
  34. package/dist/src/commands/lock.js +340 -0
  35. package/dist/src/commands/mode.d.ts +40 -0
  36. package/dist/src/commands/mode.js +134 -0
  37. package/dist/src/commands/next.d.ts +130 -0
  38. package/dist/src/commands/next.js +597 -0
  39. package/dist/src/commands/plan.d.ts +20 -0
  40. package/dist/src/commands/plan.js +105 -0
  41. package/dist/src/commands/pool.d.ts +1 -0
  42. package/dist/src/commands/pool.js +139 -0
  43. package/dist/src/commands/resume.d.ts +1 -0
  44. package/dist/src/commands/resume.js +88 -0
  45. package/dist/src/commands/spawn.d.ts +1 -0
  46. package/dist/src/commands/spawn.js +195 -0
  47. package/dist/src/commands/status.d.ts +21 -0
  48. package/dist/src/commands/status.js +122 -0
  49. package/dist/src/commands/sync.d.ts +47 -0
  50. package/dist/src/commands/sync.js +341 -0
  51. package/dist/src/commands/teardown.d.ts +1 -0
  52. package/dist/src/commands/teardown.js +87 -0
  53. package/dist/src/commands/tuition.d.ts +21 -0
  54. package/dist/src/commands/tuition.js +218 -0
  55. package/dist/src/commands/validate.d.ts +78 -0
  56. package/dist/src/commands/validate.js +430 -0
  57. package/dist/src/commands/watch.d.ts +1 -0
  58. package/dist/src/commands/watch.js +172 -0
  59. package/dist/src/cutover.d.ts +584 -0
  60. package/dist/src/cutover.js +1444 -0
  61. package/dist/src/exclusion.d.ts +389 -0
  62. package/dist/src/exclusion.js +843 -0
  63. package/dist/src/exec/env.d.ts +278 -0
  64. package/dist/src/exec/env.js +334 -0
  65. package/dist/src/fleet.d.ts +223 -0
  66. package/dist/src/fleet.js +298 -0
  67. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  68. package/dist/src/gates/adapters/http-json.js +283 -0
  69. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  70. package/dist/src/gates/adapters/migrations-command.js +373 -0
  71. package/dist/src/gates/citations.d.ts +408 -0
  72. package/dist/src/gates/citations.js +1169 -0
  73. package/dist/src/gates/coverage.d.ts +375 -0
  74. package/dist/src/gates/coverage.js +836 -0
  75. package/dist/src/gates/credentials.d.ts +233 -0
  76. package/dist/src/gates/credentials.js +752 -0
  77. package/dist/src/gates/deploy.d.ts +1 -0
  78. package/dist/src/gates/deploy.js +33 -0
  79. package/dist/src/gates/gate-classes.d.ts +56 -0
  80. package/dist/src/gates/gate-classes.js +633 -0
  81. package/dist/src/gates/manifest.d.ts +99 -0
  82. package/dist/src/gates/manifest.js +208 -0
  83. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  84. package/dist/src/gates/merge-preconditions.js +932 -0
  85. package/dist/src/gates/migrations.d.ts +1 -0
  86. package/dist/src/gates/migrations.js +36 -0
  87. package/dist/src/gates/pin.d.ts +114 -0
  88. package/dist/src/gates/pin.js +154 -0
  89. package/dist/src/gates/red-witness.d.ts +22 -0
  90. package/dist/src/gates/red-witness.js +482 -0
  91. package/dist/src/gates/release.d.ts +283 -0
  92. package/dist/src/gates/release.js +820 -0
  93. package/dist/src/gates/result.d.ts +116 -0
  94. package/dist/src/gates/result.js +91 -0
  95. package/dist/src/gates/run.d.ts +614 -0
  96. package/dist/src/gates/run.js +1614 -0
  97. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  98. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  99. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  100. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  101. package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
  102. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  103. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  104. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  105. package/dist/src/gates/scope.d.ts +131 -0
  106. package/dist/src/gates/scope.js +1018 -0
  107. package/dist/src/gates/suite.d.ts +217 -0
  108. package/dist/src/gates/suite.js +968 -0
  109. package/dist/src/gates/validate.d.ts +121 -0
  110. package/dist/src/gates/validate.js +414 -0
  111. package/dist/src/hooks.d.ts +84 -0
  112. package/dist/src/hooks.js +125 -0
  113. package/dist/src/index.d.ts +31 -0
  114. package/dist/src/index.js +30 -0
  115. package/dist/src/liveness.d.ts +321 -0
  116. package/dist/src/liveness.js +396 -0
  117. package/dist/src/lock.d.ts +256 -0
  118. package/dist/src/lock.js +792 -0
  119. package/dist/src/model-resolution.d.ts +159 -0
  120. package/dist/src/model-resolution.js +307 -0
  121. package/dist/src/modes.d.ts +149 -0
  122. package/dist/src/modes.js +258 -0
  123. package/dist/src/path-identity.d.ts +34 -0
  124. package/dist/src/path-identity.js +48 -0
  125. package/dist/src/plan.d.ts +73 -0
  126. package/dist/src/plan.js +153 -0
  127. package/dist/src/pool.d.ts +326 -0
  128. package/dist/src/pool.js +988 -0
  129. package/dist/src/roles.d.ts +461 -0
  130. package/dist/src/roles.js +776 -0
  131. package/dist/src/spawn.d.ts +482 -0
  132. package/dist/src/spawn.js +1003 -0
  133. package/dist/src/status.d.ts +116 -0
  134. package/dist/src/status.js +148 -0
  135. package/dist/src/task.d.ts +504 -0
  136. package/dist/src/task.js +307 -0
  137. package/dist/src/teardown.d.ts +39 -0
  138. package/dist/src/teardown.js +422 -0
  139. package/dist/src/tuition.d.ts +159 -0
  140. package/dist/src/tuition.js +311 -0
  141. package/dist/src/validate.d.ts +263 -0
  142. package/dist/src/validate.js +742 -0
  143. package/dist/src/version.d.ts +3 -0
  144. package/dist/src/version.js +38 -0
  145. package/dist/src/watcher.d.ts +275 -0
  146. package/dist/src/watcher.js +849 -0
  147. package/dist/src/witness/run.d.ts +299 -0
  148. package/dist/src/witness/run.js +1373 -0
  149. package/dist/src/witness/spec.d.ts +270 -0
  150. package/dist/src/witness/spec.js +475 -0
  151. package/dist/tsconfig.src.tsbuildinfo +1 -0
  152. package/gate-registry.yaml +526 -0
  153. package/gates.manifest.json +257 -0
  154. package/package.json +72 -3
  155. package/role-model-config.yaml +88 -0
  156. package/roles/README.md +128 -0
  157. package/roles/_shared-dispatch-contract.md +87 -0
  158. package/roles/adversarial-plan-reviewer.md +80 -0
  159. package/roles/clean-room-reviewer.md +140 -0
  160. package/roles/implementer.md +463 -0
  161. package/roles/investigator.md +138 -0
  162. package/roles/plan-writer.md +95 -0
  163. package/schemas/README.md +82 -0
  164. package/schemas/assurance-modes.schema.json +264 -0
  165. package/schemas/charter.schema.json +185 -0
  166. package/schemas/checklist.schema.json +114 -0
  167. package/schemas/cutover-state.schema.json +64 -0
  168. package/schemas/decision-record.schema.json +88 -0
  169. package/schemas/executor-record.schema.json +36 -0
  170. package/schemas/final-report.schema.json +90 -0
  171. package/schemas/finding.schema.json +106 -0
  172. package/schemas/gate-registry.schema.json +260 -0
  173. package/schemas/mechanism-index.schema.json +94 -0
  174. package/schemas/model-resolution.schema.json +362 -0
  175. package/schemas/plan.schema.json +300 -0
  176. package/schemas/report.schema.json +579 -0
  177. package/schemas/role-brief.schema.json +105 -0
  178. package/schemas/role-model-config.schema.json +90 -0
  179. package/schemas/status-line.schema.json +40 -0
  180. package/schemas/tuition.schema.json +191 -0
  181. package/schemas/verdict.schema.json +295 -0
  182. package/schemas/work-history.schema.json +183 -0
  183. package/schemas/write-bypass.schema.json +69 -0
  184. package/templates/charter.example.yaml +54 -0
  185. package/templates/decision-record.example.yaml +27 -0
  186. package/templates/final-report.example.yaml +80 -0
  187. package/templates/plan.example.yaml +87 -0
  188. package/templates/report.example.yaml +236 -0
  189. package/templates/warnings.md +74 -0
  190. package/templates/work-history.example.yaml +185 -0
  191. package/tuition/README.md +76 -0
  192. package/tuition/T-001.yaml +48 -0
  193. package/tuition/T-002.yaml +51 -0
  194. package/tuition/T-003.yaml +100 -0
  195. package/tuition/T-004.yaml +52 -0
  196. package/tuition/T-005.yaml +72 -0
  197. package/tuition/T-006.yaml +81 -0
  198. package/tuition/T-007.yaml +56 -0
  199. package/tuition/T-008.yaml +111 -0
  200. package/tuition/T-009.yaml +50 -0
  201. package/tuition/T-015.yaml +36 -0
  202. package/tuition/T-016.yaml +36 -0
  203. package/tuition/T-017.yaml +46 -0
  204. package/tuition/T-018.yaml +84 -0
  205. package/tuition/T-021.yaml +40 -0
  206. package/tuition/T-022.yaml +36 -0
  207. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `tiphys plan project --phase-id <id> [--plan <file>] [--out <dir>]`
3
+ * (kernel plan M3, M3-P1 step 2; D-M3-18).
4
+ *
5
+ * Emits the M2-P4 scope auditor's phase declaration from the plan, so the
6
+ * auditor's input becomes a GENERATED VIEW of one source instead of a second
7
+ * hand-authored source that can drift.
8
+ *
9
+ * The output path is `<out>/<phase-id-lowercased>.json`, defaulting to
10
+ * `delivery/plan/phase-declarations`, which is the directory
11
+ * `gates.manifest.json` passes to the auditor as `--declarations`.
12
+ *
13
+ * With no `--out` the document is written; with `--stdout` it is printed and
14
+ * nothing is written, which is what a test or a reviewer wants when checking
15
+ * WHAT would be emitted without touching the tree.
16
+ *
17
+ * Every path this command touches is operator-supplied and is classified
18
+ * before it is opened or written (D-M3-27).
19
+ */
20
+ import { mkdirSync, writeFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { EX_USAGE } from "../cli.js";
23
+ import { projectPhase, renderDeclaration } from "../plan.js";
24
+ import { refuseOpenForWrite } from "../task.js";
25
+ import { classifyContextDirectory, decodeDocument, readOperatorPath, } from "../validate.js";
26
+ const USAGE = "usage: tiphys plan project --phase-id <id> [--plan <file>] " +
27
+ "[--out <dir>] [--stdout]";
28
+ const DEFAULT_PLAN = "templates/plan.example.yaml";
29
+ const DEFAULT_OUT = join("delivery", "plan", "phase-declarations");
30
+ function parseArgs(argv) {
31
+ const args = { plan: DEFAULT_PLAN, out: DEFAULT_OUT, toStdout: false };
32
+ for (let index = 0; index < argv.length; index += 1) {
33
+ const flag = argv[index];
34
+ if (flag === "--stdout") {
35
+ args.toStdout = true;
36
+ continue;
37
+ }
38
+ const value = argv[index + 1];
39
+ if (flag !== "--phase-id" && flag !== "--plan" && flag !== "--out") {
40
+ return { usageError: `unknown option ${flag}` };
41
+ }
42
+ if (value === undefined || value.startsWith("--")) {
43
+ return { usageError: `${flag} requires a value` };
44
+ }
45
+ index += 1;
46
+ if (flag === "--phase-id")
47
+ args.phaseId = value;
48
+ else if (flag === "--plan")
49
+ args.plan = value;
50
+ else
51
+ args.out = value;
52
+ }
53
+ if (args.phaseId === undefined) {
54
+ return { usageError: "--phase-id is required" };
55
+ }
56
+ return { args };
57
+ }
58
+ export function cmdPlan(argv) {
59
+ const [subcommand, ...rest] = argv;
60
+ if (subcommand !== "project") {
61
+ process.stderr.write(`${USAGE}\n`);
62
+ return EX_USAGE;
63
+ }
64
+ const parsed = parseArgs(rest);
65
+ if (parsed.args === undefined) {
66
+ process.stderr.write(`tiphys plan project: ${parsed.usageError ?? "usage error"}\n${USAGE}\n`);
67
+ return EX_USAGE;
68
+ }
69
+ const { phaseId, plan, out, toStdout } = parsed.args;
70
+ const read = readOperatorPath(plan);
71
+ if (!read.ok) {
72
+ process.stderr.write(`tiphys plan project: ${read.reason}\n`);
73
+ return 1;
74
+ }
75
+ const decoded = decodeDocument(read.body, plan);
76
+ if (!decoded.ok) {
77
+ process.stderr.write(`tiphys plan project: ${decoded.reason}\n`);
78
+ return 1;
79
+ }
80
+ const projection = projectPhase(decoded.value, phaseId);
81
+ if (!projection.ok) {
82
+ process.stderr.write(`tiphys plan project: ${projection.reason}\n`);
83
+ return 1;
84
+ }
85
+ const body = renderDeclaration(projection.declaration);
86
+ if (toStdout) {
87
+ process.stdout.write(body);
88
+ return 0;
89
+ }
90
+ mkdirSync(out, { recursive: true });
91
+ const problem = classifyContextDirectory(out);
92
+ if (problem !== undefined) {
93
+ process.stderr.write(`tiphys plan project: ${problem}\n`);
94
+ return 1;
95
+ }
96
+ const target = join(out, projection.filename);
97
+ const refusal = refuseOpenForWrite(target);
98
+ if (refusal !== undefined) {
99
+ process.stderr.write(`tiphys plan project: ${refusal}\n`);
100
+ return 1;
101
+ }
102
+ writeFileSync(target, body, "utf8");
103
+ process.stdout.write(`${target}\n`);
104
+ return 0;
105
+ }
@@ -0,0 +1 @@
1
+ export declare function cmdPool(args: string[]): Promise<number>;
@@ -0,0 +1,139 @@
1
+ import { EX_USAGE } from "../cli.js";
2
+ import { loadFleet } from "../fleet.js";
3
+ import { poolCreate, poolDestroy, poolList } from "../pool.js";
4
+ /**
5
+ * tiphys pool <create --task <id> --project <path> [--offline] | list |
6
+ * destroy --task <id> [--discard]> (kernel plan v1, M1-P3 steps 3-4).
7
+ * Runs in a fleet home (cwd). create emits the fetched base SHA as its
8
+ * only stdout line (EXT-F-03 step 3; M1-P4 spawn captures it).
9
+ */
10
+ const USAGE = "usage: tiphys pool <create --task <id> --project <path> [--offline] | " +
11
+ "list | destroy --task <id> [--discard] [--delete-branch-force]>";
12
+ function usageError(message) {
13
+ if (message !== undefined) {
14
+ process.stderr.write(`tiphys pool: ${message}\n`);
15
+ }
16
+ process.stderr.write(`${USAGE}\n`);
17
+ return EX_USAGE;
18
+ }
19
+ function parseFlags(rest) {
20
+ const parsed = {
21
+ task: undefined,
22
+ project: undefined,
23
+ offline: false,
24
+ discard: false,
25
+ deleteBranchForce: false,
26
+ };
27
+ for (let i = 0; i < rest.length; i += 1) {
28
+ const flag = rest[i];
29
+ if (flag === "--task" && i + 1 < rest.length) {
30
+ parsed.task = rest[i + 1];
31
+ i += 1;
32
+ }
33
+ else if (flag === "--project" && i + 1 < rest.length) {
34
+ parsed.project = rest[i + 1];
35
+ i += 1;
36
+ }
37
+ else if (flag === "--offline") {
38
+ parsed.offline = true;
39
+ }
40
+ else if (flag === "--discard") {
41
+ parsed.discard = true;
42
+ }
43
+ else if (flag === "--delete-branch-force") {
44
+ parsed.deleteBranchForce = true;
45
+ }
46
+ else {
47
+ return undefined;
48
+ }
49
+ }
50
+ return parsed;
51
+ }
52
+ export async function cmdPool(args) {
53
+ const [subcommand, ...rest] = args;
54
+ if (subcommand === undefined) {
55
+ return usageError();
56
+ }
57
+ const flags = parseFlags(rest);
58
+ if (flags === undefined) {
59
+ return usageError();
60
+ }
61
+ let fleet;
62
+ try {
63
+ fleet = loadFleet(process.cwd());
64
+ }
65
+ catch (error) {
66
+ process.stderr.write(`tiphys pool: ${error.message}\n`);
67
+ return 1;
68
+ }
69
+ switch (subcommand) {
70
+ case "create": {
71
+ if (flags.task === undefined ||
72
+ flags.project === undefined ||
73
+ flags.discard ||
74
+ flags.deleteBranchForce) {
75
+ return usageError("create requires --task <id> and --project <path>");
76
+ }
77
+ const result = await poolCreate(fleet, {
78
+ taskId: flags.task,
79
+ project: flags.project,
80
+ offline: flags.offline,
81
+ });
82
+ if (!result.ok) {
83
+ process.stderr.write(`tiphys pool: ${result.reason}\n`);
84
+ return 1;
85
+ }
86
+ process.stdout.write(`${result.value.baseSha}\n`);
87
+ return 0;
88
+ }
89
+ case "list": {
90
+ if (flags.task !== undefined ||
91
+ flags.project !== undefined ||
92
+ flags.offline ||
93
+ flags.discard ||
94
+ flags.deleteBranchForce) {
95
+ return usageError("list takes no flags");
96
+ }
97
+ for (const entry of poolList(fleet)) {
98
+ // M4-P19: an entry whose pool record did not survive a reclaim is
99
+ // MARKED. The original two-field line is unchanged, so nothing
100
+ // that reads this output learns a new field it did not ask for,
101
+ // and a reconstruction can never be read as an original.
102
+ let marker = "";
103
+ if (entry.origin === "reconstructed") {
104
+ marker = " reconstructed";
105
+ }
106
+ else if (entry.origin === "unreconstructable") {
107
+ marker = ` unreconstructable (unresolved: ${(entry.unresolved ?? []).join(", ")})`;
108
+ }
109
+ process.stdout.write(`${entry.taskId} ${entry.headSha}${marker}\n`);
110
+ }
111
+ return 0;
112
+ }
113
+ case "destroy": {
114
+ if (flags.task === undefined || flags.project !== undefined || flags.offline) {
115
+ return usageError("destroy requires --task <id>");
116
+ }
117
+ const result = await poolDestroy(fleet, {
118
+ taskId: flags.task,
119
+ discard: flags.discard,
120
+ deleteBranchForce: flags.deleteBranchForce,
121
+ });
122
+ if (!result.ok) {
123
+ process.stderr.write(`tiphys pool: ${result.reason}\n`);
124
+ return 1;
125
+ }
126
+ // Name the deleted branch and its tip on success: that sha is the
127
+ // operator's only recovery handle if the destroy was a mistake
128
+ // (V-1), and it stays valid until the clone is garbage collected.
129
+ const { deletedBranch, deletedSha } = result.value;
130
+ const suffix = deletedBranch === undefined
131
+ ? ""
132
+ : ` (deleted branch ${deletedBranch} was ${deletedSha ?? "unknown"})`;
133
+ process.stdout.write(`destroyed ${flags.task}${suffix}\n`);
134
+ return 0;
135
+ }
136
+ default:
137
+ return usageError(`unknown pool subcommand "${subcommand}"`);
138
+ }
139
+ }
@@ -0,0 +1 @@
1
+ export declare function cmdResume(args: string[]): number;
@@ -0,0 +1,88 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { EX_USAGE } from "../cli.js";
4
+ import { EPHEMERAL_DIRS, classifyLayoutEntry, missingDurableEntries, } from "../fleet.js";
5
+ /**
6
+ * The plan, or the one reason it cannot be made. Deciding every entry BEFORE
7
+ * creating any of them is what makes criterion 2's "creates nothing" true of
8
+ * the refusal arms: a refusal discovered halfway through a creating loop has
9
+ * already changed the tree.
10
+ */
11
+ function planRebuild(root) {
12
+ const steps = [];
13
+ for (const name of EPHEMERAL_DIRS) {
14
+ const entry = classifyLayoutEntry(join(root, name));
15
+ if (entry.kind === "directory") {
16
+ steps.push({ name, rebuild: false });
17
+ continue;
18
+ }
19
+ if (entry.kind === "absent") {
20
+ steps.push({ name, rebuild: true });
21
+ continue;
22
+ }
23
+ return {
24
+ refusal: `tiphys resume: ${root} cannot be rehydrated: ${entry.reason}, ` +
25
+ `and resume never removes anything to make room`,
26
+ };
27
+ }
28
+ return { steps };
29
+ }
30
+ export function cmdResume(args) {
31
+ const [dir, ...extra] = args;
32
+ if (extra.length > 0) {
33
+ process.stderr.write("usage: tiphys resume [dir]\n");
34
+ return EX_USAGE;
35
+ }
36
+ const root = resolve(dir ?? ".");
37
+ const rootEntry = classifyLayoutEntry(root);
38
+ if (rootEntry.kind !== "directory") {
39
+ process.stderr.write(`tiphys resume: ${root} is not a directory, so there is no fleet home here to rebuild\n`);
40
+ return 1;
41
+ }
42
+ /* THE FIRST PRECONDITION: a fleet home is a git repository, and a clone of
43
+ one is what this command exists to repair. A directory with no `.git` is
44
+ not a fleet clone whatever else it holds, and creating three directories
45
+ inside it would be a success report about a fleet that does not exist. */
46
+ const gitEntry = classifyLayoutEntry(join(root, ".git"));
47
+ if (gitEntry.kind === "absent") {
48
+ process.stderr.write(`tiphys resume: ${root} is not a git repository, .git is absent, ` +
49
+ `so it is not a cloned fleet home; run tiphys init <dir> to create one\n`);
50
+ return 1;
51
+ }
52
+ if (gitEntry.kind === "unexaminable") {
53
+ process.stderr.write(`tiphys resume: ${gitEntry.reason}\n`);
54
+ return 1;
55
+ }
56
+ /* THE SECOND PRECONDITION, and it is a different failure from the first.
57
+ A git repository that is not a fleet home (any other clone, including the
58
+ kernel's own) passes the `.git` test. Rebuilding the ephemeral three
59
+ inside it would litter a stranger's repository and report success. The
60
+ durable entries are what a clone of a fleet home carries, so their absence
61
+ means either "not a fleet home" or "a fleet home that has lost durable
62
+ content", and resume fabricates NEITHER: durable content comes back from
63
+ the remote, never from this command. */
64
+ const missingDurable = missingDurableEntries(root);
65
+ if (missingDurable.length > 0) {
66
+ process.stderr.write(`tiphys resume: ${root} is a git repository but not a fleet home, ` +
67
+ `missing ${missingDurable.join(", ")}; durable content is restored by ` +
68
+ `fetching from the remote, never by resume\n`);
69
+ return 1;
70
+ }
71
+ const planned = planRebuild(root);
72
+ if ("refusal" in planned) {
73
+ process.stderr.write(`${planned.refusal}\n`);
74
+ return 1;
75
+ }
76
+ /* PER ENTRY, never all-or-nothing. An interrupted resume leaves a
77
+ half-rebuilt layout, and that half is the common case rather than an edge:
78
+ rebuilding the set because one member is missing is how a live worktree
79
+ that survived the reclaim gets reported as rebuilt when it was not. */
80
+ for (const step of planned.steps) {
81
+ if (!step.rebuild) {
82
+ continue;
83
+ }
84
+ mkdirSync(join(root, step.name));
85
+ process.stdout.write(`REBUILT ${step.name}/\n`);
86
+ }
87
+ return 0;
88
+ }
@@ -0,0 +1 @@
1
+ export declare function cmdSpawn(args: string[]): Promise<number>;
@@ -0,0 +1,195 @@
1
+ import { EX_USAGE } from "../cli.js";
2
+ import { loadFleet } from "../fleet.js";
3
+ import { warnIfWatcherStale } from "../liveness.js";
4
+ import { spawnTask } from "../spawn.js";
5
+ import { singleLine } from "../task.js";
6
+ /**
7
+ * tiphys spawn --task <id> --project <path> --brief <file> --shape
8
+ * ship|scout --exec <cmd> [--deadline <seconds>] [--offline]
9
+ * [--role <name>] [--tier <name>] [--phase <id>] [--adapter <specifier>]
10
+ * (kernel plan v1, M1-P4 step 4; --role, --tier and --phase added by
11
+ * M4-P3 and --adapter by M4-P4). Runs in a fleet home (cwd).
12
+ *
13
+ * --exec is REQUIRED in M1: spawn without it exits 64 with usage,
14
+ * because the multiplexer-window adapter that would make an exec-less
15
+ * spawn meaningful is M4-era work (PR-013). The payload runs to
16
+ * completion before this command returns (plan constraint C-3: nothing
17
+ * is auto-backgrounded).
18
+ *
19
+ * --offline is passed through to pool create, and is a deviation from
20
+ * the plan's enumerated flag list, recorded in the M1-P4 work history:
21
+ * meta.json's baseOffline field (criterion 13 of M1-P3, this phase's
22
+ * named obligation) can only ever be true for a worktree created under
23
+ * pool create --offline, and spawn is the only command that creates the
24
+ * worktree a task's meta describes.
25
+ */
26
+ const USAGE = "usage: tiphys spawn --task <id> --project <path> --brief <file> " +
27
+ "--shape ship|scout --exec <cmd> [--deadline <seconds>] [--offline] " +
28
+ "[--role <name>] [--tier <name>] [--phase <id>] [--adapter <specifier>]";
29
+ function usageError(message) {
30
+ if (message !== undefined) {
31
+ process.stderr.write(`tiphys spawn: ${message}\n`);
32
+ }
33
+ process.stderr.write(`${USAGE}\n`);
34
+ return EX_USAGE;
35
+ }
36
+ function parseFlags(args) {
37
+ const parsed = {
38
+ task: undefined,
39
+ project: undefined,
40
+ brief: undefined,
41
+ shape: undefined,
42
+ exec: undefined,
43
+ deadlineSeconds: undefined,
44
+ offline: false,
45
+ role: undefined,
46
+ tier: undefined,
47
+ phase: undefined,
48
+ adapter: undefined,
49
+ };
50
+ for (let i = 0; i < args.length; i += 1) {
51
+ const flag = args[i];
52
+ const value = args[i + 1];
53
+ if (flag === "--task" && value !== undefined) {
54
+ parsed.task = value;
55
+ i += 1;
56
+ }
57
+ else if (flag === "--project" && value !== undefined) {
58
+ parsed.project = value;
59
+ i += 1;
60
+ }
61
+ else if (flag === "--brief" && value !== undefined) {
62
+ parsed.brief = value;
63
+ i += 1;
64
+ }
65
+ else if (flag === "--shape" && value !== undefined) {
66
+ if (value !== "ship" && value !== "scout") {
67
+ return undefined;
68
+ }
69
+ parsed.shape = value;
70
+ i += 1;
71
+ }
72
+ else if (flag === "--exec" && value !== undefined) {
73
+ parsed.exec = value;
74
+ i += 1;
75
+ }
76
+ else if (flag === "--deadline" && value !== undefined) {
77
+ const seconds = Number(value);
78
+ // N-403: finite and positive is not enough. The adapter turns the
79
+ // deadline into an instant, and any value at or above roughly
80
+ // 8.64e12 seconds is outside the Date range, so it used to raise
81
+ // INSIDE the adapter, after pool create had made a worktree, a
82
+ // branch and a pool record. A deadline this kernel cannot
83
+ // represent is a usage error, and a usage error creates nothing.
84
+ if (!Number.isFinite(seconds) ||
85
+ seconds <= 0 ||
86
+ !Number.isFinite(new Date(Date.now() + seconds * 1000).getTime())) {
87
+ return undefined;
88
+ }
89
+ parsed.deadlineSeconds = seconds;
90
+ i += 1;
91
+ }
92
+ else if (flag === "--role" && value !== undefined) {
93
+ parsed.role = value;
94
+ i += 1;
95
+ }
96
+ else if (flag === "--tier" && value !== undefined) {
97
+ // VERBATIM, and deliberately unvalidated: this is the DECLARED TIER,
98
+ // whatever role-model-config.yaml declares, and the tier-to-model
99
+ // mapping lives in the plugin. A kernel that checked this value
100
+ // against a list would be holding the vocabulary the plugin owns.
101
+ parsed.tier = value;
102
+ i += 1;
103
+ }
104
+ else if (flag === "--phase" && value !== undefined) {
105
+ // CARRIED, never derived from the branch name (M4-D-22 is open).
106
+ parsed.phase = value;
107
+ i += 1;
108
+ }
109
+ else if (flag === "--adapter" && value !== undefined) {
110
+ // VERBATIM, and it is a MODULE SPECIFIER rather than a path: the
111
+ // kernel resolves it with Node module resolution rooted at the fleet
112
+ // home (src/adapters/load.ts), which is the one root that is
113
+ // owner-controlled. Nothing is checked here, because a check here
114
+ // would run against this process's own resolution root, which is the
115
+ // kernel checkout, and that is precisely the root this phase exists to
116
+ // keep out of the answer.
117
+ parsed.adapter = value;
118
+ i += 1;
119
+ }
120
+ else if (flag === "--offline") {
121
+ parsed.offline = true;
122
+ }
123
+ else {
124
+ return undefined;
125
+ }
126
+ }
127
+ return parsed;
128
+ }
129
+ export async function cmdSpawn(args) {
130
+ const flags = parseFlags(args);
131
+ if (flags === undefined) {
132
+ return usageError();
133
+ }
134
+ if (flags.task === undefined ||
135
+ flags.project === undefined ||
136
+ flags.brief === undefined ||
137
+ flags.shape === undefined) {
138
+ return usageError("spawn requires --task <id> --project <path> --brief <file> --shape ship|scout");
139
+ }
140
+ if (flags.exec === undefined) {
141
+ // PR-013: an exec-less spawn has no meaning in M1.
142
+ return usageError("spawn requires --exec <cmd> in M1");
143
+ }
144
+ let fleet;
145
+ try {
146
+ fleet = loadFleet(process.cwd());
147
+ }
148
+ catch (error) {
149
+ process.stderr.write(`tiphys spawn: ${singleLine(error.message)}\n`);
150
+ return 1;
151
+ }
152
+ // Liveness guard (M1-P5 step 2), completing the seam this phase left.
153
+ // It WARNS and never blocks: one stderr line when work is in flight
154
+ // and supervision has gone quiet, and the command then does exactly
155
+ // what it would have done anyway (blueprint liveness-guard contract,
156
+ // criteria 10 and 11).
157
+ warnIfWatcherStale(fleet);
158
+ const result = await spawnTask(fleet, {
159
+ taskId: flags.task,
160
+ project: flags.project,
161
+ briefFile: flags.brief,
162
+ shape: flags.shape,
163
+ exec: flags.exec,
164
+ deadlineSeconds: flags.deadlineSeconds,
165
+ offline: flags.offline,
166
+ /*
167
+ * PROJECT, ALWAYS, AND THE CLI HAS NO FLAG TO SAY OTHERWISE (M4-P8
168
+ * step 2). `tiphys spawn` launches a payload into a project worktree,
169
+ * and the declared credential escape hatch is not reachable from this
170
+ * command at all: `allowPrCredentials` is a library-seam field with no
171
+ * flag behind it, which is exactly why the orchestrator class is not
172
+ * offered here. An operator who could type --payload-class orchestrator
173
+ * would be able to ask for the pairing spawnTask exists to refuse.
174
+ */
175
+ payloadClass: "project",
176
+ role: flags.role,
177
+ declaredTier: flags.tier,
178
+ phaseId: flags.phase,
179
+ adapterSpecifier: flags.adapter,
180
+ });
181
+ if (!result.ok) {
182
+ // One reason line, structurally (CR-303): a reason may carry a
183
+ // raised error message or git output, and an operator or the M1-P6
184
+ // harness reading "the reason line" must get all of it.
185
+ process.stderr.write(`tiphys spawn: ${singleLine(result.reason)}\n`);
186
+ return 1;
187
+ }
188
+ // The payload's exit code is reported, not adopted: the task's state
189
+ // authority is meta.json plus the turn-end record (plan constraint
190
+ // C-1), and a nonzero payload is a completed task with a failing
191
+ // payload, not a failed spawn.
192
+ const { meta, exitCode } = result.value;
193
+ process.stdout.write(`spawned ${meta.id} worktree ${meta.worktree} exec exited ${String(exitCode)}\n`);
194
+ return 0;
195
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `tiphys status <emit | show>` (kernel plan M3, M3-P1 step 6; R-084).
3
+ *
4
+ * tiphys status emit --run <id> --state <state> [--project <name>]
5
+ * [--detail <text>] [--ref <r>]...
6
+ * tiphys status show
7
+ *
8
+ * Runs in a fleet home (cwd). `emit` composes a record, VALIDATES it against
9
+ * the shipped `status-line` schema before writing anything, appends one line
10
+ * to `state/status/stream.jsonl`, then rewrites the DURABLE
11
+ * `status/current.json` atomically. `show` reads `current.json` ONLY and
12
+ * never opens the stream (constraint C-1: never read current state from the
13
+ * tail of an append-only log). The two documents sit on opposite sides of the
14
+ * fleet `.gitignore` since M4-D-13, which is why the paths differ in prefix;
15
+ * src/status.ts carries the reasoning.
16
+ *
17
+ * The state vocabulary is closed and is enforced, not requested: `--state
18
+ * progress` is refused naming the permitted values, because R-084's
19
+ * sparseness is worth nothing if the emitter accepts whatever it is handed.
20
+ */
21
+ export declare function cmdStatus(argv: string[]): number;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `tiphys status <emit | show>` (kernel plan M3, M3-P1 step 6; R-084).
3
+ *
4
+ * tiphys status emit --run <id> --state <state> [--project <name>]
5
+ * [--detail <text>] [--ref <r>]...
6
+ * tiphys status show
7
+ *
8
+ * Runs in a fleet home (cwd). `emit` composes a record, VALIDATES it against
9
+ * the shipped `status-line` schema before writing anything, appends one line
10
+ * to `state/status/stream.jsonl`, then rewrites the DURABLE
11
+ * `status/current.json` atomically. `show` reads `current.json` ONLY and
12
+ * never opens the stream (constraint C-1: never read current state from the
13
+ * tail of an append-only log). The two documents sit on opposite sides of the
14
+ * fleet `.gitignore` since M4-D-13, which is why the paths differ in prefix;
15
+ * src/status.ts carries the reasoning.
16
+ *
17
+ * The state vocabulary is closed and is enforced, not requested: `--state
18
+ * progress` is refused naming the permitted values, because R-084's
19
+ * sparseness is worth nothing if the emitter accepts whatever it is handed.
20
+ */
21
+ import { basename } from "node:path";
22
+ import { EX_USAGE } from "../cli.js";
23
+ import { loadFleet } from "../fleet.js";
24
+ import { loadTypeSchema } from "./validate.js";
25
+ import { formatDiagnostics, validateInstance } from "../validate.js";
26
+ import { STATUS_STATES, emitStatus, makeStatusRecord, readCurrent, renderStatus, } from "../status.js";
27
+ const USAGE = "usage: tiphys status <emit --run <id> --state <" +
28
+ STATUS_STATES.join("|") +
29
+ "> [--project <name>] [--detail <text>] [--ref <r>] | show>";
30
+ function parseEmit(argv) {
31
+ const args = { refs: [] };
32
+ for (let index = 0; index < argv.length; index += 1) {
33
+ const flag = argv[index];
34
+ const value = argv[index + 1];
35
+ if (flag !== "--run" &&
36
+ flag !== "--state" &&
37
+ flag !== "--project" &&
38
+ flag !== "--detail" &&
39
+ flag !== "--ref" &&
40
+ flag !== "--at") {
41
+ return { usageError: `unknown option ${flag}` };
42
+ }
43
+ if (value === undefined) {
44
+ return { usageError: `${flag} requires a value` };
45
+ }
46
+ index += 1;
47
+ if (flag === "--run")
48
+ args.run = value;
49
+ else if (flag === "--state")
50
+ args.state = value;
51
+ else if (flag === "--project")
52
+ args.project = value;
53
+ else if (flag === "--detail")
54
+ args.detail = value;
55
+ else if (flag === "--at")
56
+ args.at = value;
57
+ else
58
+ args.refs.push(value);
59
+ }
60
+ if (args.run === undefined) {
61
+ return { usageError: "--run is required" };
62
+ }
63
+ if (args.state === undefined) {
64
+ return { usageError: "--state is required" };
65
+ }
66
+ return { args };
67
+ }
68
+ export function cmdStatus(argv) {
69
+ const [subcommand, ...rest] = argv;
70
+ if (subcommand === "show") {
71
+ if (rest.length > 0) {
72
+ process.stderr.write(`tiphys status show: takes no arguments\n${USAGE}\n`);
73
+ return EX_USAGE;
74
+ }
75
+ const fleet = loadFleet(process.cwd());
76
+ const current = readCurrent(fleet.root);
77
+ if (!current.ok) {
78
+ process.stderr.write(`tiphys status show: ${current.reason}\n`);
79
+ return 1;
80
+ }
81
+ process.stdout.write(`${renderStatus(current.record)}\n`);
82
+ return 0;
83
+ }
84
+ if (subcommand !== "emit") {
85
+ process.stderr.write(`${USAGE}\n`);
86
+ return EX_USAGE;
87
+ }
88
+ const parsed = parseEmit(rest);
89
+ if (parsed.args === undefined) {
90
+ process.stderr.write(`tiphys status emit: ${parsed.usageError ?? "usage error"}\n${USAGE}\n`);
91
+ return EX_USAGE;
92
+ }
93
+ const fleet = loadFleet(process.cwd());
94
+ const record = makeStatusRecord({
95
+ run: parsed.args.run,
96
+ project: parsed.args.project ?? basename(fleet.root),
97
+ state: parsed.args.state,
98
+ detail: parsed.args.detail,
99
+ refs: parsed.args.refs,
100
+ at: parsed.args.at,
101
+ });
102
+ /* The record is validated against the SHIPPED schema before anything is
103
+ written. Validating a composed record against the same document the
104
+ validator command uses is what makes the closed state vocabulary one
105
+ rule rather than two: `--state progress` fails here for exactly the
106
+ reason it would fail `tiphys validate --type status-line`. */
107
+ const diagnostics = validateInstance(loadTypeSchema("status-line"), record);
108
+ if (diagnostics.length > 0) {
109
+ for (const line of formatDiagnostics(diagnostics)) {
110
+ process.stderr.write(`${line}\n`);
111
+ }
112
+ process.stderr.write(`tiphys status emit: the permitted states are ${STATUS_STATES.join(", ")}\n`);
113
+ return 1;
114
+ }
115
+ const emitted = emitStatus(fleet.root, record);
116
+ if (!emitted.ok) {
117
+ process.stderr.write(`tiphys status emit: ${emitted.reason}\n`);
118
+ return 1;
119
+ }
120
+ process.stdout.write(`${renderStatus(emitted.record)}\n`);
121
+ return 0;
122
+ }