sortie-dogs 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/cli/main.js +13 -5
- package/dist/core/diagnostics.js +9 -1
- package/dist/core/goal-declaration-format.d.ts +4 -0
- package/dist/core/goal-declaration-format.js +33 -0
- package/dist/core/initialize.js +9 -1
- package/dist/core/validate-manifest.js +9 -1
- package/dist/core/validate-semantics.js +9 -1
- package/dist/core/worktree-parallel-dispatch.d.ts +2 -2
- package/dist/core/worktree-parallel-dispatch.js +3 -1
- package/dist/plugin/gate.d.ts +2 -0
- package/dist/plugin/gate.js +27 -0
- package/dist/plugin/index.js +139 -19
- package/dist/plugin/run-metrics.js +56 -30
- package/dist/runtime-assets.d.ts +1446 -9
- package/dist/runtime-assets.js +17 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
|
24
24
|
|
|
25
25
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
|
|
26
26
|
|
|
27
|
-
Release: [v0.9.
|
|
27
|
+
Release: [v0.9.3](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.3)
|
|
28
28
|
|
|
29
29
|
## Quick start
|
|
30
30
|
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export declare const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export declare const RUNTIME_ASSET_VERSION = "0.3.80-review-evidence-v1";
|
|
6
6
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export const RUNTIME_ASSET_VERSION = "0.3.80-review-evidence-v1";
|
package/dist/cli/main.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
3
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
4
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
5
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
return path;
|
|
9
|
+
};
|
|
2
10
|
import { open, realpath } from "node:fs/promises";
|
|
3
11
|
import { resolve } from "node:path";
|
|
4
12
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
const core = await import(`../core/validate-schema.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
|
|
6
|
-
const diagnostics = await import(`../core/diagnostics.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
|
|
7
|
-
const manifestValidator = await import(`../core/validate-manifest.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
|
|
8
|
-
const pathUtils = await import(`../core/path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
|
|
9
|
-
const initializer = await import(`../core/initialize.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
|
|
13
|
+
const core = await import(__rewriteRelativeImportExtension(`../core/validate-schema.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
14
|
+
const diagnostics = await import(__rewriteRelativeImportExtension(`../core/diagnostics.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
15
|
+
const manifestValidator = await import(__rewriteRelativeImportExtension(`../core/validate-manifest.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
16
|
+
const pathUtils = await import(__rewriteRelativeImportExtension(`../core/path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
17
|
+
const initializer = await import(__rewriteRelativeImportExtension(`../core/initialize.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
10
18
|
const LIMITS = {
|
|
11
19
|
handoffBytes: 2 * 1024 * 1024,
|
|
12
20
|
manifestBytes: 512 * 1024,
|
package/dist/core/diagnostics.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
const validateSemantics = await import(__rewriteRelativeImportExtension(`./validate-semantics.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
2
10
|
const DEFAULT_SEVERITIES = {
|
|
3
11
|
H001: "error",
|
|
4
12
|
H002: "error",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Shared planner guidance and admission enums; no inferred acceptance or dispatch authority. */
|
|
2
|
+
export declare const GOAL_DELIVERY_INTENTS: readonly ["design", "registration", "implementation", "repair", "controlled-change"];
|
|
3
|
+
export declare const GOAL_DELIVERY_MODES: readonly ["planning-only", "mvp-first", "repair-first", "controlled-change"];
|
|
4
|
+
export declare const GOAL_DECLARATION_FORMAT: string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Shared planner guidance and admission enums; no inferred acceptance or dispatch authority. */
|
|
2
|
+
export const GOAL_DELIVERY_INTENTS = ["design", "registration", "implementation", "repair", "controlled-change"];
|
|
3
|
+
export const GOAL_DELIVERY_MODES = ["planning-only", "mvp-first", "repair-first", "controlled-change"];
|
|
4
|
+
export const GOAL_DECLARATION_FORMAT = `Goal declaration belongs in the Task prompt, not in the handoff JSON or operation manifest.
|
|
5
|
+
delivery_intent must be exactly one of: ${GOAL_DELIVERY_INTENTS.join(" | ")}. Never use prose or a boolean.
|
|
6
|
+
delivery_mode is a separate optional enum: ${GOAL_DELIVERY_MODES.join(" | ")}.
|
|
7
|
+
Use flat key: value lines. Each criterion begins with its own goal_criterion_id: line.
|
|
8
|
+
Repeat the entire flat criterion block for multiple criteria. Do not use goal_criteria, goal_acceptance.criteria,
|
|
9
|
+
YAML object/list wrappers, inline { goal_criterion_id: ... }, or JSON objects for these blocks.
|
|
10
|
+
Replace every placeholder with the accepted task's actual value; preserve its acceptance and validation:
|
|
11
|
+
goal_acceptance_fingerprint: <sha256: followed by exactly 64 lowercase hexadecimal characters>
|
|
12
|
+
delivery_intent: implementation
|
|
13
|
+
delivery_mode: mvp-first
|
|
14
|
+
usable_path_established: false
|
|
15
|
+
controlled_change: false
|
|
16
|
+
goal_budget_units: <accepted positive integer>
|
|
17
|
+
goal_criterion_id: <stable criterion id>
|
|
18
|
+
goal_target: <requested behavior>
|
|
19
|
+
goal_entrypoint: <real entrypoint>
|
|
20
|
+
goal_workload: <requested workload>
|
|
21
|
+
goal_oracle_coverage: ["<actual oracle>"]
|
|
22
|
+
goal_build_boundary: <included | excluded | not-applicable>
|
|
23
|
+
goal_source: <fixed source label>
|
|
24
|
+
goal_candidate: <fixed candidate label>
|
|
25
|
+
goal_source_binding: current-protected
|
|
26
|
+
goal_candidate_binding: current-protected
|
|
27
|
+
goal_fixture: <actual fixture identity>
|
|
28
|
+
goal_proof_scope: requested-full
|
|
29
|
+
goal_expected_outcome: pass
|
|
30
|
+
goal_validation_command: <exact operation manifest validation command>
|
|
31
|
+
The example's implementation/mvp-first selections apply only to an implementation without its usable path;
|
|
32
|
+
select other enum values only from the accepted request. A declaration rejection launches no child.
|
|
33
|
+
Repair the Task prompt's named fields, not unrelated files; retry only with the corrected declaration.`;
|
package/dist/core/initialize.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
1
9
|
import { createHash } from "node:crypto";
|
|
2
10
|
import { constants } from "node:fs";
|
|
3
11
|
import { lstat, mkdir, open, readFile, realpath, rm, rmdir, stat } from "node:fs/promises";
|
|
4
12
|
import { homedir } from "node:os";
|
|
5
13
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
6
|
-
const assets = await import(`../runtime-assets.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
|
|
14
|
+
const assets = await import(__rewriteRelativeImportExtension(`../runtime-assets.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
7
15
|
const { runtimeAssets } = assets;
|
|
8
16
|
const OPEN_CODE_DIRECTORY = ".opencode";
|
|
9
17
|
const VERSION_MARKER = `${OPEN_CODE_DIRECTORY}/sortie-dogs.version`;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
const pathUtils = await import(__rewriteRelativeImportExtension(`./path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
2
10
|
const MESSAGES = {
|
|
3
11
|
H001_PATH_RELATIVE: "Manifest path must be a valid repository-relative path.",
|
|
4
12
|
H011_VALIDATION_MISSING: "A required manifest validation has not passed.",
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
const pathUtils = await import(__rewriteRelativeImportExtension(`./path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
2
10
|
const HASH_LENGTHS = {
|
|
3
11
|
sha256: 64,
|
|
4
12
|
sha512: 128,
|
|
@@ -21,7 +21,7 @@ export type ParallelDispatchPrepareResult = {
|
|
|
21
21
|
readonly reason: "scope-overlap" | "dependency-ambiguous";
|
|
22
22
|
};
|
|
23
23
|
/** Runtime capacity and mapping limits that the pure admission policy cannot observe. */
|
|
24
|
-
export type FabricDispatchSolReason = LunaFabricSolReason | "unit-count-exceeds-capacity" | "concurrent-scope-overlap" | "contract-unmappable";
|
|
24
|
+
export type FabricDispatchSolReason = LunaFabricSolReason | "unit-count-exceeds-capacity" | "concurrent-scope-overlap" | "contract-unmappable" | "target-checked-out";
|
|
25
25
|
export type ParallelDispatchFabricPrepareResult = {
|
|
26
26
|
readonly status: "prepared";
|
|
27
27
|
readonly snapshot: ParallelDispatchSnapshot;
|
|
@@ -154,7 +154,7 @@ export declare class ParallelDispatchCoordinator {
|
|
|
154
154
|
private acquire;
|
|
155
155
|
private git;
|
|
156
156
|
private readRef;
|
|
157
|
-
|
|
157
|
+
targetCheckedOut(targetRef: string): Promise<boolean>;
|
|
158
158
|
private deleteFabricSourceRefs;
|
|
159
159
|
private ensureCandidateRef;
|
|
160
160
|
private withPrepareAuthority;
|
|
@@ -2705,7 +2705,9 @@ export class ParallelDispatchCoordinator {
|
|
|
2705
2705
|
await rm(temporary, { force: true }).catch(() => undefined);
|
|
2706
2706
|
}
|
|
2707
2707
|
}
|
|
2708
|
-
|
|
2708
|
+
// Heartbeats retain live ownership. A crashed CLI must not leave a lease whose expiry exceeds
|
|
2709
|
+
// the next CLI's acquisition budget; keep room for mutex acquisition and filesystem latency.
|
|
2710
|
+
async acquire(scope = STATE_SCOPE, ttlMs = Math.floor(LOCK_TIMEOUT_MS / 2)) {
|
|
2709
2711
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
2710
2712
|
while (true) {
|
|
2711
2713
|
try {
|
package/dist/plugin/gate.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export declare function isRemoteMutation(tool: string, args: unknown): boolean;
|
|
|
60
60
|
* same command written with different spacing compares equal.
|
|
61
61
|
*/
|
|
62
62
|
export declare function normalizeCommand(command: string): string;
|
|
63
|
+
/** Canonicalize only an exact or unambiguous basename-shortened sequence of declared validations. */
|
|
64
|
+
export declare function canonicalDeclaredValidationSequence(command: string, declared: ReadonlySet<string>): string | undefined;
|
|
63
65
|
/** Unbound sessions may invoke only tools whose complete input is known to be read-only. */
|
|
64
66
|
export declare function isKnownReadOnlyTool(tool: string, args: unknown, additionalReadOnlyTools?: ReadonlySet<string>): boolean;
|
|
65
67
|
/** Canonical scopes let sibling worker bindings reject equal or ancestor ownership. */
|
package/dist/plugin/gate.js
CHANGED
|
@@ -797,6 +797,33 @@ export function normalizeCommand(command) {
|
|
|
797
797
|
? normalized
|
|
798
798
|
: `${callOperator[2]}${normalized.slice(callOperator[0].length)}`;
|
|
799
799
|
}
|
|
800
|
+
function declaredCommandMatch(segment, declared) {
|
|
801
|
+
const normalized = normalizeCommand(segment);
|
|
802
|
+
const exact = declared.filter((command) => normalizeCommand(command) === normalized);
|
|
803
|
+
if (exact.length === 1)
|
|
804
|
+
return normalizeCommand(exact[0]);
|
|
805
|
+
const firstSpace = normalized.indexOf(" ");
|
|
806
|
+
const executable = firstSpace === -1 ? normalized : normalized.slice(0, firstSpace);
|
|
807
|
+
const suffix = firstSpace === -1 ? "" : normalized.slice(firstSpace);
|
|
808
|
+
if (isAbsolute(executable))
|
|
809
|
+
return undefined;
|
|
810
|
+
const equivalent = declared.map(normalizeCommand).filter((command) => {
|
|
811
|
+
const boundary = command.indexOf(" ");
|
|
812
|
+
const declaredExecutable = boundary === -1 ? command : command.slice(0, boundary);
|
|
813
|
+
const declaredSuffix = boundary === -1 ? "" : command.slice(boundary);
|
|
814
|
+
return isAbsolute(declaredExecutable) && basename(declaredExecutable) === executable && declaredSuffix === suffix;
|
|
815
|
+
});
|
|
816
|
+
return equivalent.length === 1 ? equivalent[0] : undefined;
|
|
817
|
+
}
|
|
818
|
+
/** Canonicalize only an exact or unambiguous basename-shortened sequence of declared validations. */
|
|
819
|
+
export function canonicalDeclaredValidationSequence(command, declared) {
|
|
820
|
+
const normalized = normalizeCommand(command);
|
|
821
|
+
const segments = shellSegments(command, "posix").map((segment) => segment.trim()).filter(Boolean);
|
|
822
|
+
if (segments.length === 0 || normalized !== segments.map(normalizeCommand).join(" && "))
|
|
823
|
+
return undefined;
|
|
824
|
+
const canonical = segments.map((segment) => declaredCommandMatch(segment, [...declared]));
|
|
825
|
+
return canonical.every((segment) => segment !== undefined) ? canonical.join(" && ") : undefined;
|
|
826
|
+
}
|
|
800
827
|
/** Unbound sessions may invoke only tools whose complete input is known to be read-only. */
|
|
801
828
|
export function isKnownReadOnlyTool(tool, args, additionalReadOnlyTools = new Set()) {
|
|
802
829
|
const name = tool.toLowerCase();
|
package/dist/plugin/index.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
1
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, open, readFile, readdir, realpath, rm, stat } from "node:fs/promises";
|
|
10
|
+
import { lstat, mkdir, open, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
3
11
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
12
|
import { RUNTIME_ASSET_VERSION } from "../asset-version.js";
|
|
13
|
+
import { GOAL_DECLARATION_FORMAT, GOAL_DELIVERY_INTENTS, GOAL_DELIVERY_MODES } from "../core/goal-declaration-format.js";
|
|
5
14
|
import { ACCEPTANCE_CONTINUITY_AUTHORITY, ACCEPTANCE_CONTINUITY_EXTENSION, ACCEPTANCE_CONTINUITY_SCHEMA_VERSION, acceptanceContinuityFingerprint, inspectAcceptanceContinuity, normalizeAcceptanceCriteria, } from "../core/acceptance-continuity.js";
|
|
6
15
|
import { resolveGlobalConfigRoot } from "../core/initialize.js";
|
|
7
16
|
import { admitLunaFabric } from "../core/luna-fabric-contract.js";
|
|
@@ -19,7 +28,7 @@ import { validateManifest } from "../core/validate-manifest.js";
|
|
|
19
28
|
import { safeSchemaPointer, validateHandoffSchema, validateOperationManifestSchema, } from "../core/validate-schema.js";
|
|
20
29
|
import { DEFAULT_PLUGIN_OPTIONS, resolvePluginConfiguration, resolvePluginConfigurationSourcesWithGlobal, } from "./config.js";
|
|
21
30
|
import { CONTINUATION_CAPABILITY, CONTINUATION_MARKER, ROLLOVER_MARKER, createContinuationHooks, } from "./continuation.js";
|
|
22
|
-
import { WriteDeniedError, canonicalManifestReadScopes, canonicalManifestWriteScopes, createProjectPaths, createWriteGate, describeUnclassifiedCommand, extractWritePaths, bootstrapWritePaths, isGitMutation, isKnownReadOnlyTool, isRemoteMutation, normalizeCommand, resolveProjectRoot, safePath, writeScopesOverlap, } from "./gate.js";
|
|
31
|
+
import { WriteDeniedError, canonicalManifestReadScopes, canonicalManifestWriteScopes, canonicalDeclaredValidationSequence, createProjectPaths, createWriteGate, describeUnclassifiedCommand, extractWritePaths, bootstrapWritePaths, isGitMutation, isKnownReadOnlyTool, isRemoteMutation, normalizeCommand, resolveProjectRoot, safePath, writeScopesOverlap, } from "./gate.js";
|
|
23
32
|
import { BACKLOG_DRAIN_CAPABILITY, FastLaneController } from "./fast-lane.js";
|
|
24
33
|
import { createModelRoutingHook, } from "./model-routing-hook.js";
|
|
25
34
|
import { createTaskResultRepairHook, lastAssistantText, markConsultationFallbackRetry, taskChildSessionID, } from "./task-result-repair.js";
|
|
@@ -112,7 +121,8 @@ export class HandoffDeniedError extends Error {
|
|
|
112
121
|
const defects = normalizeDefects(options?.defects);
|
|
113
122
|
super(`Handoff denied for "${safePath(path)}": handoff and operation manifest contract.` +
|
|
114
123
|
(defects.length === 0 ? "" : ` Defects: ${describeDefects(defects)}.`) +
|
|
115
|
-
|
|
124
|
+
(path === "<goal-declaration>" ? ` ${GOAL_DECLARATION_FORMAT}` :
|
|
125
|
+
" Correct the registered handoff or its operation manifest, then read the handoff again."), options);
|
|
116
126
|
this.name = "HandoffDeniedError";
|
|
117
127
|
this.reason = reason;
|
|
118
128
|
this.defects = defects;
|
|
@@ -728,6 +738,30 @@ function taskAcceptanceCriteria(text, key = "acceptance") {
|
|
|
728
738
|
}
|
|
729
739
|
return values.length === 0 ? undefined : normalizeAcceptanceCriteria(values);
|
|
730
740
|
}
|
|
741
|
+
function canonicalTaskAcceptance(text, criteria) {
|
|
742
|
+
const lines = text.split(/\r?\n/u);
|
|
743
|
+
const headers = lines.flatMap((line, index) => /^([\t ]*)acceptance[\t ]*:/iu.exec(line) === null ? [] : [index]);
|
|
744
|
+
if (headers.length !== 1)
|
|
745
|
+
return undefined;
|
|
746
|
+
const index = headers[0];
|
|
747
|
+
const indent = /^[\t ]*/u.exec(lines[index])[0];
|
|
748
|
+
const headerIndent = indent.replaceAll("\t", " ").length;
|
|
749
|
+
let end = index + 1;
|
|
750
|
+
if (/^\s*acceptance\s*:\s*$/iu.test(lines[index])) {
|
|
751
|
+
while (end < lines.length) {
|
|
752
|
+
const line = lines[end];
|
|
753
|
+
if (line.trim().length === 0) {
|
|
754
|
+
end += 1;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (/^\s*/u.exec(line)[0].replaceAll("\t", " ").length <= headerIndent)
|
|
758
|
+
break;
|
|
759
|
+
end += 1;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
lines.splice(index, end - index, `${indent}acceptance: ${JSON.stringify(criteria)}`);
|
|
763
|
+
return lines.join("\n");
|
|
764
|
+
}
|
|
731
765
|
function taskContractText(text) {
|
|
732
766
|
const lines = text.split(/\r?\n/u);
|
|
733
767
|
const digestIndexes = lines.flatMap((line, index) => /^\s*context_digest\s*:\s*$/iu.test(line) ? [index] : []);
|
|
@@ -1058,7 +1092,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1058
1092
|
throw new Error("invalid-child-lifecycle-check-wait");
|
|
1059
1093
|
}
|
|
1060
1094
|
const pluginModuleName = "@opencode-ai/plugin";
|
|
1061
|
-
const pluginModule = await import(pluginModuleName).catch(() => undefined);
|
|
1095
|
+
const pluginModule = await import(__rewriteRelativeImportExtension(pluginModuleName)).catch(() => undefined);
|
|
1062
1096
|
const toolCandidate = pluginModule?.tool;
|
|
1063
1097
|
const toolSchema = typeof toolCandidate === "function"
|
|
1064
1098
|
? toolCandidate.schema
|
|
@@ -1328,8 +1362,11 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1328
1362
|
const unitID = authorization.taskID;
|
|
1329
1363
|
const unitBound = unitID !== undefined && goal.outstanding_reservations.some((reservation) => reservation.unit_id === unitID && reservation.session_id === identity.parentID);
|
|
1330
1364
|
const criteria = goal.acceptance_contract?.criteria.filter((criterion) => criterion.validation_command === rawCommand && criterion.expected_outcome === "pass") ?? [];
|
|
1331
|
-
if (!unitBound
|
|
1365
|
+
if (!unitBound)
|
|
1332
1366
|
throw new Error("SORTIE_VALIDATION_BUDGET_DENIED: requirement-unbound");
|
|
1367
|
+
// Exact generation and formatting checks may support acceptance without proving a criterion.
|
|
1368
|
+
if (criteria.length === 0)
|
|
1369
|
+
return;
|
|
1333
1370
|
const scope = criteria.every((criterion) => criterion.proof_scope === "requested-full") ? "full" : "targeted";
|
|
1334
1371
|
const request = { run_id: goal.goal_id, operation_id: toolInput.callID,
|
|
1335
1372
|
source_snapshot: snapshot.source, candidate: snapshot.candidate, command: [rawCommand], scope,
|
|
@@ -1481,6 +1518,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1481
1518
|
return state.receipt ?? undefined;
|
|
1482
1519
|
const records = (await ledger.readGoal()).records;
|
|
1483
1520
|
const settledEvidence = records.flatMap(({ event }) => event.kind === "unit.settled" ? event.evidence : []);
|
|
1521
|
+
if (status === "succeeded" && (state.acceptance_contract === null ||
|
|
1522
|
+
state.acceptance_contract.criteria.length === 0 ||
|
|
1523
|
+
!state.acceptance_contract.criteria.every(({ criterion_id }) => state.satisfied_criteria.includes(criterion_id))))
|
|
1524
|
+
return undefined;
|
|
1484
1525
|
if (status === "succeeded" && state.acceptance_contract !== null) {
|
|
1485
1526
|
for (const criterion of state.acceptance_contract.criteria) {
|
|
1486
1527
|
if (criterion.source_binding !== "current-protected" && criterion.candidate_binding !== "current-protected")
|
|
@@ -1518,13 +1559,45 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1518
1559
|
async function terminalGoalFromHostText(sessionID, text) {
|
|
1519
1560
|
const outcome = terminalRunOutcome(text);
|
|
1520
1561
|
if (outcome === undefined || !isCoordinatorSession(sessionID)) {
|
|
1521
|
-
return { outcome, goal: undefined, receipt: undefined };
|
|
1562
|
+
return { outcome, goal: undefined, receipt: undefined, delivery: "ready" };
|
|
1563
|
+
}
|
|
1564
|
+
let delivery = "ready";
|
|
1565
|
+
const coordinator = await getParallelCoordinator().catch(() => undefined);
|
|
1566
|
+
let parallel = await coordinator?.snapshot(sessionID).catch(() => undefined);
|
|
1567
|
+
if (parallel !== undefined && !parallel.archived) {
|
|
1568
|
+
await restoreChildLifecycles(sessionID, parallel).catch(() => undefined);
|
|
1569
|
+
const knownCalls = coordinatorTaskCalls.get(sessionID) ?? new Set();
|
|
1570
|
+
const running = parallel.tasks.filter(({ phase }) => phase === "running");
|
|
1571
|
+
if (running.length > 0 && running.every(({ call_id }) => call_id !== null && !knownCalls.has(call_id))) {
|
|
1572
|
+
const status = input.client?.session?.status;
|
|
1573
|
+
const response = status === undefined ? undefined : await status.call(input.client.session, {
|
|
1574
|
+
query: { directory: input.directory },
|
|
1575
|
+
}).catch(() => undefined);
|
|
1576
|
+
const payload = isRecord(response) && "data" in response ? response.data : response;
|
|
1577
|
+
const statuses = isRecord(payload) ? payload : undefined;
|
|
1578
|
+
const settled = statuses !== undefined && running.every(({ child_session_id }) => {
|
|
1579
|
+
const observed = child_session_id === null ? undefined : statuses[child_session_id];
|
|
1580
|
+
return isRecord(observed) && observed.type === "idle";
|
|
1581
|
+
});
|
|
1582
|
+
if (settled)
|
|
1583
|
+
parallel = await coordinator.reconcile(sessionID, knownCalls, parallel.run_id).catch(() => parallel);
|
|
1584
|
+
}
|
|
1585
|
+
if (parallel !== undefined && !parallel.archived)
|
|
1586
|
+
delivery = "running";
|
|
1522
1587
|
}
|
|
1588
|
+
if (parallel?.archived === true && parallel.terminal_reason !== "completed")
|
|
1589
|
+
delivery = "failed";
|
|
1523
1590
|
let goal = await currentGoal(sessionID).catch(() => undefined);
|
|
1524
1591
|
let receipt = goal?.receipt ?? undefined;
|
|
1525
1592
|
const proved = goal?.acceptance_contract !== null && goal?.acceptance_contract !== undefined &&
|
|
1526
1593
|
goal.acceptance_contract.criteria.every(({ criterion_id }) => goal.satisfied_criteria.includes(criterion_id));
|
|
1527
|
-
if (
|
|
1594
|
+
if (delivery === "running") {
|
|
1595
|
+
return { outcome, goal, receipt, delivery };
|
|
1596
|
+
}
|
|
1597
|
+
if (receipt === undefined && delivery === "failed") {
|
|
1598
|
+
receipt = await terminalGoal(sessionID, "stopped", "stopped").catch(() => undefined);
|
|
1599
|
+
}
|
|
1600
|
+
else if (receipt === undefined && proved) {
|
|
1528
1601
|
receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
|
|
1529
1602
|
}
|
|
1530
1603
|
else if (receipt === undefined && outcome === "DONE") {
|
|
@@ -1546,7 +1619,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1546
1619
|
goal = await currentGoal(sessionID).catch(() => goal);
|
|
1547
1620
|
if (receipt !== undefined)
|
|
1548
1621
|
rootAcceptanceContinuity.delete(sessionID);
|
|
1549
|
-
return { outcome, goal, receipt };
|
|
1622
|
+
return { outcome, goal, receipt, delivery };
|
|
1550
1623
|
}
|
|
1551
1624
|
function goalDeclarationContract(prompt) {
|
|
1552
1625
|
const lines = prompt.split(/\r?\n/u);
|
|
@@ -1642,14 +1715,14 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1642
1715
|
defects.push(contractDefect("contract", "/goal_acceptance_fingerprint", "goal_fingerprint_format"));
|
|
1643
1716
|
}
|
|
1644
1717
|
const intent = handoffValue(entries, ["delivery_intent"]);
|
|
1645
|
-
const intents =
|
|
1718
|
+
const intents = GOAL_DELIVERY_INTENTS;
|
|
1646
1719
|
if (intent === undefined)
|
|
1647
1720
|
defects.push(contractDefect("contract", "/delivery_intent", "delivery_intent_missing"));
|
|
1648
1721
|
else if (!intents.includes(intent)) {
|
|
1649
1722
|
defects.push(contractDefect("contract", "/delivery_intent", "delivery_intent_invalid"));
|
|
1650
1723
|
}
|
|
1651
1724
|
const mode = handoffValue(entries, ["delivery_mode"]);
|
|
1652
|
-
const modes =
|
|
1725
|
+
const modes = GOAL_DELIVERY_MODES;
|
|
1653
1726
|
if (mode !== undefined && !modes.includes(mode)) {
|
|
1654
1727
|
defects.push(contractDefect("contract", "/delivery_mode", "delivery_mode_invalid"));
|
|
1655
1728
|
}
|
|
@@ -3169,6 +3242,9 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3169
3242
|
contract_fingerprint: structuralAdmission.contract_fingerprint, experience: experience.trace });
|
|
3170
3243
|
}
|
|
3171
3244
|
const coordinator = await getParallelCoordinator();
|
|
3245
|
+
if (await coordinator.targetCheckedOut(`refs/heads/${structuralAdmission.contract.provenance.target_branch}`)) {
|
|
3246
|
+
return JSON.stringify({ status: "sol-serial", reason: "target-checked-out", experience: experience.trace });
|
|
3247
|
+
}
|
|
3172
3248
|
const result = await coordinator.prepareFabric(contract, ownerRoot, executionPlanPath === undefined ? undefined : await readJson(resolve(executionPlanPath), INPUT_LIMITS.parallel));
|
|
3173
3249
|
if (result.status === "sol-serial")
|
|
3174
3250
|
return JSON.stringify({ ...result, experience: experience.trace });
|
|
@@ -5257,13 +5333,18 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5257
5333
|
const terminal = runOutcome === undefined || !isCoordinatorSession(textInput.sessionID)
|
|
5258
5334
|
? undefined
|
|
5259
5335
|
: await terminalGoalFromHostText(textInput.sessionID, textOutput.text);
|
|
5260
|
-
if (runOutcome === "DONE" && terminal
|
|
5261
|
-
terminal.goal
|
|
5262
|
-
textOutput.text = replaceDoneTerminalStatus(textOutput.text,
|
|
5336
|
+
if (runOutcome === "DONE" && terminal !== undefined && (terminal.delivery === "running" ||
|
|
5337
|
+
(terminal.receipt === undefined && terminal.goal !== undefined && terminal.goal.goal_id !== null))) {
|
|
5338
|
+
textOutput.text = replaceDoneTerminalStatus(textOutput.text, terminal.delivery === "running"
|
|
5339
|
+
? "status: IN_PROGRESS — durable delivery active; same sessionでjoinまたはstale reconcileが必要"
|
|
5340
|
+
: "status: IN_PROGRESS\ngoal_control: accepted criteria remain unproved");
|
|
5263
5341
|
}
|
|
5264
|
-
if (runOutcome !== "DONE" && terminal?.receipt?.status === "succeeded") {
|
|
5342
|
+
if (runOutcome !== "DONE" && terminal?.delivery === "ready" && terminal.receipt?.status === "succeeded") {
|
|
5265
5343
|
textOutput.text = replaceTerminalStatus(textOutput.text, "status: DONE");
|
|
5266
5344
|
}
|
|
5345
|
+
if (runOutcome === "DONE" && terminal?.delivery === "failed") {
|
|
5346
|
+
textOutput.text = replaceTerminalStatus(textOutput.text, "status: INTERRUPTED — durable delivery failed");
|
|
5347
|
+
}
|
|
5267
5348
|
if (runOutcome !== undefined && isCoordinatorSession(textInput.sessionID)) {
|
|
5268
5349
|
textOutput.text = sanitizeTerminalReport(textOutput.text);
|
|
5269
5350
|
}
|
|
@@ -5815,6 +5896,13 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5815
5896
|
diagnosis.tools.add(toolInput.callID);
|
|
5816
5897
|
return;
|
|
5817
5898
|
}
|
|
5899
|
+
if (isRecord(output.args) && typeof output.args.command === "string") {
|
|
5900
|
+
const authorization = sessionAuthorizations.get(toolInput.sessionID);
|
|
5901
|
+
const canonical = authorization === undefined ? undefined
|
|
5902
|
+
: canonicalDeclaredValidationSequence(output.args.command, authorization.validationCommands);
|
|
5903
|
+
if (canonical !== undefined)
|
|
5904
|
+
output.args.command = canonical;
|
|
5905
|
+
}
|
|
5818
5906
|
await recordHostGoalStart(toolInput, output);
|
|
5819
5907
|
const coordinatorRoot = isCoordinatorSession(toolInput.sessionID) || await recoverCoordinatorRoot(toolInput.sessionID);
|
|
5820
5908
|
const readonlyDiagnosis = coordinatorRoot && toolInput.tool === "task" && isRecord(output.args)
|
|
@@ -6033,21 +6121,31 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6033
6121
|
});
|
|
6034
6122
|
}
|
|
6035
6123
|
if (assetVersionStatus === "current") {
|
|
6036
|
-
const
|
|
6037
|
-
if (
|
|
6124
|
+
const inspectedLedger = identity.acceptanceContinuity;
|
|
6125
|
+
if (inspectedLedger === undefined) {
|
|
6038
6126
|
throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
|
|
6039
6127
|
defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", `acceptance_continuity_${identity.acceptanceContinuityError ?? "missing"}`)],
|
|
6040
6128
|
});
|
|
6041
6129
|
}
|
|
6130
|
+
let ledger = inspectedLedger;
|
|
6042
6131
|
const criteria = taskAcceptanceCriteria(contractPrompt);
|
|
6043
6132
|
if (taskIDs.length !== 1 || taskIDs[0] !== identity.handoffID ||
|
|
6044
|
-
ledger.task_id !== identity.handoffID
|
|
6045
|
-
criteria.length !== ledger.criteria.length ||
|
|
6046
|
-
criteria.some((criterion, index) => criterion !== ledger.criteria[index])) {
|
|
6133
|
+
ledger.task_id !== identity.handoffID) {
|
|
6047
6134
|
throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
|
|
6048
6135
|
defects: [contractDefect("contract", "/acceptance", "acceptance_continuity_mismatch")],
|
|
6049
6136
|
});
|
|
6050
6137
|
}
|
|
6138
|
+
if (criteria === undefined || criteria.length !== ledger.criteria.length ||
|
|
6139
|
+
criteria.some((criterion, index) => criterion !== ledger.criteria[index])) {
|
|
6140
|
+
const canonical = canonicalTaskAcceptance(prompt, ledger.criteria);
|
|
6141
|
+
if (canonical === undefined) {
|
|
6142
|
+
throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
|
|
6143
|
+
defects: [contractDefect("contract", "/acceptance", "acceptance_continuity_mismatch")],
|
|
6144
|
+
});
|
|
6145
|
+
}
|
|
6146
|
+
output.args.prompt = canonical;
|
|
6147
|
+
prompt = canonical;
|
|
6148
|
+
}
|
|
6051
6149
|
const previous = rootAcceptanceContinuity.get(toolInput.sessionID);
|
|
6052
6150
|
if (previous === undefined) {
|
|
6053
6151
|
if (ledger.parent_fingerprint !== "none" &&
|
|
@@ -6069,6 +6167,24 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6069
6167
|
previous.criteria.every((criterion, index) => criterion === ledger.criteria[index]);
|
|
6070
6168
|
const strictAppend = ledger.criteria.length > previous.criteria.length &&
|
|
6071
6169
|
previous.criteria.every((criterion, index) => criterion === ledger.criteria[index]);
|
|
6170
|
+
// Serial parent identity is controller-owned. Repair only an omitted link on
|
|
6171
|
+
// an otherwise exact carry-forward/append; explicit conflicting links still fail.
|
|
6172
|
+
// Persist it so the worker's Read and later recovery see the same contract.
|
|
6173
|
+
if (reservedParallelDescriptor === undefined && ledger.parent_fingerprint === "none" &&
|
|
6174
|
+
(exactCarryForward || strictAppend)) {
|
|
6175
|
+
const handoff = JSON.parse(await readFile(handoffPaths[0], "utf8"));
|
|
6176
|
+
const validated = validateHandoffSchema(handoff);
|
|
6177
|
+
const current = validated.ok ? inspectAcceptanceContinuity(validated.value).ledger : undefined;
|
|
6178
|
+
if (current === undefined || JSON.stringify(current) !== JSON.stringify(ledger)) {
|
|
6179
|
+
throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
|
|
6180
|
+
defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", "acceptance_parent_continuity_mismatch")],
|
|
6181
|
+
});
|
|
6182
|
+
}
|
|
6183
|
+
ledger = { ...ledger, parent_fingerprint: previous.fingerprint };
|
|
6184
|
+
handoff.ext[ACCEPTANCE_CONTINUITY_EXTENSION] = ledger;
|
|
6185
|
+
await writeFile(handoffPaths[0], `${JSON.stringify(handoff, null, 2)}\n`);
|
|
6186
|
+
await inspect(handoffPaths[0], undefined, { report: true });
|
|
6187
|
+
}
|
|
6072
6188
|
if (ledger.parent_fingerprint !== previous.fingerprint || (!exactCarryForward && !strictAppend)) {
|
|
6073
6189
|
throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
|
|
6074
6190
|
defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", "acceptance_parent_continuity_mismatch")],
|
|
@@ -6215,6 +6331,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6215
6331
|
// boundary; unrelated commands continue through the ordinary write gate.
|
|
6216
6332
|
if (activeState?.parallel === "valid" && command !== undefined && authorization?.validationCommands.has(command) === true)
|
|
6217
6333
|
throw new WriteDeniedError("parallel-validation", "<parallel-unit>");
|
|
6334
|
+
const declaredSequence = command === undefined || authorization === undefined ? undefined
|
|
6335
|
+
: canonicalDeclaredValidationSequence(command, authorization.validationCommands);
|
|
6336
|
+
if (activeState?.parallel !== "valid" && declaredSequence !== undefined)
|
|
6337
|
+
return;
|
|
6218
6338
|
if (activeState?.parallel === "valid") {
|
|
6219
6339
|
const extracted = extractWritePaths(toolInput.tool, output.args);
|
|
6220
6340
|
const relativeWrite = extracted.paths.find((path) => !isAbsolute(path));
|