@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
|
@@ -2,41 +2,43 @@
|
|
|
2
2
|
* git-state-guard — deterministic repo-state snapshot/reconcile around the
|
|
3
3
|
* read-only gate children (verify, recommend).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* running `eslint --fix .` mid-verification and ad-hoc DDL against the test DB.
|
|
5
|
+
* Those children hold a `read,bash` contract whose "never modify the tree" clause
|
|
6
|
+
* is PROMPT-LEVEL ONLY: `bash` can stash, check out, commit and rewrite files, and
|
|
7
|
+
* nothing but the prompt says not to. A child that stashes the task's uncommitted
|
|
8
|
+
* work and never pops it leaves the verify judging an empty tree AND leaves an
|
|
9
|
+
* orphan stash that only detonates when something later pops it onto a moved HEAD.
|
|
10
|
+
* So this is capability-shaped rather than prompt-shaped: snapshot the repo state
|
|
11
|
+
* BEFORE the child runs, deterministically restore whatever it moved afterwards,
|
|
12
|
+
* no model in the loop.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* - The WORKTREE CONTENT as a git tree object, built through a
|
|
24
|
-
* (`read-tree --empty` + `add -A` + `write-tree`, excluding
|
|
25
|
-
* gate's own debug logs land there DURING the run).
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
14
|
+
* What is captured and reconciled — each one run against a real repo:
|
|
15
|
+
* - HEAD (sha + symbolic branch ref). A child that checked out another commit,
|
|
16
|
+
* detached, is put back on its branch AND its sha. LIMIT: a child that COMMITS
|
|
17
|
+
* on the current branch is NOT undone — the restore checks out
|
|
18
|
+
* `before.branchRef`, and that branch now points at the child's commit, so the
|
|
19
|
+
* `checked HEAD back out to <branch>` line is honest about the ref and says
|
|
20
|
+
* nothing about the sha. The move is still classed verdict-tainting, so the
|
|
21
|
+
* verdict is discarded; the commit stays.
|
|
22
|
+
* - The WORKTREE CONTENT as a git tree object, built through a THROWAWAY index
|
|
23
|
+
* (`read-tree --empty` + `add -A` + `write-tree`, excluding `.pi-tasks` — the
|
|
24
|
+
* gate's own debug logs land there DURING the run). Measured on a repo with a
|
|
25
|
+
* staged file, an untracked file, a gitignored file and a `.pi-tasks/` log: the
|
|
26
|
+
* tree holds the tracked and untracked files and neither of the other two, and
|
|
27
|
+
* the REAL index still shows the same staged path afterwards. Restoration
|
|
28
|
+
* re-materialises every changed or deleted file and deletes what the child
|
|
29
|
+
* created.
|
|
29
30
|
* - The STASH ref: entries the child pushed are dropped AFTER the worktree is
|
|
30
|
-
* restored from the snapshot
|
|
31
|
-
* truth
|
|
31
|
+
* restored from the snapshot — the snapshot, not the stash, is the source of
|
|
32
|
+
* truth — so no orphan stash survives the reconcile.
|
|
32
33
|
*
|
|
33
34
|
* The real index's staging state is deliberately NOT restored: pre-commit gate
|
|
34
35
|
* children run against a tree whose work is unstaged, and the auto-commit that
|
|
35
|
-
* follows re-stages everything with `add -A`
|
|
36
|
+
* follows re-stages everything with `git add -A` (auto-commit.ts:192).
|
|
36
37
|
*
|
|
37
|
-
* Everything is best-effort: a repo where git
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* Everything is best-effort: a repo where git fails disables the guard — capture
|
|
39
|
+
* returns `ok: false` and reconcile no-ops. Measured: a fresh `git init` with no
|
|
40
|
+
* commits answers non-zero to `rev-parse -q --verify HEAD`, which is the unborn-HEAD
|
|
41
|
+
* case the capture bails on.
|
|
40
42
|
*/
|
|
41
43
|
import { readFileSync } from 'node:fs';
|
|
42
44
|
import * as fsp from 'node:fs/promises';
|
|
@@ -48,29 +50,30 @@ import { isRegenerableArtifact } from './regenerable-artifacts.js';
|
|
|
48
50
|
const EXCLUDE_TASKS_DIR = ':(exclude).pi-tasks';
|
|
49
51
|
/**
|
|
50
52
|
* Untracked paths that are regenerable test/build OUTPUT, not graded source. A gate
|
|
51
|
-
* child creating or rewriting one of these has not mutated the work under
|
|
52
|
-
* so its verdict stands. Gitignored files never reach the snapshot
|
|
53
|
-
* them
|
|
54
|
-
* `test-results/` and `playwright-report/` above all
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* child creating or rewriting one of these has not mutated the work under
|
|
54
|
+
* judgement, so its verdict stands. Gitignored files never reach the snapshot at
|
|
55
|
+
* all — measured, `add -A` skips them — so this list is for the ones a typical
|
|
56
|
+
* project leaves UNIGNORED, `test-results/` and `playwright-report/` above all.
|
|
57
|
+
* Kept deliberately narrow: anything NOT matched here that a child modifies or
|
|
58
|
+
* deletes is graded state, and taints the verdict.
|
|
57
59
|
*
|
|
58
|
-
* The list
|
|
59
|
-
*
|
|
60
|
-
*
|
|
60
|
+
* The list lives in `regenerable-artifacts.ts` because three call sites need the
|
|
61
|
+
* same knowledge — this guard, the write-guard's deletion check, and the per-task
|
|
62
|
+
* commit — and three private copies would drift.
|
|
61
63
|
*/
|
|
62
64
|
const isBenignArtifact = isRegenerableArtifact;
|
|
63
65
|
/**
|
|
64
|
-
* Regenerable machine state that is benign EVEN WHEN TRACKED
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* DELIBERATELY narrow: snapshot BASELINE images (`*-snapshots/*.png`) are NOT here —
|
|
66
|
+
* Regenerable machine state that is benign EVEN WHEN TRACKED, so a project that
|
|
67
|
+
* commits it does not have a child's incidental rewrite discard the verdict. Two
|
|
68
|
+
* classes: the component-test build cache under `ctCacheDir`, which a component
|
|
69
|
+
* test run rewrites every time, and the test runner's `.last-run.json` run-state
|
|
70
|
+
* file.
|
|
71
|
+
*
|
|
72
|
+
* DELIBERATELY narrow. Snapshot BASELINE images (`*-snapshots/*.png`) are NOT here:
|
|
72
73
|
* a child that rewrites a baseline to make a screenshot test pass is the real
|
|
73
|
-
* mutate-to-pass catch
|
|
74
|
+
* mutate-to-pass catch. Measured on a real repo — a tracked file under a custom
|
|
75
|
+
* `ctCacheDir` and a tracked `.last-run.json` both restore WITHOUT tainting, while
|
|
76
|
+
* a tracked `tests/a-snapshots/x.png` taints.
|
|
74
77
|
*/
|
|
75
78
|
const ALWAYS_REGENERABLE_PATTERNS = [/(?:^|\/)\.last-run\.json$/];
|
|
76
79
|
/** Playwright config files that may declare a custom `ctCacheDir`. */
|
|
@@ -80,7 +83,10 @@ const CT_CONFIG_FILES = [
|
|
|
80
83
|
'playwright.config.ts',
|
|
81
84
|
'playwright.config.js'
|
|
82
85
|
];
|
|
83
|
-
/** ctCacheDir
|
|
86
|
+
/** The ctCacheDir values assumed when no config declares one. Playwright is not a
|
|
87
|
+
* dependency here, so these are not verifiable against an installed package —
|
|
88
|
+
* what IS verified is that a config declaring `ctCacheDir: './custom-cache/'` is
|
|
89
|
+
* parsed and its directory treated as regenerable. */
|
|
84
90
|
const DEFAULT_CT_CACHE_DIRS = ['.playwright-cache', 'playwright/.cache'];
|
|
85
91
|
/**
|
|
86
92
|
* The component-test cache dir(s) for this project: the `ctCacheDir` any Playwright
|
|
@@ -183,14 +189,15 @@ function pushCapped(actions, verb, paths) {
|
|
|
183
189
|
}
|
|
184
190
|
/**
|
|
185
191
|
* Restore every file recorded in `beforeTree` (content + deletions) and remove
|
|
186
|
-
* files that exist in `afterTree` but not in `beforeTree`
|
|
187
|
-
* created
|
|
192
|
+
* files that exist in `afterTree` but not in `beforeTree` — the ones the child
|
|
193
|
+
* created. Uses a throwaway index seeded from the snapshot tree; `checkout-index
|
|
188
194
|
* -a -f` re-materialises the snapshot verbatim.
|
|
189
195
|
*
|
|
190
|
-
* Returns whether any restored change was *verdict-tainting
|
|
191
|
-
* path that is tracked-in-HEAD or an untracked non-artifact (see
|
|
192
|
-
* Creations and test-runner-artifact churn restore identically
|
|
193
|
-
* Each changed path is itemised
|
|
196
|
+
* Returns whether any restored change was *verdict-tainting*: a modified or
|
|
197
|
+
* deleted path that is tracked-in-HEAD, or an untracked non-artifact (see
|
|
198
|
+
* isBenignArtifact). Creations and test-runner-artifact churn restore identically
|
|
199
|
+
* but do NOT taint. Each changed path is itemised, capped, so the gate trail says
|
|
200
|
+
* WHICH files moved.
|
|
194
201
|
*/
|
|
195
202
|
async function restoreWorktree(cwd, git, beforeTree, afterTree, tracked, ctCacheDirs, actions) {
|
|
196
203
|
const tmpIndex = path.join(os.tmpdir(), `pi-task-guard-restore-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
@@ -209,8 +216,11 @@ async function restoreWorktree(cwd, git, beforeTree, afterTree, tracked, ctCache
|
|
|
209
216
|
const trimmed = line.trim();
|
|
210
217
|
if (trimmed.length === 0)
|
|
211
218
|
continue;
|
|
212
|
-
// "M\tpath", "A\tpath", "D\tpath", "T\tpath" — no -M, so
|
|
213
|
-
// as a D + an A pair
|
|
219
|
+
// "M\tpath", "A\tpath", "D\tpath", "T\tpath" — no -M, so a rename
|
|
220
|
+
// shows as a D + an A pair and each half is classified on its own
|
|
221
|
+
// merits. Measured: renaming a tracked `a.txt` to `b.txt` yields
|
|
222
|
+
// "removed child-created file b.txt" plus "restored deleted file
|
|
223
|
+
// a.txt", and taints.
|
|
214
224
|
const tab = trimmed.indexOf('\t');
|
|
215
225
|
if (tab < 0)
|
|
216
226
|
continue;
|
|
@@ -225,7 +235,7 @@ async function restoreWorktree(cwd, git, beforeTree, afterTree, tracked, ctCache
|
|
|
225
235
|
|| (isBenignArtifact(name) && !tracked.has(name))) {
|
|
226
236
|
// Regenerable test/build output — not graded work. Either an
|
|
227
237
|
// always-regenerable class (ct cache / run-state, benign even when
|
|
228
|
-
// tracked
|
|
238
|
+
// tracked) or untracked test-runner output.
|
|
229
239
|
artifactChanges.push(name);
|
|
230
240
|
}
|
|
231
241
|
else if (code === 'D') {
|
|
@@ -268,11 +278,11 @@ async function restoreWorktree(cwd, git, beforeTree, afterTree, tracked, ctCache
|
|
|
268
278
|
* whatever a gate child moved. Ordering matters:
|
|
269
279
|
*
|
|
270
280
|
* 1. HEAD first — a child parked on another commit must be back on the original
|
|
271
|
-
* ref before the worktree comparison
|
|
272
|
-
* 2. Worktree content from the snapshot TREE
|
|
273
|
-
* have pushed
|
|
274
|
-
* 3. Child-pushed stash entries are dropped LAST, once the work they swallowed
|
|
275
|
-
*
|
|
281
|
+
* ref before the worktree comparison and restore make sense.
|
|
282
|
+
* 2. Worktree content from the snapshot TREE, never from a stash the child may
|
|
283
|
+
* have pushed: the snapshot is the authoritative "as the child found it".
|
|
284
|
+
* 3. Child-pushed stash entries are dropped LAST, once the work they swallowed is
|
|
285
|
+
* already restored — dropping first would destroy the only other copy.
|
|
276
286
|
*
|
|
277
287
|
* Never throws; failures degrade to actions[] lines so the caller can log them.
|
|
278
288
|
*/
|
|
@@ -308,9 +318,9 @@ export async function reconcileGitState(cwd, before, signal, spawnFn) {
|
|
|
308
318
|
}
|
|
309
319
|
}
|
|
310
320
|
// 3. Stash entries the child pushed. Drop stash@{0} until the ref matches the
|
|
311
|
-
// snapshot again
|
|
312
|
-
//
|
|
313
|
-
// reconstructed
|
|
321
|
+
// snapshot again, bounded at 10 so a runaway cannot loop here. A stash the
|
|
322
|
+
// child POPPED — the ref is gone or no longer contains the snapshot's tip —
|
|
323
|
+
// cannot be reconstructed, so report it instead of guessing.
|
|
314
324
|
const stashNow = async () => {
|
|
315
325
|
const s = await git(['rev-parse', '-q', '--verify', 'refs/stash']);
|
|
316
326
|
return s.exitCode === 0 ? s.stdout.trim() : null;
|
|
@@ -318,7 +328,7 @@ export async function reconcileGitState(cwd, before, signal, spawnFn) {
|
|
|
318
328
|
let stash = await stashNow();
|
|
319
329
|
if (stash !== before.stashSha) {
|
|
320
330
|
// A child that pushed/popped a stash moved graded work in or out of the tree
|
|
321
|
-
// (
|
|
331
|
+
// (a stash-and-abandon) — always verdict-tainting.
|
|
322
332
|
tainted = true;
|
|
323
333
|
if (before.stashSha === null || (await stashContains(git, stash, before.stashSha))) {
|
|
324
334
|
let dropped = 0;
|
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Implementation-turn status widget.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* `TaskRunner._deliverSpec` is preceded by `_disposeWidget()`, so the phase widget
|
|
5
|
+
* (widget.ts) is gone before the spec is handed off. Without this module the host
|
|
6
|
+
* agent's implementation turn — the longest, most visible part of a run — would
|
|
7
|
+
* show only pi's own working indicator, whose default message is the literal
|
|
8
|
+
* `"Working..."`. This keeps the SAME rich status block alive across that turn
|
|
9
|
+
* (task id · implementing/elapsed/context bar · ↳ last tool), driven by the host's
|
|
10
|
+
* own live `ctx.getContextUsage()`, which pi declares as
|
|
11
|
+
* `getContextUsage(): ContextUsage | undefined`.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
13
|
+
* Event-driven rather than poll-wrapped, because the two delivery paths differ:
|
|
11
14
|
*
|
|
12
|
-
* • /task (fire-and-forget): the command returns right after
|
|
13
|
-
* so the host runs the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* the
|
|
15
|
+
* • /task (fire-and-forget): the command returns right after
|
|
16
|
+
* `piApi.sendUserMessage(spec, {deliverAs: 'followUp'})`, so the host runs the
|
|
17
|
+
* impl turn AFTER this code is gone and only an `agent_start` handler can pick
|
|
18
|
+
* it up. Armed one-shot, and `agent_end` disarms it.
|
|
19
|
+
* • /task-auto (awaited): the caller blocks across `waitForIdle` plus any resume
|
|
20
|
+
* or steer turns. Armed sticky — each `agent_start` re-shows the widget,
|
|
21
|
+
* `agent_end` only hides it between turns, and the caller disarms in a
|
|
22
|
+
* `finally` once the whole implementation phase has settled.
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
24
|
+
* `_deliverSpec` picks between them with `oneShot: !this._implAwaited`.
|
|
25
|
+
*
|
|
26
|
+
* A single module-level slot is enough because only one task runs at a time —
|
|
27
|
+
* `orchestrator.ts` keeps one module-level `activeTask`.
|
|
22
28
|
*/
|
|
23
29
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
24
30
|
export interface ImplWidgetMeta {
|
|
@@ -36,5 +42,8 @@ export declare function armImplWidget(meta: ImplWidgetMeta, opts: {
|
|
|
36
42
|
}): void;
|
|
37
43
|
/** Tear down the widget and clear the armed slot (sticky/awaited path). */
|
|
38
44
|
export declare function disarmImplWidget(): void;
|
|
39
|
-
/** Wire the agent-lifecycle handlers that drive the widget. Call once at setup.
|
|
45
|
+
/** Wire the agent-lifecycle handlers that drive the widget. Call once at setup.
|
|
46
|
+
* All three events are pi's own: `agent_start`, `tool_execution_start` and
|
|
47
|
+
* `agent_end` each have an `on()` overload, and ToolExecutionStartEvent carries
|
|
48
|
+
* the `toolName` and `args` read below. */
|
|
40
49
|
export declare function setupImplWidget(pi: ExtensionAPI): void;
|
package/dist/task/impl-widget.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Implementation-turn status widget.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* `TaskRunner._deliverSpec` is preceded by `_disposeWidget()`, so the phase widget
|
|
5
|
+
* (widget.ts) is gone before the spec is handed off. Without this module the host
|
|
6
|
+
* agent's implementation turn — the longest, most visible part of a run — would
|
|
7
|
+
* show only pi's own working indicator, whose default message is the literal
|
|
8
|
+
* `"Working..."`. This keeps the SAME rich status block alive across that turn
|
|
9
|
+
* (task id · implementing/elapsed/context bar · ↳ last tool), driven by the host's
|
|
10
|
+
* own live `ctx.getContextUsage()`, which pi declares as
|
|
11
|
+
* `getContextUsage(): ContextUsage | undefined`.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
13
|
+
* Event-driven rather than poll-wrapped, because the two delivery paths differ:
|
|
11
14
|
*
|
|
12
|
-
* • /task (fire-and-forget): the command returns right after
|
|
13
|
-
* so the host runs the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* the
|
|
15
|
+
* • /task (fire-and-forget): the command returns right after
|
|
16
|
+
* `piApi.sendUserMessage(spec, {deliverAs: 'followUp'})`, so the host runs the
|
|
17
|
+
* impl turn AFTER this code is gone and only an `agent_start` handler can pick
|
|
18
|
+
* it up. Armed one-shot, and `agent_end` disarms it.
|
|
19
|
+
* • /task-auto (awaited): the caller blocks across `waitForIdle` plus any resume
|
|
20
|
+
* or steer turns. Armed sticky — each `agent_start` re-shows the widget,
|
|
21
|
+
* `agent_end` only hides it between turns, and the caller disarms in a
|
|
22
|
+
* `finally` once the whole implementation phase has settled.
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
24
|
+
* `_deliverSpec` picks between them with `oneShot: !this._implAwaited`.
|
|
25
|
+
*
|
|
26
|
+
* A single module-level slot is enough because only one task runs at a time —
|
|
27
|
+
* `orchestrator.ts` keeps one module-level `activeTask`.
|
|
22
28
|
*/
|
|
23
29
|
import { WIDGET_KEY, WIDGET_REFRESH_MS, buildImplLines, buildImplData } from './widget.js';
|
|
24
30
|
import { setTaskWidget } from '../remote/session-state.js';
|
|
@@ -27,7 +33,8 @@ let lastLine;
|
|
|
27
33
|
let timer = null;
|
|
28
34
|
let activeCtx = null;
|
|
29
35
|
/** Map the host's live context usage to the widget snapshot, or undefined when the
|
|
30
|
-
* token count is unknown
|
|
36
|
+
* token count is unknown. pi types `ContextUsage.tokens` as `number | null` and
|
|
37
|
+
* documents the null as "right after compaction, before next LLM response". */
|
|
31
38
|
function snapshot(ctx) {
|
|
32
39
|
const u = ctx.getContextUsage?.();
|
|
33
40
|
if (!u || u.tokens == null)
|
|
@@ -100,7 +107,10 @@ export function disarmImplWidget() {
|
|
|
100
107
|
activeCtx = null;
|
|
101
108
|
lastLine = undefined;
|
|
102
109
|
}
|
|
103
|
-
/** Wire the agent-lifecycle handlers that drive the widget. Call once at setup.
|
|
110
|
+
/** Wire the agent-lifecycle handlers that drive the widget. Call once at setup.
|
|
111
|
+
* All three events are pi's own: `agent_start`, `tool_execution_start` and
|
|
112
|
+
* `agent_end` each have an `on()` overload, and ToolExecutionStartEvent carries
|
|
113
|
+
* the `toolName` and `args` read below. */
|
|
104
114
|
export function setupImplWidget(pi) {
|
|
105
115
|
pi.on('agent_start', (_event, ctx) => {
|
|
106
116
|
if (!armed)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
/** `oneShot` mirrors the impl widget's split: fire-and-forget lets the settle
|
|
3
|
+
* event disarm; an awaited run spans resume/steer turns and disarms in its finally. */
|
|
4
|
+
export declare function armImplementationGuard(opts: {
|
|
5
|
+
oneShot: boolean;
|
|
6
|
+
}): void;
|
|
7
|
+
export declare function disarmImplementationGuard(): void;
|
|
8
|
+
/** @internal Test seam: is a turn currently guarded? */
|
|
9
|
+
export declare function implementationGuardArmed(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Present tense, unlike `formatLoopHint`, which addresses a re-spawned child
|
|
12
|
+
* about an attempt that does not exist here.
|
|
13
|
+
*
|
|
14
|
+
* It claims nothing about the call's RESULT, which this hook fires too early to
|
|
15
|
+
* see, and it does not offer "change the call" — that is an escape, not advice:
|
|
16
|
+
* one altered byte is a new key and a clean slate on both counters.
|
|
17
|
+
*/
|
|
18
|
+
export declare function blockedCallReason(toolName: string, count: number): string;
|
|
19
|
+
/** The reason on the final block, which also ends the turn. */
|
|
20
|
+
export declare function terminalCallReason(): string;
|
|
21
|
+
export declare function consumeGuardTermination(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Inert until armed. Registering ANY `tool_call` handler switches on pi's
|
|
24
|
+
* `beforeToolCall` for every call in the session, so the armed check comes first.
|
|
25
|
+
*/
|
|
26
|
+
export declare function registerImplementationGuards(pi: ExtensionAPI): void;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { LoopDetector, loopKey, LOOP_THRESHOLD, LOOP_WINDOW, MAX_LOOP_RESTARTS } from './loop-detector.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runaway guard for the IMPLEMENTATION TURN — the one model surface with none.
|
|
4
|
+
*
|
|
5
|
+
* MEASURED: one turn ran 5h16m and 6,760 tool calls, alternating two
|
|
6
|
+
* byte-identical bash commands 3,300 times each with its output frozen and zero
|
|
7
|
+
* edits. The command watchdog is per call (each took ~2.5s), the stream was never
|
|
8
|
+
* silent, and MAX_COMPACTION_RESUMES counts only compactions that PARK at idle,
|
|
9
|
+
* while all 18 of these were inside the turn. The two detectors are built only in
|
|
10
|
+
* the CHILD spawn paths, and this turn runs in the user's own session.
|
|
11
|
+
*
|
|
12
|
+
* IT BLOCKS RATHER THAN KILLS because there is no re-spawn here — the argument
|
|
13
|
+
* single-read-extension.ts already makes: "detect-and-kill only re-spawns a model
|
|
14
|
+
* that deterministically re-thrashes". pi's ctx.abort() would also empty the
|
|
15
|
+
* queued-message list into the user's editor. That also raises the bar on false
|
|
16
|
+
* positives: a `gate` child killed by mistake costs one attempt of three, this
|
|
17
|
+
* costs the user their turn.
|
|
18
|
+
*
|
|
19
|
+
* WHY PROGRESS IS READ OFF THE CALL, NOT THE RESULT. The gate profile pairs its
|
|
20
|
+
* LoopDetector with a StallDetector, which judges results. That cannot work here:
|
|
21
|
+
* pi's edit tool returns the constant `Successfully replaced N block(s) in <path>.`
|
|
22
|
+
* and puts the diff in `details`, not `content`, so every real edit to one file is
|
|
23
|
+
* byte-identical result text and scores as dead ground. An edit's ARGUMENTS carry
|
|
24
|
+
* the progress its result throws away, so an edit is what resets the window.
|
|
25
|
+
*
|
|
26
|
+
* Known blind spots, all deliberate: a bash-driven mutation (`sed -i`, `>`,
|
|
27
|
+
* `git apply`) does not reset; one varying `write` per iteration buys unlimited
|
|
28
|
+
* immunity; a repeat cycle of LOOP_WINDOW/LOOP_THRESHOLD or longer never fills the
|
|
29
|
+
* window; polling a booting server with an identical curl trips at five; and
|
|
30
|
+
* schema-invalid calls never reach this hook at all, since pi validates first.
|
|
31
|
+
*/
|
|
32
|
+
/** Tool names that mutate the tree. pi ships exactly seven core tools, and
|
|
33
|
+
* pi-task's own four (pi-worker, -search, -fetch, -docs) are all read-only. */
|
|
34
|
+
const MUTATING_TOOLS = new Set(['edit', 'write']);
|
|
35
|
+
/**
|
|
36
|
+
* Path-revisit is OFF for both detectors (that is the Infinity). MEASURED: at the
|
|
37
|
+
* default threshold, six DISTINCT edits to one file trip the path rule, because
|
|
38
|
+
* an edit names a `file_path` and no `limit`, so the first one sets the
|
|
39
|
+
* high-water mark and every later one scores as already-covered ground. Six edits
|
|
40
|
+
* to one file is the most ordinary thing an implementation turn does. mx5
|
|
41
|
+
* TASK_0002 is the same lesson from the other side: the rule killed an enforce
|
|
42
|
+
* child that was editing one file as its job.
|
|
43
|
+
*/
|
|
44
|
+
function freshDetector() {
|
|
45
|
+
return new LoopDetector(LOOP_WINDOW, LOOP_THRESHOLD, Number.POSITIVE_INFINITY);
|
|
46
|
+
}
|
|
47
|
+
/** The armed turn's state, or null outside one. One slot: one task runs at a time. */
|
|
48
|
+
let armed = null;
|
|
49
|
+
/** Built, never spread from the previous state: a leaked `terminating` would
|
|
50
|
+
* block every call for the rest of an awaited run. */
|
|
51
|
+
function freshArmedState(oneShot) {
|
|
52
|
+
return {
|
|
53
|
+
loop: freshDetector(),
|
|
54
|
+
edits: freshDetector(),
|
|
55
|
+
strikes: new Map(),
|
|
56
|
+
terminating: false,
|
|
57
|
+
oneShot
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** `oneShot` mirrors the impl widget's split: fire-and-forget lets the settle
|
|
61
|
+
* event disarm; an awaited run spans resume/steer turns and disarms in its finally. */
|
|
62
|
+
export function armImplementationGuard(opts) {
|
|
63
|
+
armed = freshArmedState(opts.oneShot);
|
|
64
|
+
}
|
|
65
|
+
export function disarmImplementationGuard() {
|
|
66
|
+
armed = null;
|
|
67
|
+
}
|
|
68
|
+
/** @internal Test seam: is a turn currently guarded? */
|
|
69
|
+
export function implementationGuardArmed() {
|
|
70
|
+
return armed !== null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Present tense, unlike `formatLoopHint`, which addresses a re-spawned child
|
|
74
|
+
* about an attempt that does not exist here.
|
|
75
|
+
*
|
|
76
|
+
* It claims nothing about the call's RESULT, which this hook fires too early to
|
|
77
|
+
* see, and it does not offer "change the call" — that is an escape, not advice:
|
|
78
|
+
* one altered byte is a new key and a clean slate on both counters.
|
|
79
|
+
*/
|
|
80
|
+
export function blockedCallReason(toolName, count) {
|
|
81
|
+
return (`Blocked: this is the ${count}th identical ${toolName} call in this turn. `
|
|
82
|
+
+ `Use what you already have, or do something different, then continue the task.`);
|
|
83
|
+
}
|
|
84
|
+
/** The reason on the final block, which also ends the turn. */
|
|
85
|
+
export function terminalCallReason() {
|
|
86
|
+
return (`Blocked: this turn repeated one call past every warning, so it is being stopped `
|
|
87
|
+
+ `here. Nothing further will run.`);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* One-shot: the guard ended a turn, and nothing in the session state says so.
|
|
91
|
+
*
|
|
92
|
+
* `terminate` lets the agent loop finish normally — the last assistant message
|
|
93
|
+
* keeps `stopReason: "toolUse"`, so `classifyTurnEnd` reads `'stop'` and the run
|
|
94
|
+
* reports a clean finish over work that was cut off mid-task. Verified against a
|
|
95
|
+
* live model: a real guard-terminated turn ends exactly that way. Same shape as
|
|
96
|
+
* `consumeWatchdogAbort`, and consumed for the same reason — one reader, then it
|
|
97
|
+
* is gone.
|
|
98
|
+
*/
|
|
99
|
+
let terminatedTurn = false;
|
|
100
|
+
export function consumeGuardTermination() {
|
|
101
|
+
const hit = terminatedTurn;
|
|
102
|
+
terminatedTurn = false;
|
|
103
|
+
return hit;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Inert until armed. Registering ANY `tool_call` handler switches on pi's
|
|
107
|
+
* `beforeToolCall` for every call in the session, so the armed check comes first.
|
|
108
|
+
*/
|
|
109
|
+
export function registerImplementationGuards(pi) {
|
|
110
|
+
pi.on('tool_call', event => {
|
|
111
|
+
const state = armed;
|
|
112
|
+
if (!state)
|
|
113
|
+
return;
|
|
114
|
+
try {
|
|
115
|
+
// Every call, whatever it is: pi terminates only when EVERY finalized
|
|
116
|
+
// result in the batch carries the flag (agent-loop.js
|
|
117
|
+
// shouldTerminateToolBatch). The batch that trips it has already
|
|
118
|
+
// finalized its earlier calls without it and so survives; the next one
|
|
119
|
+
// ends. One batch, and it is the only bound this path has.
|
|
120
|
+
if (state.terminating) {
|
|
121
|
+
return { block: true, terminate: true, reason: terminalCallReason() };
|
|
122
|
+
}
|
|
123
|
+
const call = { name: event.toolName, args: event.input };
|
|
124
|
+
const mutating = MUTATING_TOOLS.has(event.toolName);
|
|
125
|
+
const hit = mutating ? state.edits.record(call) : state.loop.record(call);
|
|
126
|
+
if (!hit) {
|
|
127
|
+
if (mutating) {
|
|
128
|
+
state.loop = freshDetector();
|
|
129
|
+
// Strikes go with the window. Keeping them made a LATER episode
|
|
130
|
+
// terminate on its first hit, skipping both warnings, because an
|
|
131
|
+
// earlier one had part-spent the budget. MEASURED over 494 real
|
|
132
|
+
// turns: this loses no catch — the incident still ends at call
|
|
133
|
+
// 173 of 6,760, both live-model loops still end — and drops one
|
|
134
|
+
// termination of a turn that was editing between episodes.
|
|
135
|
+
// A determined model is still bounded: three hits inside ONE
|
|
136
|
+
// episode terminate, which is the runaway shape (it makes no
|
|
137
|
+
// edits at all).
|
|
138
|
+
state.strikes.clear();
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const key = loopKey(call);
|
|
143
|
+
const strikes = (state.strikes.get(key) ?? 0) + 1;
|
|
144
|
+
state.strikes.set(key, strikes);
|
|
145
|
+
// Blocking alone does not stop a determined model: nothing prevents the
|
|
146
|
+
// next identical call.
|
|
147
|
+
if (strikes > MAX_LOOP_RESTARTS) {
|
|
148
|
+
state.terminating = true;
|
|
149
|
+
terminatedTurn = true;
|
|
150
|
+
return { block: true, terminate: true, reason: terminalCallReason() };
|
|
151
|
+
}
|
|
152
|
+
return { block: true, reason: blockedCallReason(event.toolName, hit.count) };
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// pi does not guard this hook, and a throw here would block a
|
|
156
|
+
// legitimate call. A broken guard must cost nothing.
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// NOT `agent_end`, which fires again for every auto-retry, every threshold
|
|
161
|
+
// compaction and every queued message — pi drives those with `agent.continue()`,
|
|
162
|
+
// each a fresh agent loop. The measured runaway compacted 18 times INSIDE its
|
|
163
|
+
// turn, so a one-shot disarm on agent_end would have retired the guard after the
|
|
164
|
+
// first ~375 of its 6,760 calls. `agent_settled` is the boundary that means what
|
|
165
|
+
// this needs: no retry, compaction or queued continuation left to run.
|
|
166
|
+
pi.on('agent_settled', () => {
|
|
167
|
+
if (!armed)
|
|
168
|
+
return;
|
|
169
|
+
if (armed.oneShot)
|
|
170
|
+
disarmImplementationGuard();
|
|
171
|
+
// An awaited run spans resume and steer turns. Counters are per TURN, so a
|
|
172
|
+
// fresh one starts clean rather than inheriting the last one's strikes.
|
|
173
|
+
else
|
|
174
|
+
armed = freshArmedState(false);
|
|
175
|
+
});
|
|
176
|
+
pi.on('session_shutdown', disarmImplementationGuard);
|
|
177
|
+
}
|