sortie-dogs 0.12.10 → 0.12.12
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/goal-bound.d.ts +1 -1
- package/dist/core/goal-bound.js +2 -1
- package/dist/core/operator-mission.js +14 -5
- package/dist/core/operator-runtime.d.ts +3 -2
- package/dist/core/operator-runtime.js +21 -9
- package/dist/core/path.d.ts +9 -0
- package/dist/core/path.js +20 -0
- package/dist/core/validate-manifest.js +8 -4
- package/dist/plugin/declared-artifacts.d.ts +8 -0
- package/dist/plugin/declared-artifacts.js +80 -0
- package/dist/plugin/gate.js +80 -10
- package/dist/plugin/index.js +55 -46
- package/dist/plugin/mission-review.js +24 -7
- package/dist/plugin/profiled.js +20 -8
- package/dist/plugin/protected-snapshot.js +29 -7
- package/dist/plugin/runtime-bridge.d.ts +2 -0
- package/dist/plugin/v2.js +7 -0
- package/dist/runtime-mission-assets.js +30 -1
- 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.12-mission-lineage-external-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.12-mission-lineage-external-v1";
|
|
@@ -38,7 +38,7 @@ export interface GoalEvidence {
|
|
|
38
38
|
readonly source_paths: readonly string[];
|
|
39
39
|
readonly candidate_paths: readonly string[];
|
|
40
40
|
/** Missing on legacy evidence: retain its original all-paths snapshot recipe. */
|
|
41
|
-
readonly source_policy?: "project-files-v1";
|
|
41
|
+
readonly source_policy?: "project-files-v1" | "declared-paths-v1";
|
|
42
42
|
};
|
|
43
43
|
readonly execution: {
|
|
44
44
|
readonly command: readonly string[];
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -62,7 +62,8 @@ export function validGoalEvidence(value, state) {
|
|
|
62
62
|
text(protectedBinding.project_root) && text(protectedBinding.manifest_path) && Array.isArray(protectedBinding.source_paths) &&
|
|
63
63
|
protectedBinding.source_paths.every(text) && Array.isArray(protectedBinding.candidate_paths) &&
|
|
64
64
|
protectedBinding.candidate_paths.every(text) &&
|
|
65
|
-
(protectedBinding.source_policy === undefined || protectedBinding.source_policy === "project-files-v1"
|
|
65
|
+
(protectedBinding.source_policy === undefined || protectedBinding.source_policy === "project-files-v1" ||
|
|
66
|
+
protectedBinding.source_policy === "declared-paths-v1");
|
|
66
67
|
const matches = criteria.length > 0 && criteria.length === value.measurement.criterion_ids.length &&
|
|
67
68
|
criteria.every((criterion) => criterion.target === value.measurement.target &&
|
|
68
69
|
criterion.entrypoint === value.measurement.entrypoint && criterion.workload === value.measurement.workload &&
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { normalizeExecutionScope } from "./path.js";
|
|
5
5
|
import { parseOperatorPlan } from "./operator-runtime.js";
|
|
6
6
|
import { profileAgent } from "./runtime-profile.js";
|
|
7
7
|
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
@@ -286,7 +286,7 @@ export function missionPlan(mission, raw) {
|
|
|
286
286
|
const entries = value[field] ?? [];
|
|
287
287
|
if (!Array.isArray(entries) || !entries.every(item => typeof item === "string"))
|
|
288
288
|
throw new Error(`mission-unit-${index + 1}: ${field} must be paths`);
|
|
289
|
-
return [...new Set(entries.map(item =>
|
|
289
|
+
return [...new Set(entries.map(item => normalizeExecutionScope(item)))];
|
|
290
290
|
};
|
|
291
291
|
const ids = value.requirement_ids ?? mission.requirements.map(item => item.id);
|
|
292
292
|
if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id => mission.requirements.some(item => item.id === id))) {
|
|
@@ -328,7 +328,7 @@ export function missionPlan(mission, raw) {
|
|
|
328
328
|
// Keep all detailed instructions in the handoff, not in a 512-character wire label.
|
|
329
329
|
criteria: units.map(unit => ({ criterion_id: unit.id, target: unit.title.slice(0, 512),
|
|
330
330
|
entrypoint: (unit.write[0] ?? unit.read[0] ?? unit.id).slice(0, 512), validation_command: unit.validation.at(-1) })) },
|
|
331
|
-
units });
|
|
331
|
+
units }, "execution");
|
|
332
332
|
}
|
|
333
333
|
export function missionPacket(mission, run) {
|
|
334
334
|
const currentReview = mission.review !== undefined && mission.review.runID === (run?.runID ?? mission.runID);
|
|
@@ -336,6 +336,12 @@ export function missionPacket(mission, run) {
|
|
|
336
336
|
return { mission_id: mission.id, phase: mission.phase, coordinator_session_id: mission.coordinator,
|
|
337
337
|
requirements: mission.requirements, original_request_refs: mission.requests.map(item => `user:${item.id}`),
|
|
338
338
|
submission: mission.submission, progress: mission.progress,
|
|
339
|
+
execution_summary: { completed_units: run?.units.filter(unit => unit.status === "succeeded").length ?? 0,
|
|
340
|
+
running_units: run?.units.filter(unit => unit.status === "running").map(unit => ({ id: unit.unit.id, title: unit.unit.title, child_session_id: unit.childSessionID })) ?? [],
|
|
341
|
+
pending_units: run?.units.filter(unit => unit.status === "pending").length ?? 0,
|
|
342
|
+
failed_units: run?.units.filter(unit => unit.status === "failed").length ?? 0,
|
|
343
|
+
historical_failed_attempts: mission.progress.filter(unit => unit.status === "failed").length,
|
|
344
|
+
accepted: mission.phase === "completed" },
|
|
339
345
|
review: mission.review ? { risk_tags: mission.review.risk, verdict: mission.review.verdict,
|
|
340
346
|
run_id: mission.review.runID, current_run: currentReview,
|
|
341
347
|
source_fingerprint: mission.review.source, reviewer_session_id: mission.review.child ?? null,
|
|
@@ -344,11 +350,14 @@ export function missionPacket(mission, run) {
|
|
|
344
350
|
passed: currentReview && mission.review.verdict === "PASS", permits_submission: reviewAccepted } : null,
|
|
345
351
|
...(run ? { run_id: run.runID, status: run.phase, decision: run.decision,
|
|
346
352
|
units: run.units.map(unit => ({ id: unit.unit.id, title: unit.unit.title, status: unit.status,
|
|
347
|
-
child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence
|
|
353
|
+
child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence,
|
|
354
|
+
...(unit.dispatchDenial ? { dispatch_denial: unit.dispatchDenial } : {}) })) } : {}),
|
|
348
355
|
next_action: mission.phase === "completed" ? "Mission completed. Report the accepted result and retained review gaps; no further dispatch or completion call is needed."
|
|
349
356
|
: mission.phase === "submitted" && mission.submission?.status === "ready"
|
|
350
357
|
? "Operator: compare the submitted candidate with the original requirements and actual evidence, then complete_mission if satisfied. Report remaining evidence gaps; they are not a review PASS."
|
|
351
|
-
: run?.phase === "awaiting-decision" ?
|
|
358
|
+
: run?.phase === "awaiting-decision" ? (run.units.some(unit => unit.dispatchDenial)
|
|
359
|
+
? "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."
|
|
360
|
+
: "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative.")
|
|
352
361
|
: run?.phase === "awaiting-acceptance" ? (currentReview && mission.review?.verdict === "evidence-gaps" && !reviewAccepted
|
|
353
362
|
? "Coordinator: the Reviewer found only missing evidence. Supply it through review_mission traces (or at most one evidence-only unit) and re-review once; do not re-implement. At the evidence-gap limit, submit_mission ready with the gaps listed."
|
|
354
363
|
: reviewAccepted
|
|
@@ -115,6 +115,7 @@ interface UnitState {
|
|
|
115
115
|
childSessionID: string | null;
|
|
116
116
|
evidence: readonly GoalEvidence[];
|
|
117
117
|
resultClass: string | null;
|
|
118
|
+
dispatchDenial?: string;
|
|
118
119
|
repairValidationAttempts: number;
|
|
119
120
|
repairValidation: {
|
|
120
121
|
readonly repair_fingerprint: string;
|
|
@@ -210,7 +211,7 @@ export interface OperatorState {
|
|
|
210
211
|
/** A later mission may replace an unexecuted cancellation, but must retain settled work's acceptance. */
|
|
211
212
|
export declare function cancelledMissionRetainsAcceptance(state: OperatorState): boolean;
|
|
212
213
|
export declare function operatorGitPathAuthorized(path: string, scopes: readonly string[], platform?: NodeJS.Platform): boolean;
|
|
213
|
-
export declare function parseOperatorPlan(value: unknown): OperatorPlan;
|
|
214
|
+
export declare function parseOperatorPlan(value: unknown, scopeFormat?: "repository" | "execution"): OperatorPlan;
|
|
214
215
|
/** Root-owned durable queue. It never executes code, changes acceptance, or accepts a candidate. */
|
|
215
216
|
export declare class OperatorRuntime {
|
|
216
217
|
readonly profile: RuntimeProfile;
|
|
@@ -284,7 +285,7 @@ export declare class OperatorRuntime {
|
|
|
284
285
|
private nextOnce;
|
|
285
286
|
admitWorker(root: string, actor: string, callID: string, args: unknown): Promise<OperatorTask>;
|
|
286
287
|
private admitWorkerOnce;
|
|
287
|
-
rejectedAdmission(root: string, callID: string): Promise<void>;
|
|
288
|
+
rejectedAdmission(root: string, callID: string, reason?: string): Promise<void>;
|
|
288
289
|
rejectDispatch(root: string, callID: string, decision?: string): Promise<void>;
|
|
289
290
|
private rejectDispatchOnce;
|
|
290
291
|
private repairPathProtected;
|
|
@@ -5,7 +5,7 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { acceptanceContinuityFingerprint, inspectAcceptanceContinuity, normalizeAcceptanceCriteria, ACCEPTANCE_CONTINUITY_EXTENSION, MAX_ACCEPTANCE_CONTINUITY_BYTES, MAX_ACCEPTANCE_CRITERIA } from "./acceptance-continuity.js";
|
|
7
7
|
import { expandGoalDeclaration, goalDeclarationDefaults, goalDeclarationFieldDiagnostics, hasGoalCommandAliasConflict } from "./goal-declaration-format.js";
|
|
8
|
-
import { normalizeManifestPath, normalizeRelativePath } from "./path.js";
|
|
8
|
+
import { normalizeExecutionScope, normalizeManifestPath, normalizeManifestScope, normalizeRelativePath } from "./path.js";
|
|
9
9
|
import { CONTRACT_TEXT_LIMITS, validateHandoffSchema, validateOperationManifestSchema } from "./validate-schema.js";
|
|
10
10
|
import { validateManifest } from "./validate-manifest.js";
|
|
11
11
|
import { profileAgent, RUNTIME_PROFILES } from "./runtime-profile.js";
|
|
@@ -44,7 +44,10 @@ const text = (value) => typeof value === "string" && value.trim().length > 0 &&
|
|
|
44
44
|
export function operatorGitPathAuthorized(path, scopes, platform = process.platform) {
|
|
45
45
|
const key = (value) => platform === "win32" ? value.toLowerCase() : value;
|
|
46
46
|
const candidate = key(path);
|
|
47
|
-
return scopes.some(scope =>
|
|
47
|
+
return scopes.some(scope => {
|
|
48
|
+
const base = key(normalizeManifestScope(scope).path);
|
|
49
|
+
return candidate === base || candidate.startsWith(`${base}/`);
|
|
50
|
+
});
|
|
48
51
|
}
|
|
49
52
|
const contractError = (diagnostic) => { throw new OperatorContractError([diagnostic]); };
|
|
50
53
|
const planError = (pointer, code, rule, repair_kind = "repair-field") => contractError({ document: "plan", pointer, code, rule, repair_kind });
|
|
@@ -80,7 +83,7 @@ function scopePaths(value, limit) {
|
|
|
80
83
|
};
|
|
81
84
|
return paths.every(normalized) && new Set(paths).size === paths.length;
|
|
82
85
|
}
|
|
83
|
-
export function parseOperatorPlan(value) {
|
|
86
|
+
export function parseOperatorPlan(value, scopeFormat = "repository") {
|
|
84
87
|
if (!record(value) || !exactKeys(value, ["schema_version", "acceptance", "acceptance_proof", "source_refs", "goal_declaration", "units", "git_lifecycle"])) {
|
|
85
88
|
return planError("/", "operator-plan-invalid", "operator-plan-fields");
|
|
86
89
|
}
|
|
@@ -204,11 +207,11 @@ export function parseOperatorPlan(value) {
|
|
|
204
207
|
for (const [pathIndex, name] of unit[field].entries()) {
|
|
205
208
|
let valid = false;
|
|
206
209
|
try {
|
|
207
|
-
valid = normalizeRelativePath(name) === name;
|
|
210
|
+
valid = (scopeFormat === "execution" ? normalizeExecutionScope(name) : normalizeRelativePath(name)) === name;
|
|
208
211
|
}
|
|
209
212
|
catch { /* Return only a typed pointer, never the path value. */ }
|
|
210
213
|
if (!valid)
|
|
211
|
-
return planError(`/units/${unitIndex}/${field}/${pathIndex}`, "operator-scope-invalid", "repository-relative-scope");
|
|
214
|
+
return planError(`/units/${unitIndex}/${field}/${pathIndex}`, "operator-scope-invalid", scopeFormat === "execution" ? "normalized-execution-scope" : "repository-relative-scope");
|
|
212
215
|
}
|
|
213
216
|
}
|
|
214
217
|
for (const name of unit.write) {
|
|
@@ -713,7 +716,7 @@ export class OperatorRuntime {
|
|
|
713
716
|
}
|
|
714
717
|
immutableReplacement = { ...immutableReplacement, goal_declaration: declaration };
|
|
715
718
|
}
|
|
716
|
-
const plan = parseOperatorPlan(immutableReplacement);
|
|
719
|
+
const plan = parseOperatorPlan(immutableReplacement, mission ? "execution" : "repository");
|
|
717
720
|
const validatedPlanHash = hash(JSON.stringify(plan));
|
|
718
721
|
const replanning = mission?.replaceRunID !== undefined;
|
|
719
722
|
if (replanning) {
|
|
@@ -834,6 +837,13 @@ export class OperatorRuntime {
|
|
|
834
837
|
const units = [];
|
|
835
838
|
const controls = [{ path: declarationPath, content: declaration }];
|
|
836
839
|
for (const [index, unit] of plan.units.entries()) {
|
|
840
|
+
for (const name of unit.write) {
|
|
841
|
+
const scoped = relative(this.projectRoot, resolve(this.projectRoot, normalizeManifestScope(name).path)).replaceAll("\\", "/");
|
|
842
|
+
if ([".git", ...Object.values(RUNTIME_PROFILES).map(profile => profile.stateDirectory)]
|
|
843
|
+
.some(directory => scoped === directory || scoped.startsWith(`${directory}/`))) {
|
|
844
|
+
return planError(`/units/${index}/write`, "operator-control-write-forbidden", "control-write-forbidden");
|
|
845
|
+
}
|
|
846
|
+
}
|
|
837
847
|
const taskID = `${runID}-${index + 1}`;
|
|
838
848
|
const manifestPath = join(directory, `${taskID}.operation-manifest.json`);
|
|
839
849
|
const manifestRelative = `${this.profile.stateDirectory}/contracts/${taskID}.operation-manifest.json`;
|
|
@@ -1229,19 +1239,21 @@ export class OperatorRuntime {
|
|
|
1229
1239
|
await this.save(state);
|
|
1230
1240
|
return admitted;
|
|
1231
1241
|
}
|
|
1232
|
-
async rejectedAdmission(root, callID) {
|
|
1233
|
-
await this.
|
|
1242
|
+
async rejectedAdmission(root, callID, reason) {
|
|
1243
|
+
await this.serial(root, () => this.rejectDispatchOnce(root, callID, "dispatch-admission-rejected", reason));
|
|
1234
1244
|
}
|
|
1235
1245
|
rejectDispatch(root, callID, decision = "native-task-rejected") {
|
|
1236
1246
|
return this.serial(root, () => this.rejectDispatchOnce(root, callID, decision));
|
|
1237
1247
|
}
|
|
1238
|
-
async rejectDispatchOnce(root, callID, decision) {
|
|
1248
|
+
async rejectDispatchOnce(root, callID, decision, reason) {
|
|
1239
1249
|
const state = await this.required(root);
|
|
1240
1250
|
const unit = state.units.find(item => item.callID === callID && item.status === "running");
|
|
1241
1251
|
if (!unit)
|
|
1242
1252
|
return;
|
|
1243
1253
|
unit.status = "failed";
|
|
1244
1254
|
unit.resultClass = "process-defect";
|
|
1255
|
+
if (reason)
|
|
1256
|
+
unit.dispatchDenial = reason.slice(0, 4000);
|
|
1245
1257
|
state.phase = "awaiting-decision";
|
|
1246
1258
|
state.decision = decision;
|
|
1247
1259
|
await this.save(state);
|
package/dist/core/path.d.ts
CHANGED
|
@@ -21,3 +21,12 @@ export declare function normalizeRelativePath(input: string): string;
|
|
|
21
21
|
* error instead of being resolved away.
|
|
22
22
|
*/
|
|
23
23
|
export declare function normalizeManifestPath(input: string): ManifestPath;
|
|
24
|
+
/** A trailing /** (or slash) declares a directory, including one not created yet.
|
|
25
|
+
* Tool destinations still use normalizeManifestPath: this notation belongs to declarations only.
|
|
26
|
+
*/
|
|
27
|
+
export declare function normalizeManifestScope(input: string): ManifestPath & {
|
|
28
|
+
readonly directory: boolean;
|
|
29
|
+
};
|
|
30
|
+
export declare function normalizeRelativeScope(input: string): string;
|
|
31
|
+
/** Execution scopes may name native absolute destinations (for example global installs). */
|
|
32
|
+
export declare function normalizeExecutionScope(input: string): string;
|
package/dist/core/path.js
CHANGED
|
@@ -52,3 +52,23 @@ export function normalizeManifestPath(input) {
|
|
|
52
52
|
}
|
|
53
53
|
return { kind: "relative", path: normalizeRelativePath(input) };
|
|
54
54
|
}
|
|
55
|
+
/** A trailing /** (or slash) declares a directory, including one not created yet.
|
|
56
|
+
* Tool destinations still use normalizeManifestPath: this notation belongs to declarations only.
|
|
57
|
+
*/
|
|
58
|
+
export function normalizeManifestScope(input) {
|
|
59
|
+
const unified = input.replaceAll("\\", "/");
|
|
60
|
+
const directory = unified.endsWith("/**") || unified.endsWith("/");
|
|
61
|
+
const path = unified.endsWith("/**") ? unified.slice(0, -3) : unified;
|
|
62
|
+
return { ...normalizeManifestPath(path), directory };
|
|
63
|
+
}
|
|
64
|
+
export function normalizeRelativeScope(input) {
|
|
65
|
+
const scope = normalizeManifestScope(input);
|
|
66
|
+
if (scope.kind !== "relative")
|
|
67
|
+
throw new RelativePathError("absolute");
|
|
68
|
+
return scope.path + (scope.directory ? "/**" : "");
|
|
69
|
+
}
|
|
70
|
+
/** Execution scopes may name native absolute destinations (for example global installs). */
|
|
71
|
+
export function normalizeExecutionScope(input) {
|
|
72
|
+
const scope = normalizeManifestScope(input);
|
|
73
|
+
return scope.path + (scope.directory ? "/**" : "");
|
|
74
|
+
}
|
|
@@ -31,7 +31,7 @@ function collectManifestPaths(paths, pointer, diagnostics) {
|
|
|
31
31
|
paths.forEach((path, index) => {
|
|
32
32
|
let normalized;
|
|
33
33
|
try {
|
|
34
|
-
normalized = pathUtils.
|
|
34
|
+
normalized = pathUtils.normalizeManifestScope(path);
|
|
35
35
|
}
|
|
36
36
|
catch (error) {
|
|
37
37
|
if (!(error instanceof pathUtils.RelativePathError))
|
|
@@ -48,7 +48,7 @@ function collectManifestPaths(paths, pointer, diagnostics) {
|
|
|
48
48
|
else if (normalized.kind === "relative") {
|
|
49
49
|
// Absolute manifest targets authorize tools only. Handoff scope/source and Git paths remain
|
|
50
50
|
// repository-relative and therefore never compare equal to an external target.
|
|
51
|
-
normalizedPaths.add(normalized.path);
|
|
51
|
+
normalizedPaths.add(normalized.path + (normalized.directory ? "/**" : ""));
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
54
|
return normalizedPaths;
|
|
@@ -79,7 +79,7 @@ function addPathDiagnostics(diagnostics, paths, allowed, code, pointer) {
|
|
|
79
79
|
const reported = new Set();
|
|
80
80
|
paths.forEach((path, index) => {
|
|
81
81
|
const normalized = normalizePath(path);
|
|
82
|
-
if (normalized !== undefined && (allowed
|
|
82
|
+
if (normalized !== undefined && (scopeAllows(allowed, normalized) || reported.has(normalized)))
|
|
83
83
|
return;
|
|
84
84
|
if (normalized !== undefined)
|
|
85
85
|
reported.add(normalized);
|
|
@@ -91,6 +91,10 @@ function addPathDiagnostics(diagnostics, paths, allowed, code, pointer) {
|
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
+
function scopeAllows(scopes, path) {
|
|
95
|
+
return scopes.has(path) || [...scopes].some(scope => scope.endsWith("/**") &&
|
|
96
|
+
(path === scope.slice(0, -3) || path.startsWith(scope.slice(0, -2))));
|
|
97
|
+
}
|
|
94
98
|
/** Compare one schema-valid handoff with one schema-valid operation manifest. */
|
|
95
99
|
export function validateManifest(handoff, manifest, changedPaths, changedPathsProvided, options = {}) {
|
|
96
100
|
const diagnostics = [];
|
|
@@ -105,7 +109,7 @@ export function validateManifest(handoff, manifest, changedPaths, changedPathsPr
|
|
|
105
109
|
handoff.sources?.forEach((source, index) => {
|
|
106
110
|
const normalized = normalizePath(source.path);
|
|
107
111
|
if (normalized !== undefined &&
|
|
108
|
-
(readableOrWritable
|
|
112
|
+
(scopeAllows(readableOrWritable, normalized) || reportedSources.has(normalized)))
|
|
109
113
|
return;
|
|
110
114
|
if (normalized !== undefined)
|
|
111
115
|
reportedSources.add(normalized);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Explicit external artifacts are filesystem inputs, not Git pathspecs. Stream complete bytes;
|
|
2
|
+
* previews are bounded independently. Follow package links only inside a declared physical root.
|
|
3
|
+
*/
|
|
4
|
+
export declare function declaredArtifacts(paths: readonly string[], previewLimit?: number): Promise<{
|
|
5
|
+
entries: (readonly [string, string, (string | undefined)?])[];
|
|
6
|
+
excerpt: string;
|
|
7
|
+
truncated: boolean;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { lstat, readdir, readlink, realpath } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
const within = (root, path) => {
|
|
6
|
+
const rest = relative(root, path);
|
|
7
|
+
return rest === "" || (rest !== ".." && !rest.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(rest));
|
|
8
|
+
};
|
|
9
|
+
/** Explicit external artifacts are filesystem inputs, not Git pathspecs. Stream complete bytes;
|
|
10
|
+
* previews are bounded independently. Follow package links only inside a declared physical root.
|
|
11
|
+
*/
|
|
12
|
+
export async function declaredArtifacts(paths, previewLimit = 0) {
|
|
13
|
+
const roots = [...new Set(paths.map(path => resolve(path)))].sort();
|
|
14
|
+
const physical = await Promise.all(roots.map(path => realpath(path).catch((error) => {
|
|
15
|
+
if (error.code === "ENOENT")
|
|
16
|
+
return path;
|
|
17
|
+
throw error;
|
|
18
|
+
})));
|
|
19
|
+
const entries = [];
|
|
20
|
+
const excerpts = [];
|
|
21
|
+
let remaining = previewLimit, truncated = false;
|
|
22
|
+
const visited = new Set();
|
|
23
|
+
const visit = async (path, ancestors) => {
|
|
24
|
+
if (visited.has(path))
|
|
25
|
+
return;
|
|
26
|
+
visited.add(path);
|
|
27
|
+
const label = path.replaceAll("\\", "/");
|
|
28
|
+
const metadata = await lstat(path).catch((error) => {
|
|
29
|
+
if (error.code === "ENOENT")
|
|
30
|
+
return undefined;
|
|
31
|
+
throw error;
|
|
32
|
+
});
|
|
33
|
+
if (!metadata) {
|
|
34
|
+
entries.push([label, "missing"]);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const canonical = await realpath(path);
|
|
38
|
+
if (metadata.isSymbolicLink()) {
|
|
39
|
+
entries.push([label, `symlink:${await readlink(path)}`, canonical]);
|
|
40
|
+
if (!physical.some(root => within(root, canonical)))
|
|
41
|
+
throw new Error("declared-artifact-link-outside-scope");
|
|
42
|
+
if (ancestors.has(canonical))
|
|
43
|
+
return; // The link identity is recorded; do not traverse cycles.
|
|
44
|
+
await visit(canonical, ancestors);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (metadata.isDirectory()) {
|
|
48
|
+
entries.push([label, "directory", canonical]);
|
|
49
|
+
const next = new Set([...ancestors, canonical]);
|
|
50
|
+
for (const child of (await readdir(path)).sort())
|
|
51
|
+
await visit(join(path, child), next);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!metadata.isFile())
|
|
55
|
+
throw new Error("declared-artifact-not-file-or-directory");
|
|
56
|
+
const hash = createHash("sha256");
|
|
57
|
+
const room = remaining;
|
|
58
|
+
let preview = Buffer.alloc(0);
|
|
59
|
+
for await (const part of createReadStream(path)) {
|
|
60
|
+
const bytes = Buffer.isBuffer(part) ? part : Buffer.from(part);
|
|
61
|
+
hash.update(bytes);
|
|
62
|
+
if (preview.length < room)
|
|
63
|
+
preview = Buffer.concat([preview, bytes.subarray(0, room - preview.length)]);
|
|
64
|
+
}
|
|
65
|
+
entries.push([label, `file:${metadata.mode}`, hash.digest("hex")]);
|
|
66
|
+
if (previewLimit > 0) {
|
|
67
|
+
if (room) {
|
|
68
|
+
const excerpt = `\n--- external file: ${label} ---\n${preview.includes(0) ? "[binary artifact: bytes fingerprinted]" : preview.toString("utf8")}`;
|
|
69
|
+
const bounded = Buffer.from(excerpt).subarray(0, remaining);
|
|
70
|
+
excerpts.push(bounded.toString("utf8"));
|
|
71
|
+
remaining -= bounded.length;
|
|
72
|
+
}
|
|
73
|
+
if (metadata.size > room || remaining === 0)
|
|
74
|
+
truncated = true;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
for (const path of roots)
|
|
78
|
+
await visit(path, new Set());
|
|
79
|
+
return { entries, excerpt: excerpts.join(""), truncated };
|
|
80
|
+
}
|
package/dist/plugin/gate.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { lstat, realpath } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, relative, resolve, win32 } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
-
import { RelativePathError, normalizeManifestPath, normalizeRelativePath } from "../core/path.js";
|
|
5
|
+
import { RelativePathError, normalizeManifestPath, normalizeManifestScope, normalizeRelativePath } from "../core/path.js";
|
|
6
6
|
import { validateOperationManifestSchema } from "../core/validate-schema.js";
|
|
7
7
|
export class WriteDeniedError extends Error {
|
|
8
8
|
reason;
|
|
@@ -144,6 +144,13 @@ function curlOutput(tokens) {
|
|
|
144
144
|
return undefined;
|
|
145
145
|
continue;
|
|
146
146
|
}
|
|
147
|
+
if (token === "-w" || token === "--write-out" || token.startsWith("--write-out=")) {
|
|
148
|
+
const format = token.startsWith("--write-out=") ? token.slice(12) : tokens[++index];
|
|
149
|
+
// curl write-out can open additional files or read a format file. Only stdout metadata is pathless.
|
|
150
|
+
if (format === undefined || format.startsWith("@") || /%output\{/u.test(format))
|
|
151
|
+
return undefined;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
147
154
|
if (token === "-o" || token === "--output") {
|
|
148
155
|
const candidate = tokens[++index];
|
|
149
156
|
if (candidate === undefined || candidate.startsWith("-") || output !== undefined)
|
|
@@ -259,8 +266,40 @@ function isRemoteGitHubMutation(tokens) {
|
|
|
259
266
|
function isReadOnlyGitCommand(tokens) {
|
|
260
267
|
const command = tokens[1]?.toLowerCase() ?? "";
|
|
261
268
|
return READ_ONLY_GIT_COMMANDS.has(command) ||
|
|
269
|
+
(command === "tag" && tokens.length >= 3 && ["--points-at", "--contains", "--list", "-l"].includes(tokens[2]) &&
|
|
270
|
+
tokens.slice(3).every(token => !token.startsWith("-"))) ||
|
|
262
271
|
(command === "branch" && tokens.length === 3 && tokens[2] === "--show-current");
|
|
263
272
|
}
|
|
273
|
+
function gitArchiveOutput(tokens) {
|
|
274
|
+
let output, tree = false, paths = false;
|
|
275
|
+
for (let index = 2; index < tokens.length; index++) {
|
|
276
|
+
const token = tokens[index];
|
|
277
|
+
if (token === "--" && tree) {
|
|
278
|
+
paths = true;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (paths || !token.startsWith("-")) {
|
|
282
|
+
tree = true;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (token === "-o" || token === "--output") {
|
|
286
|
+
const value = tokens[++index];
|
|
287
|
+
if (!value || value.startsWith("-") || output !== undefined)
|
|
288
|
+
return undefined;
|
|
289
|
+
output = value;
|
|
290
|
+
}
|
|
291
|
+
else if (token.startsWith("--output=")) {
|
|
292
|
+
if (output !== undefined || !token.slice(9))
|
|
293
|
+
return undefined;
|
|
294
|
+
output = token.slice(9);
|
|
295
|
+
}
|
|
296
|
+
else if (/^--(?:format|prefix)=.+$/u.test(token) || /^-[0-9]$/u.test(token))
|
|
297
|
+
continue;
|
|
298
|
+
else
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
return tree ? { output } : undefined;
|
|
302
|
+
}
|
|
264
303
|
function exactGitAddPaths(tokens) {
|
|
265
304
|
if (tokens.length < 4 || tokens[2] !== "--")
|
|
266
305
|
return undefined;
|
|
@@ -632,6 +671,18 @@ function shellPaths(command, powershell, depth = 0) {
|
|
|
632
671
|
else
|
|
633
672
|
ambiguous = true;
|
|
634
673
|
}
|
|
674
|
+
else if (executable === "git" && tokens[1] === "archive") {
|
|
675
|
+
const archive = gitArchiveOutput(tokens);
|
|
676
|
+
if (archive === undefined) {
|
|
677
|
+
applies = true;
|
|
678
|
+
ambiguous = true;
|
|
679
|
+
issue ??= commandIssue(source, "unsupported-git-archive-form", "use git archive --format=tar --output=<scoped-file> <local-ref>");
|
|
680
|
+
}
|
|
681
|
+
else if (archive.output !== undefined) {
|
|
682
|
+
applies = true;
|
|
683
|
+
paths.push(archive.output);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
635
686
|
else if (executable === "git" && isReadOnlyGitCommand(tokens)) {
|
|
636
687
|
// Explicitly read-only git subcommands.
|
|
637
688
|
}
|
|
@@ -677,8 +728,10 @@ function shellPaths(command, powershell, depth = 0) {
|
|
|
677
728
|
...(issue ? { issue } : {}),
|
|
678
729
|
};
|
|
679
730
|
}
|
|
680
|
-
function issuePath(issue) {
|
|
681
|
-
return `segment=${issue.segment}; cause=${issue.cause}; hint=${issue.hint};
|
|
731
|
+
function issuePath(issue, formatCorrection = false) {
|
|
732
|
+
return `segment=${issue.segment}; cause=${issue.cause}; hint=${issue.hint}; ` + (formatCorrection
|
|
733
|
+
? "retry=after-format-correction; action=correct-format-within-current-manifest; preserve operation and destinations; otherwise return exact required correction to Coordinator"
|
|
734
|
+
: "retry=false; action=return-denial-to-coordinator");
|
|
682
735
|
}
|
|
683
736
|
export function describeUnclassifiedCommand(tool, args) {
|
|
684
737
|
const extracted = extractWritePaths(tool, args);
|
|
@@ -925,7 +978,7 @@ function isWithin(root, target) {
|
|
|
925
978
|
}
|
|
926
979
|
async function canonicalManifestScopes(project, entries) {
|
|
927
980
|
const scopes = await Promise.all(entries.map(async (entry) => {
|
|
928
|
-
const normalized =
|
|
981
|
+
const normalized = normalizeManifestScope(entry);
|
|
929
982
|
const absolute = normalized.kind === "relative"
|
|
930
983
|
? project.absolute(normalized.path)
|
|
931
984
|
: resolve(normalized.path);
|
|
@@ -987,10 +1040,13 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
|
|
|
987
1040
|
}
|
|
988
1041
|
}
|
|
989
1042
|
const writable = new Set();
|
|
1043
|
+
const explicitDirectories = new Set();
|
|
990
1044
|
const externalEntries = [];
|
|
991
1045
|
try {
|
|
992
1046
|
for (const entry of manifest.write) {
|
|
993
|
-
const normalized =
|
|
1047
|
+
const normalized = normalizeManifestScope(entry);
|
|
1048
|
+
if (normalized.directory)
|
|
1049
|
+
explicitDirectories.add(normalized.path);
|
|
994
1050
|
if (normalized.kind === "relative")
|
|
995
1051
|
writable.add(normalized.path);
|
|
996
1052
|
else {
|
|
@@ -1015,7 +1071,7 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
|
|
|
1015
1071
|
}
|
|
1016
1072
|
}
|
|
1017
1073
|
catch (error) {
|
|
1018
|
-
if ((declaredDirectories.has(path) || inferredDirectories.has(path)) &&
|
|
1074
|
+
if ((explicitDirectories.has(path) || declaredDirectories.has(path) || inferredDirectories.has(path)) &&
|
|
1019
1075
|
isRecord(error) && error.code === "ENOENT") {
|
|
1020
1076
|
writableDirectories.push({ path, realPath: await nearestExistingRealPath(project.absolute(path)) });
|
|
1021
1077
|
}
|
|
@@ -1059,7 +1115,11 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
|
|
|
1059
1115
|
if (!isRecord(error) || error.code !== "ENOENT") {
|
|
1060
1116
|
throw new WriteDeniedError("manifest-unavailable", "<unknown>", { cause: error });
|
|
1061
1117
|
}
|
|
1062
|
-
|
|
1118
|
+
if (explicitDirectories.has(path.replaceAll("\\", "/"))) {
|
|
1119
|
+
externalDirectories.push({ path, realPath: await nearestExistingRealPath(path) });
|
|
1120
|
+
}
|
|
1121
|
+
else
|
|
1122
|
+
externalFiles.push({ path, anchorRealPath: await nearestExistingRealPath(resolve(path, "..")) });
|
|
1063
1123
|
}
|
|
1064
1124
|
}
|
|
1065
1125
|
const matchingExternalScopes = (absolute) => ({
|
|
@@ -1115,7 +1175,7 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
|
|
|
1115
1175
|
const absolute = resolve(manifestPath.path);
|
|
1116
1176
|
if (await project.contains(absolute)) {
|
|
1117
1177
|
const normalized = await project.toRelativePath(absolute);
|
|
1118
|
-
if (!await isWritable(normalized))
|
|
1178
|
+
if (!await isWritable(normalized) && !await isExternalWritable(absolute))
|
|
1119
1179
|
throw new WriteDeniedError("manifest-scope", normalized);
|
|
1120
1180
|
return;
|
|
1121
1181
|
}
|
|
@@ -1192,7 +1252,9 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
|
|
|
1192
1252
|
return;
|
|
1193
1253
|
if (extracted.ambiguous || (extracted.paths.length === 0 && !extracted.gitCommit)) {
|
|
1194
1254
|
if (extracted.issue !== undefined) {
|
|
1195
|
-
|
|
1255
|
+
const formatCorrection = options?.investigativeShell === true &&
|
|
1256
|
+
["unsupported-curl-form", "unsupported-git-archive-form", "unsupported-webrequest-form"].includes(extracted.issue.cause);
|
|
1257
|
+
throw new WriteDeniedError("unclassified-command", issuePath(extracted.issue, formatCorrection));
|
|
1196
1258
|
}
|
|
1197
1259
|
throw new WriteDeniedError("path-required", "<missing-path>");
|
|
1198
1260
|
}
|
|
@@ -1208,7 +1270,15 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
|
|
|
1208
1270
|
throw new WriteDeniedError("project-boundary", path, { cause: error });
|
|
1209
1271
|
}
|
|
1210
1272
|
}
|
|
1211
|
-
|
|
1273
|
+
try {
|
|
1274
|
+
await checkPath(nativeFileTool ? resolve(toolDirectory, path) : path);
|
|
1275
|
+
}
|
|
1276
|
+
catch (error) {
|
|
1277
|
+
if (options?.investigativeShell && error instanceof WriteDeniedError && error.reason === "manifest-scope") {
|
|
1278
|
+
error.message += " Coordinator: expand_unit with the exact missing output directory as dir/**, preserving settled units and cumulative spend; then dispatch the returned Task. No user approval is needed for an in-request correction.";
|
|
1279
|
+
}
|
|
1280
|
+
throw error;
|
|
1281
|
+
}
|
|
1212
1282
|
}
|
|
1213
1283
|
if (extracted.gitCommit)
|
|
1214
1284
|
await checkCachedSet();
|
package/dist/plugin/index.js
CHANGED
|
@@ -6830,58 +6830,64 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6830
6830
|
output.args.prompt = canonical;
|
|
6831
6831
|
prompt = canonical;
|
|
6832
6832
|
}
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
});
|
|
6833
|
+
// Mission admission already binds the exact generated Task to its durable run and
|
|
6834
|
+
// ordered requirements. The legacy session-wide chain is a second, incompatible
|
|
6835
|
+
// authority across cancelled missions and cold reloads (including later units).
|
|
6836
|
+
const missionDispatch = await input.runtimeBridge?.ownsMissionDispatch?.(toolInput.sessionID, toolInput.callID, ledger.task_id) === true;
|
|
6837
|
+
if (!missionDispatch) {
|
|
6838
|
+
const previous = rootAcceptanceContinuity.get(toolInput.sessionID);
|
|
6839
|
+
if (previous === undefined) {
|
|
6840
|
+
if (ledger.parent_fingerprint !== "none" &&
|
|
6841
|
+
!await recoverAcceptanceParent(toolInput.sessionID, ledger, reservedParallelDescriptor)) {
|
|
6842
|
+
throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
|
|
6843
|
+
defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", "acceptance_parent_continuity_mismatch")],
|
|
6844
|
+
});
|
|
6845
|
+
}
|
|
6847
6846
|
}
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
const exactCarryForward = ledger.criteria.length === previous.criteria.length &&
|
|
6851
|
-
previous.criteria.every((criterion, index) => criterion === ledger.criteria[index]);
|
|
6852
|
-
const strictAppend = ledger.criteria.length > previous.criteria.length &&
|
|
6853
|
-
previous.criteria.every((criterion, index) => criterion === ledger.criteria[index]);
|
|
6854
|
-
// Serial parent identity is controller-owned. Repair only an omitted link on
|
|
6855
|
-
// an otherwise exact carry-forward/append; explicit conflicting links still fail.
|
|
6856
|
-
// Persist it so the worker's Read and later recovery see the same contract.
|
|
6857
|
-
if (reservedParallelDescriptor === undefined && ledger.parent_fingerprint === "none" &&
|
|
6858
|
-
(exactCarryForward || strictAppend)) {
|
|
6859
|
-
const source = await readFile(handoffPaths[0], "utf8");
|
|
6860
|
-
const handoff = JSON.parse(source);
|
|
6861
|
-
const validated = validateHandoffSchema(handoff);
|
|
6862
|
-
const current = validated.ok ? inspectAcceptanceContinuity(validated.value).ledger : undefined;
|
|
6863
|
-
if (current === undefined || JSON.stringify(current) !== JSON.stringify(ledger)) {
|
|
6847
|
+
else if (previous.task_id === ledger.task_id && previous.fingerprint === ledger.fingerprint) {
|
|
6848
|
+
if (ledger.parent_fingerprint !== previous.parent_fingerprint) {
|
|
6864
6849
|
throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
|
|
6865
6850
|
defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", "acceptance_parent_continuity_mismatch")],
|
|
6866
6851
|
});
|
|
6867
6852
|
}
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
const
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6853
|
+
}
|
|
6854
|
+
else {
|
|
6855
|
+
const exactCarryForward = ledger.criteria.length === previous.criteria.length &&
|
|
6856
|
+
previous.criteria.every((criterion, index) => criterion === ledger.criteria[index]);
|
|
6857
|
+
const strictAppend = ledger.criteria.length > previous.criteria.length &&
|
|
6858
|
+
previous.criteria.every((criterion, index) => criterion === ledger.criteria[index]);
|
|
6859
|
+
// Serial parent identity is controller-owned. Repair only an omitted link on
|
|
6860
|
+
// an otherwise exact carry-forward/append; explicit conflicting links still fail.
|
|
6861
|
+
// Persist it so the worker's Read and later recovery see the same contract.
|
|
6862
|
+
if (reservedParallelDescriptor === undefined && ledger.parent_fingerprint === "none" &&
|
|
6863
|
+
(exactCarryForward || strictAppend)) {
|
|
6864
|
+
const source = await readFile(handoffPaths[0], "utf8");
|
|
6865
|
+
const handoff = JSON.parse(source);
|
|
6866
|
+
const validated = validateHandoffSchema(handoff);
|
|
6867
|
+
const current = validated.ok ? inspectAcceptanceContinuity(validated.value).ledger : undefined;
|
|
6868
|
+
if (current === undefined || JSON.stringify(current) !== JSON.stringify(ledger)) {
|
|
6869
|
+
throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
|
|
6870
|
+
defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", "acceptance_parent_continuity_mismatch")],
|
|
6871
|
+
});
|
|
6872
|
+
}
|
|
6873
|
+
ledger = { ...ledger, parent_fingerprint: previous.fingerprint };
|
|
6874
|
+
handoff.ext[ACCEPTANCE_CONTINUITY_EXTENSION] = ledger;
|
|
6875
|
+
const repaired = `${JSON.stringify(handoff, null, 2)}\n`;
|
|
6876
|
+
await writeFile(handoffPaths[0], repaired);
|
|
6877
|
+
try {
|
|
6878
|
+
await input.runtimeBridge?.onHostHandoffRepaired?.(toolInput.sessionID, ledger.task_id, handoffPaths[0], source, repaired);
|
|
6879
|
+
}
|
|
6880
|
+
catch (error) {
|
|
6881
|
+
await writeFile(handoffPaths[0], source);
|
|
6882
|
+
throw error;
|
|
6883
|
+
}
|
|
6884
|
+
await inspect(handoffPaths[0], undefined, { report: true });
|
|
6874
6885
|
}
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6886
|
+
if (ledger.parent_fingerprint !== previous.fingerprint || (!exactCarryForward && !strictAppend)) {
|
|
6887
|
+
throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
|
|
6888
|
+
defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", "acceptance_parent_continuity_mismatch")],
|
|
6889
|
+
});
|
|
6878
6890
|
}
|
|
6879
|
-
await inspect(handoffPaths[0], undefined, { report: true });
|
|
6880
|
-
}
|
|
6881
|
-
if (ledger.parent_fingerprint !== previous.fingerprint || (!exactCarryForward && !strictAppend)) {
|
|
6882
|
-
throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
|
|
6883
|
-
defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", "acceptance_parent_continuity_mismatch")],
|
|
6884
|
-
});
|
|
6885
6891
|
}
|
|
6886
6892
|
}
|
|
6887
6893
|
validatedRootAcceptance = ledger;
|
|
@@ -7760,7 +7766,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
7760
7766
|
return null;
|
|
7761
7767
|
const reserved = state.outstanding_reservations.length;
|
|
7762
7768
|
return { max_units: state.budget.max_units, consumed_units: state.consumed_units,
|
|
7763
|
-
reserved_units: reserved, remaining_units: Math.max(0, state.budget.max_units - state.consumed_units - reserved)
|
|
7769
|
+
reserved_units: reserved, remaining_units: Math.max(0, state.budget.max_units - state.consumed_units - reserved),
|
|
7770
|
+
settled_cost_usd: state.consumed_cost_usd, cost_limit_usd: state.budget.cost_usd,
|
|
7771
|
+
cost_status: state.consumed_cost_usd === null ? "unknown-usage" : reserved > 0 ? "in-flight-not-final" : "settled",
|
|
7772
|
+
cost_note: "Goal Worker ledger only; excludes unfinalized native requests, orchestration/review and external campaign spend. Zero settled cost does not mean free execution." };
|
|
7764
7773
|
},
|
|
7765
7774
|
registerGoalDeclaration: async (root, prompt, missionRevision) => {
|
|
7766
7775
|
if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
|
|
@@ -2,12 +2,14 @@ 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 { resolve } from "node:path";
|
|
5
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { TOOL_ENVIRONMENT } from "../runtime-mission-assets.js";
|
|
8
8
|
import { MISSION_REVIEW_REFERENCE } from "../core/operator-mission.js";
|
|
9
9
|
import { canonicalAgent } from "../core/runtime-profile.js";
|
|
10
10
|
import { taskChildSessionID } from "./task-result-repair.js";
|
|
11
|
+
import { normalizeManifestScope } from "../core/path.js";
|
|
12
|
+
import { declaredArtifacts } from "./declared-artifacts.js";
|
|
11
13
|
const exec = promisify(execFile);
|
|
12
14
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
15
|
/** Recover from native completion records, never from the pending review's inherited child ID. */
|
|
@@ -75,21 +77,29 @@ export async function completedMissionReviewPrompts(mission, profile, root, requ
|
|
|
75
77
|
/** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
|
|
76
78
|
export async function missionReviewSource(directory, run) {
|
|
77
79
|
const hash = createHash("sha256").update(JSON.stringify(run.units.map(unit => ({ unit: unit.unit, hashes: unit.hashes }))));
|
|
78
|
-
const writes = [...new Set(run.units.flatMap(unit => unit.unit.write))];
|
|
80
|
+
const writes = [...new Set(run.units.flatMap(unit => unit.unit.write).map(path => path === "." ? path : normalizeManifestScope(path).path))];
|
|
79
81
|
if (writes.length === 0)
|
|
80
82
|
return { fingerprint: `sha256:${hash.digest("hex")}`,
|
|
81
83
|
excerpt: "[Read-only units: no declared output files. Review the supplied observations, traces and validation evidence.]" };
|
|
82
84
|
// The shared dependency environment is local tooling, never reviewed or pinned source.
|
|
83
|
-
const
|
|
85
|
+
const external = [], local = [];
|
|
86
|
+
for (const path of writes) {
|
|
87
|
+
const scoped = relative(directory, resolve(directory, path)).replaceAll("\\", "/");
|
|
88
|
+
if (scoped === ".." || scoped.startsWith("../") || isAbsolute(scoped))
|
|
89
|
+
external.push(resolve(path));
|
|
90
|
+
else
|
|
91
|
+
local.push(scoped || ".");
|
|
92
|
+
}
|
|
93
|
+
const scopes = [...local, `:(exclude)${TOOL_ENVIRONMENT}`];
|
|
84
94
|
const git = async (args) => (await exec("git", args, { cwd: directory, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
85
|
-
const names = await git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...scopes]);
|
|
86
|
-
const untracked = new Set((await git(["ls-files", "-z", "--others", "--exclude-standard", "--", ...scopes])).split("\0").filter(Boolean));
|
|
95
|
+
const names = local.length ? await git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...scopes]) : "";
|
|
96
|
+
const untracked = new Set((local.length ? await git(["ls-files", "-z", "--others", "--exclude-standard", "--", ...scopes]) : "").split("\0").filter(Boolean));
|
|
87
97
|
// Explicitly declared outputs are review evidence even when gitignored (for example a probe
|
|
88
98
|
// JSON in _testenv). Their bytes must participate in staleness checks as well as the excerpt.
|
|
89
|
-
const ignored = await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...scopes]);
|
|
99
|
+
const ignored = local.length ? await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...scopes]) : "";
|
|
90
100
|
for (const path of ignored.split("\0").filter(Boolean))
|
|
91
101
|
untracked.add(path);
|
|
92
|
-
let excerpt = await git(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--", ...scopes]).catch(() => git(["diff", "--no-ext-diff", "--no-textconv", "--", ...scopes]));
|
|
102
|
+
let excerpt = local.length ? await git(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--", ...scopes]).catch(() => git(["diff", "--no-ext-diff", "--no-textconv", "--", ...scopes])) : "";
|
|
93
103
|
const unchanged = excerpt.length === 0;
|
|
94
104
|
const paths = [...new Set([...names.split("\0"), ...ignored.split("\0")].filter(Boolean))].sort();
|
|
95
105
|
const omitted = [];
|
|
@@ -128,6 +138,13 @@ export async function missionReviewSource(directory, run) {
|
|
|
128
138
|
hash.update("deleted");
|
|
129
139
|
}
|
|
130
140
|
}
|
|
141
|
+
if (external.length) {
|
|
142
|
+
const artifacts = await declaredArtifacts(external, Math.max(1, 24_000 - Buffer.byteLength(excerpt)));
|
|
143
|
+
hash.update(JSON.stringify(artifacts.entries));
|
|
144
|
+
excerpt += artifacts.excerpt;
|
|
145
|
+
if (artifacts.truncated)
|
|
146
|
+
omitted.push("external artifacts");
|
|
147
|
+
}
|
|
131
148
|
const bytes = Buffer.from(excerpt);
|
|
132
149
|
return { fingerprint: `sha256:${hash.digest("hex")}`, excerpt: (bytes.length > 24_000 ? bytes.subarray(0, 24_000).toString("utf8") : excerpt) +
|
|
133
150
|
(bytes.length > 24_000 || omitted.length ? `\n[EXCERPT TRUNCATED: ${omitted.slice(0, 20).join(", ")}; supply focused traces for missing sections, not another implementation unit]` : "") };
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -323,10 +323,11 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
323
323
|
continuationCheckpoint: async (root) => {
|
|
324
324
|
const mission = await missions.read(root);
|
|
325
325
|
const run = await operators.read(root);
|
|
326
|
-
// Cancellation
|
|
327
|
-
//
|
|
326
|
+
// Cancellation never refunds admitted work. Keep the ledger even when the last Worker
|
|
327
|
+
// was interrupted before validation; acceptance supersession is decided by mission planning,
|
|
328
|
+
// not by resetting the goal budget. This checkpoint grants no dispatch itself.
|
|
328
329
|
if (mission && (!["completed", "cancelled"].includes(mission.phase) ||
|
|
329
|
-
(mission.phase === "cancelled" && run
|
|
330
|
+
(mission.phase === "cancelled" && run))) {
|
|
330
331
|
return JSON.stringify(missionPacket(mission, run));
|
|
331
332
|
}
|
|
332
333
|
return await operators.continuationCheckpoint(root) ?? await proposals.continuationCheckpoint(root);
|
|
@@ -351,6 +352,16 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
351
352
|
return active && unit.childSessionID === child &&
|
|
352
353
|
unit.unit.validation.some(candidate => normalizeCommand(candidate) === command);
|
|
353
354
|
},
|
|
355
|
+
ownsMissionDispatch: async (root, callID, taskID) => {
|
|
356
|
+
const owner = taskOwners.get(callID);
|
|
357
|
+
if (owner?.root !== root || owner.operator)
|
|
358
|
+
return false;
|
|
359
|
+
const mission = await missions.read(root), state = await operators.read(root);
|
|
360
|
+
return mission !== undefined && state !== undefined && mission.runID === state.runID &&
|
|
361
|
+
mission.phase === "running" && state.phase === "running" &&
|
|
362
|
+
state.units.some(unit => unit.status === "running" && unit.callID === callID &&
|
|
363
|
+
/^task_id: (.+)$/m.exec(unit.task.prompt)?.[1] === taskID);
|
|
364
|
+
},
|
|
354
365
|
allowsInvestigativeShell: async (child) => {
|
|
355
366
|
const root = await rootFor(child);
|
|
356
367
|
if (!root)
|
|
@@ -466,7 +477,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
466
477
|
if (!mission.dispatchOpen && (["open", "running"].includes(mission.phase) ||
|
|
467
478
|
(mission.phase === "submitted" && mission.submission?.status !== "ready"))) {
|
|
468
479
|
return { ...packet, task: missions.task(mission),
|
|
469
|
-
next_action:
|
|
480
|
+
next_action: (run?.units.some(unit => unit.dispatchDenial) ? `${packet.next_action}\n` : "") +
|
|
481
|
+
"The previous Coordinator Task is finished. Dispatch this exact Task to continue the same mission and Coordinator session; keep the original requirements and cumulative budget." };
|
|
470
482
|
}
|
|
471
483
|
return packet;
|
|
472
484
|
}
|
|
@@ -1264,7 +1276,6 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1264
1276
|
const dispatcher = actor === root ? undefined : { sessionID: actor, callID: mission.callID };
|
|
1265
1277
|
const state = replanning ? await operators.replanMission(root, previous.runID, plan, dispatcher)
|
|
1266
1278
|
: await operators.prepareMission(root, plan, dispatcher, mission.supersededRunID, terminalChildren);
|
|
1267
|
-
await restorePriorAcceptance(root, state);
|
|
1268
1279
|
await control.registerGoalDeclaration(root, state.units[0].task.prompt, true);
|
|
1269
1280
|
control.enableUnits(root, state.units.filter(unit => unit.status === "pending").length);
|
|
1270
1281
|
await missions.update(root, item => {
|
|
@@ -1277,7 +1288,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1277
1288
|
});
|
|
1278
1289
|
return JSON.stringify(await operators.next(root, actor));
|
|
1279
1290
|
}
|
|
1280
|
-
tools[planUnits] = { description: "Coordinator (or single-unit Fast-lane Operator): declare useful units, then dispatch the returned Worker immediately. Host generates IDs, handoff, manifest and proof mapping. Keep every original requirement covered. Use write: [] for read-only verification;
|
|
1291
|
+
tools[planUnits] = { description: "Coordinator (or single-unit Fast-lane Operator): declare useful units, then dispatch the returned Worker immediately. Host generates IDs, handoff, manifest and proof mapping. Keep every original requirement covered. Use write: [] for read-only verification; use dir/** for directory outputs including not-yet-created trees. Native absolute paths support global installs and external outputs under host permissions; include their actual paths in read/write for evidence. Final validation command in each unit proves that unit; read-only diagnostic commands need no registration. Recalling with reason replaces settled work within unchanged requirements and cumulative budget; include required scope extensions here. Rejected budget, contract or control-storage preparation preserves the existing run so you can correct the plan directly.",
|
|
1281
1292
|
args: { units: { type: "array", minItems: 1, maxItems: 32, items: { type: "object", additionalProperties: false,
|
|
1282
1293
|
properties: { title: { type: "string" }, objective: { type: "string" }, read: stringList, write: stringList,
|
|
1283
1294
|
validation: stringList, requirement_ids: stringList }, required: ["title", "objective", "write", "validation"] } },
|
|
@@ -1780,7 +1791,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1780
1791
|
if (delegated) {
|
|
1781
1792
|
if (!mission || mission.runID !== state.runID)
|
|
1782
1793
|
await proposals.assertExecutionApproved(root, state.planHash);
|
|
1783
|
-
|
|
1794
|
+
if (!mission || mission.runID !== state.runID)
|
|
1795
|
+
await restorePriorAcceptance(root, state);
|
|
1784
1796
|
expandedWorker = await operators.admitWorker(root, request.sessionID, request.callID, args);
|
|
1785
1797
|
taskOwners.set(request.callID, { root, actor: request.sessionID, operator: false });
|
|
1786
1798
|
}
|
|
@@ -1847,7 +1859,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1847
1859
|
catch (error) {
|
|
1848
1860
|
if (delegated) {
|
|
1849
1861
|
taskOwners.delete(request.callID);
|
|
1850
|
-
await operators.rejectedAdmission(root, request.callID);
|
|
1862
|
+
await operators.rejectedAdmission(root, request.callID, error instanceof Error ? error.message : String(error));
|
|
1851
1863
|
}
|
|
1852
1864
|
throw error;
|
|
1853
1865
|
}
|
|
@@ -2,8 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, readFile, readdir, readlink, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { goalFingerprint } from "../core/goal-bound.js";
|
|
5
|
-
import {
|
|
5
|
+
import { normalizeManifestScope } from "../core/path.js";
|
|
6
6
|
import { RUNTIME_PROFILES } from "../core/runtime-profile.js";
|
|
7
|
+
import { declaredArtifacts } from "./declared-artifacts.js";
|
|
7
8
|
const controlRoots = new Set([".git", ...Object.values(RUNTIME_PROFILES).map(profile => profile.stateDirectory)]);
|
|
8
9
|
/** Live orchestration/Git bookkeeping is readable context, not the source being validated. */
|
|
9
10
|
export function isRuntimeControlPath(path) {
|
|
@@ -57,6 +58,19 @@ async function protectedScopeDigest(projectRoot, paths, manifestHash, sourcePoli
|
|
|
57
58
|
return undefined;
|
|
58
59
|
return goalFingerprint({ manifest_hash: `sha256:${manifestHash}`, entries });
|
|
59
60
|
}
|
|
61
|
+
function outside(projectRoot, path) {
|
|
62
|
+
const scoped = relative(projectRoot, path).replaceAll("\\", "/");
|
|
63
|
+
return scoped === ".." || scoped.startsWith("../") || isAbsolute(scoped);
|
|
64
|
+
}
|
|
65
|
+
async function declaredScopeDigest(projectRoot, paths, manifestHash, source) {
|
|
66
|
+
const local = paths.filter(path => !outside(projectRoot, path));
|
|
67
|
+
const external = paths.filter(path => outside(projectRoot, path));
|
|
68
|
+
const project = await protectedScopeDigest(projectRoot, local, manifestHash, source ? "project-files-v1" : undefined);
|
|
69
|
+
if (project === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
const artifacts = await declaredArtifacts(external).catch(() => undefined);
|
|
72
|
+
return artifacts && goalFingerprint({ project, external: artifacts.entries });
|
|
73
|
+
}
|
|
60
74
|
export async function protectedSnapshot(authorization) {
|
|
61
75
|
const manifestSource = await readFile(authorization.manifestPath).catch(() => undefined);
|
|
62
76
|
if (manifestSource === undefined)
|
|
@@ -67,20 +81,23 @@ export async function protectedSnapshot(authorization) {
|
|
|
67
81
|
const relativePath = relative(authorization.projectRoot, authorization.manifestPath).replaceAll("\\", "/");
|
|
68
82
|
const manifest = JSON.parse(manifestSource.toString("utf8"));
|
|
69
83
|
const actualPaths = (entries) => entries.map((entry) => {
|
|
70
|
-
const path =
|
|
84
|
+
const path = normalizeManifestScope(entry);
|
|
71
85
|
return path.kind === "relative" ? resolve(authorization.projectRoot, path.path) : resolve(path.path);
|
|
72
86
|
});
|
|
73
87
|
const candidatePaths = actualPaths(manifest.write);
|
|
74
88
|
const sourcePaths = [...new Set([...actualPaths(manifest.read), ...candidatePaths])];
|
|
75
|
-
const
|
|
89
|
+
const external = sourcePaths.some(path => outside(authorization.projectRoot, path));
|
|
90
|
+
const source = external ? await declaredScopeDigest(authorization.projectRoot, sourcePaths, manifestHash, true)
|
|
91
|
+
: await protectedScopeDigest(authorization.projectRoot, sourcePaths, manifestHash, "project-files-v1");
|
|
76
92
|
// Explicit outputs and the exact operation manifest remain pinned, including control-like paths.
|
|
77
|
-
const candidate = await
|
|
93
|
+
const candidate = external ? await declaredScopeDigest(authorization.projectRoot, candidatePaths, manifestHash, false)
|
|
94
|
+
: await protectedScopeDigest(authorization.projectRoot, candidatePaths, manifestHash);
|
|
78
95
|
if (source === undefined || candidate === undefined || relativePath.startsWith("../") || isAbsolute(relativePath))
|
|
79
96
|
return undefined;
|
|
80
97
|
return { binding: { manifest_hash: `sha256:${manifestHash}`, project_root: authorization.projectRoot,
|
|
81
|
-
manifest_path: relativePath, source_policy: "project-files-v1",
|
|
82
|
-
source_paths: sourcePaths.map(path => relative(authorization.projectRoot, path).replaceAll("\\", "/")),
|
|
83
|
-
candidate_paths: candidatePaths.map(path => relative(authorization.projectRoot, path).replaceAll("\\", "/")) }, source, candidate };
|
|
98
|
+
manifest_path: relativePath, source_policy: external ? "declared-paths-v1" : "project-files-v1",
|
|
99
|
+
source_paths: sourcePaths.map(path => (outside(authorization.projectRoot, path) ? path : relative(authorization.projectRoot, path)).replaceAll("\\", "/")),
|
|
100
|
+
candidate_paths: candidatePaths.map(path => (outside(authorization.projectRoot, path) ? path : relative(authorization.projectRoot, path)).replaceAll("\\", "/")) }, source, candidate };
|
|
84
101
|
}
|
|
85
102
|
export async function refreshProtectedSnapshot(projectRoot, binding) {
|
|
86
103
|
const manifestSource = await readFile(resolve(projectRoot, binding.manifest_path)).catch(() => undefined);
|
|
@@ -89,6 +106,11 @@ export async function refreshProtectedSnapshot(projectRoot, binding) {
|
|
|
89
106
|
const manifestHash = createHash("sha256").update(manifestSource).digest("hex");
|
|
90
107
|
if (`sha256:${manifestHash}` !== binding.manifest_hash)
|
|
91
108
|
return undefined;
|
|
109
|
+
if (binding.source_policy === "declared-paths-v1") {
|
|
110
|
+
const source = await declaredScopeDigest(projectRoot, binding.source_paths.map(path => resolve(projectRoot, path)), manifestHash, true);
|
|
111
|
+
const candidate = await declaredScopeDigest(projectRoot, binding.candidate_paths.map(path => resolve(projectRoot, path)), manifestHash, false);
|
|
112
|
+
return source === undefined || candidate === undefined ? undefined : { source, candidate };
|
|
113
|
+
}
|
|
92
114
|
// Legacy evidence retains its original recipe; a new policy cannot relabel stale proof as fresh.
|
|
93
115
|
if (binding.source_policy !== undefined && binding.source_policy !== "project-files-v1")
|
|
94
116
|
return undefined;
|
|
@@ -29,6 +29,8 @@ export interface RuntimeBridge {
|
|
|
29
29
|
completedReviewPrompts?(rootSessionID: string, requestedPrompt: string): Promise<readonly string[]>;
|
|
30
30
|
requiresExplicitAcceptance?(rootSessionID: string): Promise<boolean>;
|
|
31
31
|
ownsCanonicalValidation?(rootSessionID: string, unitID: string, childSessionID: string, command: string): Promise<boolean>;
|
|
32
|
+
/** A mission's hash-pinned Task has already passed durable run/acceptance admission. */
|
|
33
|
+
ownsMissionDispatch?(rootSessionID: string, callID: string, taskID: string): Promise<boolean>;
|
|
32
34
|
onHostHandoffRepaired?(rootSessionID: string, taskID: string, handoffPath: string, original: string, repaired: string): Promise<void>;
|
|
33
35
|
allowsInvestigativeShell?(sessionID: string): Promise<boolean>;
|
|
34
36
|
onSerialSettlement?(settlement: SerialDispatchSettlement): Promise<void>;
|
package/dist/plugin/v2.js
CHANGED
|
@@ -3,10 +3,17 @@ import { bindMissionProgress } from "./mission-progress.js";
|
|
|
3
3
|
import { owningServiceSessionList } from "./v2-session-history.js";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
|
+
import { V010_RUNTIME_ASSET_VERSION } from "../asset-version.js";
|
|
6
7
|
// Capture once when this module evaluates. A later package replacement must not make an old
|
|
7
8
|
// process report the replacement's bytes as its loaded adapter.
|
|
8
9
|
const loadedAdapter = Object.freeze({ adapter_url: import.meta.url,
|
|
9
10
|
adapter_sha256: createHash("sha256").update(readFileSync(new URL(import.meta.url))).digest("hex"),
|
|
11
|
+
runtime_asset_version: V010_RUNTIME_ASSET_VERSION,
|
|
12
|
+
// Named module snapshots, not a claim to identify every transitive dependency. Retain these
|
|
13
|
+
// bytes across package replacement so an unchanged adapter cannot conceal an old implementation.
|
|
14
|
+
implementation_sha256: Object.freeze(Object.fromEntries([
|
|
15
|
+
"index", "profiled", "gate", "protected-snapshot", "declared-artifacts",
|
|
16
|
+
].map(name => [`${name}.js`, createHash("sha256").update(readFileSync(new URL(`./${name}.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`, import.meta.url))).digest("hex")]))),
|
|
10
17
|
loaded_at: new Date().toISOString(), pid: process.pid });
|
|
11
18
|
const childSelectionKey = (id) => `v2-child-model-selection:${id}`;
|
|
12
19
|
function record(value) {
|
|
@@ -3,6 +3,17 @@ import { STRATEGY_TRIGGERS, SOURCE_REVIEW_RISK_TAGS } from "./core/consultation.
|
|
|
3
3
|
import { SCOUT_EVIDENCE_CODES } from "./core/scout-contract.js";
|
|
4
4
|
/** Repository-local dependency environment shared by all units; excluded from reviewed and captured source. */
|
|
5
5
|
export const TOOL_ENVIRONMENT = ".sortie-env";
|
|
6
|
+
const OPERATION_GUIDE = `## Practical operation guide
|
|
7
|
+
|
|
8
|
+
Use file paths for exact outputs and dir/** for a directory tree, including a directory that does not
|
|
9
|
+
exist yet. The host interprets dir/** consistently for writes, validation fingerprints and review.
|
|
10
|
+
Create the directory with mkdir -p dir; do not add parent directories merely to make child writes work.
|
|
11
|
+
For local source snapshots use git archive --format=tar --output=dir/source.tar <local-ref>.
|
|
12
|
+
For tag observation use git tag --points-at <commit>. For HTTPS downloads use curl -fLsS -o dir/file <url>;
|
|
13
|
+
--write-out '%{http_code} %{size_download}\\n' may print metadata to stdout. Declare all actual outputs.
|
|
14
|
+
Reuse pinned artifacts and successful checks when the request permits and the inputs are unchanged.
|
|
15
|
+
Do not repeat candidate discovery, dependency setup or validation merely because a Worker changed.
|
|
16
|
+
`;
|
|
6
17
|
function controls(profile, names) {
|
|
7
18
|
return names.map(name => ` ${profile.toolPrefix}${name}: true`).join("\n");
|
|
8
19
|
}
|
|
@@ -62,6 +73,10 @@ Ordinary defects return to Coordinator, not the user. Ask through question only
|
|
|
62
73
|
an extension beyond the original requirements, or a cumulative budget increase. Resume the same work after
|
|
63
74
|
the answer. Do not reset spend, silently shrink acceptance, or create substitute goals.
|
|
64
75
|
|
|
76
|
+
When bounded process reflections are injected, pass the relevant prevention in Coordinator feedback while
|
|
77
|
+
preserving the original requirements. After a resolved repeated process failure, record its verified cause/prevention
|
|
78
|
+
through reflection if enabled; retain concrete evidence and prefer a durable fix for recurring causes.
|
|
79
|
+
|
|
65
80
|
Retain 🐾 Sortie presentation and measured return panels. Append complete_mission's return_report verbatim
|
|
66
81
|
once. Do not invent scores, medals, costs, savings, models or successful checks. Release/publish requires the
|
|
67
82
|
existing user authorization and project gates; npm publication remains manual.
|
|
@@ -99,6 +114,10 @@ Investigate only enough to start the first useful Worker. Prefer a targeted read
|
|
|
99
114
|
inventory or speculative full design. Call ${profile.toolPrefix}plan_units with concise units:
|
|
100
115
|
title, objective, read/write file or directory scopes, validation commands, optionally requirement_ids.
|
|
101
116
|
For read-only verification, use write: []; do not invent an output file or request write access to inputs.
|
|
117
|
+
Use absolute native paths for requested global installations or other external outputs; dir/** declares
|
|
118
|
+
a directory including a not-yet-created tree. These are execution/evidence scopes, not an additional
|
|
119
|
+
permission grant: the host's native permissions still apply. Include the actual external input/output
|
|
120
|
+
paths in read/write so validation and review observe them; do not substitute a repository symlink.
|
|
102
121
|
Keep all original requirements covered; omitted requirement_ids means all. Last validation command proves
|
|
103
122
|
that unit. If your quick check shows repository-declared dependencies or the test runner are missing, keep
|
|
104
123
|
setup inside the first unit: declare its checks through the repository-local tool environment ${TOOL_ENVIRONMENT}/
|
|
@@ -114,10 +133,14 @@ check or reviewer finding, call plan_units with the corrected units and a short
|
|
|
114
133
|
preserves every requirement, failed-check history and cumulative budget. Never replace an active Worker.
|
|
115
134
|
Rejected budget/contract checks or control-storage preparation leave the old run available. Correct the
|
|
116
135
|
reported cause and call plan_units again; a local plan repair needs no cancellation or user approval.
|
|
136
|
+
On a returned denial, correct the exact command form or output scope before redispatch; reuse established
|
|
137
|
+
candidate identity, setup and evidence in the next objective instead of requesting the entire investigation again.
|
|
117
138
|
Workers freely investigate within their unit and execute exact formal checks for host recording. Do not
|
|
118
139
|
require them to predeclare exploratory commands. Require meaningful evidence, not extra testing for its
|
|
119
140
|
own sake. Do not repeat passed checks unless source changes or unresolved concerns justify it.
|
|
120
141
|
|
|
142
|
+
${OPERATION_GUIDE}
|
|
143
|
+
|
|
121
144
|
Scout is optional for one precise missing fact. Its prompt includes missing_evidence_code:
|
|
122
145
|
${SCOUT_EVIDENCE_CODES.join(" | ")}, an exact project_root and at most four known_paths.
|
|
123
146
|
Advisor is optional for one material decision; include strategy_trigger: ${STRATEGY_TRIGGERS.join(" | ")}.
|
|
@@ -183,7 +206,13 @@ are complete. Never fabricate logs, costs or exits. Do not stage outside declare
|
|
|
183
206
|
publish, or take over Coordinator decisions. The parent releases your write binding after return.
|
|
184
207
|
|
|
185
208
|
A local tool/permission/handoff defect returns PROCESS_DEFECT: local: <condition> and its exact diagnostic
|
|
186
|
-
once to Coordinator
|
|
209
|
+
once to Coordinator when its remedy requires a changed contract. If the host explicitly returns
|
|
210
|
+
action=correct-format-within-current-manifest, correct the supported command form in this same Task,
|
|
211
|
+
preserving the operation, inputs and destinations, then continue. An unchanged denied request is not a correction.
|
|
212
|
+
If correcting the form changes the intended operation or requires another output, return the exact required
|
|
213
|
+
correction to Coordinator. Do not repeat the same refused operation. Only a proven external dependency or
|
|
187
214
|
user-only choice uses TRUE_BLOCKER: external: <condition> or TRUE_BLOCKER: user-decision: <condition>.
|
|
215
|
+
|
|
216
|
+
${OPERATION_GUIDE}
|
|
188
217
|
`;
|
|
189
218
|
}
|