sortie-dogs 0.5.10 → 0.5.13
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 +80 -22
- package/dist/plugin/gate.js +96 -2
- package/dist/plugin/index.js +219 -183
- package/dist/runtime-assets.d.ts +8 -8
- package/dist/runtime-assets.js +38 -33
- 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.13](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.5.13)
|
|
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.47-autonomous-recovery-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.47-autonomous-recovery-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,9 +269,8 @@ 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,
|
|
273
|
+
stepRecoveryActive: false,
|
|
277
274
|
touched: Date.now(),
|
|
278
275
|
};
|
|
279
276
|
sessions.set(sessionID, created);
|
|
@@ -350,8 +347,53 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
350
347
|
if (!promptCallSucceeded(resumed))
|
|
351
348
|
throw new Error("resume request rejected");
|
|
352
349
|
}
|
|
350
|
+
function topLevelProtocolLines(text) {
|
|
351
|
+
const lines = [];
|
|
352
|
+
let fence;
|
|
353
|
+
for (const [index, line] of text.split(/\r?\n/u).entries()) {
|
|
354
|
+
if (fence === undefined) {
|
|
355
|
+
const opener = /^[ \t]*(`{3,}|~{3,})/u.exec(line)?.[1];
|
|
356
|
+
if (opener === undefined) {
|
|
357
|
+
if (!/^[ \t]*>/u.test(line))
|
|
358
|
+
lines.push({ index, line });
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
fence = { character: opener[0], length: opener.length };
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const closer = /^[ \t]*(`{3,}|~{3,})[ \t]*$/u.exec(line)?.[1];
|
|
365
|
+
if (closer?.[0] === fence.character && closer.length >= fence.length)
|
|
366
|
+
fence = undefined;
|
|
367
|
+
}
|
|
368
|
+
return lines;
|
|
369
|
+
}
|
|
370
|
+
function checkpointEntries(text) {
|
|
371
|
+
const entries = [];
|
|
372
|
+
for (const { index, line } of topLevelProtocolLines(text)) {
|
|
373
|
+
const explicit = /^status:\s*(DONE|BLOCKED|NEED_DECISION)\b/iu.exec(line)?.[1]?.toUpperCase();
|
|
374
|
+
const status = explicit ??
|
|
375
|
+
(/^✅[ \t]+\*\*DONE\*\*/u.test(line) ? "DONE" :
|
|
376
|
+
/^⛔[ \t]+\*\*BLOCKED\*\*/u.test(line) ? "BLOCKED" :
|
|
377
|
+
/^❓[ \t]+\*\*NEED_DECISION\*\*/u.test(line) ? "NEED_DECISION" : undefined);
|
|
378
|
+
if (status === "DONE" || status === "BLOCKED" || status === "NEED_DECISION") {
|
|
379
|
+
entries.push({ index, status });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return entries;
|
|
383
|
+
}
|
|
384
|
+
function checkpointStatus(text) {
|
|
385
|
+
return checkpointEntries(text).at(-1)?.status;
|
|
386
|
+
}
|
|
353
387
|
function terminalCheckpoint(text) {
|
|
354
|
-
|
|
388
|
+
const status = checkpointStatus(text);
|
|
389
|
+
return status === "DONE" || status === "NEED_DECISION" || (status === "BLOCKED" && trueBlockerReport(text));
|
|
390
|
+
}
|
|
391
|
+
function trueBlockerReport(text) {
|
|
392
|
+
const checkpoint = checkpointEntries(text).at(-1);
|
|
393
|
+
if (checkpoint?.status !== "BLOCKED")
|
|
394
|
+
return false;
|
|
395
|
+
return topLevelProtocolLines(text).some(({ line, index }) => index > checkpoint.index &&
|
|
396
|
+
/^TRUE_BLOCKER\s*:\s*(?:external|user-decision)\s*:\s*\S.*$/u.test(line));
|
|
355
397
|
}
|
|
356
398
|
function latestProgressLine(text) {
|
|
357
399
|
return text.split(/\r?\n/u).filter((line) => line.includes("📊")).at(-1);
|
|
@@ -382,19 +424,23 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
382
424
|
return false;
|
|
383
425
|
if (/➡️\s*(?:次action|next_action)\s*:\s*\S/iu.test(text))
|
|
384
426
|
return true;
|
|
427
|
+
if (checkpointStatus(text) === "BLOCKED" && !trueBlockerReport(text))
|
|
428
|
+
return true;
|
|
385
429
|
const percent = latestProgressPercent(text);
|
|
386
|
-
|
|
430
|
+
if (percent !== undefined)
|
|
431
|
+
return percent < 100 || batchHasIndependentRemainder(text);
|
|
432
|
+
// Coordinators sometimes omit the optional percentage while retaining a localized progress label.
|
|
433
|
+
return /📊\s*(?:進行中|处理中|in[ -]?progress)(?:\s|[::]|$)/iu.test(latestProgressLine(text) ?? "");
|
|
387
434
|
}
|
|
388
435
|
async function issueStepContinue(sessionID, state, report, agent) {
|
|
389
436
|
const resume = client?.session?.promptAsync;
|
|
390
437
|
const active = policy();
|
|
391
438
|
if (resume === undefined || !active.enabled || active.agent !== agent || state.stepContinueIssuing ||
|
|
392
|
-
state.
|
|
393
|
-
state.stepContinueTotal >= MAX_STEP_CONTINUES_PER_TURN)
|
|
439
|
+
(state.lastStepContinueRevision === state.turnRevision && state.lastStepContinueReport === report))
|
|
394
440
|
return;
|
|
441
|
+
const revision = state.turnRevision;
|
|
395
442
|
state.stepContinueIssuing = true;
|
|
396
|
-
state.
|
|
397
|
-
state.stepContinueTotal += 1;
|
|
443
|
+
state.stepRecoveryActive = true;
|
|
398
444
|
state.latestCoordinatorReport = undefined;
|
|
399
445
|
try {
|
|
400
446
|
const resumed = await resume.call(client.session, {
|
|
@@ -405,21 +451,26 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
405
451
|
parts: [{
|
|
406
452
|
type: "text",
|
|
407
453
|
synthetic: true,
|
|
408
|
-
text: `${STEP_CONTINUE_PREFIX}\n直前出力は非terminal
|
|
454
|
+
text: `${STEP_CONTINUE_PREFIX}\n直前出力は非terminal進捗またはlocal/process blocker。未達のuser order、sequential execution、no-stop制約、SOL/advisor consultation指示を保持し、interactionは真にuser-controlledな決定だけに限定する。local gate/routing/handoff/scope/retry/time/step defectはterminal reportにせず、自律修復・redispatchして継続する。進捗報告を繰り返さず、明示next_actionが欠落していれば最新の未達user要求とbatch counterから次の必要toolを特定して同じturnで実行する。\n${report}`,
|
|
409
455
|
}],
|
|
410
456
|
},
|
|
411
457
|
});
|
|
412
458
|
if (!promptCallSucceeded(resumed))
|
|
413
459
|
throw new Error("step continuation request rejected");
|
|
460
|
+
state.lastStepContinueReport = report;
|
|
461
|
+
state.lastStepContinueRevision = revision;
|
|
414
462
|
}
|
|
415
463
|
catch (error) {
|
|
416
|
-
state.
|
|
417
|
-
state.stepContinueTotal -= 1;
|
|
464
|
+
state.stepRecoveryActive = false;
|
|
418
465
|
state.latestCoordinatorReport = report;
|
|
419
466
|
console.error("[sortie-continuation] step resume failed", sessionID, error);
|
|
420
467
|
}
|
|
421
468
|
finally {
|
|
422
469
|
state.stepContinueIssuing = false;
|
|
470
|
+
if (sessions.get(sessionID) === state && state.stepRecoveryActive &&
|
|
471
|
+
state.latestCoordinatorReport !== undefined && !terminalCheckpoint(state.latestCoordinatorReport)) {
|
|
472
|
+
queueMicrotask(() => { void handleSessionIdle(sessionID); });
|
|
473
|
+
}
|
|
423
474
|
}
|
|
424
475
|
}
|
|
425
476
|
/**
|
|
@@ -453,9 +504,6 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
453
504
|
state.continueReport = undefined;
|
|
454
505
|
state.preserveCompactionScope = false;
|
|
455
506
|
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
507
|
// The accepted prompt now belongs to the host loop, not this rollover request.
|
|
460
508
|
state.active = false;
|
|
461
509
|
return true;
|
|
@@ -615,6 +663,7 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
615
663
|
state.recoverySummaryValidated = false;
|
|
616
664
|
state.resetAttemptsAfterCompaction = resetAttemptsAfterCompaction;
|
|
617
665
|
state.latestCoordinatorReport = undefined;
|
|
666
|
+
state.stepRecoveryActive = false;
|
|
618
667
|
if (resume)
|
|
619
668
|
state.attempts += 1;
|
|
620
669
|
const epoch = state.rolloverEpoch;
|
|
@@ -763,6 +812,11 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
763
812
|
trimmed.length > 0 && !trimmed.startsWith(ROLLOVER_TOKEN) &&
|
|
764
813
|
!trimmed.startsWith(AUTO_CONTINUE_PREFIX)) {
|
|
765
814
|
state.latestCoordinatorReport = output.text.trim();
|
|
815
|
+
if (terminalCheckpoint(state.latestCoordinatorReport)) {
|
|
816
|
+
state.stepRecoveryActive = false;
|
|
817
|
+
clearTimer(state.stepRecoveryTimer);
|
|
818
|
+
state.stepRecoveryTimer = undefined;
|
|
819
|
+
}
|
|
766
820
|
if (batchCheckpointNeedsContinuation(state.latestCoordinatorReport)) {
|
|
767
821
|
if (input.allowCheckpointContinuation === false) {
|
|
768
822
|
state.latestCoordinatorReport = undefined;
|
|
@@ -775,7 +829,8 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
775
829
|
await requestContinuation(input.sessionID, identity, report, true);
|
|
776
830
|
}
|
|
777
831
|
}
|
|
778
|
-
else if (nonTerminalProgress(state.latestCoordinatorReport)
|
|
832
|
+
else if (nonTerminalProgress(state.latestCoordinatorReport) ||
|
|
833
|
+
(state.stepRecoveryActive && !terminalCheckpoint(state.latestCoordinatorReport))) {
|
|
779
834
|
scheduleStepRecovery(input.sessionID, state, state.latestCoordinatorReport);
|
|
780
835
|
}
|
|
781
836
|
// The resumed coordinator completed a turn, so no late event from its prior compaction can
|
|
@@ -892,14 +947,16 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
892
947
|
state.attempts = 0;
|
|
893
948
|
state.directUsed = false;
|
|
894
949
|
state.latestCoordinatorReport = undefined;
|
|
950
|
+
if (!synthetic) {
|
|
951
|
+
state.lastStepContinueReport = undefined;
|
|
952
|
+
state.lastStepContinueRevision = undefined;
|
|
953
|
+
state.stepRecoveryActive = false;
|
|
954
|
+
}
|
|
895
955
|
state.compactingEpoch = undefined;
|
|
896
956
|
state.model = { providerID: model.providerID, modelID: model.modelID };
|
|
897
957
|
state.turnRevision += 1;
|
|
898
|
-
if (!synthetic)
|
|
899
|
-
state.stepContinueCount = 0;
|
|
900
|
-
state.stepContinueTotal = 0;
|
|
958
|
+
if (!synthetic)
|
|
901
959
|
state.limitCompacted = false;
|
|
902
|
-
}
|
|
903
960
|
},
|
|
904
961
|
blocksTool(sessionID) {
|
|
905
962
|
const state = sessions.get(sessionID);
|
|
@@ -925,7 +982,8 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
925
982
|
}
|
|
926
983
|
if (state !== undefined && !state.stepContinueIssuing &&
|
|
927
984
|
state.latestCoordinatorReport !== undefined &&
|
|
928
|
-
nonTerminalProgress(state.latestCoordinatorReport)
|
|
985
|
+
(nonTerminalProgress(state.latestCoordinatorReport) ||
|
|
986
|
+
(state.stepRecoveryActive && !terminalCheckpoint(state.latestCoordinatorReport)))) {
|
|
929
987
|
const report = state.latestCoordinatorReport;
|
|
930
988
|
const revision = state.turnRevision;
|
|
931
989
|
const identity = await readIdentity(sessionID);
|
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 = [];
|
|
@@ -885,6 +975,9 @@ export async function createWriteGate(project, value) {
|
|
|
885
975
|
catch (error) {
|
|
886
976
|
throw new WriteDeniedError("manifest-unavailable", "<unknown>", { cause: error });
|
|
887
977
|
}
|
|
978
|
+
// A missing relative descendant is a directory scope when its parent is also declared.
|
|
979
|
+
// This authorizes files below the bounded child without widening the parent declaration.
|
|
980
|
+
const inferredDirectories = new Set([...writable].filter((candidate) => [...writable].some((other) => other !== candidate && candidate.startsWith(`${other}/`))));
|
|
888
981
|
const writableDirectories = [];
|
|
889
982
|
for (const path of writable) {
|
|
890
983
|
try {
|
|
@@ -894,7 +987,8 @@ export async function createWriteGate(project, value) {
|
|
|
894
987
|
}
|
|
895
988
|
}
|
|
896
989
|
catch (error) {
|
|
897
|
-
if (declaredDirectories.has(path)
|
|
990
|
+
if ((declaredDirectories.has(path) || inferredDirectories.has(path)) &&
|
|
991
|
+
isRecord(error) && error.code === "ENOENT") {
|
|
898
992
|
writableDirectories.push({ path, realPath: await nearestExistingRealPath(project.absolute(path)) });
|
|
899
993
|
}
|
|
900
994
|
// Other missing, inaccessible, and concurrently changed paths remain exact-only scopes.
|