dorfl 0.14.2 → 0.15.0
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/advance-ci-template.d.ts +17 -16
- package/dist/advance-ci-template.d.ts.map +1 -1
- package/dist/advance-ci-template.js +42 -53
- package/dist/advance-ci-template.js.map +1 -1
- package/dist/advance-drivers.d.ts +8 -0
- package/dist/advance-drivers.d.ts.map +1 -1
- package/dist/advance-drivers.js +28 -4
- package/dist/advance-drivers.js.map +1 -1
- package/dist/advance-isolated.d.ts.map +1 -1
- package/dist/advance-isolated.js +3 -2
- package/dist/advance-isolated.js.map +1 -1
- package/dist/advance-lifecycle-template.d.ts +41 -40
- package/dist/advance-lifecycle-template.d.ts.map +1 -1
- package/dist/advance-lifecycle-template.js +309 -489
- package/dist/advance-lifecycle-template.js.map +1 -1
- package/dist/advance-loop-driver.d.ts.map +1 -1
- package/dist/advance-loop-driver.js +3 -2
- package/dist/advance-loop-driver.js.map +1 -1
- package/dist/advance-treeless-publish.d.ts +31 -0
- package/dist/advance-treeless-publish.d.ts.map +1 -1
- package/dist/advance-treeless-publish.js +40 -0
- package/dist/advance-treeless-publish.js.map +1 -1
- package/dist/advance.d.ts +63 -3
- package/dist/advance.d.ts.map +1 -1
- package/dist/advance.js +80 -12
- package/dist/advance.js.map +1 -1
- package/dist/advancing-lock.d.ts +9 -0
- package/dist/advancing-lock.d.ts.map +1 -1
- package/dist/advancing-lock.js +21 -1
- package/dist/advancing-lock.js.map +1 -1
- package/dist/agent-env.d.ts +61 -0
- package/dist/agent-env.d.ts.map +1 -0
- package/dist/agent-env.js +144 -0
- package/dist/agent-env.js.map +1 -0
- package/dist/agent-stop.d.ts +35 -0
- package/dist/agent-stop.d.ts.map +1 -1
- package/dist/agent-stop.js +62 -0
- package/dist/agent-stop.js.map +1 -1
- package/dist/apply-merge-action.d.ts +76 -34
- package/dist/apply-merge-action.d.ts.map +1 -1
- package/dist/apply-merge-action.js +339 -129
- package/dist/apply-merge-action.js.map +1 -1
- package/dist/apply-stuck-action.js +2 -2
- package/dist/apply-stuck-action.js.map +1 -1
- package/dist/ci-agent-result.d.ts +151 -0
- package/dist/ci-agent-result.d.ts.map +1 -0
- package/dist/ci-agent-result.js +314 -0
- package/dist/ci-agent-result.js.map +1 -0
- package/dist/ci-handoff-apply.d.ts +162 -0
- package/dist/ci-handoff-apply.d.ts.map +1 -0
- package/dist/ci-handoff-apply.js +497 -0
- package/dist/ci-handoff-apply.js.map +1 -0
- package/dist/ci-handoff-format.d.ts +369 -0
- package/dist/ci-handoff-format.d.ts.map +1 -0
- package/dist/ci-handoff-format.js +646 -0
- package/dist/ci-handoff-format.js.map +1 -0
- package/dist/ci-handoff-lfs.d.ts +146 -0
- package/dist/ci-handoff-lfs.d.ts.map +1 -0
- package/dist/ci-handoff-lfs.js +397 -0
- package/dist/ci-handoff-lfs.js.map +1 -0
- package/dist/ci-handoff.d.ts +99 -0
- package/dist/ci-handoff.d.ts.map +1 -0
- package/dist/ci-handoff.js +277 -0
- package/dist/ci-handoff.js.map +1 -0
- package/dist/ci-lock-outputs.d.ts +93 -0
- package/dist/ci-lock-outputs.d.ts.map +1 -0
- package/dist/ci-lock-outputs.js +223 -0
- package/dist/ci-lock-outputs.js.map +1 -0
- package/dist/ci-phase-build.d.ts +178 -0
- package/dist/ci-phase-build.d.ts.map +1 -0
- package/dist/ci-phase-build.js +989 -0
- package/dist/ci-phase-build.js.map +1 -0
- package/dist/ci-phase-driver.d.ts +247 -0
- package/dist/ci-phase-driver.d.ts.map +1 -0
- package/dist/ci-phase-driver.js +370 -0
- package/dist/ci-phase-driver.js.map +1 -0
- package/dist/ci-phase-intake.d.ts +172 -0
- package/dist/ci-phase-intake.d.ts.map +1 -0
- package/dist/ci-phase-intake.js +555 -0
- package/dist/ci-phase-intake.js.map +1 -0
- package/dist/ci-phase-tasking.d.ts +205 -0
- package/dist/ci-phase-tasking.d.ts.map +1 -0
- package/dist/ci-phase-tasking.js +906 -0
- package/dist/ci-phase-tasking.js.map +1 -0
- package/dist/ci-phase-treeless.d.ts +224 -0
- package/dist/ci-phase-treeless.d.ts.map +1 -0
- package/dist/ci-phase-treeless.js +1482 -0
- package/dist/ci-phase-treeless.js.map +1 -0
- package/dist/ci-read-token.d.ts +47 -0
- package/dist/ci-read-token.d.ts.map +1 -0
- package/dist/ci-read-token.js +80 -0
- package/dist/ci-read-token.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +447 -22
- package/dist/cli.js.map +1 -1
- package/dist/close-job-template.d.ts +10 -1
- package/dist/close-job-template.d.ts.map +1 -1
- package/dist/close-job-template.js +46 -14
- package/dist/close-job-template.js.map +1 -1
- package/dist/close-job.d.ts +20 -5
- package/dist/close-job.d.ts.map +1 -1
- package/dist/close-job.js +65 -12
- package/dist/close-job.js.map +1 -1
- package/dist/complete.d.ts.map +1 -1
- package/dist/complete.js +20 -1
- package/dist/complete.js.map +1 -1
- package/dist/config.d.ts +41 -44
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -4
- package/dist/config.js.map +1 -1
- package/dist/continue-branch.d.ts +63 -15
- package/dist/continue-branch.d.ts.map +1 -1
- package/dist/continue-branch.js +75 -8
- package/dist/continue-branch.js.map +1 -1
- package/dist/crashed-run-save.d.ts +73 -0
- package/dist/crashed-run-save.d.ts.map +1 -0
- package/dist/crashed-run-save.js +86 -0
- package/dist/crashed-run-save.js.map +1 -0
- package/dist/cwd-section.d.ts.map +1 -1
- package/dist/cwd-section.js +10 -2
- package/dist/cwd-section.js.map +1 -1
- package/dist/decision-engine.d.ts +10 -0
- package/dist/decision-engine.d.ts.map +1 -1
- package/dist/decision-engine.js.map +1 -1
- package/dist/do-config.d.ts +2 -24
- package/dist/do-config.d.ts.map +1 -1
- package/dist/do-config.js +1 -33
- package/dist/do-config.js.map +1 -1
- package/dist/do.d.ts +149 -1
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +115 -25
- package/dist/do.js.map +1 -1
- package/dist/dorfl-item-template.d.ts +98 -0
- package/dist/dorfl-item-template.d.ts.map +1 -0
- package/dist/dorfl-item-template.js +504 -0
- package/dist/dorfl-item-template.js.map +1 -0
- package/dist/env-config.d.ts.map +1 -1
- package/dist/env-config.js +7 -5
- package/dist/env-config.js.map +1 -1
- package/dist/frontmatter.d.ts +52 -0
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +136 -2
- package/dist/frontmatter.js.map +1 -1
- package/dist/gc.d.ts +13 -0
- package/dist/gc.d.ts.map +1 -1
- package/dist/gc.js +16 -2
- package/dist/gc.js.map +1 -1
- package/dist/git.d.ts +18 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +70 -5
- package/dist/git.js.map +1 -1
- package/dist/harness.d.ts +44 -5
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +24 -1
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/install-ci-action-pins.d.ts +123 -0
- package/dist/install-ci-action-pins.d.ts.map +1 -0
- package/dist/install-ci-action-pins.js +190 -0
- package/dist/install-ci-action-pins.js.map +1 -0
- package/dist/install-ci-actions-pr-creation.d.ts +70 -0
- package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
- package/dist/install-ci-actions-pr-creation.js +106 -0
- package/dist/install-ci-actions-pr-creation.js.map +1 -0
- package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
- package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
- package/dist/install-ci-capabilities/dorfl-item.js +26 -0
- package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
- package/dist/install-ci-core.d.ts +155 -37
- package/dist/install-ci-core.d.ts.map +1 -1
- package/dist/install-ci-core.js +304 -200
- package/dist/install-ci-core.js.map +1 -1
- package/dist/install-ci-github.d.ts +47 -2
- package/dist/install-ci-github.d.ts.map +1 -1
- package/dist/install-ci-github.js +113 -1
- package/dist/install-ci-github.js.map +1 -1
- package/dist/install-ci.d.ts +25 -6
- package/dist/install-ci.d.ts.map +1 -1
- package/dist/install-ci.js +72 -36
- package/dist/install-ci.js.map +1 -1
- package/dist/intake-marker.d.ts +6 -1
- package/dist/intake-marker.d.ts.map +1 -1
- package/dist/intake-marker.js.map +1 -1
- package/dist/intake-triage.d.ts.map +1 -1
- package/dist/intake-triage.js +6 -2
- package/dist/intake-triage.js.map +1 -1
- package/dist/intake-trigger-template.d.ts +31 -25
- package/dist/intake-trigger-template.d.ts.map +1 -1
- package/dist/intake-trigger-template.js +107 -292
- package/dist/intake-trigger-template.js.map +1 -1
- package/dist/intake.d.ts +186 -4
- package/dist/intake.d.ts.map +1 -1
- package/dist/intake.js +533 -416
- package/dist/intake.js.map +1 -1
- package/dist/integration-core.d.ts +154 -1
- package/dist/integration-core.d.ts.map +1 -1
- package/dist/integration-core.js +759 -486
- package/dist/integration-core.js.map +1 -1
- package/dist/integrator.d.ts +12 -0
- package/dist/integrator.d.ts.map +1 -1
- package/dist/integrator.js +21 -0
- package/dist/integrator.js.map +1 -1
- package/dist/isolation.d.ts +12 -2
- package/dist/isolation.d.ts.map +1 -1
- package/dist/isolation.js +19 -8
- package/dist/isolation.js.map +1 -1
- package/dist/item-lock.d.ts +93 -11
- package/dist/item-lock.d.ts.map +1 -1
- package/dist/item-lock.js +233 -65
- package/dist/item-lock.js.map +1 -1
- package/dist/land-question-residue.d.ts +58 -0
- package/dist/land-question-residue.d.ts.map +1 -0
- package/dist/land-question-residue.js +95 -0
- package/dist/land-question-residue.js.map +1 -0
- package/dist/lifecycle-gather.d.ts +16 -0
- package/dist/lifecycle-gather.d.ts.map +1 -1
- package/dist/lifecycle-gather.js +54 -6
- package/dist/lifecycle-gather.js.map +1 -1
- package/dist/merge-answer.d.ts +41 -0
- package/dist/merge-answer.d.ts.map +1 -0
- package/dist/merge-answer.js +60 -0
- package/dist/merge-answer.js.map +1 -0
- package/dist/merge-question-surfacer.d.ts +105 -30
- package/dist/merge-question-surfacer.d.ts.map +1 -1
- package/dist/merge-question-surfacer.js +202 -91
- package/dist/merge-question-surfacer.js.map +1 -1
- package/dist/merge-question-tick.d.ts +64 -0
- package/dist/merge-question-tick.d.ts.map +1 -0
- package/dist/merge-question-tick.js +96 -0
- package/dist/merge-question-tick.js.map +1 -0
- package/dist/mint-adr.d.ts.map +1 -1
- package/dist/mint-adr.js +20 -2
- package/dist/mint-adr.js.map +1 -1
- package/dist/needs-attention.d.ts +72 -2
- package/dist/needs-attention.d.ts.map +1 -1
- package/dist/needs-attention.js +275 -38
- package/dist/needs-attention.js.map +1 -1
- package/dist/phase-recorder.d.ts +168 -0
- package/dist/phase-recorder.d.ts.map +1 -0
- package/dist/phase-recorder.js +218 -0
- package/dist/phase-recorder.js.map +1 -0
- package/dist/phase.d.ts +94 -0
- package/dist/phase.d.ts.map +1 -0
- package/dist/phase.js +148 -0
- package/dist/phase.js.map +1 -0
- package/dist/pi-harness.d.ts.map +1 -1
- package/dist/pi-harness.js +19 -6
- package/dist/pi-harness.js.map +1 -1
- package/dist/prepare.d.ts.map +1 -1
- package/dist/prepare.js +4 -0
- package/dist/prepare.js.map +1 -1
- package/dist/prompt.d.ts +4 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +48 -1
- package/dist/prompt.js.map +1 -1
- package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
- package/dist/protocol/WORK-CONTRACT.md +1 -1
- package/dist/reap-branches.d.ts +48 -0
- package/dist/reap-branches.d.ts.map +1 -1
- package/dist/reap-branches.js +64 -0
- package/dist/reap-branches.js.map +1 -1
- package/dist/ref-write.d.ts +198 -0
- package/dist/ref-write.d.ts.map +1 -0
- package/dist/ref-write.js +79 -0
- package/dist/ref-write.js.map +1 -0
- package/dist/repo-config.js +8 -8
- package/dist/repo-config.js.map +1 -1
- package/dist/repo-mirror.d.ts +36 -0
- package/dist/repo-mirror.d.ts.map +1 -1
- package/dist/repo-mirror.js +91 -1
- package/dist/repo-mirror.js.map +1 -1
- package/dist/review-verdict.d.ts +1 -1
- package/dist/review-verdict.js +1 -1
- package/dist/run.d.ts +7 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +28 -4
- package/dist/run.js.map +1 -1
- package/dist/scan.d.ts +19 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +21 -8
- package/dist/scan.js.map +1 -1
- package/dist/sidecar.d.ts +14 -0
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +21 -0
- package/dist/sidecar.js.map +1 -1
- package/dist/single-job-warning.d.ts +63 -0
- package/dist/single-job-warning.d.ts.map +1 -0
- package/dist/single-job-warning.js +110 -0
- package/dist/single-job-warning.js.map +1 -0
- package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
- package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
- package/dist/spec-complete.d.ts +13 -6
- package/dist/spec-complete.d.ts.map +1 -1
- package/dist/spec-complete.js +23 -11
- package/dist/spec-complete.js.map +1 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +47 -14
- package/dist/start.js.map +1 -1
- package/dist/status.d.ts +12 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +11 -3
- package/dist/status.js.map +1 -1
- package/dist/tasking.d.ts +246 -1
- package/dist/tasking.d.ts.map +1 -1
- package/dist/tasking.js +452 -358
- package/dist/tasking.js.map +1 -1
- package/dist/triage-persist.d.ts.map +1 -1
- package/dist/triage-persist.js +21 -2
- package/dist/triage-persist.js.map +1 -1
- package/dist/verify-workflow-template.d.ts.map +1 -1
- package/dist/verify-workflow-template.js +8 -4
- package/dist/verify-workflow-template.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +4 -0
- package/dist/verify.js.map +1 -1
- package/dist/watch-session.d.ts +42 -13
- package/dist/watch-session.d.ts.map +1 -1
- package/dist/watch-session.js +52 -23
- package/dist/watch-session.js.map +1 -1
- package/dist/work-layout.d.ts +10 -4
- package/dist/work-layout.d.ts.map +1 -1
- package/dist/work-layout.js +11 -3
- package/dist/work-layout.js.map +1 -1
- package/dist/workspace.d.ts +65 -4
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +159 -11
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
- package/src/advance-ci-template.ts +80 -81
- package/src/advance-drivers.ts +45 -6
- package/src/advance-isolated.ts +3 -5
- package/src/advance-lifecycle-template.ts +375 -538
- package/src/advance-loop-driver.ts +3 -5
- package/src/advance-treeless-publish.ts +61 -0
- package/src/advance.ts +145 -15
- package/src/advancing-lock.ts +36 -1
- package/src/agent-env.ts +174 -0
- package/src/agent-stop.ts +81 -0
- package/src/apply-merge-action.ts +401 -154
- package/src/apply-stuck-action.ts +2 -2
- package/src/ci-agent-result.ts +421 -0
- package/src/ci-handoff-apply.ts +737 -0
- package/src/ci-handoff-format.ts +926 -0
- package/src/ci-handoff-lfs.ts +519 -0
- package/src/ci-handoff.ts +392 -0
- package/src/ci-lock-outputs.ts +273 -0
- package/src/ci-phase-build.ts +1328 -0
- package/src/ci-phase-driver.ts +548 -0
- package/src/ci-phase-intake.ts +861 -0
- package/src/ci-phase-tasking.ts +1255 -0
- package/src/ci-phase-treeless.ts +2028 -0
- package/src/ci-read-token.ts +90 -0
- package/src/cli.ts +591 -29
- package/src/close-job-template.ts +52 -15
- package/src/close-job.ts +77 -12
- package/src/complete.ts +19 -1
- package/src/config.ts +43 -48
- package/src/continue-branch.ts +121 -16
- package/src/crashed-run-save.ts +162 -0
- package/src/cwd-section.ts +10 -0
- package/src/decision-engine.ts +10 -0
- package/src/do-config.ts +2 -57
- package/src/do.ts +170 -28
- package/src/dorfl-item-template.ts +556 -0
- package/src/env-config.ts +7 -5
- package/src/frontmatter.ts +155 -2
- package/src/gc.ts +27 -2
- package/src/git.ts +83 -5
- package/src/harness.ts +58 -6
- package/src/index.ts +50 -1
- package/src/install-ci-action-pins.ts +209 -0
- package/src/install-ci-actions-pr-creation.ts +186 -0
- package/src/install-ci-capabilities/dorfl-item.ts +33 -0
- package/src/install-ci-core.ts +388 -233
- package/src/install-ci-github.ts +149 -1
- package/src/install-ci.ts +83 -37
- package/src/intake-marker.ts +6 -1
- package/src/intake-triage.ts +7 -3
- package/src/intake-trigger-template.ts +126 -327
- package/src/intake.ts +725 -544
- package/src/integration-core.ts +975 -523
- package/src/integrator.ts +28 -0
- package/src/isolation.ts +29 -10
- package/src/item-lock.ts +308 -92
- package/src/land-question-residue.ts +122 -0
- package/src/lifecycle-gather.ts +82 -10
- package/src/merge-answer.ts +82 -0
- package/src/merge-question-surfacer.ts +320 -112
- package/src/merge-question-tick.ts +182 -0
- package/src/mint-adr.ts +28 -2
- package/src/needs-attention.ts +350 -42
- package/src/phase-recorder.ts +350 -0
- package/src/phase.ts +177 -0
- package/src/pi-harness.ts +25 -5
- package/src/prepare.ts +4 -0
- package/src/prompt.ts +68 -1
- package/src/reap-branches.ts +107 -0
- package/src/ref-write.ts +324 -0
- package/src/repo-config.ts +8 -8
- package/src/repo-mirror.ts +107 -1
- package/src/review-verdict.ts +1 -1
- package/src/run.ts +47 -6
- package/src/scan.ts +37 -7
- package/src/sidecar.ts +36 -0
- package/src/single-job-warning.ts +135 -0
- package/src/spec-complete.ts +36 -14
- package/src/start.ts +55 -16
- package/src/status.ts +29 -3
- package/src/tasking.ts +593 -378
- package/src/triage-persist.ts +24 -2
- package/src/verify-workflow-template.ts +8 -4
- package/src/verify.ts +4 -0
- package/src/watch-session.ts +86 -28
- package/src/work-layout.ts +11 -3
- package/src/workspace.ts +210 -14
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import {ledgerWrite} from './ledger-write.js';
|
|
2
|
+
import {refWrite} from './ref-write.js';
|
|
3
|
+
import {integrationLand} from './integration-core.js';
|
|
4
|
+
import type {ReviewProvider} from './integrator.js';
|
|
5
|
+
import type {IssueProvider} from './issue-provider.js';
|
|
6
|
+
import {enterPhase, type Phase} from './phase.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* **Record at the seams, resume at the boundary** (spec
|
|
10
|
+
* `ci-agent-job-without-write-token` §4, task `ci-split-phase-mode-and-guards`).
|
|
11
|
+
*
|
|
12
|
+
* In the `agent` phase the job token cannot write, and dorfl must not try: every
|
|
13
|
+
* write seam is replaced by a RECORDING implementation. The first post-agent
|
|
14
|
+
* write call the recorder KNOWS captures its intent (seam, method, input) and
|
|
15
|
+
* halts the pipeline with a {@link PhaseHaltSentinel}, which the phase driver
|
|
16
|
+
* ({@link runAgentPhase}) catches. Nothing after the first write runs in the
|
|
17
|
+
* agent phase, so no agent-phase step ever depends on a write RESULT (a PR URL,
|
|
18
|
+
* `mergeNonFastForward`, `publishedHead`, a CAS rejection).
|
|
19
|
+
*
|
|
20
|
+
* A write seam call the recorder does NOT know throws
|
|
21
|
+
* {@link UnrecordedWriteError}: an unrouted write is a bug the tests catch.
|
|
22
|
+
*
|
|
23
|
+
* The write seams covered here are every seam a CI path writes through
|
|
24
|
+
* (task `ci-split-route-direct-writes-through-seams`): `ledgerWrite` (which also
|
|
25
|
+
* reaches the integrator, via `applyCompleteTransition`), `refWrite`, the review
|
|
26
|
+
* provider and the issue provider (whose READS pass through), plus the
|
|
27
|
+
* `integrationLand` seam (`integration-core.ts`): the land half of the build's
|
|
28
|
+
* rebase-to-integrate tail, whose call is the success path's boundary (task
|
|
29
|
+
* `ci-split-build-path`). Which seam calls
|
|
30
|
+
* each path records, and how the captured input maps to a handoff intent kind
|
|
31
|
+
* (`ci-handoff-format.ts`), is decided by the per-path split tasks; this module
|
|
32
|
+
* is the shared machinery.
|
|
33
|
+
*
|
|
34
|
+
* The captured `input` is the seam's in-memory argument, which may hold an env
|
|
35
|
+
* with credentials: it is NEVER serialised as-is. The per-path code builds the
|
|
36
|
+
* handoff record from it field by field.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** The write seams a CI path reaches (the recorder replaces all of them). */
|
|
40
|
+
export type WriteSeamName =
|
|
41
|
+
| 'ledgerWrite'
|
|
42
|
+
| 'refWrite'
|
|
43
|
+
| 'integrationLand'
|
|
44
|
+
| 'reviewProvider'
|
|
45
|
+
| 'issueProvider';
|
|
46
|
+
|
|
47
|
+
/** A seam call id: `<seam>.<method>`, e.g. `refWrite.pushContinuedBranch`. */
|
|
48
|
+
export type WriteSeamCall = `${WriteSeamName}.${string}`;
|
|
49
|
+
|
|
50
|
+
/** The captured intent of the first recorded write. */
|
|
51
|
+
export interface RecordedWriteIntent {
|
|
52
|
+
/** The seam the write went through. */
|
|
53
|
+
seam: WriteSeamName;
|
|
54
|
+
/** The seam method called. */
|
|
55
|
+
method: string;
|
|
56
|
+
/** The call's argument, in memory only (see the module comment). */
|
|
57
|
+
input: unknown;
|
|
58
|
+
/**
|
|
59
|
+
* What the pipeline said the boundary IS, when it said so just before the
|
|
60
|
+
* write ({@link annotatePhaseBoundary}): several outcomes reach the same seam
|
|
61
|
+
* call (a red gate, an agent failure and a STOP all bounce through
|
|
62
|
+
* `ledgerWrite.applyNeedsAttentionTransition`), and the per-path code needs to
|
|
63
|
+
* tell them apart. In memory only, like {@link input}.
|
|
64
|
+
*/
|
|
65
|
+
annotation?: unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Thrown by a recording seam on the first recorded write: it halts the
|
|
70
|
+
* pipeline so nothing after the boundary runs. The phase driver catches it; it
|
|
71
|
+
* is never a failure.
|
|
72
|
+
*/
|
|
73
|
+
export class PhaseHaltSentinel extends Error {
|
|
74
|
+
override readonly name = 'PhaseHaltSentinel';
|
|
75
|
+
constructor(readonly intent: RecordedWriteIntent) {
|
|
76
|
+
super(
|
|
77
|
+
`agent phase halted at the first write (${intent.seam}.${intent.method}); ` +
|
|
78
|
+
'the apply phase performs it.',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A write seam call the recorder does not know, in the agent phase. Always a
|
|
85
|
+
* dorfl bug: the write is not routed through the phase split.
|
|
86
|
+
*/
|
|
87
|
+
export class UnrecordedWriteError extends Error {
|
|
88
|
+
override readonly name = 'UnrecordedWriteError';
|
|
89
|
+
constructor(
|
|
90
|
+
readonly seam: WriteSeamName,
|
|
91
|
+
readonly method: string,
|
|
92
|
+
) {
|
|
93
|
+
super(
|
|
94
|
+
`agent phase: unrecorded write ${seam}.${method} (the agent job holds a ` +
|
|
95
|
+
'read-only token, and this write is not routed to the apply phase). ' +
|
|
96
|
+
'This is a dorfl bug in the phase split.',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The recording state one agent-phase run shares across every seam. */
|
|
102
|
+
export interface PhaseRecorder {
|
|
103
|
+
/** The seam calls this recorder captures (every other write throws). */
|
|
104
|
+
readonly recordable: ReadonlySet<WriteSeamCall>;
|
|
105
|
+
/** The first recorded write, once the pipeline has reached it. */
|
|
106
|
+
captured(): RecordedWriteIntent | undefined;
|
|
107
|
+
/** The first unrecorded write, if the pipeline made one. */
|
|
108
|
+
violation(): UnrecordedWriteError | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Remember what the NEXT write is (see {@link annotatePhaseBoundary}); the
|
|
111
|
+
* first recorded write captures the latest annotation. Ignored once halted.
|
|
112
|
+
*/
|
|
113
|
+
annotate(annotation: unknown): void;
|
|
114
|
+
/**
|
|
115
|
+
* Handle one write seam call: always throws. The first recorded call
|
|
116
|
+
* captures its intent and throws a {@link PhaseHaltSentinel}; any later write
|
|
117
|
+
* re-throws that same sentinel (nothing after the boundary may write). An
|
|
118
|
+
* unknown call throws (and remembers) an {@link UnrecordedWriteError}.
|
|
119
|
+
*/
|
|
120
|
+
write(seam: WriteSeamName, method: string, input: unknown): never;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Create a {@link PhaseRecorder} that captures exactly the `record` calls. */
|
|
124
|
+
export function createPhaseRecorder(options: {
|
|
125
|
+
record: Iterable<WriteSeamCall>;
|
|
126
|
+
}): PhaseRecorder {
|
|
127
|
+
const recordable = new Set<WriteSeamCall>(options.record);
|
|
128
|
+
let sentinel: PhaseHaltSentinel | undefined;
|
|
129
|
+
let violation: UnrecordedWriteError | undefined;
|
|
130
|
+
let annotation: unknown;
|
|
131
|
+
return {
|
|
132
|
+
recordable,
|
|
133
|
+
captured: () => sentinel?.intent,
|
|
134
|
+
violation: () => violation,
|
|
135
|
+
annotate(next): void {
|
|
136
|
+
if (sentinel === undefined) annotation = next;
|
|
137
|
+
},
|
|
138
|
+
write(seam, method, input): never {
|
|
139
|
+
if (violation !== undefined) {
|
|
140
|
+
throw violation;
|
|
141
|
+
}
|
|
142
|
+
if (sentinel !== undefined) {
|
|
143
|
+
throw sentinel;
|
|
144
|
+
}
|
|
145
|
+
if (!recordable.has(`${seam}.${method}`)) {
|
|
146
|
+
violation = new UnrecordedWriteError(seam, method);
|
|
147
|
+
throw violation;
|
|
148
|
+
}
|
|
149
|
+
sentinel = new PhaseHaltSentinel({
|
|
150
|
+
seam,
|
|
151
|
+
method,
|
|
152
|
+
input,
|
|
153
|
+
...(annotation === undefined ? {} : {annotation}),
|
|
154
|
+
});
|
|
155
|
+
throw sentinel;
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Replace EVERY method of the process-wide `ledgerWrite`, `refWrite` and
|
|
162
|
+
* `integrationLand` seams with a recording one, and return a function restoring the originals. Every
|
|
163
|
+
* method is enumerated from the seam object, so a seam method added later is
|
|
164
|
+
* covered (as an unrecorded write) without touching this function.
|
|
165
|
+
*/
|
|
166
|
+
export function installRecordingSeams(recorder: PhaseRecorder): () => void {
|
|
167
|
+
const previousRecorder = activeRecorder;
|
|
168
|
+
activeRecorder = recorder;
|
|
169
|
+
const restores = [
|
|
170
|
+
() => {
|
|
171
|
+
activeRecorder = previousRecorder;
|
|
172
|
+
},
|
|
173
|
+
swapMethods('ledgerWrite', ledgerWrite, recorder),
|
|
174
|
+
swapMethods('refWrite', refWrite, recorder),
|
|
175
|
+
swapMethods('integrationLand', integrationLand, recorder),
|
|
176
|
+
];
|
|
177
|
+
return () => {
|
|
178
|
+
for (const restore of restores.reverse()) {
|
|
179
|
+
restore();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The recorder the process-wide seams write into now (agent phase only). */
|
|
185
|
+
let activeRecorder: PhaseRecorder | undefined;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Say what the next write IS, for the agent-phase recorder to capture with it.
|
|
189
|
+
* A no-op outside the agent phase (no recorder is installed), so a pipeline
|
|
190
|
+
* calls it unconditionally right before a write seam call whose seam and method
|
|
191
|
+
* alone do not name the outcome (task `ci-split-build-path-non-integrate-intents`:
|
|
192
|
+
* an agent failure, a STOP and a red gate all bounce through the same seam).
|
|
193
|
+
* The annotation never reaches the arbiter; the per-path code maps it to a
|
|
194
|
+
* handoff intent.
|
|
195
|
+
*/
|
|
196
|
+
export function annotatePhaseBoundary(annotation: unknown): void {
|
|
197
|
+
activeRecorder?.annotate(annotation);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function swapMethods(
|
|
201
|
+
seam: WriteSeamName,
|
|
202
|
+
target: object,
|
|
203
|
+
recorder: PhaseRecorder,
|
|
204
|
+
): () => void {
|
|
205
|
+
const record = target as Record<string, unknown>;
|
|
206
|
+
const originals = new Map<string, unknown>();
|
|
207
|
+
for (const [method, value] of Object.entries(record)) {
|
|
208
|
+
if (typeof value !== 'function') {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
originals.set(method, value);
|
|
212
|
+
record[method] = (input: unknown) => recorder.write(seam, method, input);
|
|
213
|
+
}
|
|
214
|
+
return () => {
|
|
215
|
+
for (const [method, value] of originals) {
|
|
216
|
+
record[method] = value;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A recording {@link ReviewProvider}: every method is a write, so every call
|
|
223
|
+
* goes to the recorder. `name` is the wrapped provider's.
|
|
224
|
+
*/
|
|
225
|
+
export function recordingReviewProvider(
|
|
226
|
+
inner: ReviewProvider,
|
|
227
|
+
recorder: PhaseRecorder,
|
|
228
|
+
): ReviewProvider {
|
|
229
|
+
const seam: WriteSeamName = 'reviewProvider';
|
|
230
|
+
return {
|
|
231
|
+
name: inner.name,
|
|
232
|
+
openRequest: (input) => recorder.write(seam, 'openRequest', input),
|
|
233
|
+
postPRComment: (input) => recorder.write(seam, 'postPRComment', input),
|
|
234
|
+
postPRCommentOnBranch: (input) =>
|
|
235
|
+
recorder.write(seam, 'postPRCommentOnBranch', input),
|
|
236
|
+
closeRequestOnBranch: (input) =>
|
|
237
|
+
recorder.write(seam, 'closeRequestOnBranch', input),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* A recording {@link IssueProvider}: the reads (`getIssue`, `listComments`,
|
|
243
|
+
* `getLabels`) pass through to `inner` (the agent job's read token serves
|
|
244
|
+
* them); every write goes to the recorder.
|
|
245
|
+
*/
|
|
246
|
+
export function recordingIssueProvider(
|
|
247
|
+
inner: IssueProvider,
|
|
248
|
+
recorder: PhaseRecorder,
|
|
249
|
+
): IssueProvider {
|
|
250
|
+
const seam: WriteSeamName = 'issueProvider';
|
|
251
|
+
return {
|
|
252
|
+
name: inner.name,
|
|
253
|
+
getIssue: (input) => inner.getIssue(input),
|
|
254
|
+
listComments: (input) => inner.listComments(input),
|
|
255
|
+
getLabels: (input) => inner.getLabels(input),
|
|
256
|
+
postIssueComment: (input) =>
|
|
257
|
+
recorder.write(seam, 'postIssueComment', input),
|
|
258
|
+
addLabel: (input) => recorder.write(seam, 'addLabel', input),
|
|
259
|
+
removeLabel: (input) => recorder.write(seam, 'removeLabel', input),
|
|
260
|
+
closeIssue: (input) => recorder.write(seam, 'closeIssue', input),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The outcome of one {@link runAgentPhase}. */
|
|
265
|
+
export type AgentPhaseOutcome<T> =
|
|
266
|
+
| {
|
|
267
|
+
/** The pipeline finished without reaching a write. */
|
|
268
|
+
halted: false;
|
|
269
|
+
result: T;
|
|
270
|
+
}
|
|
271
|
+
| {
|
|
272
|
+
/** The pipeline reached its first write and stopped there. */
|
|
273
|
+
halted: true;
|
|
274
|
+
/** The captured first write. */
|
|
275
|
+
intent: RecordedWriteIntent;
|
|
276
|
+
/**
|
|
277
|
+
* True when the pipeline CAUGHT the sentinel and returned normally
|
|
278
|
+
* instead of letting it propagate. The intent still stands (the recorder
|
|
279
|
+
* is authoritative); the flag lets the per-path tasks assert their
|
|
280
|
+
* pipeline stops cleanly at the boundary.
|
|
281
|
+
*/
|
|
282
|
+
sentinelSwallowed: boolean;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The agent-phase driver: run `pipeline` in the `agent` phase with every write
|
|
287
|
+
* seam recording (the process-wide `ledgerWrite` / `refWrite`; the pipeline
|
|
288
|
+
* wraps its injected providers with {@link recordingReviewProvider} /
|
|
289
|
+
* {@link recordingIssueProvider} on the same `recorder`), catch the
|
|
290
|
+
* {@link PhaseHaltSentinel}, and restore the seams and the previous phase.
|
|
291
|
+
*
|
|
292
|
+
* The recorder, not the exception, is authoritative: a pipeline that swallows
|
|
293
|
+
* the sentinel in a broad `catch` is still reported as halted at the captured
|
|
294
|
+
* intent, and an {@link UnrecordedWriteError} is re-thrown even if the pipeline
|
|
295
|
+
* swallowed it.
|
|
296
|
+
*/
|
|
297
|
+
export async function runAgentPhase<T>(
|
|
298
|
+
recorder: PhaseRecorder,
|
|
299
|
+
pipeline: () => Promise<T>,
|
|
300
|
+
): Promise<AgentPhaseOutcome<T>> {
|
|
301
|
+
const restorePhase = enterPhase('agent');
|
|
302
|
+
const restoreSeams = installRecordingSeams(recorder);
|
|
303
|
+
let result: T | undefined;
|
|
304
|
+
let caught: unknown;
|
|
305
|
+
let threw = false;
|
|
306
|
+
try {
|
|
307
|
+
result = await pipeline();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
threw = true;
|
|
310
|
+
caught = err;
|
|
311
|
+
} finally {
|
|
312
|
+
restoreSeams();
|
|
313
|
+
restorePhase();
|
|
314
|
+
}
|
|
315
|
+
const violation = recorder.violation();
|
|
316
|
+
if (violation !== undefined) {
|
|
317
|
+
throw violation;
|
|
318
|
+
}
|
|
319
|
+
const intent = recorder.captured();
|
|
320
|
+
if (intent !== undefined) {
|
|
321
|
+
if (threw && !(caught instanceof PhaseHaltSentinel)) {
|
|
322
|
+
throw caught;
|
|
323
|
+
}
|
|
324
|
+
return {halted: true, intent, sentinelSwallowed: !threw};
|
|
325
|
+
}
|
|
326
|
+
if (threw) {
|
|
327
|
+
throw caught;
|
|
328
|
+
}
|
|
329
|
+
return {halted: false, result: result as T};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Enter `phase` for the whole (one-shot) CLI process, the `--phase` entry
|
|
334
|
+
* point: the phase guards fire from here on, and in the `agent` phase every
|
|
335
|
+
* process-wide write seam records into a recorder that knows NO write, so any
|
|
336
|
+
* write that is not inside a per-path {@link runAgentPhase} (which installs its
|
|
337
|
+
* own recorder on top) throws {@link UnrecordedWriteError}. Returns a function
|
|
338
|
+
* restoring the previous state (tests; the CLI process simply exits).
|
|
339
|
+
*/
|
|
340
|
+
export function activateProcessPhase(phase: Phase): () => void {
|
|
341
|
+
const restorePhase = enterPhase(phase);
|
|
342
|
+
const restoreSeams =
|
|
343
|
+
phase === 'agent'
|
|
344
|
+
? installRecordingSeams(createPhaseRecorder({record: []}))
|
|
345
|
+
: () => {};
|
|
346
|
+
return () => {
|
|
347
|
+
restoreSeams();
|
|
348
|
+
restorePhase();
|
|
349
|
+
};
|
|
350
|
+
}
|
package/src/phase.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **CI phase mode** (spec `ci-agent-job-without-write-token`, ADR
|
|
3
|
+
* `ci-agent-job-holds-no-write-token`, task `ci-split-phase-mode-and-guards`).
|
|
4
|
+
*
|
|
5
|
+
* In GitHub Actions every CI item runs as three jobs, and the SAME dorfl verb
|
|
6
|
+
* (`intake`, `advance`; `do` for parity) runs once per job with a hidden
|
|
7
|
+
* `--phase`:
|
|
8
|
+
*
|
|
9
|
+
* - **`lock`**: the write token, NO agent, NO repository code. Takes the lock
|
|
10
|
+
* (and the other pre-agent writes) and emits trusted facts.
|
|
11
|
+
* - **`agent`**: a read-only token (`persist-credentials: false`). Runs the
|
|
12
|
+
* agents, the gate and every local commit, and RECORDS the first write
|
|
13
|
+
* instead of performing it (see `phase-recorder.ts`).
|
|
14
|
+
* - **`apply`**: the write token, NO agent, NO repository code. Performs the
|
|
15
|
+
* recorded writes from the agent job's handoff, treated as hostile.
|
|
16
|
+
*
|
|
17
|
+
* Absent `--phase` is today's single process, byte for byte: the active phase
|
|
18
|
+
* is `undefined` and every guard below is a no-op.
|
|
19
|
+
*
|
|
20
|
+
* The active phase is PROCESS-GLOBAL on purpose: a phase run is a one-shot CI
|
|
21
|
+
* process, and the chokepoints it must guard (`agentLaunchEnv`, the harness
|
|
22
|
+
* adapters, `runVerify`, `runPrepare`, the write seams) are reached from deep
|
|
23
|
+
* inside pipelines that do not carry a context. The same value is ALSO threaded
|
|
24
|
+
* through the run context (`AdvanceContext.phase`, `DoOptions.phase`,
|
|
25
|
+
* `PerformIntakeOptions.phase`) so the pipelines can branch on it; the global
|
|
26
|
+
* is what the guards trust.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** One of the three CI jobs a dorfl verb runs as (see the module comment). */
|
|
30
|
+
export type Phase = 'lock' | 'agent' | 'apply';
|
|
31
|
+
|
|
32
|
+
/** Every {@link Phase}, in pipeline order. */
|
|
33
|
+
export const PHASES: readonly Phase[] = ['lock', 'agent', 'apply'];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A `--phase` value that is not allowed: an unknown phase name, or any phase
|
|
37
|
+
* outside GitHub Actions. A usage error, raised before anything runs.
|
|
38
|
+
*/
|
|
39
|
+
export class PhaseUsageError extends Error {
|
|
40
|
+
override readonly name = 'PhaseUsageError';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse and admit a `--phase` value. It must be one of {@link PHASES}, and it is
|
|
45
|
+
* only allowed when `GITHUB_ACTIONS=true` in `env`, so a phase cannot change
|
|
46
|
+
* laptop behaviour by accident. `allowOutsideActions` is the explicit TEST
|
|
47
|
+
* override (never wired to a flag, an env var or config).
|
|
48
|
+
*/
|
|
49
|
+
export function parsePhase(
|
|
50
|
+
raw: string,
|
|
51
|
+
options: {env?: NodeJS.ProcessEnv; allowOutsideActions?: boolean} = {},
|
|
52
|
+
): Phase {
|
|
53
|
+
const value = raw.trim();
|
|
54
|
+
if (!(PHASES as readonly string[]).includes(value)) {
|
|
55
|
+
throw new PhaseUsageError(
|
|
56
|
+
`--phase must be one of ${PHASES.join(', ')} (got '${raw}').`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const env = options.env ?? process.env;
|
|
60
|
+
if (env.GITHUB_ACTIONS !== 'true' && options.allowOutsideActions !== true) {
|
|
61
|
+
throw new PhaseUsageError(
|
|
62
|
+
'--phase is CI-only: it splits one item into the lock, agent and apply ' +
|
|
63
|
+
'jobs of a GitHub Actions workflow and requires GITHUB_ACTIONS=true. ' +
|
|
64
|
+
'Run the verb without --phase for the normal single-process behaviour.',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return value as Phase;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let current: Phase | undefined;
|
|
71
|
+
|
|
72
|
+
/** The phase this process runs as, or `undefined` (no `--phase`: today's behaviour). */
|
|
73
|
+
export function activePhase(): Phase | undefined {
|
|
74
|
+
return current;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Make `phase` the active phase and return a function restoring the previous
|
|
79
|
+
* one. The CLI enters the phase once for the whole (one-shot) process; the
|
|
80
|
+
* phase drivers and tests scope it with the returned restore.
|
|
81
|
+
*/
|
|
82
|
+
export function enterPhase(phase: Phase | undefined): () => void {
|
|
83
|
+
const previous = current;
|
|
84
|
+
current = phase;
|
|
85
|
+
return () => {
|
|
86
|
+
current = previous;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** What a {@link PhaseGuardError} refused to do. */
|
|
91
|
+
export type GuardedOperation =
|
|
92
|
+
| 'agent-launch-env'
|
|
93
|
+
| 'harness-launch'
|
|
94
|
+
| 'verify'
|
|
95
|
+
| 'prepare';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A phase guard fired: dorfl tried to launch an agent or run repository code in
|
|
99
|
+
* the `lock` or `apply` phase, whose job holds the write token. Always a dorfl
|
|
100
|
+
* BUG (the pipeline split is wrong), never a user error; defence in depth that
|
|
101
|
+
* holds whatever the workflow shape.
|
|
102
|
+
*/
|
|
103
|
+
export class PhaseGuardError extends Error {
|
|
104
|
+
override readonly name = 'PhaseGuardError';
|
|
105
|
+
constructor(
|
|
106
|
+
readonly phase: Phase,
|
|
107
|
+
readonly operation: GuardedOperation,
|
|
108
|
+
) {
|
|
109
|
+
super(
|
|
110
|
+
`phase guard: '${operation}' is not allowed in the ${phase} phase ` +
|
|
111
|
+
'(the lock and apply jobs hold the write token, so they never launch ' +
|
|
112
|
+
'an agent or run repository code). This is a dorfl bug in the phase split.',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Throw a {@link PhaseGuardError} when the active phase is `lock` or `apply`.
|
|
119
|
+
* Called at every chokepoint that launches an agent or runs repository code:
|
|
120
|
+
* `agentLaunchEnv`, each harness adapter's launch, `runVerify` and
|
|
121
|
+
* `runPrepare`. A no-op without a phase and in the `agent` phase.
|
|
122
|
+
*/
|
|
123
|
+
export function assertAgentOrRepoCodeAllowed(
|
|
124
|
+
operation: GuardedOperation,
|
|
125
|
+
): void {
|
|
126
|
+
const phase = current;
|
|
127
|
+
if (phase === 'lock' || phase === 'apply') {
|
|
128
|
+
throw new PhaseGuardError(phase, operation);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Every CLI verb that can launch an agent (autonomous or interactive) or run
|
|
134
|
+
* repository code (`prepare` / `verify`). A CI job that runs one of these must
|
|
135
|
+
* be the agent job (read-only token, no persisted credential) or pass
|
|
136
|
+
* `--phase lock|apply`. The workflow guard (task `ci-split-generate-workflows`)
|
|
137
|
+
* and the single-job warning (task `ci-split-warn-single-job-workflows`) read
|
|
138
|
+
* this set; `phase-cli.test.ts` fails when a CLI verb is in neither this set
|
|
139
|
+
* nor {@link NON_AGENT_VERBS}.
|
|
140
|
+
*/
|
|
141
|
+
export const AGENT_SPAWNING_VERBS: ReadonlySet<string> = new Set([
|
|
142
|
+
'run',
|
|
143
|
+
'verify',
|
|
144
|
+
'start',
|
|
145
|
+
'resume',
|
|
146
|
+
'work-on',
|
|
147
|
+
'complete',
|
|
148
|
+
'do',
|
|
149
|
+
'advance',
|
|
150
|
+
'intake',
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Every CLI verb that neither launches an agent nor runs repository code (it
|
|
155
|
+
* reads, or writes refs / the ledger / provider state only). The explicit
|
|
156
|
+
* complement of {@link AGENT_SPAWNING_VERBS}, so a new verb must be classified.
|
|
157
|
+
*/
|
|
158
|
+
export const NON_AGENT_VERBS: ReadonlySet<string> = new Set([
|
|
159
|
+
'config',
|
|
160
|
+
'scan',
|
|
161
|
+
'claim',
|
|
162
|
+
'prompt',
|
|
163
|
+
'gc',
|
|
164
|
+
'sync',
|
|
165
|
+
'prd-to-spec',
|
|
166
|
+
'status',
|
|
167
|
+
'requeue',
|
|
168
|
+
'promote',
|
|
169
|
+
'release-lock',
|
|
170
|
+
'migrate-stuck-locks',
|
|
171
|
+
'drop',
|
|
172
|
+
'close-merged-issues',
|
|
173
|
+
'surface-merge-questions',
|
|
174
|
+
'remote',
|
|
175
|
+
'install-ci',
|
|
176
|
+
'skills',
|
|
177
|
+
]);
|
package/src/pi-harness.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {spawn, spawnSync} from 'node:child_process';
|
|
2
2
|
import {existsSync, readFileSync, writeSync} from 'node:fs';
|
|
3
|
+
import {assertAgentOrRepoCodeAllowed} from './phase.js';
|
|
3
4
|
import {
|
|
4
5
|
NullHarness,
|
|
6
|
+
agentLaunchEnv,
|
|
5
7
|
pidAlive,
|
|
6
8
|
registerHarness,
|
|
9
|
+
type CutOffTurn,
|
|
7
10
|
type Harness,
|
|
8
11
|
type HarnessRecord,
|
|
9
12
|
type InteractiveLaunchInput,
|
|
@@ -12,7 +15,11 @@ import {
|
|
|
12
15
|
type LaunchResult,
|
|
13
16
|
} from './harness.js';
|
|
14
17
|
import {generateSessionPath} from './session-path.js';
|
|
15
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
lastAssistantTurn,
|
|
20
|
+
isOutputCappedTurn,
|
|
21
|
+
cutOffTurnOf,
|
|
22
|
+
} from './watch-session.js';
|
|
16
23
|
import {reapProcessGroup} from './reap-agent-tree.js';
|
|
17
24
|
import type {HarnessAdapter} from './config.js';
|
|
18
25
|
|
|
@@ -246,6 +253,8 @@ export class PiHarness implements Harness {
|
|
|
246
253
|
}
|
|
247
254
|
|
|
248
255
|
launch(input: LaunchInput): LaunchResult {
|
|
256
|
+
// PHASE GUARD first: no launch at all in the CI lock/apply phases.
|
|
257
|
+
assertAgentOrRepoCodeAllowed('harness-launch');
|
|
249
258
|
const sessionFile = this.resolveSessionFile(input);
|
|
250
259
|
const args = this.buildArgs(input, sessionFile);
|
|
251
260
|
const result = spawnSync(this.piBin, args, {
|
|
@@ -255,7 +264,8 @@ export class PiHarness implements Harness {
|
|
|
255
264
|
cwd: input.dir,
|
|
256
265
|
encoding: 'utf8',
|
|
257
266
|
input: input.prompt,
|
|
258
|
-
|
|
267
|
+
// No GitHub token reaches a CI agent (see agentLaunchEnv).
|
|
268
|
+
env: agentLaunchEnv(input.env),
|
|
259
269
|
maxBuffer: 64 * 1024 * 1024,
|
|
260
270
|
});
|
|
261
271
|
if (result.error) {
|
|
@@ -280,7 +290,7 @@ export class PiHarness implements Harness {
|
|
|
280
290
|
// The agent's ANSWER (task `harness-agent-output`): the LAST assistant
|
|
281
291
|
// turn's text read from the session `.jsonl` pi just wrote — NOT piped
|
|
282
292
|
// stdout (which is drained). Shares `watch-session.ts`'s reader. The
|
|
283
|
-
// same turn's
|
|
293
|
+
// same turn's stop reason/usage feed the outputCapped cap-truncation
|
|
284
294
|
// signal (observation `tasker-review-edits-payload-caps-the-verdict-response`).
|
|
285
295
|
...readAssistantOutput(sessionFile),
|
|
286
296
|
};
|
|
@@ -317,6 +327,8 @@ export class PiHarness implements Harness {
|
|
|
317
327
|
* belt-and-suspenders backstop bounding any such residual leak.
|
|
318
328
|
*/
|
|
319
329
|
launchAsync(input: LaunchInput): Promise<LaunchResult> {
|
|
330
|
+
// PHASE GUARD first: no launch at all in the CI lock/apply phases.
|
|
331
|
+
assertAgentOrRepoCodeAllowed('harness-launch');
|
|
320
332
|
const sessionFile = this.resolveSessionFile(input);
|
|
321
333
|
const args = this.buildArgs(input, sessionFile);
|
|
322
334
|
const record: PiHarnessRecord = {
|
|
@@ -333,7 +345,8 @@ export class PiHarness implements Harness {
|
|
|
333
345
|
// Same as `launch`: spawn in the repo/worktree dir so the session
|
|
334
346
|
// header `cwd` groups the dashboard correctly (invariant #3).
|
|
335
347
|
cwd: input.dir,
|
|
336
|
-
|
|
348
|
+
// No GitHub token reaches a CI agent (see agentLaunchEnv).
|
|
349
|
+
env: agentLaunchEnv(input.env),
|
|
337
350
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
338
351
|
// PROCESS-GROUP LEADER (observation
|
|
339
352
|
// `checkpoint-releases-lock-while-predecessor-agent-still-writes`): pi's
|
|
@@ -483,7 +496,7 @@ export class PiHarness implements Harness {
|
|
|
483
496
|
// Read the agent's ANSWER from the `.jsonl` at `exit` — the same
|
|
484
497
|
// last-assistant-turn read `launch` does at return (task
|
|
485
498
|
// `harness-agent-output`); the process has exited so the log is final.
|
|
486
|
-
// The same turn's
|
|
499
|
+
// The same turn's stop reason/usage feed the outputCapped signal.
|
|
487
500
|
...readAssistantOutput(sessionFile),
|
|
488
501
|
});
|
|
489
502
|
};
|
|
@@ -558,6 +571,8 @@ export class PiHarness implements Harness {
|
|
|
558
571
|
* nothing to capture, so it returns only the exit code.
|
|
559
572
|
*/
|
|
560
573
|
launchInteractive(input: InteractiveLaunchInput): InteractiveLaunchResult {
|
|
574
|
+
// PHASE GUARD first: no launch at all in the CI lock/apply phases.
|
|
575
|
+
assertAgentOrRepoCodeAllowed('harness-launch');
|
|
561
576
|
const sessionFile = this.resolveSessionFile({
|
|
562
577
|
dir: input.dir,
|
|
563
578
|
slug: input.slug,
|
|
@@ -653,6 +668,7 @@ export function piSessionExists(record: HarnessRecord): boolean {
|
|
|
653
668
|
function readAssistantOutput(sessionFile: string): {
|
|
654
669
|
output?: string;
|
|
655
670
|
outputCapped?: number;
|
|
671
|
+
cutOffTurn?: CutOffTurn;
|
|
656
672
|
} {
|
|
657
673
|
let jsonl: string;
|
|
658
674
|
try {
|
|
@@ -664,6 +680,10 @@ function readAssistantOutput(sessionFile: string): {
|
|
|
664
680
|
return {
|
|
665
681
|
output: turn.text,
|
|
666
682
|
outputCapped: isOutputCappedTurn(turn) ? turn.outputTokens : undefined,
|
|
683
|
+
// How the run's FINAL turn ended (task
|
|
684
|
+
// `a-truncated-agent-turn-routes-as-agent-failed`): a `length`/`error` stop
|
|
685
|
+
// makes an empty diff an agent failure, not a deliberate empty-diff STOP.
|
|
686
|
+
cutOffTurn: cutOffTurnOf(turn),
|
|
667
687
|
};
|
|
668
688
|
}
|
|
669
689
|
|
package/src/prepare.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {spawn} from 'node:child_process';
|
|
2
|
+
import {assertAgentOrRepoCodeAllowed} from './phase.js';
|
|
2
3
|
import {existsSync, mkdirSync, writeFileSync} from 'node:fs';
|
|
3
4
|
import {dirname, join} from 'node:path';
|
|
4
5
|
import {run} from './git.js';
|
|
@@ -93,6 +94,9 @@ export interface RunPrepareResult {
|
|
|
93
94
|
export async function runPrepare(
|
|
94
95
|
options: RunPrepareOptions,
|
|
95
96
|
): Promise<RunPrepareResult> {
|
|
97
|
+
// PHASE GUARD: the gate runs repository code, never in the CI lock/apply
|
|
98
|
+
// phases (their jobs hold the write token; see `phase.ts`).
|
|
99
|
+
assertAgentOrRepoCodeAllowed('prepare');
|
|
96
100
|
const commands = resolvePrepareCommands(options.prepare);
|
|
97
101
|
if (commands.length === 0) {
|
|
98
102
|
return {exitCode: 0, commands, passed: true, noop: true};
|