@mjasnikovs/pi-task 0.38.29 → 0.38.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/config.d.ts +70 -70
- package/dist/config/config.js +26 -35
- package/dist/config/extension-list.d.ts +6 -5
- package/dist/config/extension-list.js +3 -2
- package/dist/config/reasoning-args.d.ts +9 -7
- package/dist/config/reasoning-args.js +12 -10
- package/dist/config/reasoning.d.ts +44 -105
- package/dist/config/reasoning.js +27 -704
- package/dist/config/register.d.ts +34 -48
- package/dist/config/register.js +41 -51
- package/dist/config/tool-list.d.ts +16 -16
- package/dist/config/tool-list.js +1 -1
- package/dist/index.js +2 -0
- package/dist/remote/bridge.d.ts +19 -10
- package/dist/remote/bridge.js +3 -2
- package/dist/remote/broadcast.js +3 -1
- package/dist/remote/events.js +12 -11
- package/dist/remote/history.d.ts +1 -1
- package/dist/remote/protocol.d.ts +6 -3
- package/dist/remote/protocol.js +2 -1
- package/dist/remote/push.d.ts +16 -16
- package/dist/remote/push.js +27 -27
- package/dist/remote/register.d.ts +3 -3
- package/dist/remote/register.js +17 -19
- package/dist/remote/server.d.ts +9 -8
- package/dist/remote/server.js +15 -14
- package/dist/remote/session-state.d.ts +5 -4
- package/dist/remote/session-state.js +8 -5
- package/dist/remote/sw.d.ts +7 -6
- package/dist/remote/sw.js +7 -6
- package/dist/remote/tailscale.d.ts +4 -2
- package/dist/remote/tailscale.js +4 -2
- package/dist/remote/ui-highlight.js +6 -5
- package/dist/remote/ui-render.js +4 -4
- package/dist/remote/ui-script.js +24 -24
- package/dist/remote/ui-styles.d.ts +1 -1
- package/dist/remote/ui-styles.js +10 -13
- package/dist/remote/ui-tools.js +9 -6
- package/dist/shared/child-extensions.d.ts +29 -17
- package/dist/shared/child-extensions.js +29 -17
- package/dist/shared/child-output.d.ts +30 -24
- package/dist/shared/child-output.js +25 -17
- package/dist/shared/child-process.d.ts +47 -40
- package/dist/shared/child-process.js +50 -59
- package/dist/shared/command-watchdog.d.ts +85 -16
- package/dist/shared/command-watchdog.js +115 -21
- package/dist/shared/fs-text.d.ts +16 -10
- package/dist/shared/fs-text.js +16 -10
- package/dist/shared/git-runner.d.ts +25 -25
- package/dist/shared/git-runner.js +25 -25
- package/dist/shared/leaked-tool-call.d.ts +17 -11
- package/dist/shared/leaked-tool-call.js +23 -15
- package/dist/shared/model-endpoint.d.ts +29 -16
- package/dist/shared/model-endpoint.js +33 -21
- package/dist/shared/pi-invocation.d.ts +7 -4
- package/dist/shared/pi-invocation.js +12 -7
- package/dist/shared/pkg-version.d.ts +13 -5
- package/dist/shared/pkg-version.js +13 -5
- package/dist/shared/reasoning-capability.d.ts +35 -24
- package/dist/shared/reasoning-capability.js +35 -24
- package/dist/shared/stream-watchdog.d.ts +60 -44
- package/dist/shared/stream-watchdog.js +62 -45
- package/dist/task/accept-debt.d.ts +41 -43
- package/dist/task/accept-debt.js +73 -65
- package/dist/task/api-synthesis.d.ts +24 -21
- package/dist/task/api-synthesis.js +32 -26
- package/dist/task/apis-contract.d.ts +32 -64
- package/dist/task/apis-contract.js +32 -64
- package/dist/task/artifact-closure.d.ts +27 -13
- package/dist/task/artifact-closure.js +95 -67
- package/dist/task/auto-commit.d.ts +46 -35
- package/dist/task/auto-commit.js +51 -38
- package/dist/task/auto-io.d.ts +45 -25
- package/dist/task/auto-io.js +57 -29
- package/dist/task/auto-orchestrator.d.ts +26 -24
- package/dist/task/auto-orchestrator.js +192 -165
- package/dist/task/auto-prompts.d.ts +36 -24
- package/dist/task/auto-prompts.js +40 -26
- package/dist/task/autofix-ledger.d.ts +27 -25
- package/dist/task/autofix-ledger.js +29 -26
- package/dist/task/batch-test-task.d.ts +20 -12
- package/dist/task/batch-test-task.js +67 -60
- package/dist/task/boot-probe.d.ts +60 -44
- package/dist/task/boot-probe.js +91 -72
- package/dist/task/cancel-input.d.ts +30 -16
- package/dist/task/cancel-input.js +20 -11
- package/dist/task/cancel-points.d.ts +27 -20
- package/dist/task/cancel-points.js +30 -22
- package/dist/task/child-runner.d.ts +124 -55
- package/dist/task/child-runner.js +298 -90
- package/dist/task/child-status.d.ts +23 -16
- package/dist/task/child-status.js +23 -16
- package/dist/task/clamp-output.js +12 -5
- package/dist/task/command-run.d.ts +31 -28
- package/dist/task/command-run.js +44 -35
- package/dist/task/command-shrink.d.ts +25 -18
- package/dist/task/command-shrink.js +37 -31
- package/dist/task/command-watchdog.d.ts +9 -6
- package/dist/task/command-watchdog.js +21 -15
- package/dist/task/context-attribution.d.ts +34 -26
- package/dist/task/context-attribution.js +34 -26
- package/dist/task/context-silence.d.ts +39 -29
- package/dist/task/context-silence.js +35 -25
- package/dist/task/context-usage.d.ts +16 -9
- package/dist/task/context-usage.js +16 -9
- package/dist/task/contracts.d.ts +8 -4
- package/dist/task/contracts.js +25 -17
- package/dist/task/coverage-loop.d.ts +22 -18
- package/dist/task/coverage-loop.js +35 -30
- package/dist/task/critique-probes.d.ts +13 -14
- package/dist/task/critique-probes.js +50 -39
- package/dist/task/debug-log.d.ts +13 -5
- package/dist/task/debug-log.js +32 -20
- package/dist/task/decompose-fidelity.d.ts +11 -9
- package/dist/task/decompose-fidelity.js +38 -33
- package/dist/task/decompose-granularity.d.ts +41 -38
- package/dist/task/decompose-granularity.js +41 -38
- package/dist/task/deep-render-check.d.ts +22 -14
- package/dist/task/deep-render-check.js +40 -31
- package/dist/task/dropped-input.d.ts +12 -7
- package/dist/task/dropped-input.js +5 -2
- package/dist/task/enforce-attribution.d.ts +38 -47
- package/dist/task/enforce-attribution.js +46 -52
- package/dist/task/enforce-guidelines.d.ts +31 -20
- package/dist/task/enforce-guidelines.js +32 -21
- package/dist/task/enrichment.d.ts +7 -2
- package/dist/task/enrichment.js +26 -14
- package/dist/task/env-notes.d.ts +16 -7
- package/dist/task/env-notes.js +48 -31
- package/dist/task/env-template-closure.d.ts +4 -4
- package/dist/task/env-template-closure.js +42 -34
- package/dist/task/external-context.d.ts +28 -21
- package/dist/task/external-context.js +17 -12
- package/dist/task/failure-classifier.d.ts +4 -5
- package/dist/task/failure-classifier.js +30 -8
- package/dist/task/file-inventory.d.ts +15 -11
- package/dist/task/file-inventory.js +25 -22
- package/dist/task/final-gate-fix.d.ts +74 -86
- package/dist/task/final-gate-fix.js +97 -116
- package/dist/task/final-gate-progress.d.ts +29 -46
- package/dist/task/final-gate-progress.js +40 -51
- package/dist/task/final-gate.d.ts +64 -97
- package/dist/task/final-gate.js +192 -199
- package/dist/task/fix-child.d.ts +21 -27
- package/dist/task/fix-child.js +21 -27
- package/dist/task/foreign-path.d.ts +6 -5
- package/dist/task/foreign-path.js +0 -0
- package/dist/task/frozen-conflict.d.ts +9 -10
- package/dist/task/frozen-conflict.js +61 -64
- package/dist/task/frozen-path-guard.d.ts +35 -14
- package/dist/task/frozen-path-guard.js +56 -39
- package/dist/task/gate-child.d.ts +27 -28
- package/dist/task/gate-child.js +36 -35
- package/dist/task/gate-deps.d.ts +34 -27
- package/dist/task/gate-deps.js +169 -159
- package/dist/task/gate-tally.d.ts +77 -80
- package/dist/task/gate-tally.js +65 -68
- package/dist/task/git-state-guard.d.ts +15 -11
- package/dist/task/git-state-guard.js +76 -66
- package/dist/task/impl-widget.d.ts +25 -16
- package/dist/task/impl-widget.js +27 -17
- package/dist/task/implementation-guards.d.ts +26 -0
- package/dist/task/implementation-guards.js +177 -0
- package/dist/task/implementation-thinking.d.ts +33 -31
- package/dist/task/implementation-thinking.js +5 -6
- package/dist/task/implementation-turn.d.ts +39 -31
- package/dist/task/implementation-turn.js +41 -28
- package/dist/task/inline-markdown.d.ts +20 -7
- package/dist/task/inline-markdown.js +15 -6
- package/dist/task/launch-config-gap.js +25 -39
- package/dist/task/launch-contract.d.ts +18 -21
- package/dist/task/launch-contract.js +28 -30
- package/dist/task/launch-manifest.d.ts +6 -2
- package/dist/task/launch-manifest.js +35 -34
- package/dist/task/ledger.js +16 -14
- package/dist/task/lint-fix.d.ts +6 -8
- package/dist/task/lint-fix.js +67 -69
- package/dist/task/loop-detector.d.ts +27 -8
- package/dist/task/loop-detector.js +38 -14
- package/dist/task/mid-run-input.d.ts +17 -15
- package/dist/task/mid-run-input.js +17 -15
- package/dist/task/orchestrator.d.ts +24 -28
- package/dist/task/orchestrator.js +89 -66
- package/dist/task/orientation.d.ts +18 -23
- package/dist/task/orientation.js +24 -31
- package/dist/task/owned-freeze-conflict.d.ts +21 -20
- package/dist/task/owned-freeze-conflict.js +52 -85
- package/dist/task/owned-freeze-reassign.d.ts +40 -60
- package/dist/task/owned-freeze-reassign.js +41 -61
- package/dist/task/parsers.d.ts +4 -2
- package/dist/task/parsers.js +4 -4
- package/dist/task/phases.d.ts +41 -48
- package/dist/task/phases.js +196 -252
- package/dist/task/plan-io.d.ts +6 -7
- package/dist/task/plan-io.js +6 -7
- package/dist/task/plan-orchestrator.d.ts +10 -8
- package/dist/task/plan-orchestrator.js +14 -10
- package/dist/task/plan-prompts.d.ts +6 -5
- package/dist/task/plan-prompts.js +6 -5
- package/dist/task/plan-readonly.d.ts +4 -5
- package/dist/task/plan-readonly.js +4 -5
- package/dist/task/plan-rounds.d.ts +17 -29
- package/dist/task/plan-rounds.js +21 -34
- package/dist/task/plan-session.d.ts +58 -72
- package/dist/task/plan-session.js +61 -83
- package/dist/task/probe-gaming.d.ts +28 -27
- package/dist/task/probe-gaming.js +0 -0
- package/dist/task/prohibition-probe.d.ts +14 -16
- package/dist/task/prompts.d.ts +3 -4
- package/dist/task/prompts.js +17 -26
- package/dist/task/qa-transcript.d.ts +15 -22
- package/dist/task/qa-transcript.js +15 -21
- package/dist/task/question-box.d.ts +17 -13
- package/dist/task/question-box.js +19 -15
- package/dist/task/question-dedup.d.ts +6 -7
- package/dist/task/question-dedup.js +13 -14
- package/dist/task/question-dialog.d.ts +22 -32
- package/dist/task/question-dialog.js +22 -32
- package/dist/task/question-source.d.ts +18 -44
- package/dist/task/question-source.js +22 -51
- package/dist/task/refuted-constraint.d.ts +11 -31
- package/dist/task/refuted-constraint.js +27 -51
- package/dist/task/regenerable-artifacts.d.ts +12 -31
- package/dist/task/regenerable-artifacts.js +12 -31
- package/dist/task/render-check.d.ts +11 -22
- package/dist/task/render-check.js +33 -46
- package/dist/task/repo-health-check.d.ts +10 -14
- package/dist/task/repo-health-check.js +17 -23
- package/dist/task/requirements.d.ts +38 -71
- package/dist/task/requirements.js +78 -126
- package/dist/task/research-fanout-budget.d.ts +51 -88
- package/dist/task/research-fanout-budget.js +51 -88
- package/dist/task/research-worker.d.ts +29 -39
- package/dist/task/research-worker.js +37 -61
- package/dist/task/resume-gap.d.ts +14 -15
- package/dist/task/root-cause-repair.d.ts +9 -9
- package/dist/task/root-cause-repair.js +28 -40
- package/dist/task/run-bracket.d.ts +10 -13
- package/dist/task/run-end.d.ts +12 -22
- package/dist/task/run-end.js +8 -16
- package/dist/task/run-final-gate.d.ts +19 -21
- package/dist/task/run-final-gate.js +62 -80
- package/dist/task/runner-globs.d.ts +12 -13
- package/dist/task/runner-globs.js +12 -13
- package/dist/task/runner-resolve.d.ts +9 -9
- package/dist/task/runner-resolve.js +22 -23
- package/dist/task/script-escape.d.ts +10 -12
- package/dist/task/script-escape.js +13 -14
- package/dist/task/serve-entry.d.ts +1 -1
- package/dist/task/serve-entry.js +22 -25
- package/dist/task/service-blocks.js +4 -2
- package/dist/task/shipped-source.d.ts +11 -29
- package/dist/task/shipped-source.js +11 -29
- package/dist/task/skip-escape.js +10 -14
- package/dist/task/spec-urls.d.ts +26 -65
- package/dist/task/spec-urls.js +26 -65
- package/dist/task/spec-validation.d.ts +17 -20
- package/dist/task/spec-validation.js +17 -20
- package/dist/task/stall-detector.d.ts +23 -30
- package/dist/task/stall-detector.js +23 -30
- package/dist/task/stream-watchdog.d.ts +14 -12
- package/dist/task/stream-watchdog.js +14 -12
- package/dist/task/substitution-probe.d.ts +17 -20
- package/dist/task/substitution-probe.js +17 -20
- package/dist/task/task-gates.d.ts +36 -41
- package/dist/task/task-gates.js +95 -106
- package/dist/task/task-io.d.ts +4 -4
- package/dist/task/task-io.js +4 -4
- package/dist/task/task-parsers.js +4 -3
- package/dist/task/task-provenance.d.ts +2 -2
- package/dist/task/task-provenance.js +11 -13
- package/dist/task/task-types.d.ts +4 -3
- package/dist/task/terminal-outcome.d.ts +14 -16
- package/dist/task/terminal-outcome.js +12 -14
- package/dist/task/test-assembly.d.ts +13 -20
- package/dist/task/test-assembly.js +13 -20
- package/dist/task/timings.d.ts +5 -3
- package/dist/task/timings.js +5 -3
- package/dist/task/title-label.d.ts +9 -4
- package/dist/task/title-label.js +9 -4
- package/dist/task/type-only-answer.d.ts +44 -52
- package/dist/task/type-only-answer.js +44 -52
- package/dist/task/unfailable-command.d.ts +18 -24
- package/dist/task/unfailable-command.js +21 -27
- package/dist/task/unknown-routing.d.ts +10 -4
- package/dist/task/unknown-routing.js +10 -4
- package/dist/task/user-directives.d.ts +5 -8
- package/dist/task/user-directives.js +5 -8
- package/dist/task/verify-quality.d.ts +18 -22
- package/dist/task/verify-quality.js +45 -46
- package/dist/task/verify-reconcile.d.ts +15 -10
- package/dist/task/verify-reconcile.js +45 -43
- package/dist/task/verify-resolution.d.ts +24 -20
- package/dist/task/verify-resolution.js +51 -50
- package/dist/task/verify-work.d.ts +59 -66
- package/dist/task/verify-work.js +101 -138
- package/dist/task/widget.d.ts +15 -14
- package/dist/task/widget.js +22 -17
- package/dist/task/wiring-claims.d.ts +25 -32
- package/dist/task/wiring-claims.js +30 -35
- package/dist/task/write-guard.d.ts +39 -39
- package/dist/task/write-guard.js +48 -51
- package/dist/task/yolo.d.ts +34 -30
- package/dist/task/yolo.js +42 -37
- package/dist/workers/abstention.d.ts +21 -41
- package/dist/workers/abstention.js +27 -48
- package/dist/workers/brave-search.d.ts +4 -3
- package/dist/workers/brave-search.js +5 -2
- package/dist/workers/brave-warning.d.ts +7 -4
- package/dist/workers/brave-warning.js +19 -7
- package/dist/workers/ddg-search.d.ts +6 -6
- package/dist/workers/ddg-search.js +18 -12
- package/dist/workers/docs-cache.js +5 -2
- package/dist/workers/docs-chunk.d.ts +30 -37
- package/dist/workers/docs-chunk.js +37 -41
- package/dist/workers/docs-core.d.ts +28 -44
- package/dist/workers/docs-core.js +25 -44
- package/dist/workers/docs-index.js +4 -3
- package/dist/workers/docs-lookup.d.ts +15 -22
- package/dist/workers/docs-lookup.js +12 -21
- package/dist/workers/docs-project.d.ts +15 -9
- package/dist/workers/docs-project.js +17 -10
- package/dist/workers/docs-resolve.d.ts +19 -20
- package/dist/workers/docs-resolve.js +35 -32
- package/dist/workers/docs-retrieve.d.ts +5 -6
- package/dist/workers/docs-retrieve.js +18 -15
- package/dist/workers/exa-search.d.ts +9 -6
- package/dist/workers/exa-search.js +23 -12
- package/dist/workers/fetch-core.d.ts +13 -16
- package/dist/workers/fetch-core.js +23 -23
- package/dist/workers/focused-extractor.d.ts +13 -12
- package/dist/workers/focused-extractor.js +27 -19
- package/dist/workers/html-clean.js +24 -14
- package/dist/workers/http-request.d.ts +28 -20
- package/dist/workers/http-request.js +22 -17
- package/dist/workers/npm-version.d.ts +28 -11
- package/dist/workers/npm-version.js +24 -15
- package/dist/workers/phantom-imports.d.ts +15 -12
- package/dist/workers/phantom-imports.js +30 -24
- package/dist/workers/pi-worker-core.d.ts +65 -96
- package/dist/workers/pi-worker-core.js +93 -181
- package/dist/workers/pi-worker-docs.d.ts +24 -19
- package/dist/workers/pi-worker-docs.js +67 -76
- package/dist/workers/pi-worker-fetch.d.ts +7 -3
- package/dist/workers/pi-worker-fetch.js +27 -19
- package/dist/workers/pi-worker-search.js +12 -8
- package/dist/workers/pi-worker.d.ts +9 -4
- package/dist/workers/pi-worker.js +21 -14
- package/dist/workers/reasoning-warning.d.ts +18 -17
- package/dist/workers/reasoning-warning.js +22 -20
- package/dist/workers/research-cache.js +50 -78
- package/dist/workers/search-core.js +7 -5
- package/dist/workers/search-types.d.ts +10 -9
- package/dist/workers/search-types.js +9 -8
- package/dist/workers/session-hint.d.ts +13 -14
- package/dist/workers/session-hint.js +8 -9
- package/dist/workers/shared.d.ts +21 -25
- package/dist/workers/shared.js +0 -0
- package/dist/workers/single-read-extension.d.ts +14 -7
- package/dist/workers/single-read-extension.js +14 -7
- package/dist/workers/single-read-guard.d.ts +27 -30
- package/dist/workers/single-read-guard.js +36 -36
- package/dist/workers/typeonly-log.d.ts +12 -9
- package/dist/workers/typeonly-log.js +29 -33
- package/dist/workers/worker-channels.d.ts +15 -23
- package/dist/workers/worker-channels.js +15 -23
- package/dist/workers/worker-failure.d.ts +38 -46
- package/dist/workers/worker-failure.js +31 -39
- package/dist/workers/worker-kill.d.ts +25 -26
- package/dist/workers/worker-kill.js +16 -19
- package/dist/workers/worker-profiles.d.ts +54 -56
- package/dist/workers/worker-profiles.js +63 -39
- package/package.json +10 -8
|
@@ -12,7 +12,19 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Either pattern returns a LoopHit so the caller can kill the child and re-spawn
|
|
14
14
|
* with a hint. No I/O. No imports from index.ts. Trivially unit-testable.
|
|
15
|
+
*
|
|
16
|
+
* The three tuning constants live here rather than in child-runner.ts because
|
|
17
|
+
* worker-profiles.ts reads them at module top level to build DEFAULT_LOOP_DETECTOR.
|
|
18
|
+
* From child-runner.ts that is a cycle — child-runner → worker-profiles →
|
|
19
|
+
* child-runner — and the failure is a TDZ ReferenceError on import order, which
|
|
20
|
+
* no compile step catches. This module imports nothing, so it cannot close one.
|
|
15
21
|
*/
|
|
22
|
+
/** Recent tool calls the exact-repeat rule looks back over. */
|
|
23
|
+
export const LOOP_WINDOW = 20;
|
|
24
|
+
/** Repeats of one key within the window that trip the detector. */
|
|
25
|
+
export const LOOP_THRESHOLD = 5;
|
|
26
|
+
/** Re-spawns allowed after a loop kill — 3 attempts total with the initial one. */
|
|
27
|
+
export const MAX_LOOP_RESTARTS = 2;
|
|
16
28
|
/**
|
|
17
29
|
* JSON.stringify with sorted object keys so {a:1,b:2} and {b:2,a:1} hash equal.
|
|
18
30
|
* Arrays preserve their order (positional). undefined / primitives passthrough.
|
|
@@ -28,6 +40,14 @@ export function stableStringify(value) {
|
|
|
28
40
|
return sorted;
|
|
29
41
|
});
|
|
30
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The exact-match identity of a tool call. Exported because implementation-guards.ts
|
|
45
|
+
* keys its own per-call strike count on it, and a second hand-written copy of this
|
|
46
|
+
* expression is free to drift away from the one `record` uses.
|
|
47
|
+
*/
|
|
48
|
+
export function loopKey(call) {
|
|
49
|
+
return `${call.name}\x00${stableStringify(call.args)}`;
|
|
50
|
+
}
|
|
31
51
|
/**
|
|
32
52
|
* The primary file path a tool call targets, mirroring summarizeToolArgs' field
|
|
33
53
|
* precedence. Returns null when the call names no path (e.g. bash, or a grep
|
|
@@ -52,7 +72,11 @@ function readOffset(args) {
|
|
|
52
72
|
const o = args.offset;
|
|
53
73
|
return typeof o === 'number' && Number.isFinite(o) ? o : 0;
|
|
54
74
|
}
|
|
55
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* pi's read tool truncates its output at `DEFAULT_MAX_LINES` lines (its
|
|
77
|
+
* `core/tools/truncate` module). A `limit` at or above that asks for as much as
|
|
78
|
+
* the tool will ever return, so it is treated the same as naming no limit at all.
|
|
79
|
+
*/
|
|
56
80
|
const DEFAULT_READ_LIMIT = 2000;
|
|
57
81
|
/**
|
|
58
82
|
* The last line a call reaches, so a page can be told from a re-read. Absent,
|
|
@@ -74,7 +98,7 @@ export class LoopDetector {
|
|
|
74
98
|
threshold;
|
|
75
99
|
pathThreshold;
|
|
76
100
|
buf = [];
|
|
77
|
-
constructor(window =
|
|
101
|
+
constructor(window = LOOP_WINDOW, threshold = LOOP_THRESHOLD,
|
|
78
102
|
/** Revisits of one path needed to trip; defaults to the exact threshold. */
|
|
79
103
|
pathThreshold = threshold) {
|
|
80
104
|
this.window = window;
|
|
@@ -83,7 +107,7 @@ export class LoopDetector {
|
|
|
83
107
|
}
|
|
84
108
|
/** Record a tool call. Returns LoopHit if either threshold is breached, else null. */
|
|
85
109
|
record(call) {
|
|
86
|
-
const key =
|
|
110
|
+
const key = loopKey(call);
|
|
87
111
|
const offset = readOffset(call.args);
|
|
88
112
|
this.buf.push({ key, path: primaryPath(call.args), offset, end: readEnd(call.args, offset) });
|
|
89
113
|
if (this.buf.length > this.window)
|
|
@@ -96,9 +120,8 @@ export class LoopDetector {
|
|
|
96
120
|
if (exact >= this.threshold)
|
|
97
121
|
return { call, count: exact, windowSize: this.buf.length };
|
|
98
122
|
// 2. Path-aware loop: the same file re-targeted without forward progress.
|
|
99
|
-
// Caught here precisely because varied offset/limit
|
|
100
|
-
//
|
|
101
|
-
// the wall-clock timeout, never tripping the exact detector).
|
|
123
|
+
// Caught here precisely because varied offset/limit change the exact key on
|
|
124
|
+
// every call, so pattern 1 above can never see it.
|
|
102
125
|
const path = this.buf[this.buf.length - 1].path;
|
|
103
126
|
if (path !== null) {
|
|
104
127
|
const revisits = this.countRevisits(path);
|
|
@@ -111,15 +134,16 @@ export class LoopDetector {
|
|
|
111
134
|
/**
|
|
112
135
|
* Count same-path calls in the window that are "revisits" — accesses that end
|
|
113
136
|
* no further into the file than the furthest line already covered for that
|
|
114
|
-
* path.
|
|
115
|
-
*
|
|
137
|
+
* path. The FIRST call on a path is therefore never a revisit: it sets the
|
|
138
|
+
* high-water mark. Paging yields zero revisits and never trips; repeated
|
|
139
|
+
* whole-file re-reads, backward jumps, narrower re-reads and repeated
|
|
140
|
+
* path-targeting greps all accumulate.
|
|
116
141
|
*
|
|
117
|
-
* The comparison is on the RANGE, not the offset.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* path alone, and it is corrected the same way.
|
|
142
|
+
* The comparison is on the RANGE, not the offset. Keying on offset alone scores
|
|
143
|
+
* `{offset:80, limit:400}` after `{offset:80, limit:300}` as a revisit, even
|
|
144
|
+
* though it covers 100 lines the child has never seen. That is the same mistake
|
|
145
|
+
* SingleReadGuard made by keying on the path alone, and it is corrected the same
|
|
146
|
+
* way.
|
|
123
147
|
*/
|
|
124
148
|
countRevisits(path) {
|
|
125
149
|
let maxEnd = -1;
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What a typed message DOES while a task run owns the session.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* gate are child `pi` processes, not host turns
|
|
4
|
+
* Both surfaces — the terminal Enter listener and the remote browser — need ONE
|
|
5
|
+
* rule, because for most of a run the HOST session is idle: the spec phases and
|
|
6
|
+
* every gate are child `pi` processes, not host turns. Left to itself each surface
|
|
7
|
+
* has its own failure on an idle session:
|
|
7
8
|
*
|
|
8
|
-
* - browser → `sendUserMessage` with no delivery mode
|
|
9
|
-
* turn
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* - browser → `sendUserMessage` with no delivery mode. pi's own API docs call it
|
|
10
|
+
* "Always triggers a turn", so it opens a SECOND turn beside the run,
|
|
11
|
+
* with the write tool in hand. If that turn is still streaming when
|
|
12
|
+
* the pipeline delivers its spec, the spec's own send hits pi's
|
|
13
|
+
* guard: "Agent is already processing. Specify streamingBehavior
|
|
14
|
+
* ('steer' or 'followUp') to queue the message."
|
|
15
|
+
* - terminal → pi's `pendingUserInputs` queue, which only `getUserInput()` drains
|
|
16
|
+
* — and that is the main loop our own command handler is parked
|
|
17
|
+
* inside. The line sits there silently, then fires against a
|
|
18
|
+
* finished run.
|
|
17
19
|
*
|
|
18
|
-
* The shared rule both surfaces
|
|
20
|
+
* The shared rule both surfaces follow:
|
|
19
21
|
*
|
|
20
|
-
* agent streaming → steer the live turn
|
|
22
|
+
* agent streaming → steer the live turn
|
|
21
23
|
* agent idle, run active → HOLD it here, show it as pending, and deliver it as
|
|
22
24
|
* a steer when the next task turn starts
|
|
23
|
-
* no run active → ordinary message
|
|
25
|
+
* no run active → ordinary message
|
|
24
26
|
*
|
|
25
27
|
* Holding is what makes the two surfaces agree AND keeps the run safe: a held
|
|
26
28
|
* line never becomes a competing turn, and it is never silently replayed after
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What a typed message DOES while a task run owns the session.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* gate are child `pi` processes, not host turns
|
|
4
|
+
* Both surfaces — the terminal Enter listener and the remote browser — need ONE
|
|
5
|
+
* rule, because for most of a run the HOST session is idle: the spec phases and
|
|
6
|
+
* every gate are child `pi` processes, not host turns. Left to itself each surface
|
|
7
|
+
* has its own failure on an idle session:
|
|
7
8
|
*
|
|
8
|
-
* - browser → `sendUserMessage` with no delivery mode
|
|
9
|
-
* turn
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* - browser → `sendUserMessage` with no delivery mode. pi's own API docs call it
|
|
10
|
+
* "Always triggers a turn", so it opens a SECOND turn beside the run,
|
|
11
|
+
* with the write tool in hand. If that turn is still streaming when
|
|
12
|
+
* the pipeline delivers its spec, the spec's own send hits pi's
|
|
13
|
+
* guard: "Agent is already processing. Specify streamingBehavior
|
|
14
|
+
* ('steer' or 'followUp') to queue the message."
|
|
15
|
+
* - terminal → pi's `pendingUserInputs` queue, which only `getUserInput()` drains
|
|
16
|
+
* — and that is the main loop our own command handler is parked
|
|
17
|
+
* inside. The line sits there silently, then fires against a
|
|
18
|
+
* finished run.
|
|
17
19
|
*
|
|
18
|
-
* The shared rule both surfaces
|
|
20
|
+
* The shared rule both surfaces follow:
|
|
19
21
|
*
|
|
20
|
-
* agent streaming → steer the live turn
|
|
22
|
+
* agent streaming → steer the live turn
|
|
21
23
|
* agent idle, run active → HOLD it here, show it as pending, and deliver it as
|
|
22
24
|
* a steer when the next task turn starts
|
|
23
|
-
* no run active → ordinary message
|
|
25
|
+
* no run active → ordinary message
|
|
24
26
|
*
|
|
25
27
|
* Holding is what makes the two surfaces agree AND keeps the run safe: a held
|
|
26
28
|
* line never becomes a competing turn, and it is never silently replayed after
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* /task-cancel cancel the running task (soft-terminal — still resumable)
|
|
13
13
|
*
|
|
14
14
|
* The orchestrator persists after every phase boundary to
|
|
15
|
-
* <cwd>/.pi-tasks/TASK_NNNN.md.
|
|
16
|
-
* ctx.ui
|
|
15
|
+
* <cwd>/.pi-tasks/TASK_NNNN.md. User interaction during phases runs through the
|
|
16
|
+
* SessionUI bridge — a local ctx.ui dialog raced against a remote browser card —
|
|
17
|
+
* never through the main conversation, which only receives the final spec.
|
|
17
18
|
*/
|
|
18
19
|
import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
19
20
|
import { type WidgetState } from './widget.js';
|
|
@@ -43,12 +44,13 @@ export interface TaskRunnerOptions {
|
|
|
43
44
|
* `spawn` drives the Error-triage ladder or a real process. `runChild(name,
|
|
44
45
|
* tools, prompt)` answers every phase child BY NAME, with none of the
|
|
45
46
|
* ladder's guards — use it when the child is a premise of the test, not its
|
|
46
|
-
* subject. `runWorker(label, input)` answers every research worker by name,
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* subject. `runWorker(label, input)` answers every research worker by name, so
|
|
48
|
+
* the research phase's retry gates (`zeroRetrievalRetry`, `retryIfSilent`, the
|
|
49
|
+
* restart ladder) are reachable without matching a marker sentence inside a
|
|
50
|
+
* prompt. The EXTERNAL CONTEXT lookups and the file
|
|
49
51
|
* inventory each default to the real implementation when absent. `timeoutMs`,
|
|
50
|
-
* `sleepFor`, `childExtensions` and `logDebug` are seams too, and
|
|
51
|
-
*
|
|
52
|
+
* `sleepFor`, `childExtensions` and `logDebug` are seams too, and travel in the
|
|
53
|
+
* same field.
|
|
52
54
|
*/
|
|
53
55
|
seams?: PhaseSeams;
|
|
54
56
|
/** Called with the resolved task id once its file exists, before any phase
|
|
@@ -96,9 +98,9 @@ export declare class TaskRunner {
|
|
|
96
98
|
private readonly _deps;
|
|
97
99
|
private readonly _pc;
|
|
98
100
|
/**
|
|
99
|
-
* Per-phase wall-clock durations collected during the run
|
|
100
|
-
* `## phase timings` section on
|
|
101
|
-
*
|
|
101
|
+
* Per-phase wall-clock durations collected during the run, written to the
|
|
102
|
+
* `## phase timings` section — on completion, and again from the catch so a
|
|
103
|
+
* failed run still records what it got through. Each top-level entry is a
|
|
102
104
|
* phase (refine/research/grill/compose/critique); children are optional
|
|
103
105
|
* sub-step splits the phase chose to record via deps.recordSubStep.
|
|
104
106
|
*/
|
|
@@ -111,16 +113,15 @@ export declare class TaskRunner {
|
|
|
111
113
|
status(): WidgetState | null;
|
|
112
114
|
/** Cancel the running task by aborting the signal. */
|
|
113
115
|
cancel(): void;
|
|
114
|
-
/** Execute the full task lifecycle. Mid-run input holds instead of starting
|
|
115
|
-
* a competing turn for the whole of it, and the terminal interception is
|
|
116
|
-
* armed for the same window (`withRun`); nested inside `runGatedTask` or
|
|
117
|
-
* the `/task-auto` loop the bracket refcounts, so this changes nothing there
|
|
118
|
-
* and covers the fire-and-forget `runSingleTask` path on its own. */
|
|
119
116
|
/**
|
|
120
|
-
* Run the task and NAME how it ended
|
|
117
|
+
* Run the task and NAME how it ended (`RunEnd`), so no caller has to re-read
|
|
118
|
+
* the task file to find out.
|
|
121
119
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
120
|
+
* Mid-run input holds instead of starting a competing turn for the whole of
|
|
121
|
+
* it, and the terminal interception is armed for the same window (`withRun`);
|
|
122
|
+
* nested inside `runGatedTask` or the `/task-auto` loop the bracket refcounts,
|
|
123
|
+
* so this changes nothing there and covers the fire-and-forget
|
|
124
|
+
* `runSingleTask` path on its own.
|
|
124
125
|
*/
|
|
125
126
|
run(): Promise<RunEnd>;
|
|
126
127
|
private _run;
|
|
@@ -163,8 +164,8 @@ export interface RunSingleTaskOptions extends Pick<TaskRunnerOptions, 'resumeId'
|
|
|
163
164
|
notifyFinish?: boolean;
|
|
164
165
|
/**
|
|
165
166
|
* How the implementation turn's thinking level is read and written. Defaults
|
|
166
|
-
* to the live pi session;
|
|
167
|
-
*
|
|
167
|
+
* to the live pi session; injectable so the hold-and-restore is assertable
|
|
168
|
+
* with no real session to restore.
|
|
168
169
|
*/
|
|
169
170
|
thinkingControl?: ThinkingControl;
|
|
170
171
|
}
|
|
@@ -172,11 +173,7 @@ export interface RunSingleTaskResult {
|
|
|
172
173
|
taskId: string;
|
|
173
174
|
/**
|
|
174
175
|
* How the run ended, named by the runner rather than re-derived from disk.
|
|
175
|
-
*
|
|
176
|
-
* This was `ok: boolean` plus `sessionCancelled`, `interrupted` and `reason`
|
|
177
|
-
* — four fields for one fact, three of them smuggled out of the `withSession`
|
|
178
|
-
* closure through mutable captures. `/task-cancel` fell into the `!ok` arm
|
|
179
|
-
* and was reported as a failure; see run-end.ts.
|
|
176
|
+
* One value for one fact: see run-end.ts for the endings and their policy.
|
|
180
177
|
*/
|
|
181
178
|
end: RunEnd;
|
|
182
179
|
/**
|
|
@@ -194,9 +191,8 @@ export interface RunSingleTaskResult {
|
|
|
194
191
|
* deliver its spec. With waitForImplementation, block until the agent finishes
|
|
195
192
|
* implementing the delivered spec.
|
|
196
193
|
*
|
|
197
|
-
* The ending comes from `TaskRunner.run
|
|
198
|
-
* file's front matter —
|
|
199
|
-
* just done, and one that could not tell a cancel from a failure.
|
|
194
|
+
* The ending in the result comes from `TaskRunner.run` itself, not from re-reading
|
|
195
|
+
* the task file's front matter — which cannot tell a cancel from a failure.
|
|
200
196
|
*/
|
|
201
197
|
export declare function runSingleTask(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, opts?: RunSingleTaskOptions): Promise<RunSingleTaskResult>;
|
|
202
198
|
/**
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* /task-cancel cancel the running task (soft-terminal — still resumable)
|
|
13
13
|
*
|
|
14
14
|
* The orchestrator persists after every phase boundary to
|
|
15
|
-
* <cwd>/.pi-tasks/TASK_NNNN.md.
|
|
16
|
-
* ctx.ui
|
|
15
|
+
* <cwd>/.pi-tasks/TASK_NNNN.md. User interaction during phases runs through the
|
|
16
|
+
* SessionUI bridge — a local ctx.ui dialog raced against a remote browser card —
|
|
17
|
+
* never through the main conversation, which only receives the final spec.
|
|
17
18
|
*/
|
|
18
19
|
import * as fsp from 'node:fs/promises';
|
|
19
20
|
import * as path from 'node:path';
|
|
@@ -25,6 +26,7 @@ import { readTextFile } from '../shared/fs-text.js';
|
|
|
25
26
|
import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile } from './task-io.js';
|
|
26
27
|
import { startWidget } from './widget.js';
|
|
27
28
|
import { armImplWidget, disarmImplWidget, setupImplWidget } from './impl-widget.js';
|
|
29
|
+
import { armImplementationGuard, disarmImplementationGuard } from './implementation-guards.js';
|
|
28
30
|
import { publishViewer, publishNotify, registerBridgeCommand, getBridge } from '../remote/bridge.js';
|
|
29
31
|
import { pushNotify } from '../remote/push.js';
|
|
30
32
|
import { getConfig } from '../config/config.js';
|
|
@@ -61,11 +63,12 @@ let piApi = null;
|
|
|
61
63
|
/**
|
|
62
64
|
* The live session's thinking level, as a {@link ThinkingControl}.
|
|
63
65
|
*
|
|
64
|
-
* Goes through `piApi` rather than the command ctx because
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* cannot move the level should
|
|
66
|
+
* Goes through `piApi` rather than the command ctx because `ExtensionContext`
|
|
67
|
+
* (which the command ctx extends) carries `thinkingLevel` as a plain optional
|
|
68
|
+
* VALUE with no setter; `setThinkingLevel` lives on `ExtensionAPI`. Before
|
|
69
|
+
* `registerTask(pi)` has run there is nothing to control, so this degrades to a
|
|
70
|
+
* no-op pair rather than throwing — a task that cannot move the level should
|
|
71
|
+
* still run.
|
|
69
72
|
*/
|
|
70
73
|
function piThinkingControl() {
|
|
71
74
|
const api = piApi;
|
|
@@ -93,9 +96,9 @@ export class TaskRunner {
|
|
|
93
96
|
_deps;
|
|
94
97
|
_pc;
|
|
95
98
|
/**
|
|
96
|
-
* Per-phase wall-clock durations collected during the run
|
|
97
|
-
* `## phase timings` section on
|
|
98
|
-
*
|
|
99
|
+
* Per-phase wall-clock durations collected during the run, written to the
|
|
100
|
+
* `## phase timings` section — on completion, and again from the catch so a
|
|
101
|
+
* failed run still records what it got through. Each top-level entry is a
|
|
99
102
|
* phase (refine/research/grill/compose/critique); children are optional
|
|
100
103
|
* sub-step splits the phase chose to record via deps.recordSubStep.
|
|
101
104
|
*/
|
|
@@ -113,8 +116,8 @@ export class TaskRunner {
|
|
|
113
116
|
this._fixInstruction = opts.fixInstruction;
|
|
114
117
|
this._implAwaited = opts.implAwaited ?? false;
|
|
115
118
|
this._startedAt = Date.now();
|
|
116
|
-
//
|
|
117
|
-
//
|
|
119
|
+
// Placeholder: id/title/phase are only known once run() has allocated or
|
|
120
|
+
// read the task file.
|
|
118
121
|
this._widgetState = {
|
|
119
122
|
taskId: '',
|
|
120
123
|
title: '',
|
|
@@ -137,7 +140,7 @@ export class TaskRunner {
|
|
|
137
140
|
},
|
|
138
141
|
// Handed DOWN so the child's own snapshot carries a window and the
|
|
139
142
|
// StallDetector's churn rule can arm; resolveContextUsage below stays
|
|
140
|
-
// as the fallback for a child that still reports none
|
|
143
|
+
// as the fallback for a child that still reports none.
|
|
141
144
|
contextWindow: parentContextWindow,
|
|
142
145
|
onContextUsage: snapshot => {
|
|
143
146
|
this._widgetState.contextUsage = resolveContextUsage(snapshot, this._widgetState.contextUsage, parentContextWindow);
|
|
@@ -175,16 +178,15 @@ export class TaskRunner {
|
|
|
175
178
|
cancel() {
|
|
176
179
|
this._abort.abort();
|
|
177
180
|
}
|
|
178
|
-
/** Execute the full task lifecycle. Mid-run input holds instead of starting
|
|
179
|
-
* a competing turn for the whole of it, and the terminal interception is
|
|
180
|
-
* armed for the same window (`withRun`); nested inside `runGatedTask` or
|
|
181
|
-
* the `/task-auto` loop the bracket refcounts, so this changes nothing there
|
|
182
|
-
* and covers the fire-and-forget `runSingleTask` path on its own. */
|
|
183
181
|
/**
|
|
184
|
-
* Run the task and NAME how it ended
|
|
182
|
+
* Run the task and NAME how it ended (`RunEnd`), so no caller has to re-read
|
|
183
|
+
* the task file to find out.
|
|
185
184
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
185
|
+
* Mid-run input holds instead of starting a competing turn for the whole of
|
|
186
|
+
* it, and the terminal interception is armed for the same window (`withRun`);
|
|
187
|
+
* nested inside `runGatedTask` or the `/task-auto` loop the bracket refcounts,
|
|
188
|
+
* so this changes nothing there and covers the fire-and-forget
|
|
189
|
+
* `runSingleTask` path on its own.
|
|
188
190
|
*/
|
|
189
191
|
async run() {
|
|
190
192
|
return withRun(this._ctx, {}, () => this._run());
|
|
@@ -226,11 +228,12 @@ export class TaskRunner {
|
|
|
226
228
|
await this._onStart(id);
|
|
227
229
|
// Wire up per-task debug log (<cwd>/.pi-tasks/TASK_XXXX-debug.log).
|
|
228
230
|
const debugLogPath = path.join(tasksDir(cwd), `${id}-debug.log`);
|
|
229
|
-
//
|
|
230
|
-
// short-
|
|
231
|
-
// A caller-supplied `logDebug` seam WINS
|
|
232
|
-
// the
|
|
233
|
-
// sets one, so the file writer is
|
|
231
|
+
// `gateDebugWriter` returns undefined at level `off`, so every
|
|
232
|
+
// `logDebug?.(…)` site downstream short-circuits before it formats a string
|
|
233
|
+
// and the file is never created. A caller-supplied `logDebug` seam WINS
|
|
234
|
+
// (`??=`): it is the only way to observe the trail decisions from a
|
|
235
|
+
// runner-driven test, and production never sets one, so the file writer is
|
|
236
|
+
// unaffected.
|
|
234
237
|
this._deps.logDebug ??= gateDebugWriter((msg) => {
|
|
235
238
|
const line = `${new Date().toISOString()} ${msg}\n`;
|
|
236
239
|
fsp.appendFile(debugLogPath, line).catch(() => {
|
|
@@ -295,9 +298,9 @@ export class TaskRunner {
|
|
|
295
298
|
await postCommitPhase(phase, this._deps, this._pc, out);
|
|
296
299
|
// SAFE CHECKPOINT (phase boundary): this phase's output is on disk
|
|
297
300
|
// and the next `advance()` has not moved front-matter forward, so a
|
|
298
|
-
// resume re-enters at exactly this phase. Without
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
+
// resume re-enters at exactly this phase. Without a checkpoint here
|
|
302
|
+
// the whole refine→critique pipeline runs to completion after a
|
|
303
|
+
// cancel is requested.
|
|
301
304
|
// Throwing USER_CANCELLED reuses the existing cancellation path:
|
|
302
305
|
// handleFailure leaves the task resumable and /task-auto's catch
|
|
303
306
|
// announces the resume hint.
|
|
@@ -362,12 +365,24 @@ export class TaskRunner {
|
|
|
362
365
|
};
|
|
363
366
|
if (this._sendSpec) {
|
|
364
367
|
armImplWidget(meta, { oneShot: !this._implAwaited });
|
|
368
|
+
// Same lifetime as the widget, and for the same reason: an awaited run
|
|
369
|
+
// spans resume and steer turns, a fire-and-forget one does not.
|
|
370
|
+
armImplementationGuard({ oneShot: !this._implAwaited });
|
|
371
|
+
let delivered = false;
|
|
365
372
|
try {
|
|
366
373
|
await this._sendSpec(spec);
|
|
374
|
+
delivered = true;
|
|
367
375
|
}
|
|
368
376
|
finally {
|
|
369
|
-
|
|
377
|
+
// Arming precedes delivery because the turn can begin inside it. So a
|
|
378
|
+
// delivery that THREW leaves a guard watching a turn that will never
|
|
379
|
+
// run: pi's `prompt` rejects on a compaction already in progress, a
|
|
380
|
+
// missing model, or a failed auth. The next unrelated turn would
|
|
381
|
+
// inherit it, and this guard can end a turn outright.
|
|
382
|
+
if (this._implAwaited || !delivered) {
|
|
370
383
|
disarmImplWidget();
|
|
384
|
+
disarmImplementationGuard();
|
|
385
|
+
}
|
|
371
386
|
}
|
|
372
387
|
return;
|
|
373
388
|
}
|
|
@@ -375,11 +390,24 @@ export class TaskRunner {
|
|
|
375
390
|
throw new Error('extension not initialised (no ExtensionAPI captured)');
|
|
376
391
|
}
|
|
377
392
|
armImplWidget(meta, { oneShot: true });
|
|
378
|
-
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
393
|
+
armImplementationGuard({ oneShot: true });
|
|
394
|
+
// Same reason as the awaited path's `delivered` flag: this send can throw
|
|
395
|
+
// SYNCHRONOUSLY — the loader gates every ExtensionAPI action behind
|
|
396
|
+
// `assertActive()` — and a guard left armed over a turn that never starts
|
|
397
|
+
// is inherited by the next one, which it can terminate.
|
|
398
|
+
// Always name a delivery mode. pi's `prompt()` consults `streamingBehavior`
|
|
399
|
+
// only inside `if (this.isStreaming)`, so naming one is inert on an idle
|
|
400
|
+
// session and queues on a busy one — correct in both cases, where an
|
|
401
|
+
// isIdle() check is a check-then-act race that loses to any turn starting
|
|
402
|
+
// in between.
|
|
403
|
+
try {
|
|
404
|
+
piApi.sendUserMessage(spec, { deliverAs: 'followUp' });
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
disarmImplWidget();
|
|
408
|
+
disarmImplementationGuard();
|
|
409
|
+
throw e;
|
|
410
|
+
}
|
|
383
411
|
}
|
|
384
412
|
/**
|
|
385
413
|
* The spec as the implementer should receive it (Layer B). Layer A strips phantom
|
|
@@ -418,9 +446,8 @@ export class TaskRunner {
|
|
|
418
446
|
* deliver its spec. With waitForImplementation, block until the agent finishes
|
|
419
447
|
* implementing the delivered spec.
|
|
420
448
|
*
|
|
421
|
-
* The ending comes from `TaskRunner.run
|
|
422
|
-
* file's front matter —
|
|
423
|
-
* just done, and one that could not tell a cancel from a failure.
|
|
449
|
+
* The ending in the result comes from `TaskRunner.run` itself, not from re-reading
|
|
450
|
+
* the task file's front matter — which cannot tell a cancel from a failure.
|
|
424
451
|
*/
|
|
425
452
|
export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
426
453
|
let taskId = '';
|
|
@@ -441,10 +468,12 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
441
468
|
withSession: async (newCtx) => {
|
|
442
469
|
freshCtx = newCtx;
|
|
443
470
|
getBridge().currentCtx = newCtx; // keep remote dispatch ctx fresh across session replacement
|
|
444
|
-
// Same reason, for the terminal: pi
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
471
|
+
// Same reason, for the terminal: pi wires
|
|
472
|
+
// `setBeforeSessionInvalidate(() => this.resetExtensionUI())`, and
|
|
473
|
+
// `resetExtensionUI` calls `clearExtensionTerminalInputListeners()`.
|
|
474
|
+
// Every extension listener dies at the START of each task — precisely
|
|
475
|
+
// the window a typed /task-auto-cancel has to survive. No-op unless a
|
|
476
|
+
// run armed one.
|
|
448
477
|
rearmCancelListener(newCtx);
|
|
449
478
|
const runner = new TaskRunner({
|
|
450
479
|
ctx: newCtx,
|
|
@@ -460,7 +489,8 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
460
489
|
// here rather than at either call site.
|
|
461
490
|
const release = holdImplementationThinking(opts.thinkingControl ?? piThinkingControl());
|
|
462
491
|
try {
|
|
463
|
-
// Queue-or-run:
|
|
492
|
+
// Queue-or-run: naming a delivery mode means pi's
|
|
493
|
+
// "Agent is already processing" throw is unreachable here.
|
|
464
494
|
await newCtx.sendUserMessage(spec, { deliverAs: 'followUp' });
|
|
465
495
|
if (opts.waitForImplementation) {
|
|
466
496
|
await newCtx.waitForIdle();
|
|
@@ -502,12 +532,9 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
502
532
|
// delivered and the task file already reads `completed`:
|
|
503
533
|
//
|
|
504
534
|
// • the user interrupted and declined to steer — a pause, not a fault;
|
|
505
|
-
// • the implementation turn died with stopReason "error"
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
// Both used to reach the caller as extra fields beside a boolean, and both are
|
|
510
|
-
// endings.
|
|
535
|
+
// • the implementation turn died with stopReason "error", which must stop
|
|
536
|
+
// /task-auto here rather than let it commit and advance on a file that
|
|
537
|
+
// says `completed`.
|
|
511
538
|
let end = runEnd;
|
|
512
539
|
if (end.kind === 'completed' && interrupted)
|
|
513
540
|
end = { kind: 'interrupted' };
|
|
@@ -536,9 +563,9 @@ export const gateRunTask = (c, cwd, t, opts) => runSingleTask(c, cwd, t, {
|
|
|
536
563
|
// NO `seams` here, deliberately. Threading them would need a field on
|
|
537
564
|
// `GateParams` and another on `GateDeps`, and nothing — production or
|
|
538
565
|
// test — would set either: the gate is reached through two orchestrators
|
|
539
|
-
// that build their params from a task file.
|
|
540
|
-
//
|
|
541
|
-
//
|
|
566
|
+
// that build their params from a task file. `WorkerOutcome.reason` is what
|
|
567
|
+
// that costs: written at every `workerUnavailable` call site and read
|
|
568
|
+
// nowhere. So this stays unplumbed until a test actually needs it.
|
|
542
569
|
});
|
|
543
570
|
/**
|
|
544
571
|
* Demote a task file to a resumable state after a gate (or its implementation)
|
|
@@ -567,15 +594,13 @@ export async function markResumable(cwd, taskId) {
|
|
|
567
594
|
export async function runGatedTask(ctx, cwd, raw, opts = {}) {
|
|
568
595
|
// The GATES are part of the run, and they are child processes with the host
|
|
569
596
|
// session idle — the same hold window as the spec phases. Bracketing only
|
|
570
|
-
// TaskRunner would leave verify/enforce
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
// live). No onCancel: a typed /task-auto-cancel goes through the generic
|
|
578
|
-
// bridge dispatch here.
|
|
597
|
+
// TaskRunner would leave verify/enforce reading as "no run" while the widget
|
|
598
|
+
// still says "verifying work". The body has many early returns, so the bracket
|
|
599
|
+
// lives in this wrapper rather than in a dozen places. The same bracket arms
|
|
600
|
+
// the raw-stdin interception for the WHOLE run: without it a line typed during
|
|
601
|
+
// a plain /task lands in pi's `pendingUserInputs` and fires after the run. No
|
|
602
|
+
// onCancel: a typed /task-auto-cancel goes through the generic bridge dispatch
|
|
603
|
+
// here.
|
|
579
604
|
await withRun(ctx, {}, () => runGatedTaskInner(ctx, cwd, raw, opts));
|
|
580
605
|
}
|
|
581
606
|
async function runGatedTaskInner(ctx, cwd, raw, opts = {}) {
|
|
@@ -595,10 +620,8 @@ async function runGatedTaskInner(ctx, cwd, raw, opts = {}) {
|
|
|
595
620
|
const res = await deps.runTask(active, cwd, raw, { resumeId: opts.resumeId });
|
|
596
621
|
active = res.ctx ?? active;
|
|
597
622
|
const tag = res.taskId || 'Task';
|
|
598
|
-
// One dispatch over the named ending
|
|
599
|
-
//
|
|
600
|
-
// `cancelled` with `failed` and told the user their task had stopped and needed
|
|
601
|
-
// fixing. Resumability comes from RUN_END_POLICY; the wording stays here,
|
|
623
|
+
// One dispatch over the named ending, so a cancel can never be reported as a
|
|
624
|
+
// failure. Resumability comes from RUN_END_POLICY; the wording stays here,
|
|
602
625
|
// because `/task-auto` says `/task-auto-resume` where this says `/task-resume`.
|
|
603
626
|
if (!runSucceeded(res.end)) {
|
|
604
627
|
const policy = RUN_END_POLICY[res.end.kind];
|
|
@@ -656,7 +679,7 @@ async function handleTask(args, ctx) {
|
|
|
656
679
|
// When a gate is enabled, /task awaits the implementation and runs the same
|
|
657
680
|
// verify + enforce gates a /task-auto sub-task does. With both gates off
|
|
658
681
|
// (the default), /task stays fire-and-forget: hand the spec to the main
|
|
659
|
-
// conversation and return immediately
|
|
682
|
+
// conversation and return immediately.
|
|
660
683
|
const cfg = getConfig();
|
|
661
684
|
if (cfg.verifyWork || cfg.enforceGuidelines) {
|
|
662
685
|
await runGatedTask(ctx, cwd, raw);
|