sortie-dogs 0.13.6 → 0.13.7
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 +20 -8
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/goal-bound.d.ts +3 -0
- package/dist/core/goal-bound.js +1 -0
- package/dist/plugin/index.js +20 -8
- package/dist/plugin/mission-review.js +113 -15
- package/dist/plugin/protected-snapshot.d.ts +4 -0
- package/dist/plugin/protected-snapshot.js +38 -0
- package/dist/runtime-assets-v010.js +7 -5
- package/dist/runtime-mission-assets.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,10 +35,10 @@ implementation, validation, review, and model routing.
|
|
|
35
35
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) ·
|
|
36
36
|
[Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md)
|
|
37
37
|
|
|
38
|
-
**Current release: [v0.13.
|
|
39
|
-
([release notes](docs/release-v0.13.
|
|
38
|
+
**Current release: [v0.13.7](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.7)**
|
|
39
|
+
([release notes](docs/release-v0.13.7.md)). The default Mission runtime retains the `v010`
|
|
40
40
|
profile, command and configuration names for compatibility; these names do not mean v0.10 is installed.
|
|
41
|
-
The current asset marker is `0.13.
|
|
41
|
+
The current asset marker is `0.13.7-review-recovery-v1`.
|
|
42
42
|
|
|
43
43
|
## SWE-bench Lite: 170/300 (56.67%)
|
|
44
44
|
|
|
@@ -48,7 +48,7 @@ The fixed **Sortie-dogs v0.12.24** harness resolved **170 of 300 SWE-bench Lite
|
|
|
48
48
|
|
|
49
49
|
The single official 300-instance report and frozen predictions are hash-bound in the report. Confirmed inference expense was **$162.99**; a separate **$34.60** of usage has unknown pricing and is held against the campaign cap, **not** counted as known expense. Leaderboard registration and maintainer acceptance are separate from this official local evaluation.
|
|
50
50
|
|
|
51
|
-
Historical scores below belong to their fixed candidates, not v0.13.
|
|
51
|
+
Historical scores below belong to their fixed candidates, not v0.13.7. SWE-bench is a separate,
|
|
52
52
|
optional measurement rather than a mandatory release gate.
|
|
53
53
|
|
|
54
54
|
> **Beta:** v0.13.x is still stabilizing. Runtime behavior,
|
|
@@ -93,7 +93,19 @@ existing local bridge loads enforcement and model routing. OpenCode can reload w
|
|
|
93
93
|
but replacing an installed dependency may require a full restart. A new chat session alone does not
|
|
94
94
|
prove the newly installed plugin is loaded.
|
|
95
95
|
|
|
96
|
-
## v0.13.
|
|
96
|
+
## v0.13.7 runtime updates
|
|
97
|
+
|
|
98
|
+
PRs #158/#159 prevent late write-only reports from falsely invalidating bound non-generating
|
|
99
|
+
native checks, while preserving actual source/output freshness, failed checks and legacy recipes.
|
|
100
|
+
Whole-root grants without a concrete inventory keep full-candidate freshness. Non-Git Mission roots
|
|
101
|
+
can prepare independent Review from declared nested repositories/worktrees/plain outputs, including
|
|
102
|
+
committed content; corrupt Git/HEAD errors remain visible. Operator performs authorized local recovery
|
|
103
|
+
in the same turn without another approval, changed requirements or reset spending. Ubuntu's Git
|
|
104
|
+
filesystem-boundary diagnostic is recognized alongside the standard non-repository error.
|
|
105
|
+
The PR native observation reached Reviewer startup, not final acceptance. Release receipts:
|
|
106
|
+
`_testenv/releases/0.13.7/`; see [release notes](docs/release-v0.13.7.md).
|
|
107
|
+
|
|
108
|
+
## v0.13.6 runtime updates (retained)
|
|
97
109
|
|
|
98
110
|
PR #156 references recorded formal PASS evidence in acceptance summaries instead of fetching
|
|
99
111
|
the same successful unit's native history again for display. Missing/failed proof retains history
|
|
@@ -424,7 +436,7 @@ register stable and `v010` from the same package installation path in one host.
|
|
|
424
436
|
Project-local installation is recommended. To expose the current Mission assets globally:
|
|
425
437
|
|
|
426
438
|
```sh
|
|
427
|
-
npm install --global sortie-dogs@0.13.
|
|
439
|
+
npm install --global sortie-dogs@0.13.7
|
|
428
440
|
sortie-dogs init --global --profile v010
|
|
429
441
|
```
|
|
430
442
|
|
|
@@ -436,7 +448,7 @@ can resolve a **separate dependency** under that config root. Updating npm-globa
|
|
|
436
448
|
it. For that layout, also install the same release at the actual config root, then rerun global init:
|
|
437
449
|
|
|
438
450
|
```sh
|
|
439
|
-
npm install --prefix "$HOME/.config/opencode" sortie-dogs@0.13.
|
|
451
|
+
npm install --prefix "$HOME/.config/opencode" sortie-dogs@0.13.7
|
|
440
452
|
sortie-dogs init --global --profile v010
|
|
441
453
|
```
|
|
442
454
|
|
|
@@ -459,7 +471,7 @@ version, preserves user configuration, and stops safely on unknown ownership or
|
|
|
459
471
|
conflicting files.
|
|
460
472
|
|
|
461
473
|
Align any exact version pin or separate bridge dependency with the intended release too. An installed
|
|
462
|
-
marker of `0.13.
|
|
474
|
+
marker of `0.13.7-review-recovery-v1` identifies the assets; it does not prove an already-running
|
|
463
475
|
OpenCode process has reloaded the plugin.
|
|
464
476
|
|
|
465
477
|
There is no supported uninstall command. Remove the npm dependency separately,
|
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.13.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.13.7-review-recovery-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.13.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.13.7-review-recovery-v1";
|
|
@@ -42,6 +42,9 @@ export interface GoalEvidence {
|
|
|
42
42
|
/** New captures distinguish a whole-project output grant from explicitly named control-like artifacts.
|
|
43
43
|
* Missing on old evidence: keep its original candidate recipe, including bookkeeping bytes. */
|
|
44
44
|
readonly candidate_policy?: "project-root-artifacts-v1";
|
|
45
|
+
/** Non-generating native validation pins inputs/concrete outputs, not write-only directory bookkeeping.
|
|
46
|
+
* Missing on old evidence: retain its full source/output recipe. */
|
|
47
|
+
readonly validation_policy?: "inputs-and-concrete-outputs-v1";
|
|
45
48
|
/** Fixed when validation starts. Full manifest_hash still identifies the historical execution contract. */
|
|
46
49
|
readonly freshness?: {
|
|
47
50
|
readonly contract_hash: string;
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -66,6 +66,7 @@ export function validGoalEvidence(value, state) {
|
|
|
66
66
|
(protectedBinding.source_policy === undefined || protectedBinding.source_policy === "project-files-v1" ||
|
|
67
67
|
protectedBinding.source_policy === "declared-paths-v1") &&
|
|
68
68
|
(protectedBinding.candidate_policy === undefined || protectedBinding.candidate_policy === "project-root-artifacts-v1") &&
|
|
69
|
+
(protectedBinding.validation_policy === undefined || protectedBinding.validation_policy === "inputs-and-concrete-outputs-v1") &&
|
|
69
70
|
(protectedBinding.freshness === undefined || (protectedBinding.freshness !== null && typeof protectedBinding.freshness === "object" &&
|
|
70
71
|
HASH.test(protectedBinding.freshness.contract_hash) &&
|
|
71
72
|
Array.isArray(protectedBinding.freshness.scratch_paths) && protectedBinding.freshness.scratch_paths.every(text) &&
|
package/dist/plugin/index.js
CHANGED
|
@@ -51,7 +51,7 @@ import { collectRunMetrics, createSortieResult, createGoalReport, insertRunMetri
|
|
|
51
51
|
import { STABLE_RUNTIME_PROFILE } from "../core/runtime-profile.js";
|
|
52
52
|
import { evidenceFromObservedExecution } from "../core/observed-goal-evidence.js";
|
|
53
53
|
import { receiptBoundTerminalText } from "./receipt-presentation.js";
|
|
54
|
-
import { operationInputSnapshot, protectedSnapshot, refreshProtectedSnapshot, validationInputSnapshot } from "./protected-snapshot.js";
|
|
54
|
+
import { operationInputSnapshot, protectedSnapshot, refreshProtectedSnapshot, validatedSourceSnapshot, validationInputSnapshot } from "./protected-snapshot.js";
|
|
55
55
|
import { settledUnitUsage } from "./unit-usage.js";
|
|
56
56
|
import { goalCompletionReadiness } from "./goal-completion.js";
|
|
57
57
|
const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
|
|
@@ -1517,12 +1517,15 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1517
1517
|
? await operationInputSnapshot(authorization.projectRoot, snapshot.binding).catch(() => undefined) : undefined;
|
|
1518
1518
|
const validationInputs = (validation !== undefined || correction !== undefined) && operationInputs === undefined
|
|
1519
1519
|
? await validationInputSnapshot(authorization.projectRoot, snapshot.binding).catch(() => undefined) : undefined;
|
|
1520
|
+
const validationSource = correction && validationInputs !== undefined
|
|
1521
|
+
? await validatedSourceSnapshot(authorization.projectRoot, snapshot.binding).catch(() => undefined) : undefined;
|
|
1520
1522
|
hostGoalExecutions.set(toolInput.callID, { root, projectRoot: authorization.projectRoot,
|
|
1521
1523
|
sessionID: toolInput.sessionID,
|
|
1522
1524
|
callID: toolInput.callID, tool: toolInput.tool, command: [rawCommand], startedAt: new Date().toISOString(),
|
|
1523
1525
|
binding: snapshot.binding, source: snapshot.source, candidate: snapshot.candidate,
|
|
1524
1526
|
...(operationInputs === undefined ? {} : { operationInputs }),
|
|
1525
1527
|
...(validationInputs === undefined ? {} : { validationInputs }),
|
|
1528
|
+
...(validationSource === undefined ? {} : { validationSource }),
|
|
1526
1529
|
owner: validation?.request.owner ?? "worker", validation,
|
|
1527
1530
|
...(correction ? { correction } : {}),
|
|
1528
1531
|
...(reusedEvidence === undefined ? {} : { reusedEvidence }) });
|
|
@@ -1562,19 +1565,28 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1562
1565
|
child_session_id: execution.sessionID, call_id: execution.callID, command: execution.command,
|
|
1563
1566
|
started_at: observedStartedAt, ended_at: endedAt, exit_code: exitCode ?? null, outcome,
|
|
1564
1567
|
source: execution.source, candidate: execution.candidate });
|
|
1568
|
+
const generated = execution.validationInputs !== undefined && refreshed &&
|
|
1569
|
+
(refreshed.source !== execution.source || refreshed.candidate !== execution.candidate);
|
|
1570
|
+
// Capture this narrower recipe only from an actual non-generating native check.
|
|
1571
|
+
// Legacy records never gain new exclusions when refreshed or recovered later.
|
|
1572
|
+
const validationBinding = execution.correction && fresh && !generated && execution.validationSource !== undefined
|
|
1573
|
+
? { ...execution.binding, validation_policy: "inputs-and-concrete-outputs-v1" } : undefined;
|
|
1574
|
+
const validatedSource = validationBinding && await validatedSourceSnapshot(execution.projectRoot, validationBinding).catch(() => undefined);
|
|
1575
|
+
const proofFresh = fresh && (!validationBinding || validatedSource === execution.validationSource);
|
|
1576
|
+
const observed = proofFresh && validatedSource && validationBinding
|
|
1577
|
+
? { binding: validationBinding, source: validatedSource, candidate: validatedSource } : refreshed;
|
|
1565
1578
|
hostGoalExecutions.set(execution.callID, { ...execution,
|
|
1566
|
-
...
|
|
1579
|
+
...observed,
|
|
1567
1580
|
startedAt: observedStartedAt, endedAt, exitCode: exitCode ?? null,
|
|
1568
|
-
...(outcome === undefined ? {} : { outcome }), ...(immutableRef === undefined ? {} : { immutableRef }), fresh });
|
|
1581
|
+
...(outcome === undefined ? {} : { outcome }), ...(immutableRef === undefined ? {} : { immutableRef }), fresh: proofFresh });
|
|
1569
1582
|
if (execution.correction) {
|
|
1570
1583
|
await input.runtimeBridge?.recordReviewerCorrectionCheck?.(execution.root, execution.correction.taskID, {
|
|
1571
1584
|
dispatchCallID: execution.correction.dispatchCallID, childSessionID: execution.sessionID, callID: execution.callID,
|
|
1572
1585
|
command: execution.correction.commands, startedAt: observedStartedAt, endedAt, exitCode: exitCode ?? null,
|
|
1573
|
-
binding:
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
? { generatedInputs: execution.validationInputs } : {}),
|
|
1586
|
+
binding: proofFresh && validationBinding && validatedSource ? validationBinding : execution.binding,
|
|
1587
|
+
source: observed?.source ?? execution.source, candidate: observed?.candidate ?? execution.candidate,
|
|
1588
|
+
fresh: proofFresh && outcome === "pass",
|
|
1589
|
+
...(generated ? { generatedInputs: execution.validationInputs } : {}),
|
|
1578
1590
|
});
|
|
1579
1591
|
}
|
|
1580
1592
|
}
|
|
@@ -12,8 +12,26 @@ import { taskChildSessionID } from "./task-result-repair.js";
|
|
|
12
12
|
import { normalizeManifestScope } from "../core/path.js";
|
|
13
13
|
import { declaredArtifacts } from "./declared-artifacts.js";
|
|
14
14
|
import { canonicalDeclaredValidationMembers, normalizeCommand } from "./gate.js";
|
|
15
|
-
import { currentSnapshotProtection, refreshProtectedSnapshot, snapshotScratchExclusion, validationInputSnapshot } from "./protected-snapshot.js";
|
|
15
|
+
import { currentSnapshotProtection, isRuntimeControlPath, refreshProtectedSnapshot, snapshotScratchExclusion, validationInputSnapshot } from "./protected-snapshot.js";
|
|
16
16
|
const exec = promisify(execFile);
|
|
17
|
+
const notRepository = (error) => /fatal: not a git repository \(or (?:any of the parent directories|any parent up to mount point [^\r\n)]+)\)(?:: \.git)?/u
|
|
18
|
+
.test(String(error.stderr ?? ""));
|
|
19
|
+
async function currentGitDiff(git, paths) {
|
|
20
|
+
try {
|
|
21
|
+
return await git(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--", ...paths]);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
try {
|
|
25
|
+
await git(["rev-parse", "--verify", "HEAD"]);
|
|
26
|
+
}
|
|
27
|
+
catch (headError) {
|
|
28
|
+
if (!/fatal: Needed a single revision/u.test(String(headError.stderr ?? "")))
|
|
29
|
+
throw headError;
|
|
30
|
+
return git(["diff", "--no-ext-diff", "--no-textconv", "--", ...paths]); // Unborn HEAD only.
|
|
31
|
+
}
|
|
32
|
+
throw error; // A real HEAD with missing/corrupt objects needs repair, not an empty diff.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
17
35
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
36
|
const validationDirectoryMatches = (input, directory) => directory === undefined || typeof input.workdir !== "string" || resolve(directory, input.workdir) === resolve(directory);
|
|
19
37
|
/** Reviewer-facing evidence preserves execution/coverage; the host retains the full snapshot recipe. */
|
|
@@ -124,15 +142,23 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
|
|
|
124
142
|
}
|
|
125
143
|
const matched = [];
|
|
126
144
|
let complete;
|
|
127
|
-
let previousEnd = notBefore, failed;
|
|
145
|
+
let previousEnd = notBefore, overlap = false, failed;
|
|
128
146
|
for (const attempt of attempts.sort((a, b) => a.started - b.started || a.completed - b.completed)) {
|
|
129
|
-
|
|
147
|
+
// Bound native checks are compared with their actual input bytes below. A later
|
|
148
|
+
// report/cache write is not itself a source change. Unbound history stays conservative.
|
|
149
|
+
if (!checks && attempt.started < editedAt)
|
|
130
150
|
continue;
|
|
131
151
|
const commands = canonicalDeclaredValidationMembers(attempt.command, declared, matched.length % declared.length);
|
|
132
152
|
if (!commands)
|
|
133
153
|
continue; // Focused diagnostics are not formal proof.
|
|
134
|
-
if (attempt.started < previousEnd)
|
|
135
|
-
|
|
154
|
+
if (attempt.started < previousEnd) {
|
|
155
|
+
complete = undefined;
|
|
156
|
+
matched.length = 0;
|
|
157
|
+
failed = undefined;
|
|
158
|
+
overlap = true;
|
|
159
|
+
previousEnd = Math.max(previousEnd, attempt.completed);
|
|
160
|
+
continue; // A later complete ordered retry can recover in this same admission.
|
|
161
|
+
}
|
|
136
162
|
previousEnd = attempt.completed;
|
|
137
163
|
if (commands[0] !== declared[matched.length] || matched.length === declared.length)
|
|
138
164
|
matched.length = 0;
|
|
@@ -151,12 +177,16 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
|
|
|
151
177
|
continue;
|
|
152
178
|
}
|
|
153
179
|
matched.push({ callID: attempt.callID, member, occurrence: matched.length });
|
|
154
|
-
if (matched.length === declared.length)
|
|
180
|
+
if (matched.length === declared.length) {
|
|
155
181
|
complete = [...matched];
|
|
182
|
+
overlap = false;
|
|
183
|
+
}
|
|
156
184
|
}
|
|
157
185
|
}
|
|
158
186
|
if (failed)
|
|
159
187
|
return { ready: false, reason: `mission-review-correction-validation-failed:${failed.command.join(" && ")}`, failure: failed, nextOccurrence: 0 };
|
|
188
|
+
if (overlap && !matched.length)
|
|
189
|
+
return { ready: false, reason: "mission-review-correction-validation-order:overlap", nextOccurrence: 0 };
|
|
160
190
|
if (!complete)
|
|
161
191
|
return { ready: false, reason: `mission-review-correction-validation-missing:${declared[matched.length]}`, nextOccurrence: matched.length };
|
|
162
192
|
for (const item of complete) {
|
|
@@ -465,18 +495,87 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
|
|
|
465
495
|
const toolInput = scope.read.some(path => normalizeManifestScope(path).path.startsWith(TOOL_ENVIRONMENT));
|
|
466
496
|
const scopes = [...local, ...(toolInput ? [] : [`:(exclude)${TOOL_ENVIRONMENT}`])];
|
|
467
497
|
const git = async (args) => (await exec("git", args, { cwd: directory, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
468
|
-
|
|
469
|
-
const untracked = new Set(
|
|
498
|
+
let names = "", ignored = "", repository = true, scopedDiff = "";
|
|
499
|
+
const untracked = new Set(), currentFiles = new Set();
|
|
500
|
+
try {
|
|
501
|
+
if (local.length) {
|
|
502
|
+
names = await git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...scopes]);
|
|
503
|
+
for (const path of (await git(["ls-files", "-z", "--others", "--exclude-standard", "--", ...scopes])).split("\0").filter(Boolean))
|
|
504
|
+
untracked.add(path);
|
|
505
|
+
ignored = await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...scopes]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
if (!notRepository(error))
|
|
510
|
+
throw error;
|
|
511
|
+
repository = false;
|
|
512
|
+
// Mission storage can be outside Git while its declared outputs are repositories,
|
|
513
|
+
// worktrees or plain files. Inventory those outputs in place, not the storage root.
|
|
514
|
+
const collected = new Set(), walked = new Set();
|
|
515
|
+
const collect = async (path) => {
|
|
516
|
+
if (walked.has(path) || excluded(path))
|
|
517
|
+
return;
|
|
518
|
+
walked.add(path);
|
|
519
|
+
if (local.includes(".") && isRuntimeControlPath(path) && !local.some(scope => scope !== "." &&
|
|
520
|
+
(path === scope || path.startsWith(`${scope}/`))))
|
|
521
|
+
return;
|
|
522
|
+
if (!toolInput && (path === TOOL_ENVIRONMENT || path.startsWith(`${TOOL_ENVIRONMENT}/`)))
|
|
523
|
+
return;
|
|
524
|
+
const absolute = resolve(directory, path);
|
|
525
|
+
const stat = await lstat(absolute).catch((error) => {
|
|
526
|
+
if (error.code !== "ENOENT")
|
|
527
|
+
throw error;
|
|
528
|
+
return undefined;
|
|
529
|
+
});
|
|
530
|
+
if (!stat?.isDirectory()) {
|
|
531
|
+
collected.add(path);
|
|
532
|
+
untracked.add(path);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const scopedGit = async (args) => (await exec("git", args, { cwd: absolute, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
536
|
+
const pathspec = ["."]; // Shared tooling is rooted at the Mission, not each output repository.
|
|
537
|
+
let tracked;
|
|
538
|
+
try {
|
|
539
|
+
tracked = await scopedGit(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...pathspec]);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (!notRepository(error))
|
|
543
|
+
throw error;
|
|
544
|
+
for (const child of (await readdir(absolute)).filter(name => name !== ".git").sort()) {
|
|
545
|
+
await collect(join(path, child).replaceAll("\\", "/"));
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const prefix = (name) => join(path, name).replaceAll("\\", "/");
|
|
550
|
+
for (const name of tracked.split("\0").filter(Boolean)) {
|
|
551
|
+
collected.add(prefix(name));
|
|
552
|
+
currentFiles.add(prefix(name));
|
|
553
|
+
}
|
|
554
|
+
for (const name of (await scopedGit(["ls-files", "-z", "--others", "--exclude-standard", "--", ...pathspec])).split("\0").filter(Boolean))
|
|
555
|
+
untracked.add(prefix(name));
|
|
556
|
+
for (const name of (await scopedGit(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...pathspec])).split("\0").filter(Boolean)) {
|
|
557
|
+
collected.add(prefix(name));
|
|
558
|
+
untracked.add(prefix(name));
|
|
559
|
+
}
|
|
560
|
+
// The mission's baseline belongs to its root, not to these different repositories.
|
|
561
|
+
// With no baseline here, committed outputs still appear as current file content.
|
|
562
|
+
const delta = await currentGitDiff(scopedGit, pathspec);
|
|
563
|
+
if (delta)
|
|
564
|
+
scopedDiff += `\n--- repository output: ${path} ---\n${delta}`;
|
|
565
|
+
};
|
|
566
|
+
for (const path of local)
|
|
567
|
+
await collect(path);
|
|
568
|
+
names = [...collected].sort().join("\0");
|
|
569
|
+
}
|
|
470
570
|
// Go's ignored in-project caches may be writable during validation but are not candidate
|
|
471
571
|
// output. Keep every other ignored declared output visible and fingerprinted, including
|
|
472
572
|
// directories of generated artifacts; focused references can still pin a cache file.
|
|
473
|
-
const ignored = local.length ? await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...scopes]) : "";
|
|
474
573
|
for (const path of ignored.split("\0").filter(Boolean))
|
|
475
574
|
untracked.add(path);
|
|
476
575
|
const omitted = [];
|
|
477
576
|
const unreadable = [];
|
|
478
|
-
let diff =
|
|
479
|
-
if (local.length && displayBaseline) {
|
|
577
|
+
let diff = scopedDiff;
|
|
578
|
+
if (repository && local.length && displayBaseline) {
|
|
480
579
|
// HEAD-only diffs are empty once the author commits. Display the original mission delta
|
|
481
580
|
// or the focused correction delta without changing the full fingerprint basis.
|
|
482
581
|
const changed = (await git(["diff", "--name-only", "-z", "--no-ext-diff", displayBaseline, "--", ...scopes]))
|
|
@@ -501,9 +600,8 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
|
|
|
501
600
|
}
|
|
502
601
|
omitted.push(...changed.slice(shown.length));
|
|
503
602
|
}
|
|
504
|
-
else if (local.length) {
|
|
505
|
-
diff = await git
|
|
506
|
-
.catch(() => git(["diff", "--no-ext-diff", "--no-textconv", "--", ...scopes]));
|
|
603
|
+
else if (repository && local.length) {
|
|
604
|
+
diff = await currentGitDiff(git, scopes);
|
|
507
605
|
}
|
|
508
606
|
const unchanged = diff.length === 0;
|
|
509
607
|
let excerpt = selected + diff;
|
|
@@ -519,7 +617,7 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
|
|
|
519
617
|
try {
|
|
520
618
|
const absolute = resolve(directory, path), stat = await lstat(absolute);
|
|
521
619
|
sourceUpdate(String(stat.mode));
|
|
522
|
-
const include = includedByParent || untracked.has(path) || unchanged;
|
|
620
|
+
const include = includedByParent || untracked.has(path) || currentFiles.has(path) || unchanged;
|
|
523
621
|
const heading = `\n--- ${includedByParent || untracked.has(path) ? "new file" : "current file"}: ${path} ---\n`;
|
|
524
622
|
const room = include ? Math.max(0, 24_000 - Buffer.byteLength(excerpt) - Buffer.byteLength(heading)) : 0;
|
|
525
623
|
let preview = Buffer.alloc(0);
|
|
@@ -16,6 +16,10 @@ export declare function operationInputSnapshot(projectRoot: string, binding: Bin
|
|
|
16
16
|
/** A validation may populate write-only caches. Keep its declared read inputs stable
|
|
17
17
|
* while binding the resulting candidate (including those outputs) after the command. */
|
|
18
18
|
export declare function validationInputSnapshot(projectRoot: string, binding: Binding): Promise<string | undefined>;
|
|
19
|
+
/** Pin a non-generating check's inputs and concrete source outputs, not incidental
|
|
20
|
+
* files created later under a directory execution grant (for example result reports).
|
|
21
|
+
* Exact outputs and tracked source remain protected even when omitted from read. */
|
|
22
|
+
export declare function validatedSourceSnapshot(projectRoot: string, binding: Binding): Promise<string | undefined>;
|
|
19
23
|
export declare function protectedSnapshot(authorization: {
|
|
20
24
|
manifestPath: string;
|
|
21
25
|
manifestHash: string;
|
|
@@ -210,6 +210,40 @@ export async function validationInputSnapshot(projectRoot, binding) {
|
|
|
210
210
|
? declaredScopeDigest(projectRoot, paths, hash, true, [], binding)
|
|
211
211
|
: protectedScopeDigest(projectRoot, paths, hash, binding.source_policy, [], binding);
|
|
212
212
|
}
|
|
213
|
+
/** Pin a non-generating check's inputs and concrete source outputs, not incidental
|
|
214
|
+
* files created later under a directory execution grant (for example result reports).
|
|
215
|
+
* Exact outputs and tracked source remain protected even when omitted from read. */
|
|
216
|
+
export async function validatedSourceSnapshot(projectRoot, binding) {
|
|
217
|
+
const current = await snapshotManifest(projectRoot, binding);
|
|
218
|
+
if (!current?.manifest.read.length)
|
|
219
|
+
return undefined; // An absent input set retains the full-source recipe.
|
|
220
|
+
const scopes = [...binding.source_paths.map(path => resolve(projectRoot, path)),
|
|
221
|
+
...current.manifest.write.map(path => resolve(projectRoot, normalizeManifestScope(path).path))];
|
|
222
|
+
const protection = [...new Set([...(binding.freshness?.protected_paths ?? []),
|
|
223
|
+
...await currentSnapshotProtection(projectRoot, current.manifest)])];
|
|
224
|
+
const reads = current.manifest.read.map(entry => resolve(projectRoot, normalizeManifestScope(entry).path));
|
|
225
|
+
const currentBinding = binding.freshness ? { ...binding, freshness: { ...binding.freshness, protected_paths: protection } } : binding;
|
|
226
|
+
// A populated directory grant without a concrete output/input inventory cannot safely
|
|
227
|
+
// acquire the report-only recipe. Keep the original full-candidate freshness instead.
|
|
228
|
+
for (const entry of binding.validation_policy === undefined ? current.manifest.write : []) {
|
|
229
|
+
const output = resolve(projectRoot, normalizeManifestScope(entry).path);
|
|
230
|
+
if (reads.some(input => !outside(input, output)) || snapshotScratchExcluded(currentBinding, output) ||
|
|
231
|
+
protection.some(path => path !== output && !outside(output, path) && !reads.includes(path)))
|
|
232
|
+
continue;
|
|
233
|
+
const stat = await lstat(output).catch((error) => {
|
|
234
|
+
if (error.code !== "ENOENT")
|
|
235
|
+
throw error;
|
|
236
|
+
return undefined;
|
|
237
|
+
});
|
|
238
|
+
if (stat?.isDirectory() && (await readdir(output)).length)
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
const paths = [...new Set([...reads,
|
|
242
|
+
...protection.filter(path => scopes.some(scope => !outside(scope, path)))])];
|
|
243
|
+
return binding.source_policy === "declared-paths-v1"
|
|
244
|
+
? declaredScopeDigest(projectRoot, paths, current.hash, true, [], currentBinding)
|
|
245
|
+
: protectedScopeDigest(projectRoot, paths, current.hash, binding.source_policy, [], currentBinding);
|
|
246
|
+
}
|
|
213
247
|
export async function protectedSnapshot(authorization, options = {}) {
|
|
214
248
|
const manifestSource = await readFile(authorization.manifestPath).catch(() => undefined);
|
|
215
249
|
if (manifestSource === undefined)
|
|
@@ -262,6 +296,10 @@ export async function refreshProtectedSnapshot(projectRoot, binding) {
|
|
|
262
296
|
const current = await snapshotManifest(projectRoot, binding);
|
|
263
297
|
if (!current)
|
|
264
298
|
return undefined;
|
|
299
|
+
if (binding.validation_policy === "inputs-and-concrete-outputs-v1") {
|
|
300
|
+
const source = await validatedSourceSnapshot(projectRoot, binding);
|
|
301
|
+
return source === undefined ? undefined : { source, candidate: source };
|
|
302
|
+
}
|
|
265
303
|
const manifestHash = current.hash;
|
|
266
304
|
const sourcePaths = binding.source_paths.map(path => resolve(projectRoot, path));
|
|
267
305
|
const candidatePaths = binding.candidate_paths.map(path => resolve(projectRoot, path));
|
|
@@ -45,9 +45,9 @@ The status token, its icon, TRUE_INTERRUPTION and TRUE_BLOCKER are protocol toke
|
|
|
45
45
|
even when the surrounding conclusion is translated. Translate only the display labels and keep their order.
|
|
46
46
|
|
|
47
47
|
DONE requires a succeeded ${profile.toolPrefix}complete_operator receipt only when a current active operator contract
|
|
48
|
-
owns the turn; the host renders the measured return report from that receipt. Without it,
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
owns the turn; the host renders the measured return report from that receipt. Without it, continue authorized recovery;
|
|
49
|
+
only a real stop, exhausted budget, unrecoverable internal failure or user-only decision permits a terminal return.
|
|
50
|
+
Failed proposals, child failures and refused control calls need local repair first. A cancelled or completed
|
|
51
51
|
historical operator run does not gate a later ordinary turn, but an explicit cancel in the same turn still requires
|
|
52
52
|
INTERRUPTED. A later ordinary turn with no active operator contract or accepted execution criteria may use DONE
|
|
53
53
|
without that receipt; the host verifies that exact uncontracted turn boundary before preserving it.
|
|
@@ -57,8 +57,10 @@ A genuine interruption also requires the canonical machine line \`TRUE_INTERRUPT
|
|
|
57
57
|
A refused control operation is a local process defect, not a terminal blocker. Read the returned status and
|
|
58
58
|
next_action, apply that one correction, and continue in the same turn. Never reissue an unchanged refused
|
|
59
59
|
request: an active contract returns ${profile.toolPrefix}operator_status and the existing next Task, and an
|
|
60
|
-
unavailable contract-repair validation resume returns the preserved run state and its decision.
|
|
61
|
-
|
|
60
|
+
unavailable contract-repair validation resume returns the preserved run state and its decision. Stop repeating an
|
|
61
|
+
unchanged refused request, not the work: if Operator can Fix within existing permissions, requirements and budget,
|
|
62
|
+
repair or use another supported route in the same turn. Reuse fresh successful checks; show cause, correction and result.
|
|
63
|
+
Only when no authorized recovery remains, stop retrying and return one INTERRUPTED checkpoint naming that refusal.
|
|
62
64
|
|
|
63
65
|
${canonicalFixture("TERMINAL_STATUS_SEMANTICS_FIXTURE")}
|
|
64
66
|
|
|
@@ -206,8 +206,9 @@ Coordinator's internal Worker/Reviewer/Scout/Advisor tasks stay foreground. Unit
|
|
|
206
206
|
New unrelated chat does not cancel, restart, replace, or extend an active Mission. Adopt steering only
|
|
207
207
|
through start_mission intent=continue; frozen Worker contract changes use intent=replace/cancellation.
|
|
208
208
|
Do not poll or re-run successful checks. Inspect operator_status only to recover missing durable state.
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
Operator fixes ordinary path, environment, registration and Review-preparation defects directly when authorized;
|
|
210
|
+
otherwise resume the same author with concrete feedback, not a user turn. Retain mission, evidence and cumulative spend.
|
|
211
|
+
Ask through question only for a user-only choice, an extension beyond the original requirements, or a cumulative budget increase. Resume the same work after
|
|
211
212
|
the answer. For an approved Mission Worker-unit increase, root calls ${profile.toolPrefix}extend_mission_budget
|
|
212
213
|
with operator_status.mission_id and the new cumulative max_units (not the increment), then resumes the same
|
|
213
214
|
Coordinator. This does not change a separate campaign dollar cap or dispatch a Worker. Do not reset spend,
|