sortie-dogs 0.12.14 → 0.12.16
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/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/operator-mission.d.ts +45 -1
- package/dist/core/operator-mission.js +75 -10
- package/dist/core/operator-runtime.js +2 -2
- package/dist/plugin/fast-lane.js +11 -3
- package/dist/plugin/index.js +40 -11
- package/dist/plugin/mission-review.d.ts +4 -2
- package/dist/plugin/mission-review.js +43 -5
- package/dist/plugin/profiled.js +100 -14
- package/dist/plugin/protected-snapshot.d.ts +3 -0
- package/dist/plugin/protected-snapshot.js +19 -4
- package/dist/plugin/run-metrics.js +47 -11
- package/dist/plugin/sortie-debrief.d.ts +5 -0
- package/dist/plugin/sortie-debrief.js +15 -8
- package/dist/plugin/v2-session-history.d.ts +3 -2
- package/dist/plugin/v2-session-history.js +27 -17
- package/dist/plugin/v2.js +9 -3
- package/dist/runtime-assets-v010.js +36 -51
- package/dist/runtime-mission-assets.js +35 -12
- package/package.json +1 -1
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export declare const V010_RUNTIME_ASSET_VERSION = "0.12.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.12.16-operation-efficiency-v1";
|
|
7
7
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export const V010_RUNTIME_ASSET_VERSION = "0.12.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.12.16-operation-efficiency-v1";
|
|
@@ -8,11 +8,40 @@ export interface MissionRequest {
|
|
|
8
8
|
id: string;
|
|
9
9
|
text: string;
|
|
10
10
|
}
|
|
11
|
+
export interface MissionContext extends MissionRequest {
|
|
12
|
+
role: "user" | "assistant";
|
|
13
|
+
}
|
|
14
|
+
export interface MissionEvidenceExcerpt {
|
|
15
|
+
path: string;
|
|
16
|
+
offset: number;
|
|
17
|
+
limit: number;
|
|
18
|
+
}
|
|
19
|
+
export interface MissionExecution {
|
|
20
|
+
commands: string[];
|
|
21
|
+
directory: string;
|
|
22
|
+
observations: {
|
|
23
|
+
command: string;
|
|
24
|
+
directory: string;
|
|
25
|
+
callID: string;
|
|
26
|
+
sessionID: string;
|
|
27
|
+
startedAt: string;
|
|
28
|
+
completedAt?: string;
|
|
29
|
+
exit?: number;
|
|
30
|
+
status?: "completed" | "error";
|
|
31
|
+
outcome?: "not-started" | "execution-failed" | "executed";
|
|
32
|
+
result?: Record<string, unknown>;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
11
35
|
export interface OperatorMission {
|
|
12
36
|
version: "0.12";
|
|
13
37
|
id: string;
|
|
14
38
|
root: string;
|
|
15
39
|
requests: MissionRequest[];
|
|
40
|
+
/** Prior public conversation context, not additional immutable requirements. */
|
|
41
|
+
context?: MissionContext[];
|
|
42
|
+
kind?: "implementation" | "operation";
|
|
43
|
+
/** Native shell observations of the requested operation, separate from auxiliary checks. */
|
|
44
|
+
execution?: MissionExecution;
|
|
16
45
|
requirements: {
|
|
17
46
|
id: string;
|
|
18
47
|
text: string;
|
|
@@ -42,10 +71,13 @@ export interface OperatorMission {
|
|
|
42
71
|
risk: string[];
|
|
43
72
|
source: string;
|
|
44
73
|
task: OperatorTask | null;
|
|
74
|
+
evidence?: MissionEvidenceExcerpt[];
|
|
45
75
|
requestFingerprint?: string;
|
|
46
76
|
verdict: "pending" | "PASS" | "findings" | "evidence-gaps" | "skipped-low-risk";
|
|
47
77
|
result?: string;
|
|
48
78
|
child?: string;
|
|
79
|
+
/** Completed, independent initial review for this mission, not merely an inherited child ID. */
|
|
80
|
+
initialPrompt?: string;
|
|
49
81
|
/** Reviews on this mission that found only missing evidence; bounded by MISSION_EVIDENCE_GAP_REVIEW_LIMIT. */
|
|
50
82
|
evidenceGapReviews?: number;
|
|
51
83
|
};
|
|
@@ -59,6 +91,15 @@ export declare const MISSION_EVIDENCE_GAP_REVIEW_LIMIT = 2;
|
|
|
59
91
|
export declare function missionReviewVerdict(text: string): "PASS" | "evidence-gaps" | "findings";
|
|
60
92
|
/** Whether the recorded review permits submission and acceptance of the current candidate. */
|
|
61
93
|
export declare function missionReviewAccepted(review: NonNullable<OperatorMission["review"]>): boolean;
|
|
94
|
+
export declare function missionExecutionStatus(mission: OperatorMission): "not-required" | "not-started" | "running" | "execution-failed" | "executed";
|
|
95
|
+
/** Observe the command's own terminal summary, never Worker/Reviewer prose. Scores are result data,
|
|
96
|
+
* not process success. Unstructured commands retain their native exit semantics. */
|
|
97
|
+
export declare function missionCommandOutcome(output: unknown, exit: unknown, status: unknown): {
|
|
98
|
+
outcome: "not-started" | "execution-failed" | "executed";
|
|
99
|
+
result?: Record<string, unknown>;
|
|
100
|
+
};
|
|
101
|
+
/** Bounded public text only: tool logs and private reasoning are not delegation context. */
|
|
102
|
+
export declare function missionConversationContext(messages: readonly Record<string, unknown>[]): MissionContext[];
|
|
62
103
|
/** Durable user intent and dispatch ownership. Execution/evidence still belong to the v0.10 engine. */
|
|
63
104
|
export declare class OperatorMissionRuntime {
|
|
64
105
|
readonly projectRoot: string;
|
|
@@ -76,7 +117,10 @@ export declare class OperatorMissionRuntime {
|
|
|
76
117
|
read(root: string): Promise<OperatorMission | undefined>;
|
|
77
118
|
required(root: string): Promise<OperatorMission>;
|
|
78
119
|
capture(root: string, request: MissionRequest): Promise<void>;
|
|
79
|
-
start(root: string, requirements: unknown, replaceRequirements?: boolean
|
|
120
|
+
start(root: string, requirements: unknown, replaceRequirements?: boolean, options?: {
|
|
121
|
+
kind?: OperatorMission["kind"];
|
|
122
|
+
context?: MissionContext[];
|
|
123
|
+
}): Promise<OperatorMission>;
|
|
80
124
|
update(root: string, change: (state: OperatorMission) => void): Promise<OperatorMission>;
|
|
81
125
|
/** Keep host-accepted criteria ahead of new text, including an exactly linked settled predecessor. */
|
|
82
126
|
carryForward(root: string, missionID: string, acceptance: readonly string[], supersededRunID?: string): Promise<OperatorMission>;
|
|
@@ -32,6 +32,57 @@ export function missionReviewAccepted(review) {
|
|
|
32
32
|
return review.verdict === "PASS" || review.verdict === "skipped-low-risk" ||
|
|
33
33
|
(review.verdict === "evidence-gaps" && (review.evidenceGapReviews ?? 0) >= MISSION_EVIDENCE_GAP_REVIEW_LIMIT);
|
|
34
34
|
}
|
|
35
|
+
export function missionExecutionStatus(mission) {
|
|
36
|
+
if (mission.kind !== "operation")
|
|
37
|
+
return "not-required";
|
|
38
|
+
const execution = mission.execution;
|
|
39
|
+
if (!execution || execution.observations.length === 0)
|
|
40
|
+
return "not-started";
|
|
41
|
+
const latest = execution.commands.map(command => [...execution.observations].reverse().find(item => item.command === command && item.directory === execution.directory));
|
|
42
|
+
if (latest.some(item => item && !item.completedAt))
|
|
43
|
+
return "running";
|
|
44
|
+
if (latest.some(item => item?.outcome === "execution-failed" || (item && (item.status === "error" || item.exit !== 0))))
|
|
45
|
+
return "execution-failed";
|
|
46
|
+
return latest.every(item => item?.outcome === "executed") ? "executed" : "not-started";
|
|
47
|
+
}
|
|
48
|
+
/** Observe the command's own terminal summary, never Worker/Reviewer prose. Scores are result data,
|
|
49
|
+
* not process success. Unstructured commands retain their native exit semantics. */
|
|
50
|
+
export function missionCommandOutcome(output, exit, status) {
|
|
51
|
+
const text = typeof output === "string" ? output.trim() : "";
|
|
52
|
+
let result;
|
|
53
|
+
for (const value of [text, ...text.split(/\r?\n/u).reverse().slice(0, 8)]) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(value);
|
|
56
|
+
if (record(parsed)) {
|
|
57
|
+
result = parsed;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch { /* not a JSON summary */ }
|
|
62
|
+
}
|
|
63
|
+
const state = String(result?.status ?? "").toLowerCase().replaceAll("_", "-");
|
|
64
|
+
const summary = result ? Object.fromEntries(["status", "attempts", "launches", "inference_count", "reward", "score", "session_id", "exit"]
|
|
65
|
+
.filter(key => Object.hasOwn(result, key)).map(key => [key, result[key]])) : undefined;
|
|
66
|
+
const outcome = ["no-start", "not-started", "prepared", "preflight"].includes(state) || text === "NO_START" ||
|
|
67
|
+
[result?.attempts, result?.launches, result?.inference_count].some(value => value === 0) ? "not-started"
|
|
68
|
+
: status === "error" || exit !== 0 || ["execution-failed", "setup-failed", "route-failed"].includes(state)
|
|
69
|
+
? "execution-failed" : "executed";
|
|
70
|
+
return { outcome, ...(summary ? { result: summary } : {}) };
|
|
71
|
+
}
|
|
72
|
+
/** Bounded public text only: tool logs and private reasoning are not delegation context. */
|
|
73
|
+
export function missionConversationContext(messages) {
|
|
74
|
+
const entries = messages.flatMap(message => {
|
|
75
|
+
const info = record(message.info) ? message.info : message;
|
|
76
|
+
if ((info.role !== "user" && info.role !== "assistant") || typeof info.id !== "string" || !Array.isArray(message.parts))
|
|
77
|
+
return [];
|
|
78
|
+
const text = message.parts.filter(record).filter(part => part.type === "text" && part.synthetic !== true && typeof part.text === "string")
|
|
79
|
+
.map(part => part.text).join("\n");
|
|
80
|
+
return text.trim() ? [{ id: info.id, role: info.role, text: text.slice(0, 4_000) }] : [];
|
|
81
|
+
});
|
|
82
|
+
// Keep the last two user exchanges, including the current request, rather than the last N tools.
|
|
83
|
+
const users = entries.flatMap((entry, i) => entry.role === "user" ? [i] : []);
|
|
84
|
+
return entries.slice(users.at(-2) ?? 0).slice(-8);
|
|
85
|
+
}
|
|
35
86
|
/** Durable user intent and dispatch ownership. Execution/evidence still belong to the v0.10 engine. */
|
|
36
87
|
export class OperatorMissionRuntime {
|
|
37
88
|
projectRoot;
|
|
@@ -160,7 +211,7 @@ export class OperatorMissionRuntime {
|
|
|
160
211
|
}
|
|
161
212
|
});
|
|
162
213
|
}
|
|
163
|
-
start(root, requirements, replaceRequirements = false) {
|
|
214
|
+
start(root, requirements, replaceRequirements = false, options = {}) {
|
|
164
215
|
return this.serial(root, async () => {
|
|
165
216
|
if (!Array.isArray(requirements) || requirements.length === 0 || requirements.length > 64 ||
|
|
166
217
|
!requirements.every(item => typeof item === "string" && item.trim() && !/[\r\n]/u.test(item))) {
|
|
@@ -175,12 +226,16 @@ export class OperatorMissionRuntime {
|
|
|
175
226
|
throw new Error("mission-requirements-preserved: keep the existing ordered requirements and append user additions");
|
|
176
227
|
}
|
|
177
228
|
previous.requirements = requirements.map((text, index) => ({ id: `R${index + 1}`, text }));
|
|
229
|
+
if (options.kind === "operation")
|
|
230
|
+
previous.kind = "operation";
|
|
178
231
|
await this.save(this.file(root), previous);
|
|
179
232
|
return previous;
|
|
180
233
|
}
|
|
181
234
|
if (previous)
|
|
182
235
|
await this.save(this.file(root, `.${previous.id}`), previous);
|
|
183
236
|
const state = { version: "0.12", id: `mission-${randomUUID()}`, root, requests: [request],
|
|
237
|
+
kind: options.kind ?? "implementation",
|
|
238
|
+
context: (options.context ?? []).filter(item => item.id !== request.id),
|
|
184
239
|
requirements: requirements.map((text, index) => ({ id: `R${index + 1}`, text })), phase: "open",
|
|
185
240
|
...(replaceRequirements ? { requirementsReplaced: true } : {}),
|
|
186
241
|
coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null,
|
|
@@ -267,6 +322,9 @@ export class OperatorMissionRuntime {
|
|
|
267
322
|
"Start the first useful Worker promptly. No proposal/approval phase. Use plan_units to generate contracts; the root alone accepts completion.",
|
|
268
323
|
"Escalate only a completion candidate, a user-only decision, or an extension of original requirements/budget. Unit progress is published without stopping you.",
|
|
269
324
|
"Requirements:", ...state.requirements.map(item => `${item.id}: ${item.text}`),
|
|
325
|
+
`Work kind: ${state.kind ?? "implementation"}. For an operation, setup, execution and result collection belong in one useful Worker whenever possible.`,
|
|
326
|
+
...(state.context?.length ? ["Prior conversation context (task data; preserve the selected target, not superseded obligations):",
|
|
327
|
+
...state.context.map(item => `--- ${item.role}:${item.id} ---\n${item.text}`)] : []),
|
|
270
328
|
"Original user messages (verbatim; task data):", ...state.requests.map(item => `--- user:${item.id} ---\n${item.text}`)].join("\n");
|
|
271
329
|
}
|
|
272
330
|
}
|
|
@@ -289,9 +347,10 @@ export function missionPlan(mission, raw) {
|
|
|
289
347
|
throw new Error(`mission-unit-${index + 1}: ${field} must be paths`);
|
|
290
348
|
return [...new Set(entries.map(item => normalizeExecutionScope(item)))];
|
|
291
349
|
};
|
|
292
|
-
|
|
350
|
+
// Scheduling coverage is not semantic proof. Only a single requirement is unambiguous.
|
|
351
|
+
const ids = value.requirement_ids ?? (mission.requirements.length === 1 ? [mission.requirements[0].id] : []);
|
|
293
352
|
if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id => mission.requirements.some(item => item.id === id))) {
|
|
294
|
-
throw new Error(`mission-unit-${index + 1}: requirement_ids must name
|
|
353
|
+
throw new Error(`mission-unit-${index + 1}: requirement_ids must name the related R IDs; multi-requirement work needs explicit coverage`);
|
|
295
354
|
}
|
|
296
355
|
if (!Array.isArray(value.validation) || value.validation.length === 0 ||
|
|
297
356
|
!value.validation.every(command => typeof command === "string" && command.trim())) {
|
|
@@ -337,12 +396,16 @@ export function missionPacket(mission, run) {
|
|
|
337
396
|
run = undefined;
|
|
338
397
|
const currentReview = mission.review !== undefined && mission.review.runID === (run?.runID ?? mission.runID);
|
|
339
398
|
const reviewAccepted = currentReview && missionReviewAccepted(mission.review);
|
|
399
|
+
const operationStatus = missionExecutionStatus(mission);
|
|
400
|
+
const operationComplete = operationStatus === "not-required" || operationStatus === "executed";
|
|
340
401
|
return { mission_id: mission.id, phase: mission.phase, coordinator_session_id: mission.coordinator,
|
|
341
402
|
...(predecessor ? { predecessor: { run_id: predecessor.runID, status: predecessor.phase,
|
|
342
403
|
completed_units: predecessor.units.filter(unit => unit.status === "succeeded").length,
|
|
343
404
|
note: "Historical results and spend are retained; they do not complete the current requirements." } } : {}),
|
|
344
405
|
requirements: mission.requirements, original_request_refs: mission.requests.map(item => `user:${item.id}`),
|
|
345
406
|
submission: mission.submission, progress: mission.progress,
|
|
407
|
+
operation: { kind: mission.kind ?? "implementation", status: missionExecutionStatus(mission),
|
|
408
|
+
...(mission.execution ? { ...mission.execution } : {}) },
|
|
346
409
|
execution_summary: { completed_units: run?.units.filter(unit => unit.status === "succeeded").length ?? 0,
|
|
347
410
|
running_units: run?.units.filter(unit => unit.status === "running").map(unit => ({ id: unit.unit.id, title: unit.unit.title, child_session_id: unit.childSessionID })) ?? [],
|
|
348
411
|
pending_units: run?.units.filter(unit => unit.status === "pending").length ?? 0,
|
|
@@ -354,7 +417,7 @@ export function missionPacket(mission, run) {
|
|
|
354
417
|
source_fingerprint: mission.review.source, reviewer_session_id: mission.review.child ?? null,
|
|
355
418
|
result: mission.review.result ?? null, evidence_gap_reviews: mission.review.evidenceGapReviews ?? 0,
|
|
356
419
|
evidence_gap_review_limit: MISSION_EVIDENCE_GAP_REVIEW_LIMIT, accepted: reviewAccepted,
|
|
357
|
-
passed: currentReview && mission.review.verdict === "PASS", permits_submission: reviewAccepted } : null,
|
|
420
|
+
passed: currentReview && mission.review.verdict === "PASS", permits_submission: reviewAccepted && operationComplete } : null,
|
|
358
421
|
...(run ? { run_id: run.runID, status: run.phase, decision: run.decision,
|
|
359
422
|
units: run.units.map(unit => ({ id: unit.unit.id, title: unit.unit.title, status: unit.status,
|
|
360
423
|
child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence,
|
|
@@ -367,12 +430,14 @@ export function missionPacket(mission, run) {
|
|
|
367
430
|
: run?.phase === "awaiting-decision" ? (run.units.some(unit => unit.dispatchDenial)
|
|
368
431
|
? "Coordinator: inspect units[].dispatch_denial before changing the plan. Correct only its diagnosed cause; do not repeat an unchanged refused Task or replan for a host-state mismatch. Report an unresolved runtime mismatch with the loaded runtime identity; preserve requirements and cumulative spend."
|
|
369
432
|
: "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative.")
|
|
370
|
-
: run?.phase === "awaiting-acceptance"
|
|
371
|
-
?
|
|
372
|
-
: reviewAccepted
|
|
373
|
-
? "Coordinator:
|
|
374
|
-
:
|
|
375
|
-
|
|
433
|
+
: run?.phase === "awaiting-acceptance" && !operationComplete
|
|
434
|
+
? `Requested operation is ${operationStatus}. Continue the actual operation or report its blocker; auxiliary checks and review disposition cannot complete it.`
|
|
435
|
+
: run?.phase === "awaiting-acceptance" ? (currentReview && mission.review?.verdict === "evidence-gaps" && !reviewAccepted
|
|
436
|
+
? "Coordinator: the Reviewer found only missing evidence. Supply focused original-file excerpts through review_mission evidence and traces; do not re-implement. At the evidence-gap limit, review ends with the gaps listed, not with execution proof."
|
|
437
|
+
: reviewAccepted
|
|
438
|
+
? "Coordinator: review permits submission. Submit the candidate with any remaining gaps; do not repeat passed validation or review. Operator performs final acceptance."
|
|
439
|
+
: "Coordinator: address recorded findings or obtain the required independent review, then submit_mission. Operator compares all requirements with source/evidence before complete_mission.")
|
|
440
|
+
: "Coordinator: continue the next useful unit within original requirements. Return only a completion candidate, user-only decision, or scope/budget extension." };
|
|
376
441
|
}
|
|
377
442
|
/** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
|
|
378
443
|
export function missionReviewTask(mission) {
|
|
@@ -915,9 +915,9 @@ export class OperatorRuntime {
|
|
|
915
915
|
"validation:", ...unit.validation.map(value => ` - ${value}`),
|
|
916
916
|
mission ? "Read-only investigation commands are unrestricted. Use shell to reproduce and diagnose without asking for command registration. Keep all writes, including generated/transient outputs and cleanup, inside unit.write. Run formal validation exactly as listed, in order and in separate calls, so the host records its real result. If a write scope or formal check must change, return the precise change to your Coordinator; it can extend/redeclare immediately within the original requirements. Diagnostic success is not formal acceptance evidence."
|
|
917
917
|
: "Execute validation in its declared order. Earlier entries may be approved generator, build, formatter, or exact cleanup commands required before canonical criterion tests. Every persistent or transient generator output must be declared in unit.write. Cleanup may remove only declared unit.write outputs and must be an explicit ordered command after generation and before post-commit or canonical validation; never add an ignore rule or remove an undeclared path. If any necessary command, input, output, or cleanup is missing, do not run an undeclared command or variant and do not use resume evidence tooling to invent permission; return a contract-repair decision.",
|
|
918
|
-
"Preserve existing public API success and error return semantics unless acceptance explicitly changes them, and cover those compatibility boundaries in the declared validation.",
|
|
918
|
+
...(mission ? [] : ["Preserve existing public API success and error return semantics unless acceptance explicitly changes them, and cover those compatibility boundaries in the declared validation."]),
|
|
919
919
|
mission
|
|
920
|
-
? "
|
|
920
|
+
? "Complete the assigned work and listed validation within this Task. For a known operation, proceed through setup, execution and result collection; preparation alone is not execution. Preserve existing public behavior when changing source. Do not spawn nested subagents. The Coordinator handles any applicable independent review after your return; review is not a prerequisite to execution. Return actual results, including failed or not-started operations, and any exact contract correction needed."
|
|
921
921
|
: "Do not spawn nested subagents for consultation. Required consultations belong to the root before dispatch; use the confirmed decisions and evidence declared in the unit objective and inputs. If required consultation results or user decisions are missing, return the exact contract gap to the parent instead of attempting a deeper Task, inventing consent, or asking the user to repeat an already recorded decision.",
|
|
922
922
|
...(plan.git_lifecycle !== undefined && index === plan.units.length - 1 ? [
|
|
923
923
|
`git_post_commit_validation: ${JSON.stringify(plan.git_lifecycle.post_commit_validation)}`,
|
package/dist/plugin/fast-lane.js
CHANGED
|
@@ -28,6 +28,10 @@ function lineValue(prompt, key) {
|
|
|
28
28
|
const values = [...prompt.matchAll(new RegExp(`^\\s*${escapedKey}\\s*:\\s*(.+?)\\s*$`, "gmu"))];
|
|
29
29
|
return values.length === 1 ? values[0][1] : undefined;
|
|
30
30
|
}
|
|
31
|
+
/** Keep an unambiguous, known Strategy trigger when Japanese sentence punctuation joins its prose. */
|
|
32
|
+
function normalizeAdvisorPrompt(prompt) {
|
|
33
|
+
return prompt.replace(/^([ \t]*strategy_trigger[ \t]*:[ \t]*(?:architecture-choice|cross-boundary-tradeoff|material-uncertainty))[。.][ \t]*(?=\S)/mu, "$1\n");
|
|
34
|
+
}
|
|
31
35
|
function hasReviewEvidence(prompt) {
|
|
32
36
|
if (lineValue(prompt, "canonical_validation_exit") !== "0")
|
|
33
37
|
return false;
|
|
@@ -416,13 +420,17 @@ export class FastLaneController {
|
|
|
416
420
|
return;
|
|
417
421
|
}
|
|
418
422
|
if (role === "dog-advisor") {
|
|
419
|
-
const
|
|
423
|
+
const normalized = normalizeAdvisorPrompt(prompt);
|
|
424
|
+
const trigger = lineValue(normalized, "strategy_trigger");
|
|
420
425
|
if (trigger === undefined || !STRATEGY_TRIGGER_SET.has(trigger)) {
|
|
421
426
|
const error = new FastLaneDeniedError("ADVISOR_TRIGGER_REQUIRED");
|
|
422
|
-
error.message += `; required: strategy_trigger: <${STRATEGY_TRIGGERS.join(" | ")}
|
|
427
|
+
error.message += `; required: strategy_trigger: <${STRATEGY_TRIGGERS.join(" | ")}> on its own line; ` +
|
|
428
|
+
"correct this Task header and redispatch the same consultation before proceeding, not runtime policy";
|
|
423
429
|
throw error;
|
|
424
430
|
}
|
|
425
|
-
|
|
431
|
+
if (normalized !== prompt)
|
|
432
|
+
args.prompt = normalized;
|
|
433
|
+
const basis = fallbackBasis(normalized);
|
|
426
434
|
const dispatches = state.advisorRequests.get(basis) ?? 0;
|
|
427
435
|
const retry = lineValue(prompt, "fallback_retry");
|
|
428
436
|
if ((dispatches === 0 && retry !== undefined) || (dispatches === 1 && retry !== "true")) {
|
package/dist/plugin/index.js
CHANGED
|
@@ -51,7 +51,7 @@ import { collectRunMetrics, createSortieResult, createGoalReport, insertRunMetri
|
|
|
51
51
|
import { STABLE_RUNTIME_PROFILE } from "../core/runtime-profile.js";
|
|
52
52
|
import { evidenceFromObservedExecution } from "../core/observed-goal-evidence.js";
|
|
53
53
|
import { receiptBoundTerminalText } from "./receipt-presentation.js";
|
|
54
|
-
import { protectedSnapshot, refreshProtectedSnapshot } from "./protected-snapshot.js";
|
|
54
|
+
import { operationInputSnapshot, protectedSnapshot, refreshProtectedSnapshot } from "./protected-snapshot.js";
|
|
55
55
|
import { settledUnitUsage } from "./unit-usage.js";
|
|
56
56
|
import { goalCompletionReadiness } from "./goal-completion.js";
|
|
57
57
|
const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
|
|
@@ -1477,10 +1477,15 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1477
1477
|
}
|
|
1478
1478
|
}
|
|
1479
1479
|
}
|
|
1480
|
+
const mission = await new OperatorMissionRuntime(input.directory, runtimeProfile).read(root);
|
|
1481
|
+
const operationInputs = mission?.kind === "operation" && mission.execution?.commands.includes(rawCommand) &&
|
|
1482
|
+
resolve(input.directory, typeof args?.workdir === "string" ? args.workdir : ".") === mission.execution.directory
|
|
1483
|
+
? await operationInputSnapshot(authorization.projectRoot, snapshot.binding).catch(() => undefined) : undefined;
|
|
1480
1484
|
hostGoalExecutions.set(toolInput.callID, { root, projectRoot: authorization.projectRoot,
|
|
1481
1485
|
sessionID: toolInput.sessionID,
|
|
1482
1486
|
callID: toolInput.callID, tool: toolInput.tool, command: [rawCommand], startedAt: new Date().toISOString(),
|
|
1483
1487
|
binding: snapshot.binding, source: snapshot.source, candidate: snapshot.candidate,
|
|
1488
|
+
...(operationInputs === undefined ? {} : { operationInputs }),
|
|
1484
1489
|
owner: validation?.request.owner ?? "worker", validation,
|
|
1485
1490
|
...(reusedEvidence === undefined ? {} : { reusedEvidence }) });
|
|
1486
1491
|
}
|
|
@@ -1508,10 +1513,11 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1508
1513
|
? new Date(timing.start).toISOString() : execution.startedAt;
|
|
1509
1514
|
const endedAt = typeof timing?.end === "number" && Number.isFinite(timing.end)
|
|
1510
1515
|
? new Date(timing.end).toISOString() : new Date().toISOString();
|
|
1511
|
-
//
|
|
1512
|
-
//
|
|
1513
|
-
|
|
1514
|
-
|
|
1516
|
+
// Existing operations produce declared outputs. Preserve their read-only inputs during execution,
|
|
1517
|
+
// then bind ordinary evidence to all post-operation bytes, including those outputs.
|
|
1518
|
+
const fresh = refreshed !== undefined && (execution.operationInputs === undefined
|
|
1519
|
+
? refreshed.source === execution.source
|
|
1520
|
+
: await operationInputSnapshot(execution.projectRoot, execution.binding).catch(() => undefined) === execution.operationInputs);
|
|
1515
1521
|
const immutableRef = outcome === undefined || execution.reusedEvidence !== undefined ? undefined : goalFingerprint({ root: execution.root,
|
|
1516
1522
|
child_session_id: execution.sessionID, call_id: execution.callID, command: execution.command,
|
|
1517
1523
|
started_at: observedStartedAt, ended_at: endedAt, exit_code: exitCode ?? null, outcome,
|
|
@@ -4770,6 +4776,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
4770
4776
|
remedy: "No write-gate state changed after one retry; stop this candidate and record the local blocker.",
|
|
4771
4777
|
},
|
|
4772
4778
|
};
|
|
4779
|
+
let conflicts = [];
|
|
4773
4780
|
const deny = (reason, defects = []) => {
|
|
4774
4781
|
const detail = remedies[reason] ?? {
|
|
4775
4782
|
recoverable: false,
|
|
@@ -4798,6 +4805,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
4798
4805
|
reason,
|
|
4799
4806
|
...detail,
|
|
4800
4807
|
...(reported.length === 0 ? {} : { defects: reported }),
|
|
4808
|
+
...(reason === "manifest-overlap" && conflicts.length ? { conflicts } : {}),
|
|
4801
4809
|
escalation,
|
|
4802
4810
|
});
|
|
4803
4811
|
};
|
|
@@ -4884,10 +4892,23 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
4884
4892
|
if (diagnostics.some(({ severity }) => severity === "error") ||
|
|
4885
4893
|
inspectionFingerprint(handoffValidation.value, diagnostics, validation.value) !== inspectedEntry.fingerprint)
|
|
4886
4894
|
return denyHandoffMismatch(handoffValue, contractDefects(diagnostics));
|
|
4887
|
-
const conflictsWithActiveAuthorization = (writeScopes, readScopes) =>
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4895
|
+
const conflictsWithActiveAuthorization = (writeScopes, readScopes) => {
|
|
4896
|
+
conflicts = [];
|
|
4897
|
+
for (const [ownerSessionID, authorization] of sessionAuthorizations) {
|
|
4898
|
+
if (ownerSessionID === sessionID || authorization.suspended)
|
|
4899
|
+
continue;
|
|
4900
|
+
for (const [requested, held, access] of [[writeScopes, authorization.writeScopes, "write/write"],
|
|
4901
|
+
[writeScopes, authorization.readScopes, "write/read"], [readScopes, authorization.writeScopes, "read/write"]]) {
|
|
4902
|
+
for (const left of requested)
|
|
4903
|
+
for (const right of held) {
|
|
4904
|
+
if (conflicts.length < 8 && writeScopesOverlap([left], [right])) {
|
|
4905
|
+
conflicts.push({ session_id: ownerSessionID, requested: left, held: right, access });
|
|
4906
|
+
}
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
return conflicts.length > 0;
|
|
4911
|
+
};
|
|
4891
4912
|
const acquireDurableLease = async (readScopes, writeScopes) => {
|
|
4892
4913
|
if (activeSessions.get(sessionID)?.parallel !== "valid")
|
|
4893
4914
|
return undefined;
|
|
@@ -5103,7 +5124,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5103
5124
|
for (const [sessionID, state] of activeSessions) {
|
|
5104
5125
|
if (state.expiresAt > now)
|
|
5105
5126
|
continue;
|
|
5106
|
-
if (state.inFlightCalls.size > 0)
|
|
5127
|
+
if (state.inFlightCalls.size > 0 || childHasInFlightParentTask(sessionID))
|
|
5107
5128
|
state.expiresAt = now + ACTIVE_SESSION_CACHE.ttlMilliseconds;
|
|
5108
5129
|
else
|
|
5109
5130
|
expireSession(sessionID);
|
|
@@ -5116,7 +5137,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5116
5137
|
for (const [sessionID, state] of coordinatorRoots) {
|
|
5117
5138
|
if (state.expiresAt > now)
|
|
5118
5139
|
continue;
|
|
5119
|
-
const hasInFlightChild = [...sessionRoots.entries()].some(([childID, rootID]) => rootID === sessionID && (activeSessions.get(childID)?.inFlightCalls.size ?? 0) > 0);
|
|
5140
|
+
const hasInFlightChild = [...sessionRoots.entries()].some(([childID, rootID]) => rootID === sessionID && ((activeSessions.get(childID)?.inFlightCalls.size ?? 0) > 0 || childHasInFlightParentTask(childID)));
|
|
5120
5141
|
if (hasInFlightChild)
|
|
5121
5142
|
state.expiresAt = now + ACTIVE_SESSION_CACHE.ttlMilliseconds;
|
|
5122
5143
|
else {
|
|
@@ -6565,6 +6586,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6565
6586
|
diagnosis.accepting = false;
|
|
6566
6587
|
diagnosisCalls.delete(toolInput.callID ?? "");
|
|
6567
6588
|
}
|
|
6589
|
+
const returnedMissionWorker = completedChildSessionID !== undefined &&
|
|
6590
|
+
await input.runtimeBridge?.allowsInvestigativeShell?.(completedChildSessionID);
|
|
6568
6591
|
parallelCalls.delete(toolInput.callID ?? "");
|
|
6569
6592
|
activeSessions.get(toolInput.sessionID ?? "")?.inFlightCalls.delete(toolInput.callID ?? "");
|
|
6570
6593
|
if (coordinatorTaskFinished && diagnosis === undefined) {
|
|
@@ -6584,6 +6607,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6584
6607
|
evictSession(completedChildSessionID);
|
|
6585
6608
|
void childLifecycles.get(completedChildSessionID)?.check();
|
|
6586
6609
|
}
|
|
6610
|
+
else if (completedChildSessionID !== undefined && activeSessions.get(completedChildSessionID)?.parallel !== "valid" &&
|
|
6611
|
+
returnedMissionWorker) {
|
|
6612
|
+
// Keep same-worker recovery identity, but a returned serial Task must not keep a live
|
|
6613
|
+
// write reservation. Rebinding the retained manifest reacquires it before any later write.
|
|
6614
|
+
await releaseWriteGate(completedChildSessionID);
|
|
6615
|
+
}
|
|
6587
6616
|
}
|
|
6588
6617
|
},
|
|
6589
6618
|
"tool.execute.before": async (toolInput, output) => {
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { OperatorState } from "../core/operator-runtime.js";
|
|
2
|
-
import { type OperatorMission } from "../core/operator-mission.js";
|
|
2
|
+
import { type MissionEvidenceExcerpt, type OperatorMission } from "../core/operator-mission.js";
|
|
3
3
|
import { type RuntimeProfile } from "../core/runtime-profile.js";
|
|
4
|
+
/** A child ID alone cannot establish the initial phase after a restart or failed dispatch. */
|
|
5
|
+
export declare function initialMissionReviewPrompt(mission: OperatorMission, prompts: readonly string[]): string | undefined;
|
|
4
6
|
/** Recover from native completion records, never from the pending review's inherited child ID. */
|
|
5
7
|
export declare function completedMissionReviewPrompts(mission: OperatorMission | undefined, profile: RuntimeProfile, root: string, requestedPrompt: string, host: {
|
|
6
8
|
get(id: string): Promise<unknown>;
|
|
7
9
|
messages(id: string): Promise<readonly Record<string, unknown>[]>;
|
|
8
10
|
}): Promise<string[]>;
|
|
9
11
|
/** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
|
|
10
|
-
export declare function missionReviewSource(directory: string, run: OperatorState): Promise<{
|
|
12
|
+
export declare function missionReviewSource(directory: string, run: OperatorState, evidence?: readonly MissionEvidenceExcerpt[]): Promise<{
|
|
11
13
|
fingerprint: string;
|
|
12
14
|
excerpt: string;
|
|
13
15
|
}>;
|
|
@@ -2,8 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
4
|
import { lstat, readlink } from "node:fs/promises";
|
|
5
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
7
8
|
import { TOOL_ENVIRONMENT } from "../runtime-mission-assets.js";
|
|
8
9
|
import { MISSION_REVIEW_REFERENCE } from "../core/operator-mission.js";
|
|
9
10
|
import { canonicalAgent } from "../core/runtime-profile.js";
|
|
@@ -12,11 +13,21 @@ import { normalizeManifestScope } from "../core/path.js";
|
|
|
12
13
|
import { declaredArtifacts } from "./declared-artifacts.js";
|
|
13
14
|
const exec = promisify(execFile);
|
|
14
15
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
/** A child ID alone cannot establish the initial phase after a restart or failed dispatch. */
|
|
17
|
+
export function initialMissionReviewPrompt(mission, prompts) {
|
|
18
|
+
return [mission.review?.initialPrompt, ...prompts].find(prompt => typeof prompt === "string" &&
|
|
19
|
+
prompt.startsWith(`candidate_id: ${mission.id}\n`) && /^review_phase: initial$/mu.test(prompt) &&
|
|
20
|
+
/^canonical_validation_exit: 0$/mu.test(prompt));
|
|
21
|
+
}
|
|
15
22
|
/** Recover from native completion records, never from the pending review's inherited child ID. */
|
|
16
23
|
export async function completedMissionReviewPrompts(mission, profile, root, requestedPrompt, host) {
|
|
17
24
|
if (!mission || mission.root !== root || !mission.coordinator || ["completed", "cancelled"].includes(mission.phase) ||
|
|
18
25
|
mission.review?.task?.prompt !== requestedPrompt)
|
|
19
26
|
return [];
|
|
27
|
+
// The completion hook has already bound this prompt to a real independent Reviewer child.
|
|
28
|
+
// Avoid a second V2 history read when its page/list API is temporarily unavailable.
|
|
29
|
+
if (initialMissionReviewPrompt(mission, []))
|
|
30
|
+
return [mission.review.initialPrompt];
|
|
20
31
|
const coordinator = await host.get(mission.coordinator);
|
|
21
32
|
if (!record(coordinator) || coordinator.parentID !== root || canonicalAgent(profile, coordinator.agent) !== "dog-operator")
|
|
22
33
|
return [];
|
|
@@ -75,12 +86,38 @@ export async function completedMissionReviewPrompts(mission, profile, root, requ
|
|
|
75
86
|
return prompts;
|
|
76
87
|
}
|
|
77
88
|
/** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
|
|
78
|
-
export async function missionReviewSource(directory, run) {
|
|
89
|
+
export async function missionReviewSource(directory, run, evidence = []) {
|
|
79
90
|
const hash = createHash("sha256").update(JSON.stringify(run.units.map(unit => ({ unit: unit.unit, hashes: unit.hashes }))));
|
|
91
|
+
let selected = "";
|
|
92
|
+
for (const entry of evidence) {
|
|
93
|
+
const absolute = resolve(directory, entry.path);
|
|
94
|
+
const allowed = run.units.flatMap(unit => [...unit.unit.read, ...unit.unit.write]).some(scope => {
|
|
95
|
+
const root = resolve(directory, scope === "." ? scope : normalizeManifestScope(scope).path), rest = relative(root, absolute);
|
|
96
|
+
return rest === "" || (rest !== ".." && !rest.startsWith(`..${sep}`) && !isAbsolute(rest));
|
|
97
|
+
});
|
|
98
|
+
if (!allowed || !Number.isSafeInteger(entry.offset) || entry.offset < 1 ||
|
|
99
|
+
!Number.isSafeInteger(entry.limit) || entry.limit < 1 || entry.limit > 200)
|
|
100
|
+
throw new Error("mission-review-evidence: select up to 200 lines from a declared input/output");
|
|
101
|
+
const info = await lstat(absolute);
|
|
102
|
+
if (!info.isFile())
|
|
103
|
+
throw new Error("mission-review-evidence: select a regular file");
|
|
104
|
+
hash.update(JSON.stringify(entry)).update(String(info.size));
|
|
105
|
+
const stream = createReadStream(absolute);
|
|
106
|
+
stream.on("data", chunk => hash.update(chunk));
|
|
107
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
108
|
+
selected += `\n--- evidence: ${entry.path}:${entry.offset} ---\n`;
|
|
109
|
+
let number = 0;
|
|
110
|
+
for await (const line of lines) {
|
|
111
|
+
number++;
|
|
112
|
+
if (number >= entry.offset && number < entry.offset + entry.limit && Buffer.byteLength(selected) < 12_000) {
|
|
113
|
+
selected += `${number}: ${line.slice(0, 2_000)}\n`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
80
117
|
const writes = [...new Set(run.units.flatMap(unit => unit.unit.write).map(path => path === "." ? path : normalizeManifestScope(path).path))];
|
|
81
118
|
if (writes.length === 0)
|
|
82
119
|
return { fingerprint: `sha256:${hash.digest("hex")}`,
|
|
83
|
-
excerpt: "[Read-only units: no declared output files. Review the supplied observations, traces and validation evidence.]" };
|
|
120
|
+
excerpt: selected + "[Read-only units: no declared output files. Review the supplied observations, traces and validation evidence.]" };
|
|
84
121
|
// The shared dependency environment is local tooling, never reviewed or pinned source.
|
|
85
122
|
const external = [], local = [];
|
|
86
123
|
for (const path of writes) {
|
|
@@ -99,8 +136,9 @@ export async function missionReviewSource(directory, run) {
|
|
|
99
136
|
const ignored = local.length ? await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...scopes]) : "";
|
|
100
137
|
for (const path of ignored.split("\0").filter(Boolean))
|
|
101
138
|
untracked.add(path);
|
|
102
|
-
|
|
103
|
-
const unchanged =
|
|
139
|
+
const diff = local.length ? await git(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--", ...scopes]).catch(() => git(["diff", "--no-ext-diff", "--no-textconv", "--", ...scopes])) : "";
|
|
140
|
+
const unchanged = diff.length === 0;
|
|
141
|
+
let excerpt = selected + diff;
|
|
104
142
|
const paths = [...new Set([...names.split("\0"), ...ignored.split("\0")].filter(Boolean))].sort();
|
|
105
143
|
const omitted = [];
|
|
106
144
|
for (const path of paths) {
|