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
|
@@ -12,10 +12,8 @@ import {runConcurrent} from './concurrency.js';
|
|
|
12
12
|
import {scan, type ScanReport} from './scan.js';
|
|
13
13
|
import {run as runProcess} from './git.js';
|
|
14
14
|
import {jobWorktreeDoDriver} from './do.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
TREELESS_RUNGS,
|
|
18
|
-
} from './advance-treeless-publish.js';
|
|
15
|
+
import {TREELESS_RUNGS} from './advance-treeless-publish.js';
|
|
16
|
+
import {refWrite} from './ref-write.js';
|
|
19
17
|
import type {Config} from './config.js';
|
|
20
18
|
import type {
|
|
21
19
|
RunTick,
|
|
@@ -206,7 +204,7 @@ export async function advanceOnce(
|
|
|
206
204
|
TREELESS_RUNGS.has(result.rung) &&
|
|
207
205
|
options.context.cwd !== undefined
|
|
208
206
|
) {
|
|
209
|
-
await
|
|
207
|
+
await refWrite.publishTreelessResult({
|
|
210
208
|
cwd: options.context.cwd,
|
|
211
209
|
arbiter: options.context.arbiter ?? 'origin',
|
|
212
210
|
// Large liveness ceiling (C2, task `c2-rebase-until-real-on-durable-main-
|
|
@@ -134,6 +134,8 @@ export async function pushTreelessResult(
|
|
|
134
134
|
const {cwd, arbiter, retries, env, note} = params;
|
|
135
135
|
const jitterMs = params.jitterMs ?? DEFAULT_TREELESS_JITTER_MS;
|
|
136
136
|
for (let i = 0; i <= retries; i++) {
|
|
137
|
+
// Write-seam implementation: callers reach this through
|
|
138
|
+
// `refWrite.publishTreelessResult` (guard: `write-sites-through-seams.test.ts`).
|
|
137
139
|
const push = await runAsync(
|
|
138
140
|
'git',
|
|
139
141
|
['push', '--quiet', arbiter, 'HEAD:main'],
|
|
@@ -207,6 +209,65 @@ export async function pushTreelessResult(
|
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
/** The result of {@link checkTreelessPublishScope}. */
|
|
213
|
+
export type TreelessPublishScope =
|
|
214
|
+
/** `HEAD` is `base`: the rung committed nothing, so there is nothing to push. */
|
|
215
|
+
| {kind: 'nothing'}
|
|
216
|
+
/** `HEAD` is exactly one commit whose parent is `base`: the rung's own. */
|
|
217
|
+
| {kind: 'rung-commit'; commit: string}
|
|
218
|
+
/** Anything else: publishing `HEAD:main` would carry commits that are not the rung's. */
|
|
219
|
+
| {kind: 'refused'; message: string};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check, BEFORE a tree-less publish, that `HEAD:main` would publish exactly the
|
|
223
|
+
* rung's own commit (task `ci-split-treeless-rungs`; observation
|
|
224
|
+
* `advance-lifecycle-push-trigger-lands-an-unreviewed-work-branch-on-main`).
|
|
225
|
+
* {@link pushTreelessResult} pushes `HEAD:main` from whatever is checked out, so
|
|
226
|
+
* a checkout not based on the fetched `<arbiter>/main` tip (`base`) would
|
|
227
|
+
* publish every commit between the two. The CI apply phase runs the rung on a
|
|
228
|
+
* fresh checkout of `base` and calls this first: the new commit's parent must be
|
|
229
|
+
* `base` (the publish's own retry may then rebase only that commit). Read-only.
|
|
230
|
+
*/
|
|
231
|
+
export async function checkTreelessPublishScope(params: {
|
|
232
|
+
cwd: string;
|
|
233
|
+
base: string;
|
|
234
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
235
|
+
}): Promise<TreelessPublishScope> {
|
|
236
|
+
const {cwd, base, env} = params;
|
|
237
|
+
const head = await runAsync('git', ['rev-parse', 'HEAD'], cwd, {env});
|
|
238
|
+
if (head.status !== 0) {
|
|
239
|
+
return {kind: 'refused', message: 'the checkout has no HEAD commit'};
|
|
240
|
+
}
|
|
241
|
+
const tip = head.stdout.trim();
|
|
242
|
+
if (tip === base) return {kind: 'nothing'};
|
|
243
|
+
const list = await runAsync('git', ['rev-list', `${base}..${tip}`], cwd, {
|
|
244
|
+
env,
|
|
245
|
+
});
|
|
246
|
+
const commits = list.stdout.split('\n').filter((l) => l.trim() !== '');
|
|
247
|
+
const parents = await runAsync(
|
|
248
|
+
'git',
|
|
249
|
+
['rev-list', '--parents', '-n', '1', tip],
|
|
250
|
+
cwd,
|
|
251
|
+
{env},
|
|
252
|
+
);
|
|
253
|
+
const parentList = parents.stdout.trim().split(/\s+/).slice(1);
|
|
254
|
+
if (
|
|
255
|
+
list.status !== 0 ||
|
|
256
|
+
commits.length !== 1 ||
|
|
257
|
+
parentList.length !== 1 ||
|
|
258
|
+
parentList[0] !== base
|
|
259
|
+
) {
|
|
260
|
+
return {
|
|
261
|
+
kind: 'refused',
|
|
262
|
+
message:
|
|
263
|
+
`HEAD (${tip}) is not one commit on top of the fetched main (${base}): ` +
|
|
264
|
+
`${commits.length} commit(s) would be published, so the tree-less publish ` +
|
|
265
|
+
'is refused (it would land commits that are not the rung result)',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {kind: 'rung-commit', commit: tip};
|
|
269
|
+
}
|
|
270
|
+
|
|
210
271
|
/**
|
|
211
272
|
* The rung kinds that commit a tree-less result LOCALLY (sidecar / `needsAnswers`
|
|
212
273
|
* / `triaged:` marker) and therefore need {@link pushTreelessResult} to reach the
|
package/src/advance.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {Phase} from './phase.js';
|
|
1
2
|
import {existsSync, readFileSync} from 'node:fs';
|
|
2
3
|
import {join} from 'node:path';
|
|
3
4
|
import {type WorkFolderKey, workItemPath, workItemRel} from './work-layout.js';
|
|
@@ -401,9 +402,10 @@ export interface AdvanceContext {
|
|
|
401
402
|
* Resolved `strictMergeApproval` boolean (sibling task
|
|
402
403
|
* `strict-merge-approval-gate`). Default OFF ⇒ honour the prior approval +
|
|
403
404
|
* land on a green re-verify (cheap, the SPEC-applied OQ6 default). ON ⇒
|
|
404
|
-
* re-surface the merge-question when
|
|
405
|
-
*
|
|
406
|
-
* GitHub's "dismiss stale approvals when the base
|
|
405
|
+
* re-surface the merge-question when `main`'s code (outside `work/`) moved
|
|
406
|
+
* since the question was asked, i.e. since its recorded `askedAtMain` (the
|
|
407
|
+
* host-agnostic analogue of GitHub's "dismiss stale approvals when the base
|
|
408
|
+
* changes").
|
|
407
409
|
*/
|
|
408
410
|
strictMergeApproval?: boolean;
|
|
409
411
|
/**
|
|
@@ -433,6 +435,13 @@ export interface AdvanceContext {
|
|
|
433
435
|
stuckAction?: StuckActionHandler;
|
|
434
436
|
/** Sink for human-readable progress notes. */
|
|
435
437
|
note?: (message: string) => void;
|
|
438
|
+
/**
|
|
439
|
+
* The CI phase this run executes as (`--phase lock|agent|apply`, CI-only;
|
|
440
|
+
* see `phase.ts`). `undefined` ⇒ today's single process. Threaded next to
|
|
441
|
+
* `env` and `note` so the per-path phase splits can branch on it; the phase
|
|
442
|
+
* guards read the process-wide active phase.
|
|
443
|
+
*/
|
|
444
|
+
phase?: Phase;
|
|
436
445
|
}
|
|
437
446
|
|
|
438
447
|
/** The options one `advance` tick consumes. */
|
|
@@ -494,7 +503,7 @@ export interface ReadSignalsInput {
|
|
|
494
503
|
* to the `spec` sidecar type, so `advance spec:<slug>` resolves its
|
|
495
504
|
* signals/folders (`FOLDERS_FOR_TYPE['spec']`) and orchestrates `do spec:<slug>`.
|
|
496
505
|
*/
|
|
497
|
-
function sidecarTypeFor(namespace: SlugNamespace): SidecarType {
|
|
506
|
+
export function sidecarTypeFor(namespace: SlugNamespace): SidecarType {
|
|
498
507
|
return namespace === 'observation'
|
|
499
508
|
? 'observation'
|
|
500
509
|
: namespace === 'spec'
|
|
@@ -1186,8 +1195,8 @@ async function applyRung(input: RungExecInput): Promise<RungExecResult> {
|
|
|
1186
1195
|
* the item to needs-attention through its own shared seam, so `main` never
|
|
1187
1196
|
* received a failing tree. The apply rung SHORT-CIRCUITS — the sidecar is
|
|
1188
1197
|
* LEFT IN PLACE so the open answer stays surfaced for a human follow-up.
|
|
1189
|
-
* - `restale` ⇒ `strictMergeApproval` was ON and
|
|
1190
|
-
*
|
|
1198
|
+
* - `restale` ⇒ `strictMergeApproval` was ON and `main`'s code moved
|
|
1199
|
+
* since the question was asked; the apply rung appends
|
|
1191
1200
|
* a follow-up question + re-pauses (the human re-confirms against the new
|
|
1192
1201
|
* base).
|
|
1193
1202
|
* - `hold` / `drop` ⇒ no land; fall through to the normal apply path so the
|
|
@@ -1263,7 +1272,9 @@ async function maybeRunMergeAction(
|
|
|
1263
1272
|
if (result.outcome === 'restale') {
|
|
1264
1273
|
// `strictMergeApproval` re-surface: append a follow-up question and
|
|
1265
1274
|
// re-pause. The previous answer stays recorded in the entry; the human
|
|
1266
|
-
// re-confirms against the new
|
|
1275
|
+
// re-confirms against the new `main` in the appended question, which
|
|
1276
|
+
// records that `main` as its `askedAtMain` (so a re-answer while `main`
|
|
1277
|
+
// stays put lands instead of re-staling forever).
|
|
1267
1278
|
const apply = context.applyPersist ?? applyAnsweredQuestions;
|
|
1268
1279
|
try {
|
|
1269
1280
|
const applied = apply({
|
|
@@ -1273,11 +1284,12 @@ async function maybeRunMergeAction(
|
|
|
1273
1284
|
appendQuestions: [
|
|
1274
1285
|
{
|
|
1275
1286
|
question:
|
|
1276
|
-
|
|
1277
|
-
`
|
|
1287
|
+
`\`main\` moved since the merge question for \`work/${input.slug}\` ` +
|
|
1288
|
+
`was asked (strictMergeApproval is ON). Re-confirm: still land?`,
|
|
1278
1289
|
context: result.message,
|
|
1279
1290
|
default: 'merge | hold | drop',
|
|
1280
1291
|
kind: 'merge',
|
|
1292
|
+
...(result.main === undefined ? {} : {askedAtMain: result.main}),
|
|
1281
1293
|
},
|
|
1282
1294
|
],
|
|
1283
1295
|
note,
|
|
@@ -1432,6 +1444,87 @@ async function maybeRunStuckAction(
|
|
|
1432
1444
|
}
|
|
1433
1445
|
}
|
|
1434
1446
|
|
|
1447
|
+
/**
|
|
1448
|
+
* What a TREE-LESS rung needs to run on this item, read off the same state the
|
|
1449
|
+
* rung body reads (spec `ci-agent-job-without-write-token` decision 11, task
|
|
1450
|
+
* `ci-split-treeless-rungs`): the CI lock phase publishes it as `needsAgent`, and
|
|
1451
|
+
* the apply phase recomputes it on its fresh checkout. It mirrors the rung
|
|
1452
|
+
* bodies above branch for branch, so it lives next to them:
|
|
1453
|
+
*
|
|
1454
|
+
* - `triage-observation`: a note already carrying `triaged:`, or the legacy
|
|
1455
|
+
* back-fill (an engine-written `## Applied answers` record without the
|
|
1456
|
+
* marker), is deterministic; under `observationTriage: auto` the triage gate
|
|
1457
|
+
* agent runs; otherwise the surface path runs the `surface-questions` agent
|
|
1458
|
+
* unless the observation provably has nothing to ask (the surface
|
|
1459
|
+
* short-circuit, which then surfaces only the engine-built triage question);
|
|
1460
|
+
* - `surface`: the `surface-questions` agent, or the same short-circuit;
|
|
1461
|
+
* - `apply`: an answered `kind: merge` entry answered `merge` runs the agent
|
|
1462
|
+
* job (task `ci-split-answered-merge-action`: no model, but `prepare` and
|
|
1463
|
+
* `verify` run the branch's code on the rebased tip), and `hold` / `drop`
|
|
1464
|
+
* run none; the CI phases refine the `merge` case against the arbiter (a
|
|
1465
|
+
* work branch that is absent or already on `main` needs none, see
|
|
1466
|
+
* `ci-phase-treeless.ts`), which this tree-only read cannot see. An
|
|
1467
|
+
* answered `kind: stuck` entry and a task/spec content answer run no agent;
|
|
1468
|
+
* an answered observation runs the agentic decision.
|
|
1469
|
+
*
|
|
1470
|
+
* A missing item file needs no agent (the rung is a benign `vanished` skip).
|
|
1471
|
+
*/
|
|
1472
|
+
export type TreelessAgentNeed =
|
|
1473
|
+
| {needsAgent: boolean}
|
|
1474
|
+
| {unsplit: string; needsAgent?: undefined};
|
|
1475
|
+
|
|
1476
|
+
/** See {@link TreelessAgentNeed}. */
|
|
1477
|
+
export function treelessAgentNeed(input: RungExecInput): TreelessAgentNeed {
|
|
1478
|
+
const {item, context} = input;
|
|
1479
|
+
const cwd = context.cwd;
|
|
1480
|
+
const itemPath = findItemPath(cwd, input.namespace, input.slug);
|
|
1481
|
+
if (itemPath === undefined) return {needsAgent: false};
|
|
1482
|
+
const sidecarExists = existsSync(join(cwd, sidecarPathFor(item)));
|
|
1483
|
+
switch (input.classification.kind) {
|
|
1484
|
+
case 'triage-observation': {
|
|
1485
|
+
const body = readFileSync(join(cwd, itemPath), 'utf8');
|
|
1486
|
+
const fm = parseFrontmatter(body);
|
|
1487
|
+
if (fm.triaged !== undefined && fm.triaged !== '') {
|
|
1488
|
+
return {needsAgent: false};
|
|
1489
|
+
}
|
|
1490
|
+
if (hasAppliedAnswersRecord(body)) return {needsAgent: false};
|
|
1491
|
+
if (context.observationTriage === 'auto') return {needsAgent: true};
|
|
1492
|
+
return {
|
|
1493
|
+
needsAgent: !isNothingToSurfaceObservation(
|
|
1494
|
+
cwd,
|
|
1495
|
+
input,
|
|
1496
|
+
itemPath,
|
|
1497
|
+
sidecarExists,
|
|
1498
|
+
),
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
case 'surface':
|
|
1502
|
+
return {
|
|
1503
|
+
needsAgent: !isNothingToSurfaceObservation(
|
|
1504
|
+
cwd,
|
|
1505
|
+
input,
|
|
1506
|
+
itemPath,
|
|
1507
|
+
sidecarExists,
|
|
1508
|
+
),
|
|
1509
|
+
};
|
|
1510
|
+
case 'apply':
|
|
1511
|
+
const merge = detectAnsweredMergeAction(cwd, item);
|
|
1512
|
+
if (merge !== undefined) {
|
|
1513
|
+
return {needsAgent: merge.verb === 'merge'};
|
|
1514
|
+
}
|
|
1515
|
+
if (detectAnsweredStuckAction(cwd, item) !== undefined) {
|
|
1516
|
+
return {needsAgent: false};
|
|
1517
|
+
}
|
|
1518
|
+
return {
|
|
1519
|
+
needsAgent:
|
|
1520
|
+
input.namespace === 'observation' &&
|
|
1521
|
+
!(context.applyFollowups && context.applyFollowups.length > 0),
|
|
1522
|
+
};
|
|
1523
|
+
default:
|
|
1524
|
+
return {unsplit: `the ${input.classification.kind} rung`};
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1435
1528
|
/**
|
|
1436
1529
|
* The cross-tick-window BENIGN SKIP shared by all three rungs that need to
|
|
1437
1530
|
* resolve an item file (surface / triage / apply). The lifecycle pool enumerated
|
|
@@ -1462,7 +1555,7 @@ function vanishedSkip(input: {
|
|
|
1462
1555
|
* folders it may rest in (the SAME folder set {@link readNeedsAnswers} searches).
|
|
1463
1556
|
* Returns the path RELATIVE to `cwd`, or `undefined` when no file exists.
|
|
1464
1557
|
*/
|
|
1465
|
-
function findItemPath(
|
|
1558
|
+
export function findItemPath(
|
|
1466
1559
|
cwd: string,
|
|
1467
1560
|
namespace: SlugNamespace,
|
|
1468
1561
|
slug: string,
|
|
@@ -1547,8 +1640,14 @@ async function applyAgenticDecision(
|
|
|
1547
1640
|
if (verdict.outcome === 'ask') {
|
|
1548
1641
|
// ask-follow-up → the EXISTING append/re-pause loop. One BATCH of follow-ups.
|
|
1549
1642
|
const apply = context.applyPersist ?? applyAnsweredQuestions;
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1643
|
+
const questions = (
|
|
1644
|
+
verdict.questions !== undefined && verdict.questions.length > 0
|
|
1645
|
+
? verdict.questions
|
|
1646
|
+
: [verdict.question ?? '']
|
|
1647
|
+
)
|
|
1648
|
+
.map((q) => q.trim())
|
|
1649
|
+
.filter((q) => q !== '');
|
|
1650
|
+
if (questions.length === 0) {
|
|
1552
1651
|
return {
|
|
1553
1652
|
exitCode: 1,
|
|
1554
1653
|
outcome: 'usage-error',
|
|
@@ -1560,7 +1659,7 @@ async function applyAgenticDecision(
|
|
|
1560
1659
|
cwd,
|
|
1561
1660
|
item,
|
|
1562
1661
|
itemPath,
|
|
1563
|
-
appendQuestions:
|
|
1662
|
+
appendQuestions: questions.map((question) => ({question})),
|
|
1564
1663
|
note,
|
|
1565
1664
|
});
|
|
1566
1665
|
return {
|
|
@@ -1857,9 +1956,21 @@ export async function performAdvance(
|
|
|
1857
1956
|
// ITSELF acquires the SAME `task-<slug>`/`prd-<slug>` ref (the create-only CAS
|
|
1858
1957
|
// with NO re-entrancy/auto-steal, per the ADR), so taking it here too would
|
|
1859
1958
|
// DEADLOCK the tick against itself. The inner `do`'s claim/task lock IS the
|
|
1860
|
-
// single exclusion point for those rungs. The `work/advancing/<entry>.md`
|
|
1861
|
-
//
|
|
1959
|
+
// single exclusion point for those rungs. The legacy `work/advancing/<entry>.md`
|
|
1960
|
+
// marker is GONE (removed by the capstone cut-over, task
|
|
1961
|
+
// `cutover-retire-slicing-advancing-markers-and-trim-folder-sets`): there is no
|
|
1962
|
+
// marker CAS for any rung, only the per-item lock ref (see `advancing-lock.ts`).
|
|
1862
1963
|
const unifiedForRung = isTreeLessRung(classification.kind);
|
|
1964
|
+
// An answered `merge` may take over the lock a finished propose build of the
|
|
1965
|
+
// SAME item keeps held (the `propose-pr` marker; task
|
|
1966
|
+
// `wire-merge-questions-into-the-advance-tick`, decisions 2 and 4). Any other
|
|
1967
|
+
// held lock still makes this tick back off.
|
|
1968
|
+
const takeOverProposeKept = answersMerge(
|
|
1969
|
+
repoPath,
|
|
1970
|
+
item,
|
|
1971
|
+
resolved.namespace,
|
|
1972
|
+
classification,
|
|
1973
|
+
);
|
|
1863
1974
|
const acquire =
|
|
1864
1975
|
options.acquireLock ??
|
|
1865
1976
|
((lockItem: string) =>
|
|
@@ -1868,6 +1979,7 @@ export async function performAdvance(
|
|
|
1868
1979
|
cwd,
|
|
1869
1980
|
arbiter,
|
|
1870
1981
|
acquireUnified: unifiedForRung,
|
|
1982
|
+
takeOverProposeKept,
|
|
1871
1983
|
note,
|
|
1872
1984
|
}));
|
|
1873
1985
|
const lock = await acquire(item);
|
|
@@ -1965,6 +2077,24 @@ function isTreeLessRung(kind: TickClassification['kind']): boolean {
|
|
|
1965
2077
|
);
|
|
1966
2078
|
}
|
|
1967
2079
|
|
|
2080
|
+
/**
|
|
2081
|
+
* Is this tick the apply of a task whose merge question is answered `merge`
|
|
2082
|
+
* (the case that may take over a propose-kept lock)? Read off the same tree the
|
|
2083
|
+
* classification read.
|
|
2084
|
+
*/
|
|
2085
|
+
export function answersMerge(
|
|
2086
|
+
repoPath: string,
|
|
2087
|
+
item: string,
|
|
2088
|
+
namespace: SlugNamespace,
|
|
2089
|
+
classification: TickClassification,
|
|
2090
|
+
): boolean {
|
|
2091
|
+
return (
|
|
2092
|
+
classification.kind === 'apply' &&
|
|
2093
|
+
namespace === 'task' &&
|
|
2094
|
+
detectAnsweredMergeAction(repoPath, item)?.verb === 'merge'
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
1968
2098
|
/** Dispatch the classified rung to the executor seam (winner-only). */
|
|
1969
2099
|
function dispatchRung(
|
|
1970
2100
|
executor: RungExecutor,
|
package/src/advancing-lock.ts
CHANGED
|
@@ -4,7 +4,11 @@ import {runAsync, type RunResult} from './git.js';
|
|
|
4
4
|
import {ledgerWrite} from './ledger-write.js';
|
|
5
5
|
import {resolveSidecarIdentity} from './sidecar.js';
|
|
6
6
|
import {readFrontmatterField} from './frontmatter.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
acquireItemLock,
|
|
9
|
+
releaseItemLock,
|
|
10
|
+
takeOverProposeKeptLock,
|
|
11
|
+
} from './item-lock.js';
|
|
8
12
|
import {realSleep, type Sleep} from './retry-backoff.js';
|
|
9
13
|
|
|
10
14
|
/**
|
|
@@ -110,6 +114,15 @@ export interface AcquireAdvancingLockOptions {
|
|
|
110
114
|
* nothing). This module stays rung-agnostic — it only knows "unified or not".
|
|
111
115
|
*/
|
|
112
116
|
acquireUnified?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* When the unified acquire finds the item locked, TAKE OVER the lock if (and
|
|
119
|
+
* only if) a finished propose build of the same item keeps it held (its entry
|
|
120
|
+
* carries the `propose-pr` marker, see `takeOverProposeKeptLock`). The advance
|
|
121
|
+
* tick sets it for an answered `merge` (task
|
|
122
|
+
* `wire-merge-questions-into-the-advance-tick`, decisions 2 and 4); any other
|
|
123
|
+
* held lock is still `lost`. Ignored unless `acquireUnified`.
|
|
124
|
+
*/
|
|
125
|
+
takeOverProposeKept?: boolean;
|
|
113
126
|
/** Environment for child git processes. */
|
|
114
127
|
env?: NodeJS.ProcessEnv;
|
|
115
128
|
/** Sink for human-readable progress notes. */
|
|
@@ -215,6 +228,28 @@ async function runAcquire(
|
|
|
215
228
|
`failed to acquire the item lock for '${entry}': ${lock.message}`,
|
|
216
229
|
);
|
|
217
230
|
}
|
|
231
|
+
if (lock.outcome === 'lost' && options.takeOverProposeKept === true) {
|
|
232
|
+
const taken = await takeOverProposeKeptLock({
|
|
233
|
+
item: options.item,
|
|
234
|
+
action: 'advance',
|
|
235
|
+
cwd,
|
|
236
|
+
arbiter,
|
|
237
|
+
holder: by,
|
|
238
|
+
env,
|
|
239
|
+
});
|
|
240
|
+
if (taken.outcome === 'error') {
|
|
241
|
+
throw new AdvancingLockUsageError(
|
|
242
|
+
`failed to take over the item lock for '${entry}': ${taken.message}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (taken.outcome === 'acquired') {
|
|
246
|
+
const message = `LOCKED '${entry}' for advancing on ${arbiter}: ${taken.message}.`;
|
|
247
|
+
note(message);
|
|
248
|
+
return {exitCode: 0, outcome: 'acquired', message, entry};
|
|
249
|
+
}
|
|
250
|
+
note(taken.message);
|
|
251
|
+
return {exitCode: 2, outcome: 'lost', message: taken.message, entry};
|
|
252
|
+
}
|
|
218
253
|
if (lock.outcome === 'lost') {
|
|
219
254
|
note(lock.message);
|
|
220
255
|
return {exitCode: 2, outcome: 'lost', message: lock.message, entry};
|
package/src/agent-env.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **No GitHub token in a CI agent's environment.**
|
|
3
|
+
*
|
|
4
|
+
* In CI, dorfl runs an agent (with shell) over text any GitHub user can write:
|
|
5
|
+
* an issue body, a comment. Whatever the agent's environment holds, a prompt
|
|
6
|
+
* injection in that text can ask it to print. So every autonomous agent launch
|
|
7
|
+
* in GitHub Actions gets its environment with the GitHub credentials removed
|
|
8
|
+
* ({@link scrubAgentEnv}), whatever the workflow put in the step environment.
|
|
9
|
+
*
|
|
10
|
+
* This is only the cheap layer. It does not stop an agent from reading the
|
|
11
|
+
* token another way while it shares a job with one (its parent processes'
|
|
12
|
+
* environment, `.git/config` when the checkout persists a credential, `sudo` on
|
|
13
|
+
* a hosted runner). The real boundary is running the agent in a job whose
|
|
14
|
+
* token cannot write, see spec `ci-agent-job-without-write-token` (under
|
|
15
|
+
* `work/specs/`).
|
|
16
|
+
*
|
|
17
|
+
* On a laptop nothing is removed: it is the human's own machine, and a local
|
|
18
|
+
* tool the agent uses (an MCP server, a `gh` call) may legitimately need the
|
|
19
|
+
* human's token.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Names that carry a GitHub credential by convention (case-insensitive):
|
|
24
|
+
* `GH_TOKEN`, `GITHUB_TOKEN`, any `GH_*_TOKEN` / `GITHUB_*_TOKEN` (e.g.
|
|
25
|
+
* `GH_ENTERPRISE_TOKEN`), and the dorfl-specific `DORFL_GH_TOKEN`,
|
|
26
|
+
* `DORFL_GIT_TOKEN`, `DORFL_GITHUB_TOKEN` with any infix
|
|
27
|
+
* (`DORFL_*_GH_TOKEN` / `DORFL_*_GIT_TOKEN` / `DORFL_*_GITHUB_TOKEN`).
|
|
28
|
+
*/
|
|
29
|
+
const GITHUB_TOKEN_NAME =
|
|
30
|
+
/^(?:(?:GH|GITHUB)_(?:\w+_)?TOKEN|DORFL_(?:\w+_)?(?:GH|GIT|GITHUB)_TOKEN)$/i;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The GitHub Actions runtime credentials a step can see: the OIDC request pair
|
|
34
|
+
* (it mints an ID token, which a cloud role may trust for writes) and the
|
|
35
|
+
* runtime token (artifact/cache service).
|
|
36
|
+
*/
|
|
37
|
+
const ACTIONS_RUNTIME_NAME =
|
|
38
|
+
/^ACTIONS_(?:ID_TOKEN_REQUEST_(?:TOKEN|URL)|RUNTIME_TOKEN)$/i;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The runner's workflow-command FILES. Not credentials, but writing to them
|
|
42
|
+
* injects environment variables, PATH entries or outputs into LATER steps of
|
|
43
|
+
* the job, which may hold a token. The agent has no use for them.
|
|
44
|
+
*/
|
|
45
|
+
const RUNNER_COMMAND_FILES = new Set([
|
|
46
|
+
'GITHUB_ENV',
|
|
47
|
+
'GITHUB_PATH',
|
|
48
|
+
'GITHUB_OUTPUT',
|
|
49
|
+
'GITHUB_STATE',
|
|
50
|
+
'GITHUB_STEP_SUMMARY',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* GitHub token VALUE shapes, anywhere in a value (a token inside a URL counts):
|
|
55
|
+
* personal (`ghp_`), OAuth (`gho_`), user-to-server (`ghu_`), server-to-server
|
|
56
|
+
* incl. the Actions `GITHUB_TOKEN` (`ghs_`), refresh (`ghr_`) and fine-grained
|
|
57
|
+
* PATs (`github_pat_`). Matching on the value catches a token the workflow
|
|
58
|
+
* exported under ANY name, which a name list alone would miss.
|
|
59
|
+
*/
|
|
60
|
+
const GITHUB_TOKEN_VALUE =
|
|
61
|
+
/(?:^|[^A-Za-z0-9_])(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/;
|
|
62
|
+
|
|
63
|
+
/** `GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_<n>` / `GIT_CONFIG_VALUE_<n>`. */
|
|
64
|
+
const GIT_CONFIG_ENV_ENTRY = /^GIT_CONFIG_(?:COUNT|KEY_\d+|VALUE_\d+)$/;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Git config text that carries or fetches a credential: an HTTP auth header, a
|
|
68
|
+
* credential helper or askpass command, or an HTTP(S) URL with userinfo
|
|
69
|
+
* (`https://user:token@host`). A plain `insteadOf` rewrite is NOT matched,
|
|
70
|
+
* including an SSH one (`ssh://git@github.com/`, whose userinfo is no secret).
|
|
71
|
+
*/
|
|
72
|
+
const CREDENTIAL_CONFIG_TEXT =
|
|
73
|
+
/extraheader|authorization|credential\.|askpass|https?:\/\/[^/@\s]+@/i;
|
|
74
|
+
|
|
75
|
+
/** The result of {@link scrubAgentEnv}. */
|
|
76
|
+
export interface ScrubbedEnv {
|
|
77
|
+
/** The environment to launch the child with (a COPY; the input is untouched). */
|
|
78
|
+
env: NodeJS.ProcessEnv;
|
|
79
|
+
/** The NAMES that were removed (never the values), sorted, for diagnostics. */
|
|
80
|
+
removed: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Should this single variable be withheld from an agent? True for the GitHub
|
|
85
|
+
* token names, the Actions runtime credentials, the runner command files, and
|
|
86
|
+
* any variable whose VALUE contains a GitHub token.
|
|
87
|
+
*/
|
|
88
|
+
export function isWriteCredentialVar(
|
|
89
|
+
name: string,
|
|
90
|
+
value: string | undefined,
|
|
91
|
+
): boolean {
|
|
92
|
+
if (GITHUB_TOKEN_NAME.test(name) || ACTIONS_RUNTIME_NAME.test(name)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (RUNNER_COMMAND_FILES.has(name.toUpperCase())) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return value !== undefined && GITHUB_TOKEN_VALUE.test(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Return a copy of `env` with every GitHub credential removed: the variables
|
|
103
|
+
* {@link isWriteCredentialVar} flags, any OTHER variable holding the SAME value
|
|
104
|
+
* as one of those (an alias such as `TOKEN=$GH_TOKEN` of a token whose shape is
|
|
105
|
+
* not recognised, e.g. a GitHub Enterprise Server token), and git's environment
|
|
106
|
+
* config (`GIT_CONFIG_PARAMETERS`, `GIT_CONFIG_COUNT/KEY_n/VALUE_n`) when it
|
|
107
|
+
* carries a credential. The `GIT_CONFIG_COUNT/KEY_n/VALUE_n` block is removed
|
|
108
|
+
* WHOLE whenever any part of it is: removing one entry would leave a `COUNT`
|
|
109
|
+
* pointing at a missing key, which makes every git command fail.
|
|
110
|
+
*
|
|
111
|
+
* A denylist on purpose: the agent needs an open-ended set of variables (PATH,
|
|
112
|
+
* HOME, the provider key whose name the consumer chooses, node/pnpm/proxy
|
|
113
|
+
* settings), and an allowlist would break a consumer's harness.
|
|
114
|
+
*/
|
|
115
|
+
export function scrubAgentEnv(env: NodeJS.ProcessEnv): ScrubbedEnv {
|
|
116
|
+
const out: NodeJS.ProcessEnv = {...env};
|
|
117
|
+
const removed = new Set<string>();
|
|
118
|
+
const secretValues = new Set<string>();
|
|
119
|
+
for (const [name, value] of Object.entries(env)) {
|
|
120
|
+
if (isWriteCredentialVar(name, value)) {
|
|
121
|
+
delete out[name];
|
|
122
|
+
removed.add(name);
|
|
123
|
+
// Only a value long enough to be a secret is used for alias matching, so
|
|
124
|
+
// an empty or trivially short value cannot knock out unrelated variables.
|
|
125
|
+
if (value !== undefined && value.length >= 16) {
|
|
126
|
+
secretValues.add(value);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const [name, value] of Object.entries(out)) {
|
|
131
|
+
if (value !== undefined && secretValues.has(value)) {
|
|
132
|
+
delete out[name];
|
|
133
|
+
removed.add(name);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Judge the block on the ORIGINAL env: an entry already removed above still
|
|
137
|
+
// condemns the whole block.
|
|
138
|
+
const gitConfigEntries = Object.keys(env).filter((k) =>
|
|
139
|
+
GIT_CONFIG_ENV_ENTRY.test(k),
|
|
140
|
+
);
|
|
141
|
+
if (
|
|
142
|
+
gitConfigEntries.some(
|
|
143
|
+
(k) => removed.has(k) || CREDENTIAL_CONFIG_TEXT.test(env[k] ?? ''),
|
|
144
|
+
)
|
|
145
|
+
) {
|
|
146
|
+
for (const k of gitConfigEntries) {
|
|
147
|
+
delete out[k];
|
|
148
|
+
removed.add(k);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (CREDENTIAL_CONFIG_TEXT.test(out.GIT_CONFIG_PARAMETERS ?? '')) {
|
|
152
|
+
delete out.GIT_CONFIG_PARAMETERS;
|
|
153
|
+
removed.add('GIT_CONFIG_PARAMETERS');
|
|
154
|
+
}
|
|
155
|
+
return {env: out, removed: [...removed].sort()};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Is dorfl running in GitHub Actions (where agents lose the GitHub tokens)?
|
|
160
|
+
* Checks the process env too, so a caller that builds a child env from scratch
|
|
161
|
+
* (without `GITHUB_ACTIONS`) cannot switch the filter off by accident.
|
|
162
|
+
*/
|
|
163
|
+
export function inGitHubActions(env: NodeJS.ProcessEnv): boolean {
|
|
164
|
+
return env.GITHUB_ACTIONS === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The environment an autonomous agent is launched with: in GitHub Actions, the
|
|
169
|
+
* caller's env with every GitHub credential removed ({@link scrubAgentEnv}); on
|
|
170
|
+
* a laptop, the caller's env unchanged.
|
|
171
|
+
*/
|
|
172
|
+
export function agentEnvFor(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
173
|
+
return inGitHubActions(env) ? scrubAgentEnv(env).env : env;
|
|
174
|
+
}
|
package/src/agent-stop.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {runAsync} from './git.js';
|
|
2
2
|
import {JOB_RECORD_FILENAME} from './workspace.js';
|
|
3
3
|
import type {NewQuestion} from './sidecar.js';
|
|
4
|
+
import type {CutOffTurn} from './harness.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* **The build-agent → runner REPORTING CHANNEL** (task `agent-stop-signal`).
|
|
@@ -363,6 +364,86 @@ export function emptyDiffStopReason(slug: string): string {
|
|
|
363
364
|
);
|
|
364
365
|
}
|
|
365
366
|
|
|
367
|
+
/**
|
|
368
|
+
* The provider error text is capped at this many characters in the failure
|
|
369
|
+
* detail: pi records the raw provider body (often a JSON blob), and the detail
|
|
370
|
+
* becomes the needs-attention reason on the lock, which must stay one readable
|
|
371
|
+
* line rather than a pasted payload.
|
|
372
|
+
*/
|
|
373
|
+
const MAX_CUT_OFF_ERROR_LENGTH = 300;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The `agent-failed` detail for a run whose FINAL model turn was CUT OFF
|
|
377
|
+
* ({@link CutOffTurn}) and which left no source change (task
|
|
378
|
+
* `a-truncated-agent-turn-routes-as-agent-failed`). Names the stop cause so the
|
|
379
|
+
* operator reads a harness failure, never "the agent found nothing to build".
|
|
380
|
+
* The provider's `errorMessage` (an `error` stop) is appended VERBATIM (bounded)
|
|
381
|
+
* so the shared failure-cause classifier can still recognise an infra signature
|
|
382
|
+
* in it (an overloaded provider reads `transient-infra`).
|
|
383
|
+
*/
|
|
384
|
+
export function cutOffTurnFailureDetail(
|
|
385
|
+
slug: string,
|
|
386
|
+
cutOff: CutOffTurn,
|
|
387
|
+
): string {
|
|
388
|
+
const cause =
|
|
389
|
+
cutOff.cause === 'length'
|
|
390
|
+
? "stopReason 'length': the turn hit the model's per-turn output-token cap"
|
|
391
|
+
: "stopReason 'error': the provider ended the turn with an error";
|
|
392
|
+
const error =
|
|
393
|
+
cutOff.errorMessage === undefined
|
|
394
|
+
? ''
|
|
395
|
+
: ` (${boundErrorMessage(cutOff.errorMessage)})`;
|
|
396
|
+
return (
|
|
397
|
+
`the agent's final model turn was cut off (${cause}${error}) before it ` +
|
|
398
|
+
`produced any source change building '${slug}'; this is a harness failure, ` +
|
|
399
|
+
'not a judgement that there is nothing to build. Requeue to retry.'
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function boundErrorMessage(text: string): string {
|
|
404
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
405
|
+
return oneLine.length <= MAX_CUT_OFF_ERROR_LENGTH
|
|
406
|
+
? oneLine
|
|
407
|
+
: `${oneLine.slice(0, MAX_CUT_OFF_ERROR_LENGTH)}…`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The CUT-OFF-TURN guard in front of the empty-diff STOP backstop (task
|
|
412
|
+
* `a-truncated-agent-turn-routes-as-agent-failed`): returns the `agent-failed`
|
|
413
|
+
* detail when the run's final model turn was cut off ({@link CutOffTurn}), the
|
|
414
|
+
* agent raised NO in-band STOP sentinel, AND the work branch is empty; else
|
|
415
|
+
* `undefined` (the caller proceeds to its usual STOP resolution / gate).
|
|
416
|
+
*
|
|
417
|
+
* Only a NORMAL end of turn may reach the empty-diff backstop: that route reads
|
|
418
|
+
* an empty diff as the agent's deliberate "nothing to build" and surfaces a
|
|
419
|
+
* DISPOSE-defaulted question, which for a truncated turn puts a real, unbuilt
|
|
420
|
+
* task one default answer away from `tasks/cancelled/`. A sentinel still wins
|
|
421
|
+
* (the agent DID report a deliberate STOP), and a non-empty diff still goes to
|
|
422
|
+
* the gate (the gate, not the stop reason, judges built work). Shared by `do`
|
|
423
|
+
* (in-place and remote, hence the CI agent phase) and `run` so every path with
|
|
424
|
+
* the empty-diff backstop applies the same rule.
|
|
425
|
+
*/
|
|
426
|
+
export async function resolveCutOffTurnFailure(params: {
|
|
427
|
+
cutOffTurn: CutOffTurn | undefined;
|
|
428
|
+
output: string | undefined;
|
|
429
|
+
slug: string;
|
|
430
|
+
cwd: string;
|
|
431
|
+
arbiter: string;
|
|
432
|
+
env?: NodeJS.ProcessEnv;
|
|
433
|
+
}): Promise<string | undefined> {
|
|
434
|
+
const {cutOffTurn, output, slug, cwd, arbiter, env} = params;
|
|
435
|
+
if (cutOffTurn === undefined) {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
if (parseStopSentinel(output) !== undefined) {
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
if (!(await isWorkBranchDiffEmpty({cwd, arbiter, env}))) {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
return cutOffTurnFailureDetail(slug, cutOffTurn);
|
|
445
|
+
}
|
|
446
|
+
|
|
366
447
|
/**
|
|
367
448
|
* The DISCRIMINATED classification of a `saveAgentStop` reason (spec
|
|
368
449
|
* `surface-stuck-as-questions-and-retire-stuck-lock-state` resolved decision #2,
|