@tiphys/kernel 0.0.0 → 0.1.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 +611 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +278 -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 +14 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +59 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +828 -0
- package/dist/src/checks.js +3314 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +57 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +320 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/doctor.d.ts +35 -0
- package/dist/src/commands/doctor.js +605 -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 +128 -0
- package/dist/src/commands/lock.d.ts +1 -0
- package/dist/src/commands/lock.js +229 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -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 +128 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +146 -0
- package/dist/src/commands/status.d.ts +19 -0
- package/dist/src/commands/status.js +120 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +79 -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 +360 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/exec/env.d.ts +128 -0
- package/dist/src/exec/env.js +190 -0
- package/dist/src/fleet.d.ts +51 -0
- package/dist/src/fleet.js +80 -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 +1163 -0
- package/dist/src/gates/coverage.d.ts +284 -0
- package/dist/src/gates/coverage.js +701 -0
- package/dist/src/gates/credentials.d.ts +74 -0
- package/dist/src/gates/credentials.js +533 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -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 +390 -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 +566 -0
- package/dist/src/gates/run.js +1536 -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 +42 -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 +927 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +32 -0
- package/dist/src/hooks.js +62 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +178 -0
- package/dist/src/lock.js +500 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +2 -0
- package/dist/src/path-identity.js +10 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +130 -0
- package/dist/src/pool.js +721 -0
- package/dist/src/roles.d.ts +430 -0
- package/dist/src/roles.js +734 -0
- package/dist/src/spawn.d.ts +177 -0
- package/dist/src/spawn.js +332 -0
- package/dist/src/status.d.ts +91 -0
- package/dist/src/status.js +119 -0
- package/dist/src/task.d.ts +264 -0
- package/dist/src/task.js +305 -0
- package/dist/src/teardown.d.ts +32 -0
- package/dist/src/teardown.js +314 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +230 -0
- package/dist/src/validate.js +732 -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 +859 -0
- package/dist/src/witness/run.d.ts +274 -0
- package/dist/src/witness/run.js +1327 -0
- package/dist/src/witness/spec.d.ts +102 -0
- package/dist/src/witness/spec.js +253 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +390 -0
- package/gates.manifest.json +195 -0
- package/package.json +57 -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 +460 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +81 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +166 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/decision-record.schema.json +88 -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/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 +289 -0
- package/schemas/work-history.schema.json +183 -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,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE MINIMAL IN-REPO SCHEMA VALIDATOR (kernel plan M2, M2-P1 step 4;
|
|
3
|
+
* M2-D-04; DR-0013 clause 6).
|
|
4
|
+
*
|
|
5
|
+
* M1 shipped zero runtime dependencies. DR-0013 decided the library
|
|
6
|
+
* question for M3 (Ajv 8.20.0 exact, Draft 2020-12) and made THIS module the
|
|
7
|
+
* seam: M3-P1 retires the engine below and keeps the boundary. That promise
|
|
8
|
+
* is only keepable if two things are true here, and both are enforced by
|
|
9
|
+
* this module's tests rather than asserted in prose:
|
|
10
|
+
*
|
|
11
|
+
* 1. THE DIAGNOSTIC CONTRACT IS `INVALID <json-pointer> <message>`, one
|
|
12
|
+
* line per failure, in a deterministic order. That string is this
|
|
13
|
+
* module's public output. Everything else about how a failure was
|
|
14
|
+
* found is an implementation detail and no test may assert on it.
|
|
15
|
+
* 2. THE MESSAGE TEXTS BELOW ARE THE CONTRACT, not this engine's wording.
|
|
16
|
+
* They are listed in `DIAGNOSTIC_MESSAGES` so a future engine has one
|
|
17
|
+
* table to map onto instead of a search through call sites.
|
|
18
|
+
*
|
|
19
|
+
* Without those two, DR-0013's "all existing M2 validation tests are re-run
|
|
20
|
+
* against it" would be a rewrite of the tests rather than an engine swap,
|
|
21
|
+
* and "the module boundary is preserved" would be false at the moment it was
|
|
22
|
+
* acted on.
|
|
23
|
+
*
|
|
24
|
+
* DETERMINISTIC ORDERING, stated so a replacement engine can reproduce it:
|
|
25
|
+
* every diagnostic is collected during a traversal that visits object
|
|
26
|
+
* properties in sorted key order and array items in index order, and the
|
|
27
|
+
* collected list is then sorted by (pointer, message) with ASCII
|
|
28
|
+
* lexicographic comparison. The final sort is what makes the order a
|
|
29
|
+
* property of the CONTRACT rather than of the traversal, so an engine that
|
|
30
|
+
* walks differently still emits the same lines in the same order.
|
|
31
|
+
*
|
|
32
|
+
* THE CLOSED KEYWORD SET, AND WHY IT IS LOUD (M2-D-04). Ten validation
|
|
33
|
+
* keywords plus local `$ref`. A schema document containing any other
|
|
34
|
+
* keyword is a LOAD ERROR naming the keyword and its location. It is never
|
|
35
|
+
* ignored, because a validator that silently skips `oneOf` reports a
|
|
36
|
+
* document valid while never having checked the constraint that mattered,
|
|
37
|
+
* which is the same shape as "green by omission" one layer down. The set is
|
|
38
|
+
* deliberately NOT grown to anticipate M3's five artifact schemas; DR-0013
|
|
39
|
+
* decided that question against extension.
|
|
40
|
+
*/
|
|
41
|
+
/** The ten validation keywords plus local `$ref`. Nothing else validates. */
|
|
42
|
+
export declare const VALIDATION_KEYWORDS: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Keys that carry no constraint and are permitted for documentation and for
|
|
45
|
+
* holding subschemas. They are listed separately from the validation
|
|
46
|
+
* keywords on purpose: a reader must be able to see that permitting
|
|
47
|
+
* `description` is a decision about annotations and not a hole in the closed
|
|
48
|
+
* set.
|
|
49
|
+
*/
|
|
50
|
+
export declare const ANNOTATION_KEYS: readonly string[];
|
|
51
|
+
export interface Diagnostic {
|
|
52
|
+
pointer: string;
|
|
53
|
+
message: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `INVALID <json-pointer> <message>`. The module's public contract.
|
|
57
|
+
*
|
|
58
|
+
* Pointers are JSON Pointers in RFC 6901 URI-fragment form, so the document
|
|
59
|
+
* root is `#` and a nested location is `#/gates/0/id`. The fragment form is
|
|
60
|
+
* chosen over the bare form for one mechanical reason: the bare pointer to
|
|
61
|
+
* the root is the EMPTY STRING, which would render this line with two
|
|
62
|
+
* consecutive spaces and make the three fields ambiguous to split. A
|
|
63
|
+
* diagnostic is reported at the pointer of the OFFENDING LOCATION, not of
|
|
64
|
+
* its parent, so a missing required property points at the property that
|
|
65
|
+
* should have been there.
|
|
66
|
+
*/
|
|
67
|
+
export declare function formatDiagnostic(diagnostic: Diagnostic): string;
|
|
68
|
+
export declare function formatDiagnostics(diagnostics: Diagnostic[]): string[];
|
|
69
|
+
/**
|
|
70
|
+
* THE MESSAGE CONTRACT. One entry per way a value can fail. A replacement
|
|
71
|
+
* engine maps its own errors onto these; a test asserts on these and never
|
|
72
|
+
* on anything else.
|
|
73
|
+
*/
|
|
74
|
+
export declare const DIAGNOSTIC_MESSAGES: {
|
|
75
|
+
type: (expected: string, observed: string) => string;
|
|
76
|
+
required: (name: string) => string;
|
|
77
|
+
additionalProperties: (name: string) => string;
|
|
78
|
+
enum: (value: string, permitted: string) => string;
|
|
79
|
+
const: (value: string, required: string) => string;
|
|
80
|
+
minimum: (value: string, minimum: string) => string;
|
|
81
|
+
minItems: (count: string, minimum: string) => string;
|
|
82
|
+
pattern: (value: string, pattern: string) => string;
|
|
83
|
+
/**
|
|
84
|
+
* Authored by src/gates/manifest.ts, which cannot express "keyed by name"
|
|
85
|
+
* in the closed keyword set. It lives in THIS table anyway (CR-811): the
|
|
86
|
+
* table's stated purpose is to give a future engine one place to map onto
|
|
87
|
+
* instead of a search through call sites, and a message that sits outside
|
|
88
|
+
* it defeats that purpose whatever module emits it.
|
|
89
|
+
*/
|
|
90
|
+
duplicateId: (id: string) => string;
|
|
91
|
+
/** A $ref chain that returns to itself without consuming an instance. */
|
|
92
|
+
cyclicRef: (reference: string) => string;
|
|
93
|
+
unresolvedRef: (reference: string) => string;
|
|
94
|
+
};
|
|
95
|
+
export type SchemaDocument = Record<string, unknown>;
|
|
96
|
+
export type SchemaLoad = {
|
|
97
|
+
ok: true;
|
|
98
|
+
schema: SchemaDocument;
|
|
99
|
+
} | {
|
|
100
|
+
ok: false;
|
|
101
|
+
reason: string;
|
|
102
|
+
};
|
|
103
|
+
/** The document root, in URI-fragment form. */
|
|
104
|
+
export declare const ROOT_POINTER = "#";
|
|
105
|
+
/** The JSON type name of a value, as the `type` keyword uses it. */
|
|
106
|
+
export declare function jsonTypeOf(value: unknown): string;
|
|
107
|
+
/**
|
|
108
|
+
* Load a schema document, refusing loudly on any keyword outside the closed
|
|
109
|
+
* set. `name` appears in the reason so a caller with several documents knows
|
|
110
|
+
* which one failed.
|
|
111
|
+
*/
|
|
112
|
+
export declare function loadSchema(document: unknown, name: string): SchemaLoad;
|
|
113
|
+
/**
|
|
114
|
+
* Validate an instance against a loaded schema. The returned list is sorted
|
|
115
|
+
* by (pointer, message), which is the deterministic order the contract
|
|
116
|
+
* promises and which is now produced by `sortDiagnostics` in the shared
|
|
117
|
+
* engine so the two modules cannot drift on it.
|
|
118
|
+
*/
|
|
119
|
+
export declare function validate(schema: SchemaDocument, instance: unknown): Diagnostic[];
|
|
120
|
+
/** Validate and format in one step: the shape most callers want. */
|
|
121
|
+
export declare function validateToLines(schema: SchemaDocument, instance: unknown): string[];
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE MINIMAL IN-REPO SCHEMA VALIDATOR (kernel plan M2, M2-P1 step 4;
|
|
3
|
+
* M2-D-04; DR-0013 clause 6).
|
|
4
|
+
*
|
|
5
|
+
* M1 shipped zero runtime dependencies. DR-0013 decided the library
|
|
6
|
+
* question for M3 (Ajv 8.20.0 exact, Draft 2020-12) and made THIS module the
|
|
7
|
+
* seam: M3-P1 retires the engine below and keeps the boundary. That promise
|
|
8
|
+
* is only keepable if two things are true here, and both are enforced by
|
|
9
|
+
* this module's tests rather than asserted in prose:
|
|
10
|
+
*
|
|
11
|
+
* 1. THE DIAGNOSTIC CONTRACT IS `INVALID <json-pointer> <message>`, one
|
|
12
|
+
* line per failure, in a deterministic order. That string is this
|
|
13
|
+
* module's public output. Everything else about how a failure was
|
|
14
|
+
* found is an implementation detail and no test may assert on it.
|
|
15
|
+
* 2. THE MESSAGE TEXTS BELOW ARE THE CONTRACT, not this engine's wording.
|
|
16
|
+
* They are listed in `DIAGNOSTIC_MESSAGES` so a future engine has one
|
|
17
|
+
* table to map onto instead of a search through call sites.
|
|
18
|
+
*
|
|
19
|
+
* Without those two, DR-0013's "all existing M2 validation tests are re-run
|
|
20
|
+
* against it" would be a rewrite of the tests rather than an engine swap,
|
|
21
|
+
* and "the module boundary is preserved" would be false at the moment it was
|
|
22
|
+
* acted on.
|
|
23
|
+
*
|
|
24
|
+
* DETERMINISTIC ORDERING, stated so a replacement engine can reproduce it:
|
|
25
|
+
* every diagnostic is collected during a traversal that visits object
|
|
26
|
+
* properties in sorted key order and array items in index order, and the
|
|
27
|
+
* collected list is then sorted by (pointer, message) with ASCII
|
|
28
|
+
* lexicographic comparison. The final sort is what makes the order a
|
|
29
|
+
* property of the CONTRACT rather than of the traversal, so an engine that
|
|
30
|
+
* walks differently still emits the same lines in the same order.
|
|
31
|
+
*
|
|
32
|
+
* THE CLOSED KEYWORD SET, AND WHY IT IS LOUD (M2-D-04). Ten validation
|
|
33
|
+
* keywords plus local `$ref`. A schema document containing any other
|
|
34
|
+
* keyword is a LOAD ERROR naming the keyword and its location. It is never
|
|
35
|
+
* ignored, because a validator that silently skips `oneOf` reports a
|
|
36
|
+
* document valid while never having checked the constraint that mattered,
|
|
37
|
+
* which is the same shape as "green by omission" one layer down. The set is
|
|
38
|
+
* deliberately NOT grown to anticipate M3's five artifact schemas; DR-0013
|
|
39
|
+
* decided that question against extension.
|
|
40
|
+
*/
|
|
41
|
+
import { sortDiagnostics, validateInstance, } from "../validate.js";
|
|
42
|
+
/** The ten validation keywords plus local `$ref`. Nothing else validates. */
|
|
43
|
+
export const VALIDATION_KEYWORDS = [
|
|
44
|
+
"type",
|
|
45
|
+
"required",
|
|
46
|
+
"properties",
|
|
47
|
+
"additionalProperties",
|
|
48
|
+
"enum",
|
|
49
|
+
"items",
|
|
50
|
+
"minimum",
|
|
51
|
+
"minItems",
|
|
52
|
+
"pattern",
|
|
53
|
+
"const",
|
|
54
|
+
"$ref",
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* Keys that carry no constraint and are permitted for documentation and for
|
|
58
|
+
* holding subschemas. They are listed separately from the validation
|
|
59
|
+
* keywords on purpose: a reader must be able to see that permitting
|
|
60
|
+
* `description` is a decision about annotations and not a hole in the closed
|
|
61
|
+
* set.
|
|
62
|
+
*/
|
|
63
|
+
export const ANNOTATION_KEYS = [
|
|
64
|
+
"$schema",
|
|
65
|
+
"$id",
|
|
66
|
+
"title",
|
|
67
|
+
"description",
|
|
68
|
+
"$defs",
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* `INVALID <json-pointer> <message>`. The module's public contract.
|
|
72
|
+
*
|
|
73
|
+
* Pointers are JSON Pointers in RFC 6901 URI-fragment form, so the document
|
|
74
|
+
* root is `#` and a nested location is `#/gates/0/id`. The fragment form is
|
|
75
|
+
* chosen over the bare form for one mechanical reason: the bare pointer to
|
|
76
|
+
* the root is the EMPTY STRING, which would render this line with two
|
|
77
|
+
* consecutive spaces and make the three fields ambiguous to split. A
|
|
78
|
+
* diagnostic is reported at the pointer of the OFFENDING LOCATION, not of
|
|
79
|
+
* its parent, so a missing required property points at the property that
|
|
80
|
+
* should have been there.
|
|
81
|
+
*/
|
|
82
|
+
export function formatDiagnostic(diagnostic) {
|
|
83
|
+
return `INVALID ${diagnostic.pointer} ${diagnostic.message}`;
|
|
84
|
+
}
|
|
85
|
+
export function formatDiagnostics(diagnostics) {
|
|
86
|
+
return diagnostics.map(formatDiagnostic);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* THE MESSAGE CONTRACT. One entry per way a value can fail. A replacement
|
|
90
|
+
* engine maps its own errors onto these; a test asserts on these and never
|
|
91
|
+
* on anything else.
|
|
92
|
+
*/
|
|
93
|
+
export const DIAGNOSTIC_MESSAGES = {
|
|
94
|
+
type: (expected, observed) => `expected type ${expected} but found ${observed}`,
|
|
95
|
+
required: (name) => `required property ${name} is missing`,
|
|
96
|
+
additionalProperties: (name) => `property ${name} is not permitted here`,
|
|
97
|
+
enum: (value, permitted) => `value ${value} is not one of the permitted values ${permitted}`,
|
|
98
|
+
const: (value, required) => `value ${value} does not equal the required constant ${required}`,
|
|
99
|
+
minimum: (value, minimum) => `value ${value} is below the minimum ${minimum}`,
|
|
100
|
+
minItems: (count, minimum) => `array has ${count} items, fewer than the required minimum ${minimum}`,
|
|
101
|
+
pattern: (value, pattern) => `value ${value} does not match the required pattern ${pattern}`,
|
|
102
|
+
/**
|
|
103
|
+
* Authored by src/gates/manifest.ts, which cannot express "keyed by name"
|
|
104
|
+
* in the closed keyword set. It lives in THIS table anyway (CR-811): the
|
|
105
|
+
* table's stated purpose is to give a future engine one place to map onto
|
|
106
|
+
* instead of a search through call sites, and a message that sits outside
|
|
107
|
+
* it defeats that purpose whatever module emits it.
|
|
108
|
+
*/
|
|
109
|
+
duplicateId: (id) => `gate id ${id} is declared more than once`,
|
|
110
|
+
/** A $ref chain that returns to itself without consuming an instance. */
|
|
111
|
+
cyclicRef: (reference) => `schema reference ${reference} is cyclic`,
|
|
112
|
+
unresolvedRef: (reference) => `schema reference ${reference} does not resolve`,
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Own-property test. `properties["__proto__"]` resolves through the prototype
|
|
116
|
+
* chain to `Object.prototype`, which is an object, so a naive lookup treats
|
|
117
|
+
* `__proto__` as a DECLARED property and lets it through
|
|
118
|
+
* `additionalProperties: false` (CR-808). Ajv rejects it, so this was also a
|
|
119
|
+
* seam divergence.
|
|
120
|
+
*/
|
|
121
|
+
function ownProperty(container, name) {
|
|
122
|
+
return Object.prototype.hasOwnProperty.call(container, name)
|
|
123
|
+
? container[name]
|
|
124
|
+
: undefined;
|
|
125
|
+
}
|
|
126
|
+
function pointerSegment(segment) {
|
|
127
|
+
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
128
|
+
}
|
|
129
|
+
function childPointer(pointer, segment) {
|
|
130
|
+
return `${pointer}/${pointerSegment(segment)}`;
|
|
131
|
+
}
|
|
132
|
+
/** The document root, in URI-fragment form. */
|
|
133
|
+
export const ROOT_POINTER = "#";
|
|
134
|
+
function isPlainObject(value) {
|
|
135
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
/** The JSON type name of a value, as the `type` keyword uses it. */
|
|
138
|
+
export function jsonTypeOf(value) {
|
|
139
|
+
if (value === null) {
|
|
140
|
+
return "null";
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
return "array";
|
|
144
|
+
}
|
|
145
|
+
const primitive = typeof value;
|
|
146
|
+
if (primitive === "number") {
|
|
147
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
148
|
+
}
|
|
149
|
+
if (primitive === "boolean") {
|
|
150
|
+
return "boolean";
|
|
151
|
+
}
|
|
152
|
+
if (primitive === "string") {
|
|
153
|
+
return "string";
|
|
154
|
+
}
|
|
155
|
+
return "object";
|
|
156
|
+
}
|
|
157
|
+
const PERMITTED_TYPES = [
|
|
158
|
+
"object",
|
|
159
|
+
"array",
|
|
160
|
+
"string",
|
|
161
|
+
"number",
|
|
162
|
+
"integer",
|
|
163
|
+
"boolean",
|
|
164
|
+
"null",
|
|
165
|
+
];
|
|
166
|
+
/**
|
|
167
|
+
* Walk a schema document and refuse anything outside the closed set. The
|
|
168
|
+
* walk knows WHERE subschemas live (`properties/*`, `items`, `$defs/*` and
|
|
169
|
+
* the root), so a property literally named "oneOf" inside `properties` is a
|
|
170
|
+
* property name and not a keyword, and is not confused for one.
|
|
171
|
+
*/
|
|
172
|
+
function checkSchemaNode(node, pointer, problems) {
|
|
173
|
+
if (!isPlainObject(node)) {
|
|
174
|
+
problems.push(`${pointer} is not a schema object`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const key of Object.keys(node).sort()) {
|
|
178
|
+
if (VALIDATION_KEYWORDS.includes(key) || ANNOTATION_KEYS.includes(key)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
problems.push(`unsupported schema keyword ${key} at ${pointer}`);
|
|
182
|
+
}
|
|
183
|
+
const type = node["type"];
|
|
184
|
+
if (type !== undefined) {
|
|
185
|
+
if (typeof type !== "string" || !PERMITTED_TYPES.includes(type)) {
|
|
186
|
+
problems.push(`unsupported type value at ${pointer}: this validator accepts a single type name from ${PERMITTED_TYPES.join(", ")}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const additional = node["additionalProperties"];
|
|
190
|
+
if (additional !== undefined && typeof additional !== "boolean") {
|
|
191
|
+
problems.push(`unsupported additionalProperties at ${pointer}: this validator accepts a boolean only`);
|
|
192
|
+
}
|
|
193
|
+
const required = node["required"];
|
|
194
|
+
if (required !== undefined && !Array.isArray(required)) {
|
|
195
|
+
problems.push(`required at ${pointer} is not an array`);
|
|
196
|
+
}
|
|
197
|
+
const enumeration = node["enum"];
|
|
198
|
+
if (enumeration !== undefined && !Array.isArray(enumeration)) {
|
|
199
|
+
problems.push(`enum at ${pointer} is not an array`);
|
|
200
|
+
}
|
|
201
|
+
const reference = node["$ref"];
|
|
202
|
+
if (reference !== undefined && typeof reference !== "string") {
|
|
203
|
+
problems.push(`$ref at ${pointer} is not a string`);
|
|
204
|
+
}
|
|
205
|
+
if (reference !== undefined) {
|
|
206
|
+
// CR-802. JSON Schema 2020-12 APPLIES keywords sitting beside `$ref`,
|
|
207
|
+
// and Ajv does too. This engine followed the reference and returned,
|
|
208
|
+
// dropping every sibling without a word: a KNOWN keyword in a position
|
|
209
|
+
// the validator silently ignores, which is the harder half of the
|
|
210
|
+
// attack the closed keyword set exists to stop, and a verdict change at
|
|
211
|
+
// the exact seam DR-0013 clause 6 promises M3-P1 can swap across.
|
|
212
|
+
//
|
|
213
|
+
// Refusing at load rather than implementing sibling application is the
|
|
214
|
+
// choice this module's philosophy already made everywhere else: a
|
|
215
|
+
// schema this engine cannot evaluate the way the specification says is
|
|
216
|
+
// rejected, never partially honoured. A schema that never uses the
|
|
217
|
+
// construct behaves identically under both engines.
|
|
218
|
+
const siblings = Object.keys(node)
|
|
219
|
+
.filter((key) => key !== "$ref" && VALIDATION_KEYWORDS.includes(key))
|
|
220
|
+
.sort();
|
|
221
|
+
if (siblings.length > 0) {
|
|
222
|
+
problems.push(`$ref at ${pointer} has sibling keyword(s) ${siblings.join(", ")}: ` +
|
|
223
|
+
"this validator does not apply keywords beside a $ref, and " +
|
|
224
|
+
"silently ignoring them would validate less than the schema says");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const properties = node["properties"];
|
|
228
|
+
if (properties !== undefined) {
|
|
229
|
+
if (!isPlainObject(properties)) {
|
|
230
|
+
problems.push(`properties at ${pointer} is not an object`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
for (const name of Object.keys(properties).sort()) {
|
|
234
|
+
checkSchemaNode(ownProperty(properties, name), `${childPointer(pointer, "properties")}/${pointerSegment(name)}`, problems);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const defs = node["$defs"];
|
|
239
|
+
if (defs !== undefined) {
|
|
240
|
+
if (!isPlainObject(defs)) {
|
|
241
|
+
problems.push(`$defs at ${pointer} is not an object`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
for (const name of Object.keys(defs).sort()) {
|
|
245
|
+
checkSchemaNode(ownProperty(defs, name), `${childPointer(pointer, "$defs")}/${pointerSegment(name)}`, problems);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const patternSource = node["pattern"];
|
|
250
|
+
if (patternSource !== undefined) {
|
|
251
|
+
if (typeof patternSource !== "string") {
|
|
252
|
+
problems.push(`pattern at ${pointer} is not a string`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Compiled HERE so an unusable pattern is a load failure with a
|
|
256
|
+
// reason, not a throw escaping mid-validation. CR-801's derivation
|
|
257
|
+
// named this exact asymmetry: `new RegExp` on a CALLER-supplied
|
|
258
|
+
// pattern was guarded and `new RegExp` on a SCHEMA-supplied one was
|
|
259
|
+
// not, same call, same failure, one guarded.
|
|
260
|
+
try {
|
|
261
|
+
new RegExp(patternSource);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
problems.push(`pattern at ${pointer} is not a valid expression: ${error.message}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const items = node["items"];
|
|
269
|
+
if (items !== undefined) {
|
|
270
|
+
checkSchemaNode(items, childPointer(pointer, "items"), problems);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Load a schema document, refusing loudly on any keyword outside the closed
|
|
275
|
+
* set. `name` appears in the reason so a caller with several documents knows
|
|
276
|
+
* which one failed.
|
|
277
|
+
*/
|
|
278
|
+
export function loadSchema(document, name) {
|
|
279
|
+
const problems = [];
|
|
280
|
+
checkSchemaNode(document, ROOT_POINTER, problems);
|
|
281
|
+
if (problems.length > 0) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
reason: `${name}: ${problems.sort().join("; ")}`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return { ok: true, schema: document };
|
|
288
|
+
}
|
|
289
|
+
function resolveRef(root, reference) {
|
|
290
|
+
// `#` is the whole document, which is how a recursive schema is normally
|
|
291
|
+
// written. It was unhandled until a control in test/gates.test.ts exercised
|
|
292
|
+
// a legitimate recursive schema and got "does not resolve"; the cycle fix
|
|
293
|
+
// that motivated the control would otherwise have shipped beside a gap
|
|
294
|
+
// that made every recursive schema unusable.
|
|
295
|
+
if (reference === "#") {
|
|
296
|
+
return root;
|
|
297
|
+
}
|
|
298
|
+
if (!reference.startsWith("#/")) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
let node = root;
|
|
302
|
+
for (const raw of reference.slice(2).split("/")) {
|
|
303
|
+
const segment = raw.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
304
|
+
if (!isPlainObject(node)) {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
node = ownProperty(node, segment);
|
|
308
|
+
}
|
|
309
|
+
return isPlainObject(node) ? node : undefined;
|
|
310
|
+
}
|
|
311
|
+
/* ------------------------------------------------------------------ */
|
|
312
|
+
/* THE ENGINE, RETIRED (DR-0013 clause 6, M3-P1 step 8). */
|
|
313
|
+
/* ------------------------------------------------------------------ */
|
|
314
|
+
/**
|
|
315
|
+
* WHAT WAS RETIRED AND WHAT WAS KEPT, stated because the distinction is the
|
|
316
|
+
* whole of DR-0013 clause 6.
|
|
317
|
+
*
|
|
318
|
+
* RETIRED: keyword SEMANTICS. `type`, `required`, `properties`,
|
|
319
|
+
* `additionalProperties`, `enum`, `items`, `minimum`, `minItems`, `pattern`
|
|
320
|
+
* and `const` are no longer evaluated here. `validate` hands the schema and
|
|
321
|
+
* the instance to `src/validate.ts`, which is Ajv 8.20.0 under the policies
|
|
322
|
+
* DR-0013 clause 4 fixed. Two engines with potentially different semantics
|
|
323
|
+
* are not maintained.
|
|
324
|
+
*
|
|
325
|
+
* KEPT, and each for a reason DR-0013 names:
|
|
326
|
+
*
|
|
327
|
+
* 1. THE MODULE BOUNDARY. `loadSchema`, `validate`, `validateToLines`,
|
|
328
|
+
* `formatDiagnostic`, `formatDiagnostics` and `DIAGNOSTIC_MESSAGES` are
|
|
329
|
+
* unchanged in name, signature and meaning, so every M2 caller and every
|
|
330
|
+
* M2 test is untouched.
|
|
331
|
+
* 2. THE CLOSED KEYWORD SET, as a POLICY LINTER. `loadSchema` still refuses
|
|
332
|
+
* a gate schema containing a keyword outside M2-D-04's set. DR-0013
|
|
333
|
+
* clause 7 says this explicitly: prohibiting otherwise-valid but
|
|
334
|
+
* unapproved keywords "is a small schema-aware POLICY LINTER, never a
|
|
335
|
+
* reimplementation of keyword semantics". Under Ajv `oneOf` compiles
|
|
336
|
+
* fine, so without this linter M2's gate schemas would silently acquire
|
|
337
|
+
* a vocabulary nobody approved.
|
|
338
|
+
* 3. THE REFERENCE POLICY. Measured 2026-08-08 with the pinned Ajv:
|
|
339
|
+
* compiling `{type:"object",properties:{x:{$ref:"#/$defs/a"}},
|
|
340
|
+
* $defs:{a:{$ref:"#/$defs/a"}}}` raises
|
|
341
|
+
* `RangeError: Maximum call stack size exceeded`. That is CR-807's
|
|
342
|
+
* failure wearing a different engine's clothes, so the cycle and
|
|
343
|
+
* unresolved-reference analysis stays HERE, in front of the engine,
|
|
344
|
+
* where it produces a diagnostic instead of a stack overflow. It
|
|
345
|
+
* evaluates no other keyword.
|
|
346
|
+
*/
|
|
347
|
+
/**
|
|
348
|
+
* Reference-only joint walk. Follows `$ref` and descends `properties` and
|
|
349
|
+
* `items`, and does nothing else. `refChain` holds the references followed
|
|
350
|
+
* since the last INSTANCE node was consumed, so a legitimate recursive schema
|
|
351
|
+
* (which always consumes an instance node between two follows) is not
|
|
352
|
+
* mistaken for a cycle. This is the same rule the retired engine used and the
|
|
353
|
+
* same diagnostics, because both are contract.
|
|
354
|
+
*/
|
|
355
|
+
function collectReferenceDiagnostics(root, schema, instance, pointer, into, refChain, seenNodes) {
|
|
356
|
+
const reference = schema["$ref"];
|
|
357
|
+
if (typeof reference === "string") {
|
|
358
|
+
if (refChain.includes(reference)) {
|
|
359
|
+
into.push({ pointer, message: DIAGNOSTIC_MESSAGES.cyclicRef(reference) });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const target = resolveRef(root, reference);
|
|
363
|
+
if (target === undefined) {
|
|
364
|
+
into.push({
|
|
365
|
+
pointer,
|
|
366
|
+
message: DIAGNOSTIC_MESSAGES.unresolvedRef(reference),
|
|
367
|
+
});
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
collectReferenceDiagnostics(root, target, instance, pointer, into, [...refChain, reference], seenNodes);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (Array.isArray(instance)) {
|
|
374
|
+
const items = schema["items"];
|
|
375
|
+
if (isPlainObject(items)) {
|
|
376
|
+
for (let index = 0; index < instance.length; index += 1) {
|
|
377
|
+
collectReferenceDiagnostics(root, items, instance[index], `${pointer}/${index}`, into, [], seenNodes);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (isPlainObject(instance)) {
|
|
383
|
+
const properties = isPlainObject(schema["properties"])
|
|
384
|
+
? schema["properties"]
|
|
385
|
+
: {};
|
|
386
|
+
for (const name of Object.keys(instance).sort()) {
|
|
387
|
+
const subschema = ownProperty(properties, name);
|
|
388
|
+
if (isPlainObject(subschema)) {
|
|
389
|
+
collectReferenceDiagnostics(root, subschema, ownProperty(instance, name), childPointer(pointer, name), into, [], seenNodes);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Validate an instance against a loaded schema. The returned list is sorted
|
|
396
|
+
* by (pointer, message), which is the deterministic order the contract
|
|
397
|
+
* promises and which is now produced by `sortDiagnostics` in the shared
|
|
398
|
+
* engine so the two modules cannot drift on it.
|
|
399
|
+
*/
|
|
400
|
+
export function validate(schema, instance) {
|
|
401
|
+
const referenceProblems = [];
|
|
402
|
+
collectReferenceDiagnostics(schema, schema, instance, ROOT_POINTER, referenceProblems, [], new Set());
|
|
403
|
+
if (referenceProblems.length > 0) {
|
|
404
|
+
// A schema whose references do not resolve, or loop, cannot be compiled
|
|
405
|
+
// at all: the engine would throw. Report and stop, exactly as the retired
|
|
406
|
+
// engine returned at the offending reference node.
|
|
407
|
+
return sortDiagnostics(referenceProblems);
|
|
408
|
+
}
|
|
409
|
+
return validateInstance(schema, instance);
|
|
410
|
+
}
|
|
411
|
+
/** Validate and format in one step: the shape most callers want. */
|
|
412
|
+
export function validateToLines(schema, instance) {
|
|
413
|
+
return formatDiagnostics(validate(schema, instance));
|
|
414
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Fleet } from "./fleet.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Turn-end hook (kernel plan v1, M1-P4 step 3; R-082b, PR-209).
|
|
4
|
+
*
|
|
5
|
+
* The hook is a generated script in the TASK directory (never inside the
|
|
6
|
+
* worktree, FM-059) that the executor invokes when the payload command
|
|
7
|
+
* exits. It writes tasks/<id>/turn-end as JSON:
|
|
8
|
+
*
|
|
9
|
+
* {"endedAt": "<ISO-8601>", "exitCode": <number>}
|
|
10
|
+
*
|
|
11
|
+
* That file is the M1-P5 watcher's wake signal. It is a notification,
|
|
12
|
+
* not the task's state: meta.json status plus this exit code together
|
|
13
|
+
* are the single current-state authority (plan constraint C-1), and a
|
|
14
|
+
* MISSING turn-end file never means success (tuition T-002: an agent
|
|
15
|
+
* that dies quietly leaves no turn-end and a dirty worktree).
|
|
16
|
+
*
|
|
17
|
+
* The hook takes the payload's exit code as its single argument, so any
|
|
18
|
+
* adapter (the M1 subprocess adapter, and the window or cloud-session
|
|
19
|
+
* adapters of the M4 era) can invoke it without knowing anything else
|
|
20
|
+
* about the task. It is written with the .mjs extension so it parses as
|
|
21
|
+
* ESM regardless of the nearest package.json, since it runs inside the
|
|
22
|
+
* fleet home rather than inside this package.
|
|
23
|
+
*/
|
|
24
|
+
/** The generated hook script for a task. */
|
|
25
|
+
export declare function turnEndHookPath(fleet: Fleet, taskId: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Generate the hook script. The turn-end path is baked in as a literal,
|
|
28
|
+
* so the hook needs no fleet resolution and no environment at all.
|
|
29
|
+
*/
|
|
30
|
+
export declare function renderTurnEndHook(turnEndFile: string): string;
|
|
31
|
+
/** Write the hook for a task and return its path. */
|
|
32
|
+
export declare function writeTurnEndHook(fleet: Fleet, taskId: string): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { taskDir, turnEndPath } from "./task.js";
|
|
4
|
+
/**
|
|
5
|
+
* Turn-end hook (kernel plan v1, M1-P4 step 3; R-082b, PR-209).
|
|
6
|
+
*
|
|
7
|
+
* The hook is a generated script in the TASK directory (never inside the
|
|
8
|
+
* worktree, FM-059) that the executor invokes when the payload command
|
|
9
|
+
* exits. It writes tasks/<id>/turn-end as JSON:
|
|
10
|
+
*
|
|
11
|
+
* {"endedAt": "<ISO-8601>", "exitCode": <number>}
|
|
12
|
+
*
|
|
13
|
+
* That file is the M1-P5 watcher's wake signal. It is a notification,
|
|
14
|
+
* not the task's state: meta.json status plus this exit code together
|
|
15
|
+
* are the single current-state authority (plan constraint C-1), and a
|
|
16
|
+
* MISSING turn-end file never means success (tuition T-002: an agent
|
|
17
|
+
* that dies quietly leaves no turn-end and a dirty worktree).
|
|
18
|
+
*
|
|
19
|
+
* The hook takes the payload's exit code as its single argument, so any
|
|
20
|
+
* adapter (the M1 subprocess adapter, and the window or cloud-session
|
|
21
|
+
* adapters of the M4 era) can invoke it without knowing anything else
|
|
22
|
+
* about the task. It is written with the .mjs extension so it parses as
|
|
23
|
+
* ESM regardless of the nearest package.json, since it runs inside the
|
|
24
|
+
* fleet home rather than inside this package.
|
|
25
|
+
*/
|
|
26
|
+
/** The generated hook script for a task. */
|
|
27
|
+
export function turnEndHookPath(fleet, taskId) {
|
|
28
|
+
return join(taskDir(fleet, taskId), "turn-end-hook.mjs");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate the hook script. The turn-end path is baked in as a literal,
|
|
32
|
+
* so the hook needs no fleet resolution and no environment at all.
|
|
33
|
+
*/
|
|
34
|
+
export function renderTurnEndHook(turnEndFile) {
|
|
35
|
+
return `#!/usr/bin/env node
|
|
36
|
+
// Generated by tiphys spawn (kernel plan v1, M1-P4 step 3). Invoked by the
|
|
37
|
+
// executor adapter when the payload command exits, with the payload's exit
|
|
38
|
+
// code as its single argument. Writes the turn-end record the M1-P5 watcher
|
|
39
|
+
// wakes on. Do not edit: spawn regenerates it.
|
|
40
|
+
import { writeFileSync } from "node:fs";
|
|
41
|
+
|
|
42
|
+
const exitCode = Number(process.argv[2]);
|
|
43
|
+
if (!Number.isInteger(exitCode)) {
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
"tiphys turn-end hook: expected one integer exit-code argument\\n",
|
|
46
|
+
);
|
|
47
|
+
process.exit(64);
|
|
48
|
+
}
|
|
49
|
+
writeFileSync(
|
|
50
|
+
${JSON.stringify(turnEndFile)},
|
|
51
|
+
\`\${JSON.stringify({ endedAt: new Date().toISOString(), exitCode }, null, 2)}\\n\`,
|
|
52
|
+
);
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
/** Write the hook for a task and return its path. */
|
|
56
|
+
export function writeTurnEndHook(fleet, taskId) {
|
|
57
|
+
const path = turnEndHookPath(fleet, taskId);
|
|
58
|
+
writeFileSync(path, renderTurnEndHook(turnEndPath(fleet, taskId)), {
|
|
59
|
+
mode: 0o755,
|
|
60
|
+
});
|
|
61
|
+
return path;
|
|
62
|
+
}
|