sortie-dogs 0.5.8 → 0.5.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/README.md +9 -3
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/worktree-commit-artifact.js +2 -0
- package/dist/plugin/continuation.js +9 -18
- package/dist/plugin/fast-lane.js +3 -8
- package/dist/plugin/gate.js +95 -2
- package/dist/plugin/index.js +219 -183
- package/dist/runtime-assets.d.ts +7 -7
- package/dist/runtime-assets.js +80 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
|
20
20
|
|
|
21
21
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
|
|
22
22
|
|
|
23
|
-
Release: [v0.5.
|
|
23
|
+
Release: [v0.5.12](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.5.12)
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
@@ -164,13 +164,19 @@ options. OpenCode plugin normalization may omit factory options, so use the
|
|
|
164
164
|
global file for durable global settings.
|
|
165
165
|
|
|
166
166
|
- `operationManifestPath` moves the manifest; the path is project-relative.
|
|
167
|
-
- `handoffPaths` lists the handoff files the plugin inspects. A worker can only
|
|
167
|
+
- `handoffPaths` lists the handoff files the plugin inspects. A worker can only
|
|
168
168
|
bind after one of these files passes inspection, so an empty list disables
|
|
169
169
|
binding entirely. Relative entries are also candidate-relative in a nested
|
|
170
170
|
repository: a child candidate may use its own `handoff.json` while OpenCode is
|
|
171
171
|
opened at the parent workspace. For operational work the coordinator creates
|
|
172
172
|
that valid handoff before dispatch and sends its exact absolute path; the
|
|
173
|
-
binding child must use the built-in Read tool on it immediately before bind.
|
|
173
|
+
binding child must use the built-in Read tool on it immediately before bind.
|
|
174
|
+
New coordinator contracts are emitted under the candidate-relative
|
|
175
|
+
`.sortie-dogs/contracts/` directory as `handoff.<id>.json` and
|
|
176
|
+
`<id>.operation-manifest.json`. The directory is ignored by this repository's
|
|
177
|
+
`.gitignore`; legacy root/scoped paths and configured custom paths remain
|
|
178
|
+
readable and preflight-compatible, but are never moved or deleted.
|
|
179
|
+
Remove the directory only when no Sortie run is active.
|
|
174
180
|
- `readOnlyTools` adds host-specific tool names that never change files, such as
|
|
175
181
|
MCP tools. Unknown tools are denied for a bound session by default.
|
|
176
182
|
- `dedicatedWorkerModel` selects the single model every worker role resolves to.
|
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.46-contract-directory-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.46-contract-directory-v1";
|
|
@@ -749,6 +749,8 @@ async function status(context, staged) {
|
|
|
749
749
|
const controls = new Set([
|
|
750
750
|
`handoff.${context.descriptor.task_id}.json`,
|
|
751
751
|
`${context.descriptor.task_id}.operation-manifest.json`,
|
|
752
|
+
`.sortie-dogs/contracts/handoff.${context.descriptor.task_id}.json`,
|
|
753
|
+
`.sortie-dogs/contracts/${context.descriptor.task_id}.operation-manifest.json`,
|
|
752
754
|
]);
|
|
753
755
|
return entries.filter(({ code, path }) => code !== "A" || !controls.has(path));
|
|
754
756
|
}
|
|
@@ -33,8 +33,6 @@ export const DEFAULT_MAX_AUTO_CONTINUES = 10;
|
|
|
33
33
|
*/
|
|
34
34
|
export const STEP_EXHAUSTED_PATTERN = /(?:最大step(?:s|数)?(?:に)?到達|step(?:s)?\s*(?:limit|budget)\s*(?:reached|exhausted)|maximum\s+steps?\s+reached)[\s\S]{0,2500}(?:残作業|未完了|未完|次action|next\s+action|remaining\s+work)/iu;
|
|
35
35
|
const MAX_TRACKED_SESSIONS = 256;
|
|
36
|
-
const MAX_STEP_CONTINUES_PER_SEGMENT = 2;
|
|
37
|
-
const MAX_STEP_CONTINUES_PER_TURN = 4;
|
|
38
36
|
const DEFAULT_TIMINGS = {
|
|
39
37
|
/** Compaction is expensive; one rollover per minute per session is the canonical ceiling. */
|
|
40
38
|
cooldownMilliseconds: 60_000,
|
|
@@ -271,8 +269,6 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
271
269
|
resumeAttempts: 0,
|
|
272
270
|
textCompleting: false,
|
|
273
271
|
directUsed: false,
|
|
274
|
-
stepContinueCount: 0,
|
|
275
|
-
stepContinueTotal: 0,
|
|
276
272
|
stepContinueIssuing: false,
|
|
277
273
|
touched: Date.now(),
|
|
278
274
|
};
|
|
@@ -383,18 +379,18 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
383
379
|
if (/➡️\s*(?:次action|next_action)\s*:\s*\S/iu.test(text))
|
|
384
380
|
return true;
|
|
385
381
|
const percent = latestProgressPercent(text);
|
|
386
|
-
|
|
382
|
+
if (percent !== undefined)
|
|
383
|
+
return percent < 100 || batchHasIndependentRemainder(text);
|
|
384
|
+
// Coordinators sometimes omit the optional percentage while retaining a localized progress label.
|
|
385
|
+
return /📊\s*(?:進行中|处理中|in[ -]?progress)(?:\s|[::]|$)/iu.test(latestProgressLine(text) ?? "");
|
|
387
386
|
}
|
|
388
387
|
async function issueStepContinue(sessionID, state, report, agent) {
|
|
389
388
|
const resume = client?.session?.promptAsync;
|
|
390
389
|
const active = policy();
|
|
391
390
|
if (resume === undefined || !active.enabled || active.agent !== agent || state.stepContinueIssuing ||
|
|
392
|
-
state.
|
|
393
|
-
state.stepContinueTotal >= MAX_STEP_CONTINUES_PER_TURN)
|
|
391
|
+
state.lastStepContinueReport === report)
|
|
394
392
|
return;
|
|
395
393
|
state.stepContinueIssuing = true;
|
|
396
|
-
state.stepContinueCount += 1;
|
|
397
|
-
state.stepContinueTotal += 1;
|
|
398
394
|
state.latestCoordinatorReport = undefined;
|
|
399
395
|
try {
|
|
400
396
|
const resumed = await resume.call(client.session, {
|
|
@@ -411,10 +407,9 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
411
407
|
});
|
|
412
408
|
if (!promptCallSucceeded(resumed))
|
|
413
409
|
throw new Error("step continuation request rejected");
|
|
410
|
+
state.lastStepContinueReport = report;
|
|
414
411
|
}
|
|
415
412
|
catch (error) {
|
|
416
|
-
state.stepContinueCount -= 1;
|
|
417
|
-
state.stepContinueTotal -= 1;
|
|
418
413
|
state.latestCoordinatorReport = report;
|
|
419
414
|
console.error("[sortie-continuation] step resume failed", sessionID, error);
|
|
420
415
|
}
|
|
@@ -453,9 +448,6 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
453
448
|
state.continueReport = undefined;
|
|
454
449
|
state.preserveCompactionScope = false;
|
|
455
450
|
state.recoverySummaryValidated = false;
|
|
456
|
-
// A successful rollover starts a new bounded context segment. Keep ordinary synthetic turns
|
|
457
|
-
// from resetting this guard, but allow two fresh progress recoveries after compaction.
|
|
458
|
-
state.stepContinueCount = 0;
|
|
459
451
|
// The accepted prompt now belongs to the host loop, not this rollover request.
|
|
460
452
|
state.active = false;
|
|
461
453
|
return true;
|
|
@@ -892,14 +884,13 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
892
884
|
state.attempts = 0;
|
|
893
885
|
state.directUsed = false;
|
|
894
886
|
state.latestCoordinatorReport = undefined;
|
|
887
|
+
if (!synthetic)
|
|
888
|
+
state.lastStepContinueReport = undefined;
|
|
895
889
|
state.compactingEpoch = undefined;
|
|
896
890
|
state.model = { providerID: model.providerID, modelID: model.modelID };
|
|
897
891
|
state.turnRevision += 1;
|
|
898
|
-
if (!synthetic)
|
|
899
|
-
state.stepContinueCount = 0;
|
|
900
|
-
state.stepContinueTotal = 0;
|
|
892
|
+
if (!synthetic)
|
|
901
893
|
state.limitCompacted = false;
|
|
902
|
-
}
|
|
903
894
|
},
|
|
904
895
|
blocksTool(sessionID) {
|
|
905
896
|
const state = sessions.get(sessionID);
|
package/dist/plugin/fast-lane.js
CHANGED
|
@@ -121,8 +121,7 @@ export class FastLaneController {
|
|
|
121
121
|
}
|
|
122
122
|
manualCompactionForbidden(sessionID) {
|
|
123
123
|
const state = this.sessions.get(sessionID);
|
|
124
|
-
return state !== undefined &&
|
|
125
|
-
(!state.backlogDrain || state.totalWorkerDispatches >= state.workerLimit);
|
|
124
|
+
return state !== undefined && !state.backlogDrain;
|
|
126
125
|
}
|
|
127
126
|
terminalInstructionRequired(sessionID) {
|
|
128
127
|
const state = this.sessions.get(sessionID);
|
|
@@ -134,8 +133,8 @@ export class FastLaneController {
|
|
|
134
133
|
}
|
|
135
134
|
backlogContinuationAllowed(sessionID) {
|
|
136
135
|
const state = this.sessions.get(sessionID);
|
|
137
|
-
return state?.backlogDrain === true && state.workerDispatches
|
|
138
|
-
|
|
136
|
+
return state?.backlogDrain === true && state.workerDispatches >= 1 &&
|
|
137
|
+
!state.continuationPending;
|
|
139
138
|
}
|
|
140
139
|
enableBacklogDrain(sessionID, maxUnits) {
|
|
141
140
|
const state = this.sessions.get(sessionID);
|
|
@@ -238,10 +237,6 @@ export class FastLaneController {
|
|
|
238
237
|
throw new FastLaneDeniedError("WORKER_LIMIT");
|
|
239
238
|
}
|
|
240
239
|
}
|
|
241
|
-
else if (state.backlogDrain &&
|
|
242
|
-
(state.workerDispatches >= 1 || state.totalWorkerDispatches >= state.workerLimit)) {
|
|
243
|
-
throw new FastLaneDeniedError("WORKER_LIMIT");
|
|
244
|
-
}
|
|
245
240
|
else if (state.workerInFlight) {
|
|
246
241
|
throw new FastLaneDeniedError("WORKER_LIMIT");
|
|
247
242
|
}
|
package/dist/plugin/gate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, relative, resolve, win32 } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { RelativePathError, normalizeManifestPath, normalizeRelativePath } from "../core/path.js";
|
|
6
6
|
import { validateOperationManifestSchema } from "../core/validate-schema.js";
|
|
@@ -377,6 +377,22 @@ function depthOnePowerShellLiteral(source) {
|
|
|
377
377
|
function commandIssue(segment, cause, hint) {
|
|
378
378
|
return { segment: segment.trim().slice(0, 240), cause, hint };
|
|
379
379
|
}
|
|
380
|
+
function isLiteralPowerShellPath(value) {
|
|
381
|
+
return value.length > 0 && !/[\u0000-\u001f\u007f$*?\[\]@(){},;|&`<>]/u.test(value);
|
|
382
|
+
}
|
|
383
|
+
function robocopyOutput(tokens) {
|
|
384
|
+
if (tokens.length !== 4)
|
|
385
|
+
return undefined;
|
|
386
|
+
const source = tokens[1];
|
|
387
|
+
const destination = tokens[2];
|
|
388
|
+
const file = tokens[3];
|
|
389
|
+
const literal = (value) => value.length > 0 && !/[\u0000-\u001f\u007f$*?]/u.test(value);
|
|
390
|
+
if (!literal(source) || !literal(destination) || !literal(file) ||
|
|
391
|
+
file === "." || file === ".." || /[\\/]/u.test(file) ||
|
|
392
|
+
source.startsWith("/") || destination.startsWith("/") || file.startsWith("/"))
|
|
393
|
+
return undefined;
|
|
394
|
+
return win32.join(destination, file);
|
|
395
|
+
}
|
|
380
396
|
function shellPaths(command, powershell, depth = 0) {
|
|
381
397
|
const paths = [];
|
|
382
398
|
let applies = false;
|
|
@@ -486,6 +502,18 @@ function shellPaths(command, powershell, depth = 0) {
|
|
|
486
502
|
if (destination !== undefined)
|
|
487
503
|
requiredDirectories.push(dirname(destination));
|
|
488
504
|
}
|
|
505
|
+
else if (/^robocopy(?:\.exe)?$/u.test(executable)) {
|
|
506
|
+
applies = true;
|
|
507
|
+
const destination = robocopyOutput(tokens);
|
|
508
|
+
if (destination === undefined) {
|
|
509
|
+
ambiguous = true;
|
|
510
|
+
issue ??= commandIssue(source, "unsupported-robocopy-form", "use literal Robocopy.exe <source-dir> <destination-dir> <single-file>");
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
paths.push(destination);
|
|
514
|
+
requiredDirectories.push(dirname(destination));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
489
517
|
else if (executable === "tar") {
|
|
490
518
|
const mode = tarMode(tokens);
|
|
491
519
|
if (mode === undefined) {
|
|
@@ -502,6 +530,68 @@ function shellPaths(command, powershell, depth = 0) {
|
|
|
502
530
|
else if (executable === "find" && isDirectFindHash(tokens)) {
|
|
503
531
|
// Exact member lookup plus sha256sum is read-only; a following tee owns any output path.
|
|
504
532
|
}
|
|
533
|
+
else if (executable === "copy-item") {
|
|
534
|
+
applies = true;
|
|
535
|
+
const destinations = [];
|
|
536
|
+
const sources = [];
|
|
537
|
+
const positional = [];
|
|
538
|
+
let unsupported = false;
|
|
539
|
+
let force = false;
|
|
540
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
541
|
+
const token = tokens[index];
|
|
542
|
+
if (/^-(?:path|literalpath)$/iu.test(token)) {
|
|
543
|
+
const candidate = tokens[index + 1];
|
|
544
|
+
if (candidate !== undefined && !candidate.startsWith("-"))
|
|
545
|
+
sources.push(candidate);
|
|
546
|
+
else
|
|
547
|
+
unsupported = true;
|
|
548
|
+
index += 1;
|
|
549
|
+
}
|
|
550
|
+
else if (/^-destination$/iu.test(token)) {
|
|
551
|
+
const candidate = tokens[index + 1];
|
|
552
|
+
if (candidate !== undefined && !candidate.startsWith("-"))
|
|
553
|
+
destinations.push(candidate);
|
|
554
|
+
else
|
|
555
|
+
unsupported = true;
|
|
556
|
+
index += 1;
|
|
557
|
+
}
|
|
558
|
+
else if (/^-force$/iu.test(token)) {
|
|
559
|
+
if (force)
|
|
560
|
+
unsupported = true;
|
|
561
|
+
force = true;
|
|
562
|
+
}
|
|
563
|
+
else if (token.startsWith("-")) {
|
|
564
|
+
unsupported = true;
|
|
565
|
+
}
|
|
566
|
+
else if (!token.startsWith("-")) {
|
|
567
|
+
positional.push(token);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const namedForm = sources.length === 1 && destinations.length === 1 && positional.length === 0;
|
|
571
|
+
const namedSourceForm = sources.length === 1 && destinations.length === 0 && positional.length === 1;
|
|
572
|
+
const namedDestinationForm = sources.length === 0 && destinations.length === 1 && positional.length === 1;
|
|
573
|
+
const positionalForm = sources.length === 0 && destinations.length === 0 && positional.length === 2;
|
|
574
|
+
const copySource = namedForm || namedSourceForm
|
|
575
|
+
? sources[0]
|
|
576
|
+
: namedDestinationForm || positionalForm
|
|
577
|
+
? positional[0]
|
|
578
|
+
: undefined;
|
|
579
|
+
const destination = namedForm || namedDestinationForm
|
|
580
|
+
? destinations[0]
|
|
581
|
+
: namedSourceForm || positionalForm
|
|
582
|
+
? positional.at(-1)
|
|
583
|
+
: undefined;
|
|
584
|
+
if (unsupported
|
|
585
|
+
|| copySource === undefined
|
|
586
|
+
|| destination === undefined
|
|
587
|
+
|| !isLiteralPowerShellPath(copySource)
|
|
588
|
+
|| !isLiteralPowerShellPath(destination)) {
|
|
589
|
+
ambiguous = true;
|
|
590
|
+
issue ??= commandIssue(source, "unsupported-copy-item-form", "use one literal source, one literal destination, and optional -Force");
|
|
591
|
+
}
|
|
592
|
+
else
|
|
593
|
+
paths.push(destination);
|
|
594
|
+
}
|
|
505
595
|
else if (POWERSHELL_WRITE_COMMANDS.has(executable)) {
|
|
506
596
|
applies = true;
|
|
507
597
|
const named = [];
|
|
@@ -701,7 +791,10 @@ export function normalizeCommand(command) {
|
|
|
701
791
|
normalized += character;
|
|
702
792
|
}
|
|
703
793
|
}
|
|
704
|
-
|
|
794
|
+
const callOperator = /^& (["'])((?:[A-Za-z]:[\\/]|\\\\|\/)[^\s"']+)\1(?= |$)/u.exec(normalized);
|
|
795
|
+
return callOperator === null
|
|
796
|
+
? normalized
|
|
797
|
+
: `${callOperator[2]}${normalized.slice(callOperator[0].length)}`;
|
|
705
798
|
}
|
|
706
799
|
/** Unbound sessions may invoke only tools whose complete input is known to be read-only. */
|
|
707
800
|
export function isKnownReadOnlyTool(tool, args, additionalReadOnlyTools = new Set()) {
|