sortie-dogs 0.8.3 → 0.9.1
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/README.md +1 -1
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/goal-bound.d.ts +251 -0
- package/dist/core/goal-bound.js +232 -0
- package/dist/core/run-flight-ledger.d.ts +35 -0
- package/dist/core/run-flight-ledger.js +190 -3
- package/dist/core/types.d.ts +2 -0
- package/dist/core/validation-budget.d.ts +29 -0
- package/dist/core/validation-budget.js +29 -0
- package/dist/core/worktree-commit-artifact.d.ts +11 -0
- package/dist/core/worktree-commit-artifact.js +35 -3
- package/dist/core/worktree-integration-queue.js +26 -5
- package/dist/core/worktree-parallel-dispatch.js +44 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/plugin/continuation.d.ts +8 -1
- package/dist/plugin/continuation.js +23 -5
- package/dist/plugin/fast-lane.d.ts +1 -1
- package/dist/plugin/fast-lane.js +2 -2
- package/dist/plugin/index.js +943 -38
- package/dist/plugin/run-metrics.d.ts +60 -2
- package/dist/plugin/run-metrics.js +148 -14
- package/dist/runtime-assets.d.ts +8 -8
- package/dist/runtime-assets.js +137 -91
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
|
24
24
|
|
|
25
25
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
|
|
26
26
|
|
|
27
|
-
Release: [v0.
|
|
27
|
+
Release: [v0.9.1](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.1)
|
|
28
28
|
|
|
29
29
|
## Quick start
|
|
30
30
|
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export declare const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export declare const RUNTIME_ASSET_VERSION = "0.3.76-goal-control-report-v1";
|
|
6
6
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export const RUNTIME_ASSET_VERSION = "0.3.76-goal-control-report-v1";
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { ValidationOutcome } from "./validation-budget.js";
|
|
2
|
+
export declare const GOAL_BOUND_SCHEMA_VERSION: "0.1";
|
|
3
|
+
export declare const GOAL_BOUND_METADATA_KEY: "sortie-dogs.goal-bound/v1";
|
|
4
|
+
export type GoalDeliveryMode = "planning-only" | "mvp-first" | "repair-first" | "controlled-change";
|
|
5
|
+
export type GoalStopReason = "completed" | "stopped" | "stop_budget" | "stop_no_progress" | "external_dependency" | "awaiting_user" | "agent_changed" | "persistence_unavailable";
|
|
6
|
+
export type GoalPhase = "active" | "stopped" | "terminal";
|
|
7
|
+
export interface GoalBudget {
|
|
8
|
+
readonly max_units: number;
|
|
9
|
+
readonly time_ms: number | null;
|
|
10
|
+
readonly cost_usd: number | null;
|
|
11
|
+
readonly source: "policy-default" | "accepted-plan" | "user-revision";
|
|
12
|
+
}
|
|
13
|
+
export interface GoalEvidence {
|
|
14
|
+
readonly evidence_id: string;
|
|
15
|
+
readonly goal_id: string;
|
|
16
|
+
readonly goal_revision: number;
|
|
17
|
+
readonly scope_epoch: number;
|
|
18
|
+
readonly acceptance_fingerprint: string;
|
|
19
|
+
readonly measurement: {
|
|
20
|
+
readonly criterion_ids: readonly string[];
|
|
21
|
+
readonly target: string;
|
|
22
|
+
readonly entrypoint: string;
|
|
23
|
+
readonly workload: string;
|
|
24
|
+
readonly oracle_coverage: readonly string[];
|
|
25
|
+
readonly build_boundary: "included" | "excluded" | "not-applicable";
|
|
26
|
+
};
|
|
27
|
+
readonly identity: {
|
|
28
|
+
readonly source: string;
|
|
29
|
+
readonly candidate: string;
|
|
30
|
+
readonly fixture: string;
|
|
31
|
+
};
|
|
32
|
+
/** Host-owned snapshot recipe. Required when acceptance binds to the current protected source/candidate. */
|
|
33
|
+
readonly protected_binding?: {
|
|
34
|
+
readonly manifest_hash: string;
|
|
35
|
+
readonly project_root: string;
|
|
36
|
+
readonly manifest_path: string;
|
|
37
|
+
readonly source_paths: readonly string[];
|
|
38
|
+
readonly candidate_paths: readonly string[];
|
|
39
|
+
};
|
|
40
|
+
readonly execution: {
|
|
41
|
+
readonly command: readonly string[];
|
|
42
|
+
readonly exit_code: number | null;
|
|
43
|
+
readonly outcome: "pass" | "fail" | "skip" | "cancel";
|
|
44
|
+
readonly started_at: string;
|
|
45
|
+
readonly ended_at: string;
|
|
46
|
+
readonly units: readonly string[];
|
|
47
|
+
};
|
|
48
|
+
readonly proof_scope: "requested-full" | "supporting-proxy" | "document-deliverable" | "expected-negative";
|
|
49
|
+
}
|
|
50
|
+
export interface GoalAcceptanceCriterion {
|
|
51
|
+
readonly criterion_id: string;
|
|
52
|
+
readonly target: string;
|
|
53
|
+
readonly entrypoint: string;
|
|
54
|
+
readonly workload: string;
|
|
55
|
+
readonly oracle_coverage: readonly string[];
|
|
56
|
+
readonly build_boundary: "included" | "excluded" | "not-applicable";
|
|
57
|
+
readonly source: string;
|
|
58
|
+
readonly candidate: string;
|
|
59
|
+
readonly source_binding?: "declared" | "current-protected";
|
|
60
|
+
readonly candidate_binding?: "declared" | "current-protected";
|
|
61
|
+
readonly validation_command?: string;
|
|
62
|
+
readonly fixture: string;
|
|
63
|
+
readonly proof_scope: "requested-full" | "document-deliverable" | "expected-negative";
|
|
64
|
+
readonly expected_outcome: "pass" | "fail";
|
|
65
|
+
}
|
|
66
|
+
export interface GoalAcceptanceContract {
|
|
67
|
+
readonly criteria: readonly GoalAcceptanceCriterion[];
|
|
68
|
+
}
|
|
69
|
+
export interface GoalTerminalReceipt {
|
|
70
|
+
readonly goal_id: string;
|
|
71
|
+
readonly terminal_revision: number;
|
|
72
|
+
readonly acceptance_fingerprint: string;
|
|
73
|
+
readonly started_at: string;
|
|
74
|
+
readonly ended_at: string;
|
|
75
|
+
readonly status: "succeeded" | "stopped";
|
|
76
|
+
readonly stop_reason: GoalStopReason;
|
|
77
|
+
readonly unit_ids: readonly string[];
|
|
78
|
+
readonly session_ids: readonly string[];
|
|
79
|
+
readonly evidence_refs: readonly string[];
|
|
80
|
+
readonly milestone_at: string | null;
|
|
81
|
+
}
|
|
82
|
+
interface GoalEventBase {
|
|
83
|
+
readonly at: string;
|
|
84
|
+
}
|
|
85
|
+
export type GoalFlightEvent = (GoalEventBase & {
|
|
86
|
+
readonly kind: "goal.accepted";
|
|
87
|
+
readonly goal_id: string;
|
|
88
|
+
readonly revision: 1;
|
|
89
|
+
readonly scope_epoch: 1;
|
|
90
|
+
readonly acceptance_fingerprint: string;
|
|
91
|
+
readonly origin_user_message_id: string;
|
|
92
|
+
readonly origin_session_id: string;
|
|
93
|
+
readonly selected_agent: string;
|
|
94
|
+
readonly delivery: GoalDeliveryMode;
|
|
95
|
+
readonly budget: GoalBudget;
|
|
96
|
+
readonly acceptance_contract: GoalAcceptanceContract | null;
|
|
97
|
+
}) | (GoalEventBase & {
|
|
98
|
+
readonly kind: "goal.user-continued";
|
|
99
|
+
readonly goal_id: string;
|
|
100
|
+
readonly origin_user_message_id: string;
|
|
101
|
+
readonly session_id: string;
|
|
102
|
+
readonly selected_agent: string;
|
|
103
|
+
}) | (GoalEventBase & {
|
|
104
|
+
readonly kind: "goal.revised";
|
|
105
|
+
readonly goal_id: string;
|
|
106
|
+
readonly revision: number;
|
|
107
|
+
readonly scope_epoch: number;
|
|
108
|
+
readonly acceptance_fingerprint: string;
|
|
109
|
+
readonly origin_user_message_id: string;
|
|
110
|
+
readonly session_id: string;
|
|
111
|
+
readonly selected_agent: string;
|
|
112
|
+
readonly delivery: GoalDeliveryMode;
|
|
113
|
+
readonly budget: GoalBudget;
|
|
114
|
+
readonly acceptance_contract: GoalAcceptanceContract | null;
|
|
115
|
+
}) | (GoalEventBase & {
|
|
116
|
+
readonly kind: "ticket.issued";
|
|
117
|
+
readonly ticket_id: string;
|
|
118
|
+
readonly goal_id: string;
|
|
119
|
+
readonly revision: number;
|
|
120
|
+
readonly scope_epoch: number;
|
|
121
|
+
readonly checkpoint: string;
|
|
122
|
+
readonly sequence: number;
|
|
123
|
+
readonly session_id: string;
|
|
124
|
+
readonly origin_user_message_id: string;
|
|
125
|
+
}) | (GoalEventBase & {
|
|
126
|
+
readonly kind: "ticket.consumed";
|
|
127
|
+
readonly ticket_id: string;
|
|
128
|
+
readonly goal_id: string;
|
|
129
|
+
readonly receiving_message_id: string;
|
|
130
|
+
readonly session_id: string;
|
|
131
|
+
readonly origin_user_message_id: string;
|
|
132
|
+
}) | (GoalEventBase & {
|
|
133
|
+
readonly kind: "dispatch.reserved";
|
|
134
|
+
readonly reservation_id: string;
|
|
135
|
+
readonly goal_id: string;
|
|
136
|
+
readonly unit_id: string;
|
|
137
|
+
readonly session_id: string;
|
|
138
|
+
readonly ticket_id: string | null;
|
|
139
|
+
}) | (GoalEventBase & {
|
|
140
|
+
readonly kind: "unit.settled";
|
|
141
|
+
readonly reservation_id: string;
|
|
142
|
+
readonly receipt_id: string;
|
|
143
|
+
readonly goal_id: string;
|
|
144
|
+
readonly unit_id: string;
|
|
145
|
+
readonly disposition: "succeeded" | "failed" | "cancelled";
|
|
146
|
+
/** Missing on pre-v0.9.1 ledgers and therefore interpreted as an acceptance result. */
|
|
147
|
+
readonly result_class?: "acceptance" | "process-defect" | "interrupted";
|
|
148
|
+
readonly progress_fingerprint: string | null;
|
|
149
|
+
readonly evidence: readonly GoalEvidence[];
|
|
150
|
+
readonly elapsed_ms: number | null;
|
|
151
|
+
readonly cost_usd: number | null;
|
|
152
|
+
}) | (GoalEventBase & {
|
|
153
|
+
readonly kind: "validation.admission";
|
|
154
|
+
readonly goal_id: string;
|
|
155
|
+
readonly reservation_id: string;
|
|
156
|
+
readonly operation_id: string;
|
|
157
|
+
readonly evidence_key: string;
|
|
158
|
+
readonly scope: "targeted" | "full" | null;
|
|
159
|
+
readonly decision: "ALLOW" | "DENY";
|
|
160
|
+
readonly reason: string;
|
|
161
|
+
readonly consumed: number;
|
|
162
|
+
readonly limit: number;
|
|
163
|
+
}) | (GoalEventBase & {
|
|
164
|
+
readonly kind: "validation.settled";
|
|
165
|
+
readonly goal_id: string;
|
|
166
|
+
readonly reservation_id: string;
|
|
167
|
+
readonly operation_id: string;
|
|
168
|
+
readonly evidence_key: string;
|
|
169
|
+
readonly outcome: ValidationOutcome;
|
|
170
|
+
readonly exit_code: number | null;
|
|
171
|
+
}) | (GoalEventBase & {
|
|
172
|
+
readonly kind: "goal.replanned";
|
|
173
|
+
readonly goal_id: string;
|
|
174
|
+
readonly revision: number;
|
|
175
|
+
readonly reason: "no-progress";
|
|
176
|
+
}) | (GoalEventBase & {
|
|
177
|
+
readonly kind: "goal.terminal";
|
|
178
|
+
readonly goal_id: string;
|
|
179
|
+
readonly receipt: GoalTerminalReceipt;
|
|
180
|
+
});
|
|
181
|
+
export interface GoalFlightEventRecord {
|
|
182
|
+
readonly sequence: number;
|
|
183
|
+
readonly previous_hash: string | null;
|
|
184
|
+
readonly event_hash: string;
|
|
185
|
+
readonly event: GoalFlightEvent;
|
|
186
|
+
}
|
|
187
|
+
export interface GoalTicketState {
|
|
188
|
+
readonly ticket_id: string;
|
|
189
|
+
readonly revision: number;
|
|
190
|
+
readonly scope_epoch: number;
|
|
191
|
+
readonly checkpoint: string;
|
|
192
|
+
readonly sequence: number;
|
|
193
|
+
readonly session_id: string;
|
|
194
|
+
readonly origin_user_message_id: string;
|
|
195
|
+
readonly receiving_message_id: string | null;
|
|
196
|
+
}
|
|
197
|
+
export interface GoalFlightState {
|
|
198
|
+
readonly goal_id: string | null;
|
|
199
|
+
readonly revision: number;
|
|
200
|
+
readonly scope_epoch: number;
|
|
201
|
+
readonly acceptance_fingerprint: string | null;
|
|
202
|
+
readonly origin_user_message_id: string | null;
|
|
203
|
+
readonly latest_user_message_id: string | null;
|
|
204
|
+
readonly selected_agent: string | null;
|
|
205
|
+
readonly delivery: GoalDeliveryMode | null;
|
|
206
|
+
readonly budget: GoalBudget | null;
|
|
207
|
+
readonly acceptance_contract: GoalAcceptanceContract | null;
|
|
208
|
+
readonly consumed_units: number;
|
|
209
|
+
readonly consumed_time_ms: number | null;
|
|
210
|
+
readonly consumed_cost_usd: number | null;
|
|
211
|
+
readonly phase: GoalPhase;
|
|
212
|
+
readonly stop_reason: GoalStopReason | null;
|
|
213
|
+
readonly no_progress_results: number;
|
|
214
|
+
readonly replan_used: boolean;
|
|
215
|
+
readonly replan_required: boolean;
|
|
216
|
+
readonly tickets: readonly GoalTicketState[];
|
|
217
|
+
readonly outstanding_reservations: readonly {
|
|
218
|
+
readonly reservation_id: string;
|
|
219
|
+
readonly unit_id: string;
|
|
220
|
+
readonly session_id: string;
|
|
221
|
+
}[];
|
|
222
|
+
readonly validation_budget: {
|
|
223
|
+
readonly consumed: number;
|
|
224
|
+
readonly reservations: readonly {
|
|
225
|
+
readonly reservation_id: string;
|
|
226
|
+
readonly operation_id: string;
|
|
227
|
+
readonly evidence_key: string;
|
|
228
|
+
}[];
|
|
229
|
+
readonly evidence_keys: readonly string[];
|
|
230
|
+
readonly limit: number | null;
|
|
231
|
+
};
|
|
232
|
+
readonly unit_ids: readonly string[];
|
|
233
|
+
readonly session_ids: readonly string[];
|
|
234
|
+
readonly evidence_refs: readonly string[];
|
|
235
|
+
readonly satisfied_criteria: readonly string[];
|
|
236
|
+
readonly receipt: GoalTerminalReceipt | null;
|
|
237
|
+
}
|
|
238
|
+
export declare class GoalBoundError extends Error {
|
|
239
|
+
readonly code: "invalid" | "transition" | "budget" | "ticket" | "evidence";
|
|
240
|
+
constructor(code: "invalid" | "transition" | "budget" | "ticket" | "evidence", message: string);
|
|
241
|
+
}
|
|
242
|
+
export declare function goalFingerprint(value: unknown): string;
|
|
243
|
+
export declare function selectGoalDelivery(input: {
|
|
244
|
+
readonly declared_intent: "design" | "registration" | "implementation" | "repair" | "controlled-change";
|
|
245
|
+
readonly requested_usable_path_established: boolean;
|
|
246
|
+
readonly irreversible_or_major_scope: boolean;
|
|
247
|
+
readonly explicit_mode?: GoalDeliveryMode;
|
|
248
|
+
}): GoalDeliveryMode;
|
|
249
|
+
export declare function validGoalEvidence(value: GoalEvidence, state: Pick<GoalFlightState, "goal_id" | "revision" | "scope_epoch" | "acceptance_fingerprint" | "acceptance_contract">): boolean;
|
|
250
|
+
export declare function reduceGoalFlight(records: readonly GoalFlightEventRecord[]): GoalFlightState;
|
|
251
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const GOAL_BOUND_SCHEMA_VERSION = "0.1";
|
|
3
|
+
export const GOAL_BOUND_METADATA_KEY = "sortie-dogs.goal-bound/v1";
|
|
4
|
+
export class GoalBoundError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "GoalBoundError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const HASH = /^sha256:[a-f0-9]{64}$/u;
|
|
13
|
+
const text = (value) => typeof value === "string" && value.length > 0 && value.length <= 512;
|
|
14
|
+
const integer = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
15
|
+
const instant = (value) => text(value) && Number.isFinite(Date.parse(value));
|
|
16
|
+
export function goalFingerprint(value) {
|
|
17
|
+
const canonical = (item) => Array.isArray(item) ? item.map(canonical) :
|
|
18
|
+
typeof item === "object" && item !== null
|
|
19
|
+
? Object.fromEntries(Object.keys(item).sort().map((key) => [key, canonical(item[key])]))
|
|
20
|
+
: item;
|
|
21
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(canonical(value))).digest("hex")}`;
|
|
22
|
+
}
|
|
23
|
+
export function selectGoalDelivery(input) {
|
|
24
|
+
if (input.declared_intent === "design" || input.declared_intent === "registration")
|
|
25
|
+
return "planning-only";
|
|
26
|
+
if (input.declared_intent === "controlled-change" || input.irreversible_or_major_scope)
|
|
27
|
+
return "controlled-change";
|
|
28
|
+
if (input.explicit_mode !== undefined)
|
|
29
|
+
return input.explicit_mode;
|
|
30
|
+
if (input.declared_intent === "repair")
|
|
31
|
+
return "repair-first";
|
|
32
|
+
return "mvp-first";
|
|
33
|
+
}
|
|
34
|
+
function validAcceptanceContract(value) {
|
|
35
|
+
return value === null || (Array.isArray(value.criteria) && value.criteria.length > 0 &&
|
|
36
|
+
new Set(value.criteria.map(({ criterion_id }) => criterion_id)).size === value.criteria.length &&
|
|
37
|
+
value.criteria.every((criterion) => text(criterion.criterion_id) && text(criterion.target) &&
|
|
38
|
+
text(criterion.entrypoint) && text(criterion.workload) && criterion.oracle_coverage.length > 0 &&
|
|
39
|
+
criterion.oracle_coverage.every(text) && new Set(criterion.oracle_coverage).size === criterion.oracle_coverage.length &&
|
|
40
|
+
(criterion.build_boundary === "included" || criterion.build_boundary === "excluded" || criterion.build_boundary === "not-applicable") &&
|
|
41
|
+
text(criterion.source) && text(criterion.candidate) && text(criterion.fixture) &&
|
|
42
|
+
(criterion.source_binding === undefined || criterion.source_binding === "declared" || criterion.source_binding === "current-protected") &&
|
|
43
|
+
(criterion.candidate_binding === undefined || criterion.candidate_binding === "declared" || criterion.candidate_binding === "current-protected") &&
|
|
44
|
+
(criterion.validation_command === undefined || text(criterion.validation_command)) &&
|
|
45
|
+
(criterion.proof_scope === "requested-full" || criterion.proof_scope === "document-deliverable" || criterion.proof_scope === "expected-negative") &&
|
|
46
|
+
(criterion.expected_outcome === "pass" || criterion.expected_outcome === "fail")));
|
|
47
|
+
}
|
|
48
|
+
function sameStrings(left, right) {
|
|
49
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
50
|
+
}
|
|
51
|
+
export function validGoalEvidence(value, state) {
|
|
52
|
+
if (typeof value !== "object" || value === null || typeof value.measurement !== "object" || value.measurement === null ||
|
|
53
|
+
typeof value.identity !== "object" || value.identity === null || typeof value.execution !== "object" || value.execution === null ||
|
|
54
|
+
!Array.isArray(value.measurement.criterion_ids) || !Array.isArray(value.measurement.oracle_coverage) ||
|
|
55
|
+
!Array.isArray(value.execution.command) || !Array.isArray(value.execution.units))
|
|
56
|
+
return false;
|
|
57
|
+
const criteria = state.acceptance_contract?.criteria.filter(({ criterion_id }) => value.measurement.criterion_ids.includes(criterion_id)) ?? [];
|
|
58
|
+
const protectedBinding = value.protected_binding;
|
|
59
|
+
const protectedBindingValid = protectedBinding !== undefined && HASH.test(protectedBinding.manifest_hash) &&
|
|
60
|
+
text(protectedBinding.project_root) && text(protectedBinding.manifest_path) && Array.isArray(protectedBinding.source_paths) &&
|
|
61
|
+
protectedBinding.source_paths.every(text) && Array.isArray(protectedBinding.candidate_paths) &&
|
|
62
|
+
protectedBinding.candidate_paths.every(text);
|
|
63
|
+
const matches = criteria.length > 0 && criteria.length === value.measurement.criterion_ids.length &&
|
|
64
|
+
criteria.every((criterion) => criterion.target === value.measurement.target &&
|
|
65
|
+
criterion.entrypoint === value.measurement.entrypoint && criterion.workload === value.measurement.workload &&
|
|
66
|
+
sameStrings(criterion.oracle_coverage, value.measurement.oracle_coverage) &&
|
|
67
|
+
criterion.build_boundary === value.measurement.build_boundary &&
|
|
68
|
+
(criterion.source_binding === "current-protected"
|
|
69
|
+
? protectedBindingValid && HASH.test(value.identity.source)
|
|
70
|
+
: criterion.source === value.identity.source) &&
|
|
71
|
+
(criterion.candidate_binding === "current-protected"
|
|
72
|
+
? protectedBindingValid && HASH.test(value.identity.candidate)
|
|
73
|
+
: criterion.candidate === value.identity.candidate) &&
|
|
74
|
+
(criterion.validation_command === undefined ||
|
|
75
|
+
(value.execution.command.length === 1 && value.execution.command[0] === criterion.validation_command)) &&
|
|
76
|
+
criterion.fixture === value.identity.fixture &&
|
|
77
|
+
criterion.proof_scope === value.proof_scope && criterion.expected_outcome === value.execution.outcome);
|
|
78
|
+
return text(value.evidence_id) && value.goal_id === state.goal_id && value.goal_revision === state.revision &&
|
|
79
|
+
value.scope_epoch === state.scope_epoch && value.acceptance_fingerprint === state.acceptance_fingerprint &&
|
|
80
|
+
value.measurement.criterion_ids.length > 0 && value.measurement.criterion_ids.every(text) &&
|
|
81
|
+
new Set(value.measurement.criterion_ids).size === value.measurement.criterion_ids.length &&
|
|
82
|
+
text(value.measurement.target) && text(value.measurement.entrypoint) && text(value.measurement.workload) &&
|
|
83
|
+
value.measurement.oracle_coverage.length > 0 && value.measurement.oracle_coverage.every(text) &&
|
|
84
|
+
text(value.identity.source) && text(value.identity.candidate) && text(value.identity.fixture) &&
|
|
85
|
+
value.execution.command.length > 0 && value.execution.command.every(text) &&
|
|
86
|
+
value.execution.exit_code === 0 && instant(value.execution.started_at) &&
|
|
87
|
+
instant(value.execution.ended_at) && Date.parse(value.execution.ended_at) >= Date.parse(value.execution.started_at) &&
|
|
88
|
+
value.execution.units.length > 0 && value.execution.units.every(text) &&
|
|
89
|
+
new Set(value.execution.units).size === value.execution.units.length && matches;
|
|
90
|
+
}
|
|
91
|
+
function initial() {
|
|
92
|
+
return { goal_id: null, revision: 0, scope_epoch: 0, acceptance_fingerprint: null,
|
|
93
|
+
origin_user_message_id: null, latest_user_message_id: null, selected_agent: null, delivery: null, budget: null,
|
|
94
|
+
acceptance_contract: null, consumed_units: 0,
|
|
95
|
+
consumed_time_ms: 0, consumed_cost_usd: 0, phase: "stopped", stop_reason: null,
|
|
96
|
+
no_progress_results: 0, replan_used: false, replan_required: false, tickets: [],
|
|
97
|
+
outstanding_reservations: [], validation_budget: { consumed: 0, reservations: [], evidence_keys: [], limit: null },
|
|
98
|
+
unit_ids: [], session_ids: [], evidence_refs: [], satisfied_criteria: [], receipt: null };
|
|
99
|
+
}
|
|
100
|
+
function requireState(condition, code, message) {
|
|
101
|
+
if (!condition)
|
|
102
|
+
throw new GoalBoundError(code, message);
|
|
103
|
+
}
|
|
104
|
+
function addUnique(values, value) {
|
|
105
|
+
return values.includes(value) ? values : [...values, value];
|
|
106
|
+
}
|
|
107
|
+
export function reduceGoalFlight(records) {
|
|
108
|
+
let state = initial();
|
|
109
|
+
let previous = null;
|
|
110
|
+
for (const [index, record] of records.entries()) {
|
|
111
|
+
requireState(record.sequence === index + 1 && record.previous_hash === previous && HASH.test(record.event_hash), "invalid", "Goal ledger chain is malformed.");
|
|
112
|
+
previous = record.event_hash;
|
|
113
|
+
const event = record.event;
|
|
114
|
+
requireState(instant(event.at), "invalid", "Goal event timestamp is invalid.");
|
|
115
|
+
if (event.kind === "goal.accepted") {
|
|
116
|
+
requireState(state.goal_id === null || state.phase === "terminal", "transition", "An active goal already owns this root.");
|
|
117
|
+
requireState(text(event.goal_id) && HASH.test(event.acceptance_fingerprint) && text(event.origin_user_message_id) &&
|
|
118
|
+
text(event.origin_session_id) && text(event.selected_agent) && event.budget.max_units > 0 &&
|
|
119
|
+
validAcceptanceContract(event.acceptance_contract), "invalid", "Accepted goal identity is incomplete.");
|
|
120
|
+
state = { ...initial(), goal_id: event.goal_id, revision: 1, scope_epoch: 1,
|
|
121
|
+
acceptance_fingerprint: event.acceptance_fingerprint, origin_user_message_id: event.origin_user_message_id,
|
|
122
|
+
latest_user_message_id: event.origin_user_message_id, selected_agent: event.selected_agent,
|
|
123
|
+
delivery: event.delivery, budget: event.budget, acceptance_contract: event.acceptance_contract, phase: "active",
|
|
124
|
+
session_ids: [event.origin_session_id] };
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
requireState(state.goal_id !== null && event.goal_id === state.goal_id, "transition", "Goal identity mismatch.");
|
|
128
|
+
if (event.kind === "goal.user-continued") {
|
|
129
|
+
requireState(state.phase !== "terminal" && text(event.origin_user_message_id) && text(event.session_id) && text(event.selected_agent), "transition", "Terminal goals cannot continue.");
|
|
130
|
+
state = { ...state, latest_user_message_id: event.origin_user_message_id, selected_agent: event.selected_agent,
|
|
131
|
+
session_ids: addUnique(state.session_ids, event.session_id), phase: "active", stop_reason: null, receipt: null,
|
|
132
|
+
tickets: [] };
|
|
133
|
+
}
|
|
134
|
+
else if (event.kind === "goal.revised") {
|
|
135
|
+
requireState(state.phase !== "terminal" && event.revision === state.revision + 1 && event.scope_epoch === state.scope_epoch + 1 &&
|
|
136
|
+
HASH.test(event.acceptance_fingerprint) && event.budget.max_units >= state.consumed_units &&
|
|
137
|
+
validAcceptanceContract(event.acceptance_contract), "transition", "Scope revision is stale or resets consumed budget.");
|
|
138
|
+
state = { ...state, revision: event.revision, scope_epoch: event.scope_epoch,
|
|
139
|
+
acceptance_fingerprint: event.acceptance_fingerprint, latest_user_message_id: event.origin_user_message_id,
|
|
140
|
+
selected_agent: event.selected_agent, delivery: event.delivery, budget: event.budget,
|
|
141
|
+
acceptance_contract: event.acceptance_contract,
|
|
142
|
+
session_ids: addUnique(state.session_ids, event.session_id), phase: "active", stop_reason: null,
|
|
143
|
+
tickets: [] };
|
|
144
|
+
}
|
|
145
|
+
else if (event.kind === "ticket.issued") {
|
|
146
|
+
requireState(state.phase === "active" && event.revision === state.revision && event.scope_epoch === state.scope_epoch &&
|
|
147
|
+
event.origin_user_message_id === state.latest_user_message_id && event.sequence === state.tickets.length + 1 &&
|
|
148
|
+
!state.tickets.some((ticket) => ticket.receiving_message_id === null), "ticket", "Continuation ticket is stale, future, or duplicates an outstanding ticket.");
|
|
149
|
+
state = { ...state, tickets: [...state.tickets, { ...event, receiving_message_id: null }] };
|
|
150
|
+
}
|
|
151
|
+
else if (event.kind === "ticket.consumed") {
|
|
152
|
+
const ticket = state.tickets.find((candidate) => candidate.ticket_id === event.ticket_id);
|
|
153
|
+
requireState(state.phase === "active" && ticket !== undefined && ticket.receiving_message_id === null &&
|
|
154
|
+
ticket.session_id === event.session_id && ticket.origin_user_message_id === event.origin_user_message_id &&
|
|
155
|
+
ticket.revision === state.revision && ticket.scope_epoch === state.scope_epoch, "ticket", "Continuation ticket is unknown, stale, duplicate, or cross-origin.");
|
|
156
|
+
state = { ...state, tickets: state.tickets.map((candidate) => candidate.ticket_id === event.ticket_id
|
|
157
|
+
? { ...candidate, receiving_message_id: event.receiving_message_id } : candidate),
|
|
158
|
+
session_ids: addUnique(state.session_ids, event.session_id) };
|
|
159
|
+
}
|
|
160
|
+
else if (event.kind === "dispatch.reserved") {
|
|
161
|
+
requireState(state.phase === "active" && !state.replan_required, "transition", "Dispatch requires terminal handling or one bounded replan.");
|
|
162
|
+
requireState(state.budget !== null && state.consumed_units + state.outstanding_reservations.length < state.budget.max_units, "budget", "Goal unit budget exhausted.");
|
|
163
|
+
requireState(state.budget.time_ms === null || (state.consumed_time_ms !== null && state.consumed_time_ms < state.budget.time_ms), "budget", "Goal time budget is exhausted or usage is unknown.");
|
|
164
|
+
requireState(state.budget.cost_usd === null || (state.consumed_cost_usd !== null && state.consumed_cost_usd < state.budget.cost_usd), "budget", "Goal cost budget is exhausted or usage is unknown.");
|
|
165
|
+
requireState(!state.outstanding_reservations.some((entry) => entry.reservation_id === event.reservation_id), "transition", "Dispatch reservation already exists.");
|
|
166
|
+
if (event.ticket_id !== null)
|
|
167
|
+
requireState(state.tickets.some((ticket) => ticket.ticket_id === event.ticket_id && ticket.receiving_message_id !== null), "ticket", "Dispatch ticket was not consumed.");
|
|
168
|
+
state = { ...state, outstanding_reservations: [...state.outstanding_reservations,
|
|
169
|
+
{ reservation_id: event.reservation_id, unit_id: event.unit_id, session_id: event.session_id }],
|
|
170
|
+
session_ids: addUnique(state.session_ids, event.session_id) };
|
|
171
|
+
}
|
|
172
|
+
else if (event.kind === "unit.settled") {
|
|
173
|
+
const reservation = state.outstanding_reservations.find((entry) => entry.reservation_id === event.reservation_id);
|
|
174
|
+
requireState(reservation?.unit_id === event.unit_id, "transition", "Unit settlement is unknown or duplicate.");
|
|
175
|
+
requireState(event.evidence.every((entry) => validGoalEvidence(entry, state)), "evidence", "Unit evidence is not bound to the current goal revision.");
|
|
176
|
+
requireState(event.evidence.every((entry) => entry.execution.units.includes(event.unit_id)), "evidence", "Unit evidence does not identify its reserved unit.");
|
|
177
|
+
const newCriteria = event.evidence.flatMap((entry) => entry.measurement.criterion_ids)
|
|
178
|
+
.filter((criterionID) => !state.satisfied_criteria.includes(criterionID));
|
|
179
|
+
const progress = newCriteria.length > 0;
|
|
180
|
+
requireState(event.progress_fingerprint === (progress ? goalFingerprint(event.evidence) : null), "evidence", "Progress fingerprint does not represent newly accepted evidence.");
|
|
181
|
+
const acceptanceFailure = event.disposition === "failed" &&
|
|
182
|
+
(event.result_class === undefined || event.result_class === "acceptance");
|
|
183
|
+
const nextNoProgress = progress ? 0 : acceptanceFailure
|
|
184
|
+
? state.no_progress_results + 1
|
|
185
|
+
: state.no_progress_results;
|
|
186
|
+
const elapsed = state.consumed_time_ms === null || event.elapsed_ms === null ? null : state.consumed_time_ms + event.elapsed_ms;
|
|
187
|
+
const cost = state.consumed_cost_usd === null || event.cost_usd === null ? null : state.consumed_cost_usd + event.cost_usd;
|
|
188
|
+
state = { ...state, consumed_units: state.consumed_units + 1, consumed_time_ms: elapsed, consumed_cost_usd: cost,
|
|
189
|
+
no_progress_results: nextNoProgress, replan_required: nextNoProgress >= 2,
|
|
190
|
+
outstanding_reservations: state.outstanding_reservations.filter((entry) => entry.reservation_id !== event.reservation_id),
|
|
191
|
+
unit_ids: addUnique(state.unit_ids, event.unit_id),
|
|
192
|
+
evidence_refs: event.evidence.reduce((refs, evidence) => addUnique(refs, evidence.evidence_id), state.evidence_refs),
|
|
193
|
+
satisfied_criteria: event.evidence.reduce((ids, evidence) => evidence.measurement.criterion_ids.reduce(addUnique, ids), state.satisfied_criteria) };
|
|
194
|
+
}
|
|
195
|
+
else if (event.kind === "validation.admission") {
|
|
196
|
+
requireState(event.limit > 0 && Number.isSafeInteger(event.limit) && Number.isSafeInteger(event.consumed) &&
|
|
197
|
+
event.consumed >= 0 && event.consumed <= event.limit && event.operation_id.length > 0 && event.evidence_key.length > 0, "invalid", "Validation admission is malformed.");
|
|
198
|
+
if (event.decision === "ALLOW") {
|
|
199
|
+
requireState(event.scope !== null && event.consumed === state.validation_budget.consumed + 1 &&
|
|
200
|
+
(state.validation_budget.limit === null || state.validation_budget.limit === event.limit) &&
|
|
201
|
+
!state.validation_budget.evidence_keys.includes(event.evidence_key) &&
|
|
202
|
+
!state.validation_budget.reservations.some((entry) => entry.reservation_id === event.reservation_id), "budget", "Validation admission is stale, duplicated, or exhausted.");
|
|
203
|
+
state = { ...state, validation_budget: { consumed: event.consumed, limit: event.limit,
|
|
204
|
+
evidence_keys: [...state.validation_budget.evidence_keys, event.evidence_key],
|
|
205
|
+
reservations: [...state.validation_budget.reservations,
|
|
206
|
+
{ reservation_id: event.reservation_id, operation_id: event.operation_id, evidence_key: event.evidence_key }] } };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (event.kind === "validation.settled") {
|
|
210
|
+
const reservation = state.validation_budget.reservations.find((entry) => entry.reservation_id === event.reservation_id);
|
|
211
|
+
requireState(reservation?.operation_id === event.operation_id && reservation.evidence_key === event.evidence_key, "transition", "Validation settlement is unknown or mismatched.");
|
|
212
|
+
state = { ...state, validation_budget: { ...state.validation_budget,
|
|
213
|
+
reservations: state.validation_budget.reservations.filter((entry) => entry.reservation_id !== event.reservation_id) } };
|
|
214
|
+
}
|
|
215
|
+
else if (event.kind === "goal.replanned") {
|
|
216
|
+
requireState(state.phase === "active" && state.replan_required && !state.replan_used && event.revision === state.revision, "transition", "Bounded replan is unavailable.");
|
|
217
|
+
state = { ...state, replan_used: true, replan_required: false, no_progress_results: 0 };
|
|
218
|
+
}
|
|
219
|
+
else if (event.kind === "goal.terminal") {
|
|
220
|
+
const allAccepted = state.acceptance_contract !== null && state.acceptance_contract.criteria.every(({ criterion_id }) => state.satisfied_criteria.includes(criterion_id));
|
|
221
|
+
requireState(state.phase !== "terminal" && state.outstanding_reservations.length === 0 && event.receipt.goal_id === state.goal_id &&
|
|
222
|
+
event.receipt.terminal_revision === state.revision && event.receipt.acceptance_fingerprint === state.acceptance_fingerprint &&
|
|
223
|
+
event.receipt.unit_ids.every((id) => state.unit_ids.includes(id)) && event.receipt.session_ids.every((id) => state.session_ids.includes(id)) &&
|
|
224
|
+
event.receipt.evidence_refs.every((id) => state.evidence_refs.includes(id)) &&
|
|
225
|
+
(event.receipt.status !== "succeeded" || (event.receipt.stop_reason === "completed" && allAccepted && !state.replan_required)) &&
|
|
226
|
+
(event.receipt.status !== "stopped" || event.receipt.stop_reason !== "completed"), "transition", "Terminal receipt is not bound to settled goal state.");
|
|
227
|
+
state = { ...state, phase: event.receipt.status === "succeeded" ? "terminal" : "stopped",
|
|
228
|
+
stop_reason: event.receipt.stop_reason, tickets: [], receipt: event.receipt };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return state;
|
|
232
|
+
}
|
|
@@ -2,6 +2,8 @@ import type { EvidenceCapsuleStore } from "./evidence-capsule.js";
|
|
|
2
2
|
import type { AcceptanceCompileGapCode, AcceptanceCompileResult } from "./acceptance-compiler.js";
|
|
3
3
|
import { type ChildTerminalIdentity, type ChildTerminalEvidence, type ChildTerminalDisposition } from "./child-terminal-reconciliation.js";
|
|
4
4
|
import type { TerminalRescueAcceptedBase } from "./terminal-rescue-policy.js";
|
|
5
|
+
import type { ValidationBudgetRequest, ValidationBudgetDecision, ValidationOutcome } from "./validation-budget.js";
|
|
6
|
+
import { type GoalFlightEvent, type GoalFlightEventRecord, type GoalFlightState } from "./goal-bound.js";
|
|
5
7
|
export declare const RUN_FLIGHT_LEDGER_SCHEMA_VERSION: "0.1";
|
|
6
8
|
export declare const MAX_RUN_FLIGHT_EVENTS = 2048;
|
|
7
9
|
export declare const MAX_RUN_FLIGHT_LEDGER_BYTES: number;
|
|
@@ -247,6 +249,23 @@ export type RunFlightEvent = (EventBase & {
|
|
|
247
249
|
readonly command_fingerprint: string;
|
|
248
250
|
readonly result: "passed" | "failed";
|
|
249
251
|
readonly artifact_id: string | null;
|
|
252
|
+
}) | (EventBase & {
|
|
253
|
+
readonly kind: "validation.admission";
|
|
254
|
+
readonly reservation_id: string;
|
|
255
|
+
readonly operation_id: string;
|
|
256
|
+
readonly evidence_key: string;
|
|
257
|
+
readonly scope: "targeted" | "full" | null;
|
|
258
|
+
readonly decision: "ALLOW" | "DENY";
|
|
259
|
+
readonly reason: string;
|
|
260
|
+
readonly consumed: number;
|
|
261
|
+
readonly limit: number;
|
|
262
|
+
}) | (EventBase & {
|
|
263
|
+
readonly kind: "validation.settled";
|
|
264
|
+
readonly reservation_id: string;
|
|
265
|
+
readonly operation_id: string;
|
|
266
|
+
readonly evidence_key: string;
|
|
267
|
+
readonly outcome: ValidationOutcome;
|
|
268
|
+
readonly exit_code: number | null;
|
|
250
269
|
}) | (EventBase & {
|
|
251
270
|
readonly kind: "unit.completed";
|
|
252
271
|
readonly unit_id: string;
|
|
@@ -308,6 +327,11 @@ export interface RunFlightState {
|
|
|
308
327
|
readonly resource_budget_reserved: FlightResourceUsage;
|
|
309
328
|
readonly child_counts: Readonly<Record<FlightRole, number>>;
|
|
310
329
|
readonly validation_reruns: number;
|
|
330
|
+
readonly validation_budget: {
|
|
331
|
+
readonly consumed: number;
|
|
332
|
+
readonly reservations: number;
|
|
333
|
+
readonly limit: number | null;
|
|
334
|
+
};
|
|
311
335
|
readonly observations: readonly FlightObservation[];
|
|
312
336
|
readonly terminal_disposition: "succeeded" | "failed" | "cancelled" | null;
|
|
313
337
|
readonly plan_decisions: readonly {
|
|
@@ -340,11 +364,22 @@ export declare class RunFlightLedger {
|
|
|
340
364
|
#private;
|
|
341
365
|
private constructor();
|
|
342
366
|
static open(filePath: string, evidence: RunFlightEvidenceAccess): Promise<RunFlightLedger>;
|
|
367
|
+
/** Root goal checkpoints share this owner and lock/write discipline without inventing a Git run. */
|
|
368
|
+
static openGoal(filePath: string): Promise<RunFlightLedger>;
|
|
343
369
|
read(): Promise<{
|
|
344
370
|
readonly records: readonly RunFlightEventRecord[];
|
|
345
371
|
readonly state: RunFlightState;
|
|
346
372
|
}>;
|
|
347
373
|
append(event: RunFlightEvent): Promise<RunFlightState>;
|
|
348
374
|
appendCompileResult(result: AcceptanceCompileResult, at: string): Promise<RunFlightState>;
|
|
375
|
+
reserveValidation(request: ValidationBudgetRequest, limit: number): Promise<ValidationBudgetDecision & {
|
|
376
|
+
readonly reservation_id: string | null;
|
|
377
|
+
}>;
|
|
378
|
+
settleValidation(reservation_id: string, request: ValidationBudgetRequest, outcome: ValidationOutcome, exit_code: number | null): Promise<RunFlightState | GoalFlightState>;
|
|
379
|
+
readGoal(): Promise<{
|
|
380
|
+
readonly records: readonly GoalFlightEventRecord[];
|
|
381
|
+
readonly state: GoalFlightState;
|
|
382
|
+
}>;
|
|
383
|
+
appendGoal(event: GoalFlightEvent): Promise<GoalFlightState>;
|
|
349
384
|
}
|
|
350
385
|
export {};
|