dorfl 0.1.2 → 0.2.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-drivers.d.ts +1 -1
- package/dist/advance-drivers.d.ts.map +1 -1
- package/dist/advance-drivers.js +15 -3
- package/dist/advance-drivers.js.map +1 -1
- package/dist/advance-lifecycle-template.d.ts.map +1 -1
- package/dist/advance-lifecycle-template.js +76 -4
- package/dist/advance-lifecycle-template.js.map +1 -1
- package/dist/advance-treeless-publish.d.ts +23 -0
- package/dist/advance-treeless-publish.d.ts.map +1 -1
- package/dist/advance-treeless-publish.js +41 -0
- package/dist/advance-treeless-publish.js.map +1 -1
- package/dist/advance.d.ts +59 -7
- package/dist/advance.d.ts.map +1 -1
- package/dist/advance.js +389 -82
- package/dist/advance.js.map +1 -1
- package/dist/advancing-lock.d.ts +30 -2
- package/dist/advancing-lock.d.ts.map +1 -1
- package/dist/advancing-lock.js +50 -0
- package/dist/advancing-lock.js.map +1 -1
- package/dist/agent-launch.d.ts +12 -0
- package/dist/agent-launch.d.ts.map +1 -1
- package/dist/agent-launch.js +22 -12
- package/dist/agent-launch.js.map +1 -1
- package/dist/agent-stop.d.ts +40 -2
- package/dist/agent-stop.d.ts.map +1 -1
- package/dist/agent-stop.js +30 -2
- package/dist/agent-stop.js.map +1 -1
- package/dist/apply-decide.d.ts +17 -3
- package/dist/apply-decide.d.ts.map +1 -1
- package/dist/apply-decide.js +36 -7
- package/dist/apply-decide.js.map +1 -1
- package/dist/apply-merge-action.d.ts +17 -5
- package/dist/apply-merge-action.d.ts.map +1 -1
- package/dist/apply-merge-action.js +43 -8
- package/dist/apply-merge-action.js.map +1 -1
- package/dist/apply-persist.d.ts +57 -26
- package/dist/apply-persist.d.ts.map +1 -1
- package/dist/apply-persist.js +166 -37
- package/dist/apply-persist.js.map +1 -1
- package/dist/apply-stuck-action.d.ts +151 -0
- package/dist/apply-stuck-action.d.ts.map +1 -0
- package/dist/apply-stuck-action.js +125 -0
- package/dist/apply-stuck-action.js.map +1 -0
- package/dist/brand.d.ts +12 -1
- package/dist/brand.d.ts.map +1 -1
- package/dist/brand.js +2 -1
- package/dist/brand.js.map +1 -1
- package/dist/cli-spinner.d.ts +1 -1
- package/dist/cli-spinner.js +1 -1
- package/dist/cli.d.ts +10 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +320 -63
- package/dist/cli.js.map +1 -1
- package/dist/close-job-template.js +1 -1
- package/dist/complete.d.ts +6 -3
- package/dist/complete.d.ts.map +1 -1
- package/dist/complete.js +91 -37
- package/dist/complete.js.map +1 -1
- package/dist/config.d.ts +55 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +56 -4
- package/dist/config.js.map +1 -1
- package/dist/continue-branch.d.ts.map +1 -1
- package/dist/continue-branch.js +22 -0
- package/dist/continue-branch.js.map +1 -1
- package/dist/decision-engine.d.ts +38 -10
- package/dist/decision-engine.d.ts.map +1 -1
- package/dist/decision-engine.js +10 -6
- package/dist/decision-engine.js.map +1 -1
- package/dist/do-autopick.d.ts.map +1 -1
- package/dist/do-autopick.js +11 -1
- package/dist/do-autopick.js.map +1 -1
- package/dist/do-config.d.ts +1 -1
- package/dist/do.d.ts +49 -56
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +419 -30
- package/dist/do.js.map +1 -1
- package/dist/env-config.d.ts.map +1 -1
- package/dist/env-config.js +12 -4
- package/dist/env-config.js.map +1 -1
- package/dist/failure-cause.d.ts +3 -2
- package/dist/failure-cause.d.ts.map +1 -1
- package/dist/failure-cause.js +28 -1
- package/dist/failure-cause.js.map +1 -1
- package/dist/format.d.ts +6 -6
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +10 -27
- package/dist/format.js.map +1 -1
- package/dist/frontmatter.d.ts +16 -4
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +27 -1
- package/dist/frontmatter.js.map +1 -1
- package/dist/gc.d.ts +65 -6
- package/dist/gc.d.ts.map +1 -1
- package/dist/gc.js +126 -9
- package/dist/gc.js.map +1 -1
- package/dist/github.d.ts +14 -0
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +73 -0
- package/dist/github.js.map +1 -1
- package/dist/harness.d.ts +25 -0
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/install-ci-branch-protection.d.ts +132 -38
- package/dist/install-ci-branch-protection.d.ts.map +1 -1
- package/dist/install-ci-branch-protection.js +190 -48
- package/dist/install-ci-branch-protection.js.map +1 -1
- package/dist/install-ci-core.d.ts +21 -1
- package/dist/install-ci-core.d.ts.map +1 -1
- package/dist/install-ci-core.js +10 -3
- package/dist/install-ci-core.js.map +1 -1
- package/dist/install-ci-github.d.ts +32 -0
- package/dist/install-ci-github.d.ts.map +1 -1
- package/dist/install-ci-github.js +73 -0
- package/dist/install-ci-github.js.map +1 -1
- package/dist/install-skills.d.ts +123 -0
- package/dist/install-skills.d.ts.map +1 -0
- package/dist/install-skills.js +100 -0
- package/dist/install-skills.js.map +1 -0
- package/dist/intake-trigger-template.js +1 -1
- package/dist/intake.d.ts +17 -4
- package/dist/intake.d.ts.map +1 -1
- package/dist/intake.js +23 -7
- package/dist/intake.js.map +1 -1
- package/dist/integration-core.d.ts +10 -7
- package/dist/integration-core.d.ts.map +1 -1
- package/dist/integration-core.js +174 -60
- package/dist/integration-core.js.map +1 -1
- package/dist/integrator.d.ts +1 -1
- package/dist/integrator.d.ts.map +1 -1
- package/dist/integrator.js +19 -3
- package/dist/integrator.js.map +1 -1
- package/dist/isolation.d.ts +2 -2
- package/dist/isolation.d.ts.map +1 -1
- package/dist/isolation.js +9 -0
- package/dist/isolation.js.map +1 -1
- package/dist/item-lock.d.ts +196 -129
- package/dist/item-lock.d.ts.map +1 -1
- package/dist/item-lock.js +329 -254
- package/dist/item-lock.js.map +1 -1
- package/dist/ledger-write.d.ts +31 -26
- package/dist/ledger-write.d.ts.map +1 -1
- package/dist/ledger-write.js +100 -122
- package/dist/ledger-write.js.map +1 -1
- package/dist/lifecycle-gather.d.ts +17 -0
- package/dist/lifecycle-gather.d.ts.map +1 -1
- package/dist/lifecycle-gather.js +7 -1
- package/dist/lifecycle-gather.js.map +1 -1
- package/dist/lifecycle-pools.d.ts +40 -3
- package/dist/lifecycle-pools.d.ts.map +1 -1
- package/dist/lifecycle-pools.js +26 -6
- package/dist/lifecycle-pools.js.map +1 -1
- package/dist/merge-question-surfacer.d.ts +15 -1
- package/dist/merge-question-surfacer.d.ts.map +1 -1
- package/dist/merge-question-surfacer.js +18 -3
- package/dist/merge-question-surfacer.js.map +1 -1
- package/dist/migrate-stuck-locks.d.ts +129 -0
- package/dist/migrate-stuck-locks.d.ts.map +1 -0
- package/dist/migrate-stuck-locks.js +355 -0
- package/dist/migrate-stuck-locks.js.map +1 -0
- package/dist/needs-attention.d.ts +293 -16
- package/dist/needs-attention.d.ts.map +1 -1
- package/dist/needs-attention.js +532 -57
- package/dist/needs-attention.js.map +1 -1
- package/dist/orphan-sidecar.d.ts +8 -4
- package/dist/orphan-sidecar.d.ts.map +1 -1
- package/dist/orphan-sidecar.js +35 -2
- package/dist/orphan-sidecar.js.map +1 -1
- package/dist/pi-harness.d.ts +16 -0
- package/dist/pi-harness.d.ts.map +1 -1
- package/dist/pi-harness.js +82 -2
- package/dist/pi-harness.js.map +1 -1
- package/dist/placement.d.ts +3 -3
- package/dist/placement.js +1 -1
- package/dist/prd-to-spec.d.ts.map +1 -1
- package/dist/prd-to-spec.js +9 -5
- package/dist/prd-to-spec.js.map +1 -1
- package/dist/protocol/CLAIM-PROTOCOL.md +17 -10
- package/dist/protocol/REVIEW-PROTOCOL.md +4 -1
- package/dist/protocol/SURFACE-PROTOCOL.md +16 -2
- package/dist/protocol/TASKING-PROTOCOL.md +3 -1
- package/dist/protocol/WORK-CONTRACT.md +22 -18
- package/dist/protocol/task-template.md +1 -1
- package/dist/reap-branches.d.ts +12 -9
- package/dist/reap-branches.d.ts.map +1 -1
- package/dist/reap-branches.js +25 -7
- package/dist/reap-branches.js.map +1 -1
- package/dist/recover-isolated.d.ts +12 -0
- package/dist/recover-isolated.d.ts.map +1 -1
- package/dist/recover-isolated.js +5 -0
- package/dist/recover-isolated.js.map +1 -1
- package/dist/repo-config.d.ts +23 -2
- package/dist/repo-config.d.ts.map +1 -1
- package/dist/repo-config.js +53 -7
- package/dist/repo-config.js.map +1 -1
- package/dist/repo-mirror.d.ts.map +1 -1
- package/dist/repo-mirror.js +18 -2
- package/dist/repo-mirror.js.map +1 -1
- package/dist/review-verdict.js +1 -1
- package/dist/review-verdict.js.map +1 -1
- package/dist/run.d.ts +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +54 -19
- package/dist/run.js.map +1 -1
- package/dist/scan.d.ts +17 -8
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +22 -8
- package/dist/scan.js.map +1 -1
- package/dist/select-priority.d.ts +2 -2
- package/dist/select-priority.js +1 -1
- package/dist/select-priority.js.map +1 -1
- package/dist/sidecar-apply.js +1 -1
- package/dist/sidecar-apply.js.map +1 -1
- package/dist/sidecar.d.ts +45 -10
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +82 -2
- package/dist/sidecar.js.map +1 -1
- package/dist/skills/answer-questions/SKILL.md +89 -0
- package/dist/skills/capture-signal/SKILL.md +52 -0
- package/dist/skills/convert-from-prd-to-spec/SKILL.md +90 -0
- package/dist/skills/drive-tasks/SKILL.md +218 -0
- package/dist/skills/from-idea/SKILL.md +83 -0
- package/dist/skills/merge-prs/SKILL.md +70 -0
- package/dist/skills/orchestrate/SKILL.md +101 -0
- package/dist/skills/promote/SKILL.md +35 -0
- package/dist/skills/review/SKILL.md +16 -0
- package/dist/skills/setup/SKILL.md +258 -0
- package/dist/skills/setup/protocol/ADR-FORMAT.md +47 -0
- package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +224 -0
- package/dist/skills/setup/protocol/REVIEW-PROTOCOL.md +122 -0
- package/dist/skills/setup/protocol/SURFACE-PROTOCOL.md +135 -0
- package/dist/skills/setup/protocol/TASKING-PROTOCOL.md +124 -0
- package/dist/skills/setup/protocol/WORK-CONTRACT.md +280 -0
- package/dist/skills/setup/protocol/spec-template.md +71 -0
- package/dist/skills/setup/protocol/task-template.md +65 -0
- package/dist/skills/surface-questions/SKILL.md +16 -0
- package/dist/skills/to-spec/SKILL.md +34 -0
- package/dist/skills/to-task/SKILL.md +19 -0
- package/dist/skills/triage-observations/SKILL.md +78 -0
- package/dist/skills/work/SKILL.md +51 -0
- package/dist/slug-namespace.d.ts +5 -5
- package/dist/slug-namespace.js +5 -5
- package/dist/slug-namespace.js.map +1 -1
- package/dist/spec-complete.d.ts.map +1 -1
- package/dist/spec-complete.js +2 -3
- package/dist/spec-complete.js.map +1 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +51 -57
- package/dist/start.js.map +1 -1
- package/dist/surface-gate.d.ts.map +1 -1
- package/dist/surface-gate.js +10 -3
- package/dist/surface-gate.js.map +1 -1
- package/dist/surface-persist.js +1 -1
- package/dist/surface-persist.js.map +1 -1
- package/dist/tasker-review-loop.d.ts +4 -3
- package/dist/tasker-review-loop.d.ts.map +1 -1
- package/dist/tasker-review-loop.js.map +1 -1
- package/dist/tasking-lock.d.ts +2 -2
- package/dist/tasking-lock.d.ts.map +1 -1
- package/dist/tasking-lock.js +53 -32
- package/dist/tasking-lock.js.map +1 -1
- package/dist/tasking.d.ts +40 -6
- package/dist/tasking.d.ts.map +1 -1
- package/dist/tasking.js +224 -12
- package/dist/tasking.js.map +1 -1
- package/dist/triage-persist.d.ts +10 -6
- package/dist/triage-persist.d.ts.map +1 -1
- package/dist/triage-persist.js +34 -4
- package/dist/triage-persist.js.map +1 -1
- package/dist/vendor/incur/agents.d.ts +58 -0
- package/dist/vendor/incur/agents.d.ts.map +1 -0
- package/dist/vendor/incur/agents.js +343 -0
- package/dist/vendor/incur/agents.js.map +1 -0
- package/dist/verify-workflow-template.js +1 -1
- package/dist/watch-session.d.ts +11 -3
- package/dist/watch-session.d.ts.map +1 -1
- package/dist/watch-session.js +94 -7
- package/dist/watch-session.js.map +1 -1
- package/dist/work-layout.d.ts +7 -3
- package/dist/work-layout.d.ts.map +1 -1
- package/dist/work-layout.js +6 -3
- package/dist/work-layout.js.map +1 -1
- package/dist/workspace.d.ts +1 -1
- package/package.json +2 -2
- package/src/advance-drivers.ts +16 -3
- package/src/advance-lifecycle-template.ts +89 -4
- package/src/advance-treeless-publish.ts +45 -0
- package/src/advance.ts +467 -87
- package/src/advancing-lock.ts +96 -1
- package/src/agent-launch.ts +37 -12
- package/src/agent-stop.ts +60 -2
- package/src/apply-decide.ts +36 -7
- package/src/apply-merge-action.ts +45 -7
- package/src/apply-persist.ts +228 -54
- package/src/apply-stuck-action.ts +260 -0
- package/src/brand.ts +14 -2
- package/src/cli-spinner.ts +1 -1
- package/src/cli.ts +401 -70
- package/src/close-job-template.ts +1 -1
- package/src/complete.ts +110 -52
- package/src/config.ts +107 -13
- package/src/continue-branch.ts +22 -0
- package/src/decision-engine.ts +55 -17
- package/src/do-autopick.ts +12 -1
- package/src/do-config.ts +1 -1
- package/src/do.ts +511 -38
- package/src/env-config.ts +12 -4
- package/src/failure-cause.ts +30 -1
- package/src/format.ts +10 -30
- package/src/frontmatter.ts +35 -5
- package/src/gc.ts +172 -9
- package/src/github.ts +78 -0
- package/src/harness.ts +25 -0
- package/src/index.ts +10 -0
- package/src/install-ci-branch-protection.ts +282 -57
- package/src/install-ci-core.ts +30 -3
- package/src/install-ci-github.ts +86 -0
- package/src/install-skills.ts +166 -0
- package/src/intake-trigger-template.ts +1 -1
- package/src/intake.ts +46 -11
- package/src/integration-core.ts +199 -65
- package/src/integrator.ts +22 -9
- package/src/isolation.ts +11 -2
- package/src/item-lock.ts +485 -341
- package/src/ledger-write.ts +135 -150
- package/src/lifecycle-gather.ts +29 -6
- package/src/lifecycle-pools.ts +67 -9
- package/src/merge-question-surfacer.ts +28 -4
- package/src/migrate-stuck-locks.ts +451 -0
- package/src/needs-attention.ts +857 -84
- package/src/orphan-sidecar.ts +47 -6
- package/src/pi-harness.ts +82 -2
- package/src/placement.ts +3 -3
- package/src/prd-to-spec.ts +13 -5
- package/src/reap-branches.ts +38 -14
- package/src/recover-isolated.ts +17 -0
- package/src/repo-config.ts +55 -6
- package/src/repo-mirror.ts +23 -1
- package/src/review-verdict.ts +1 -1
- package/src/run.ts +64 -18
- package/src/scan.ts +22 -8
- package/src/select-priority.ts +3 -3
- package/src/sidecar-apply.ts +1 -1
- package/src/sidecar.ts +138 -11
- package/src/slug-namespace.ts +7 -7
- package/src/spec-complete.ts +2 -3
- package/src/start.ts +53 -59
- package/src/surface-gate.ts +10 -3
- package/src/surface-persist.ts +1 -1
- package/src/tasker-review-loop.ts +4 -3
- package/src/tasking-lock.ts +52 -36
- package/src/tasking.ts +261 -17
- package/src/triage-persist.ts +56 -12
- package/src/vendor/incur/LICENSE +21 -0
- package/src/vendor/incur/README.md +19 -0
- package/src/vendor/incur/agents.ts +392 -0
- package/src/verify-workflow-template.ts +1 -1
- package/src/watch-session.ts +102 -7
- package/src/work-layout.ts +6 -3
- package/src/workspace.ts +1 -1
package/dist/needs-attention.js
CHANGED
|
@@ -3,10 +3,12 @@ import { tmpdir } from 'node:os';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { workFolderPrefix, workFolderRel, workItemRel, isWorkItemFile, } from './work-layout.js';
|
|
5
5
|
import { run, runAsync } from './git.js';
|
|
6
|
-
import { branchAheadOf } from './continue-branch.js';
|
|
6
|
+
import { branchAheadOf, rebaseContinuedBranchOntoMain, } from './continue-branch.js';
|
|
7
7
|
import { acquireItemLock, releaseItemLock, readItemLock, itemLockRef, lockEntryFor, parseLockEntry, } from './item-lock.js';
|
|
8
8
|
import { ledgerWrite } from './ledger-write.js';
|
|
9
9
|
import { workBranchRef } from './slug-namespace.js';
|
|
10
|
+
import { appendQuestions, newSidecar, parseSidecar, resolveSidecarIdentity, serialiseSidecar, sidecarPathFor, } from './sidecar.js';
|
|
11
|
+
import { parseFrontmatter, setNeedsAnswersMarker } from './frontmatter.js';
|
|
10
12
|
import { retryWithBackoff, realSleep, } from './retry-backoff.js';
|
|
11
13
|
/**
|
|
12
14
|
* The **needs-attention mechanism** (ADR `ledger-status-on-per-item-lock-refs`;
|
|
@@ -146,7 +148,7 @@ export async function routeToNeedsAttention(options) {
|
|
|
146
148
|
}
|
|
147
149
|
/**
|
|
148
150
|
* The clean re-queue (ADR §12 / WORK-CONTRACT return path): once the human has
|
|
149
|
-
* resolved the cause, move the stuck item back to `work/
|
|
151
|
+
* resolved the cause, move the stuck item back to `work/tasks/ready/<slug>.md` and
|
|
150
152
|
* commit it so the item can be re-claimed (it must not rot stuck). It recovers a
|
|
151
153
|
* task stuck in EITHER `work/needs-attention/<slug>.md` (the resolved-surface
|
|
152
154
|
* path) OR `work/in-progress/<slug>.md` (a claim that never surfaced — an
|
|
@@ -162,12 +164,26 @@ export async function routeToNeedsAttention(options) {
|
|
|
162
164
|
* it is the durable artifact the next claim CONTINUES from (the continue-
|
|
163
165
|
* detection in `continue-branch.ts` feeds both onboarding paths). This
|
|
164
166
|
* function only does the ledger move.
|
|
165
|
-
* - **`--
|
|
166
|
-
* `
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
167
|
+
* - **`--reconcile` = NON-DESTRUCTIVE RECOVERY (middle rung).** When
|
|
168
|
+
* `reconcile` is set AND the arbiter's `work/<slug>` exists + is ahead of
|
|
169
|
+
* main, re-sync the hub mirror to the arbiter (a prune-fetch that clears
|
|
170
|
+
* the stale-ref residue that historically resurrected a supposedly
|
|
171
|
+
* `--reset`-ed branch) and RETRY the rebase of the kept branch onto latest
|
|
172
|
+
* `<arbiter>/main` in a SCRATCH worktree (never touching the caller's
|
|
173
|
+
* tree, `--force-with-lease` only, NEVER a bare force). Clean rebase =>
|
|
174
|
+
* push the reconciled tip back to the arbiter and fall through to the
|
|
175
|
+
* standard keep+continue path (lock released, branch preserved). Genuine
|
|
176
|
+
* content conflict AFTER the clean mirror re-sync => item stays stuck,
|
|
177
|
+
* branch UNTOUCHED (NEVER deleted), message references the deferred
|
|
178
|
+
* mirror-side resolve follow-on and mentions `--reset` LAST as the
|
|
179
|
+
* destructive last resort. Branch absent / not-ahead => fall through to
|
|
180
|
+
* the default keep+continue path (which handles the fresh-claim case).
|
|
181
|
+
* - **`--reset` = DISCARD + FRESH (destructive last resort).** When `reset`
|
|
182
|
+
* is set, DELETE the remote `work/<slug>` branch on `arbiter` FIRST (+ drop
|
|
183
|
+
* any stale local branch), THEN the backlog move. Delete-before-move
|
|
184
|
+
* closes the claim-race window; a FAILED delete ABORTS (no backlog move)
|
|
185
|
+
* so the item stays in needs-attention. The next claim then finds NO
|
|
186
|
+
* arbiter branch and cuts fresh — no special claim-time logic.
|
|
171
187
|
* - **`-m "<note>"` = HANDOFF NOTE.** When `message` is set, APPEND a dated
|
|
172
188
|
* `## Requeue YYYY-MM-DD` section to the item BODY (append-only; accumulates
|
|
173
189
|
* over repeated requeues) for the next agent. Applies to BOTH modes.
|
|
@@ -189,6 +205,43 @@ async function readLocalItemLock(slug, cwd, env) {
|
|
|
189
205
|
}
|
|
190
206
|
return parseLockEntry(show.stdout);
|
|
191
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Delete the remote `work/task-<slug>` branch on the arbiter using the SAME
|
|
210
|
+
* write-through ordering the `requeue --reset` recovery verb uses:
|
|
211
|
+
*
|
|
212
|
+
* 1. Delete the LOCAL tracking ref (`refs/remotes/<arbiter>/work/<...>`)
|
|
213
|
+
* — the ref the continue-detection path READS; if we skip this the
|
|
214
|
+
* staleness silently resurrects a supposedly-discarded branch (verified in
|
|
215
|
+
* `work/notes/observations/requeue-reset-does-not-prune-hub-mirror-stale-branch-ref.md`).
|
|
216
|
+
* 2. Delete any LOCAL head `work/<...>` (best-effort).
|
|
217
|
+
* 3. `git push <arbiter> --delete work/<...>` — the ARBITER delete, the
|
|
218
|
+
* source of truth. A `remote ref does not exist` / `unable to delete`
|
|
219
|
+
* stderr is TOLERATED as `already-gone` so the primitive is IDEMPOTENT
|
|
220
|
+
* and safely callable on an item with no work branch (an observation, or
|
|
221
|
+
* a task never built).
|
|
222
|
+
*
|
|
223
|
+
* Extracted from {@link returnToBacklog}'s `--reset` path so the apply-rung
|
|
224
|
+
* `kind: 'stuck'` answered `reset` verb (task
|
|
225
|
+
* `apply-resolve-reset-flag-discards-work-branch`) can dispatch through the
|
|
226
|
+
* SAME primitive without re-implementing branch deletion — the two callers
|
|
227
|
+
* MUST stay behaviourally identical (delete-before-move / delete-before-clear;
|
|
228
|
+
* local-first write-through; already-gone tolerance).
|
|
229
|
+
*/
|
|
230
|
+
export async function deleteRemoteWorkBranchIfPresent(options) {
|
|
231
|
+
const { cwd, arbiter, slug, env } = options;
|
|
232
|
+
const branch = workBranchRef('task', slug);
|
|
233
|
+
await gitSoftAsync(['update-ref', '-d', `refs/remotes/${arbiter}/${branch}`], cwd, env);
|
|
234
|
+
await gitSoftAsync(['branch', '-D', branch], cwd, env);
|
|
235
|
+
const del = await gitSoftAsync(['push', arbiter, '--delete', branch], cwd, env);
|
|
236
|
+
if (del.status === 0) {
|
|
237
|
+
return { branch, status: 'deleted', stderr: '' };
|
|
238
|
+
}
|
|
239
|
+
const stderr = del.stderr.trim();
|
|
240
|
+
if (/remote ref does not exist|unable to delete/i.test(stderr)) {
|
|
241
|
+
return { branch, status: 'already-gone', stderr };
|
|
242
|
+
}
|
|
243
|
+
return { branch, status: 'failed', stderr };
|
|
244
|
+
}
|
|
192
245
|
export async function returnToBacklog(options) {
|
|
193
246
|
const note = options.note ?? (() => { });
|
|
194
247
|
const { cwd, slug, env } = options;
|
|
@@ -249,6 +302,62 @@ export async function returnToBacklog(options) {
|
|
|
249
302
|
'in-progress run).',
|
|
250
303
|
};
|
|
251
304
|
}
|
|
305
|
+
// `--reconcile`: the NON-DESTRUCTIVE recovery rung (task
|
|
306
|
+
// `requeue-reconcile-nondestructive-recovery-verb`, parent observation
|
|
307
|
+
// `rebase-conflict-on-continue-needs-nondestructive-recovery-not-reset`).
|
|
308
|
+
// When the kept `work/<slug>` exists + is ahead of `<arbiter>/main`, re-sync
|
|
309
|
+
// the mirror (prune-fetch — the exact step whose absence let
|
|
310
|
+
// `requeue-reset-does-not-prune-hub-mirror-stale-branch-ref` silently
|
|
311
|
+
// resurrect a stale branch) and RETRY the rebase in a SCRATCH worktree. Clean
|
|
312
|
+
// rebase => push the reconciled tip back (`--force-with-lease`, never bare
|
|
313
|
+
// force, WORK branch only — ADR §11) and fall through to the standard
|
|
314
|
+
// keep+continue path. Genuine content conflict AFTER the clean re-sync =>
|
|
315
|
+
// return with a message that LEADS with what happened, references the
|
|
316
|
+
// deferred mirror-side resolve path, and mentions `--reset` LAST as the
|
|
317
|
+
// destructive last resort. Branch absent / not-ahead => fall through to the
|
|
318
|
+
// default keep+continue path (which handles the fresh-claim case). NEVER
|
|
319
|
+
// deletes the remote branch — this verb's contract is "keep the work".
|
|
320
|
+
let reconciled;
|
|
321
|
+
if (options.reconcile) {
|
|
322
|
+
if (options.reset) {
|
|
323
|
+
return {
|
|
324
|
+
moved: false,
|
|
325
|
+
reasonNotMoved: `requeue for '${slug}': --reconcile and --reset are mutually ` +
|
|
326
|
+
'exclusive (non-destructive recovery vs destructive last resort). ' +
|
|
327
|
+
'Pick one.',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const attempt = await attemptReconcile({
|
|
331
|
+
cwd,
|
|
332
|
+
slug,
|
|
333
|
+
arbiter,
|
|
334
|
+
env,
|
|
335
|
+
note,
|
|
336
|
+
});
|
|
337
|
+
if (attempt.kind === 'conflict') {
|
|
338
|
+
const branch = workBranchRef('task', slug);
|
|
339
|
+
const message = `requeue --reconcile for '${slug}': re-synced the ${arbiter} mirror ` +
|
|
340
|
+
`and RETRIED the rebase of ${branch} onto latest ${arbiter}/main, but ` +
|
|
341
|
+
`the rebase still conflicts on genuine content (${attempt.detail}). The ` +
|
|
342
|
+
'kept branch is left UNTOUCHED on the arbiter (nothing deleted) and the ' +
|
|
343
|
+
'item is left stuck. A supported mirror-side "resolve against latest ' +
|
|
344
|
+
'main" command that fetches the kept branch into a scratch worktree, ' +
|
|
345
|
+
'rebases, and re-pushes is planned but not yet built (see observation ' +
|
|
346
|
+
'`rebase-conflict-on-continue-needs-nondestructive-recovery-not-reset`, ' +
|
|
347
|
+
'point 2 of its LIVE residue). LAST RESORT: `requeue --reset` ' +
|
|
348
|
+
'DESTRUCTIVELY discards the branch and starts fresh.';
|
|
349
|
+
note(message);
|
|
350
|
+
return { moved: false, reasonNotMoved: message };
|
|
351
|
+
}
|
|
352
|
+
if (attempt.kind === 'reconciled') {
|
|
353
|
+
reconciled = true;
|
|
354
|
+
note(`Reconciled '${slug}': re-synced the ${arbiter} mirror, rebased the ` +
|
|
355
|
+
`kept ${workBranchRef('task', slug)} onto latest ${arbiter}/main, and ` +
|
|
356
|
+
'pushed the reconciled tip back (non-destructive; branch preserved).');
|
|
357
|
+
}
|
|
358
|
+
// 'no-branch' => fall through to the default keep+continue path (branch
|
|
359
|
+
// absent / not ahead — the existing default guard handles both cases).
|
|
360
|
+
}
|
|
252
361
|
// `--reset`: DELETE the remote work branch (before the backlog move). The
|
|
253
362
|
// deletion is WRITE-THROUGH: the LOCAL refs that drive continue-detection
|
|
254
363
|
// (`refs/remotes/<arbiter>/work/<slug>` AND any local head `work/<slug>`) are
|
|
@@ -262,38 +371,26 @@ export async function returnToBacklog(options) {
|
|
|
262
371
|
// continue. Delete-before-move also closes the claim-race window.
|
|
263
372
|
let deletedRemoteBranch = false;
|
|
264
373
|
if (options.reset) {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const alreadyGone = /remote ref does not exist|unable to delete/i.test(stderr);
|
|
283
|
-
if (!alreadyGone) {
|
|
284
|
-
const message = `requeue --reset for '${slug}': failed to delete the remote branch ` +
|
|
285
|
-
`${branch} on ${arbiter} (${stderr || 'unknown error'}); ` +
|
|
286
|
-
'aborting the requeue — item left in needs-attention (no backlog move). ' +
|
|
287
|
-
'The local tracking ref was already cleared (write-through ordering); ' +
|
|
288
|
-
'a subsequent fetch will restore it from the arbiter — the local store ' +
|
|
289
|
-
'is BEHIND the arbiter (self-healing), never AHEAD (which would drive a ' +
|
|
290
|
-
'stale continue).';
|
|
291
|
-
note(message);
|
|
292
|
-
return { moved: false, reasonNotMoved: message };
|
|
293
|
-
}
|
|
374
|
+
const dropped = await deleteRemoteWorkBranchIfPresent({
|
|
375
|
+
cwd,
|
|
376
|
+
arbiter,
|
|
377
|
+
slug,
|
|
378
|
+
env,
|
|
379
|
+
});
|
|
380
|
+
if (dropped.status === 'failed') {
|
|
381
|
+
const stderr = dropped.stderr;
|
|
382
|
+
const message = `requeue --reset for '${slug}': failed to delete the remote branch ` +
|
|
383
|
+
`${dropped.branch} on ${arbiter} (${stderr || 'unknown error'}); ` +
|
|
384
|
+
'aborting the requeue — item left in needs-attention (no backlog move). ' +
|
|
385
|
+
'The local tracking ref was already cleared (write-through ordering); ' +
|
|
386
|
+
'a subsequent fetch will restore it from the arbiter — the local store ' +
|
|
387
|
+
'is BEHIND the arbiter (self-healing), never AHEAD (which would drive a ' +
|
|
388
|
+
'stale continue).';
|
|
389
|
+
note(message);
|
|
390
|
+
return { moved: false, reasonNotMoved: message };
|
|
294
391
|
}
|
|
295
392
|
deletedRemoteBranch = true;
|
|
296
|
-
note(`Deleted the remote branch ${branch} on ${arbiter} (--reset).`);
|
|
393
|
+
note(`Deleted the remote branch ${dropped.branch} on ${arbiter} (--reset).`);
|
|
297
394
|
}
|
|
298
395
|
// DEFAULT (keep+continue) REQUEUE-SAFETY GUARD: a claimable item's continue-
|
|
299
396
|
// branch MUST be reachable by ANY worker, so before releasing the lock verify
|
|
@@ -304,14 +401,34 @@ export async function returnToBacklog(options) {
|
|
|
304
401
|
// discards the branch by design).
|
|
305
402
|
if (!options.reset) {
|
|
306
403
|
const branch = workBranchRef('task', slug);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
404
|
+
// Split the guard into TWO cases (task
|
|
405
|
+
// `default-requeue-succeeds-when-no-work-branch-exists`):
|
|
406
|
+
// (a) the arbiter branch does NOT EXIST at all (never pushed, or a prior
|
|
407
|
+
// `--reset` already deleted it) — there is NO continue-branch a future
|
|
408
|
+
// worker would resume from, so the guard's precondition is vacuously
|
|
409
|
+
// satisfied. Degrade gracefully to the same effective outcome as
|
|
410
|
+
// `--reset` (nothing to discard) and proceed with the keep+continue
|
|
411
|
+
// backlog move: no arbiter delete (there is nothing to delete), no
|
|
412
|
+
// forcing the caller into the destructive `--reset` verb.
|
|
413
|
+
// (b) the arbiter branch EXISTS but is NOT ahead of `<arbiter>/main` — a
|
|
414
|
+
// real anomaly (the continue-branch would resume from a state already
|
|
415
|
+
// reachable from main). Preserve today's refusal so the case surfaces.
|
|
416
|
+
const tip = gitSoftRun(['rev-parse', '--verify', '--quiet', `${arbiter}/${branch}^{commit}`], cwd, env);
|
|
417
|
+
const arbiterBranchExists = tip.status === 0 && tip.stdout.trim() !== '';
|
|
418
|
+
if (!arbiterBranchExists) {
|
|
419
|
+
note(`'${slug}' has no work branch on ${arbiter} — requeueing to backlog ` +
|
|
420
|
+
'for a FRESH claim (nothing to continue from; no --reset needed).');
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const onArbiter = branchAheadOf(cwd, `${arbiter}/${branch}`, `${arbiter}/main`, env);
|
|
424
|
+
if (!onArbiter) {
|
|
425
|
+
const message = `the work branch ${branch} isn't on ${arbiter} (the continue ` +
|
|
426
|
+
`branch a cross-machine worker would resume from) — push it first, or ` +
|
|
427
|
+
'`requeue --reset` to discard and start fresh. Item left stuck (lock not ' +
|
|
428
|
+
'released).';
|
|
429
|
+
note(message);
|
|
430
|
+
return { moved: false, reasonNotMoved: message };
|
|
431
|
+
}
|
|
315
432
|
}
|
|
316
433
|
}
|
|
317
434
|
const commitMessage = `chore(${slug}): return to backlog for re-claiming`;
|
|
@@ -394,7 +511,85 @@ export async function returnToBacklog(options) {
|
|
|
394
511
|
return { moved: false, reasonNotMoved: message };
|
|
395
512
|
}
|
|
396
513
|
note(`Returned '${slug}' to backlog (released the lock; body rests in pool).`);
|
|
397
|
-
return { moved: true, commitMessage, deletedRemoteBranch };
|
|
514
|
+
return { moved: true, commitMessage, deletedRemoteBranch, reconciled };
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* The `--reconcile` recovery attempt — the non-destructive middle rung of the
|
|
518
|
+
* `requeue` escalation ladder. Runs in a SCRATCH worktree so the caller's cwd
|
|
519
|
+
* tree/HEAD/index is NEVER touched (parity with the tree-less move machinery):
|
|
520
|
+
*
|
|
521
|
+
* 1. **Re-sync the mirror.** `git fetch --prune <arbiter>` on the caller's
|
|
522
|
+
* cwd — the exact prune step whose absence let
|
|
523
|
+
* `requeue-reset-does-not-prune-hub-mirror-stale-branch-ref` silently
|
|
524
|
+
* resurrect a supposedly-`--reset`-ed branch. This clears the stale
|
|
525
|
+
* remote-tracking residue that historically fooled the retry.
|
|
526
|
+
* 2. **Guard.** The arbiter's `work/<slug>` must EXIST + be ahead of main
|
|
527
|
+
* (via {@link branchAheadOf} on the freshly-fetched
|
|
528
|
+
* `<arbiter>/work/<slug>` vs `<arbiter>/main`). Absent / not-ahead =>
|
|
529
|
+
* `'no-branch'` and the caller falls through to the default keep+continue
|
|
530
|
+
* path.
|
|
531
|
+
* 3. **Rebase in a scratch worktree.** `git worktree add --detach <scratch>
|
|
532
|
+
* <arbiter>/work/<slug>` and run {@link rebaseContinuedBranchOntoMain}
|
|
533
|
+
* against `<arbiter>/main`. A CLEAN rebase is a full non-destructive fix;
|
|
534
|
+
* a CONFLICT is `--abort`ed (never auto-resolved) => `'conflict'`.
|
|
535
|
+
* 4. **Push the reconciled tip back.** `git push <arbiter> HEAD:work/<slug>
|
|
536
|
+
* --force-with-lease=work/<slug>:<observed-arbiter-tip>` from the scratch
|
|
537
|
+
* worktree. `--force-with-lease` ONLY, NEVER bare `--force`, NEVER
|
|
538
|
+
* `:main`, the WORK branch ONLY (ADR §11). A rejected push (stale lease
|
|
539
|
+
* or otherwise) => `'conflict'` — non-destructive by construction, the
|
|
540
|
+
* user can retry.
|
|
541
|
+
*
|
|
542
|
+
* Cleanup of the scratch worktree is best-effort in a `finally` (never fails
|
|
543
|
+
* the reconcile on cleanup).
|
|
544
|
+
*/
|
|
545
|
+
async function attemptReconcile(params) {
|
|
546
|
+
const { cwd, slug, arbiter, env, note } = params;
|
|
547
|
+
const branch = workBranchRef('task', slug);
|
|
548
|
+
const arbBranchRef = `refs/remotes/${arbiter}/${branch}`;
|
|
549
|
+
const arbMainRef = `refs/remotes/${arbiter}/main`;
|
|
550
|
+
// 1. Re-sync the mirror to the arbiter (prune-fetch clears stale refs).
|
|
551
|
+
await gitSoftAsync(['fetch', '--prune', '--quiet', arbiter], cwd, env);
|
|
552
|
+
// 2. Guard: branch must exist + be ahead of main.
|
|
553
|
+
if (!branchAheadOf(cwd, arbBranchRef, arbMainRef, env)) {
|
|
554
|
+
return { kind: 'no-branch' };
|
|
555
|
+
}
|
|
556
|
+
// 3. Scratch worktree — the caller's tree is NEVER touched.
|
|
557
|
+
const worktree = join(tmpdir(), `dorfl-reconcile-${slug}-${process.pid}-${Date.now()}`);
|
|
558
|
+
const wtCreate = gitSoftRun(['worktree', 'add', '--quiet', '--detach', worktree, arbBranchRef], cwd, env);
|
|
559
|
+
if (wtCreate.status !== 0) {
|
|
560
|
+
return {
|
|
561
|
+
kind: 'conflict',
|
|
562
|
+
detail: `could not create scratch worktree (${wtCreate.stderr.trim() || `exit ${wtCreate.status}`})`,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
const rebase = rebaseContinuedBranchOntoMain(worktree, arbMainRef, env);
|
|
567
|
+
if (rebase.kind === 'conflict') {
|
|
568
|
+
return { kind: 'conflict', detail: 'rebase conflicted after re-sync' };
|
|
569
|
+
}
|
|
570
|
+
// 4. Push the reconciled tip back (`--force-with-lease`, WORK branch only).
|
|
571
|
+
const observedTip = gitSoftRun(['rev-parse', '--verify', '--quiet', `${arbBranchRef}^{commit}`], cwd, env).stdout.trim();
|
|
572
|
+
const lease = observedTip === '' ? `${branch}:` : `${branch}:${observedTip}`;
|
|
573
|
+
const push = gitSoftRun([
|
|
574
|
+
'push',
|
|
575
|
+
arbiter,
|
|
576
|
+
`HEAD:refs/heads/${branch}`,
|
|
577
|
+
`--force-with-lease=${lease}`,
|
|
578
|
+
], worktree, env);
|
|
579
|
+
if (push.status !== 0) {
|
|
580
|
+
return {
|
|
581
|
+
kind: 'conflict',
|
|
582
|
+
detail: `push of reconciled tip rejected (${push.stderr.trim() || `exit ${push.status}`})`,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
// Advance the local remote-tracking ref so subsequent reads see the truth.
|
|
586
|
+
await gitSoftAsync(['fetch', '--quiet', arbiter], cwd, env);
|
|
587
|
+
void note;
|
|
588
|
+
return { kind: 'reconciled' };
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
await gitSoftAsync(['worktree', 'remove', '--force', worktree], cwd, env);
|
|
592
|
+
}
|
|
398
593
|
}
|
|
399
594
|
export async function promoteFromPreBacklog(options) {
|
|
400
595
|
const note = options.note ?? (() => { });
|
|
@@ -456,13 +651,13 @@ export async function promoteFromPreBacklog(options) {
|
|
|
456
651
|
const hasSource = (await gitSoftAsync(['cat-file', '-e', `${arbiter}/main:${sourceRel}`], cwd, env)).status === 0;
|
|
457
652
|
const hasDest = (await gitSoftAsync(['cat-file', '-e', `${arbiter}/main:${destRel}`], cwd, env)).status === 0;
|
|
458
653
|
if (!hasSource && !hasDest) {
|
|
459
|
-
const message = `'${slug}' is not staged in
|
|
460
|
-
|
|
461
|
-
'staged?).';
|
|
654
|
+
const message = `'${slug}' is not staged in ${workFolderPrefix('tasks-backlog')} on ${arbiter}/main ` +
|
|
655
|
+
`(and not already in ${workFolderPrefix('tasks-ready')}) — nothing to promote ` +
|
|
656
|
+
'(wrong slug, or never staged?).';
|
|
462
657
|
note(message);
|
|
463
658
|
return { moved: false, reasonNotMoved: message };
|
|
464
659
|
}
|
|
465
|
-
const commitMessage = `chore(${slug}): promote
|
|
660
|
+
const commitMessage = `chore(${slug}): promote ${workFolderPrefix('tasks-backlog')} -> ${workFolderPrefix('tasks-ready')}`;
|
|
466
661
|
const moved = await runTreelessLedgerMove({
|
|
467
662
|
cwd,
|
|
468
663
|
slug,
|
|
@@ -490,7 +685,7 @@ export async function promoteFromPreBacklog(options) {
|
|
|
490
685
|
base,
|
|
491
686
|
sourceRel,
|
|
492
687
|
destRel,
|
|
493
|
-
// The body is carried byte-for-byte from
|
|
688
|
+
// The body is carried byte-for-byte from tasks/backlog into the
|
|
494
689
|
// pool — promotion is a placement decision, not a content transform.
|
|
495
690
|
transformBody: (body) => body,
|
|
496
691
|
commitMessage,
|
|
@@ -500,12 +695,12 @@ export async function promoteFromPreBacklog(options) {
|
|
|
500
695
|
},
|
|
501
696
|
});
|
|
502
697
|
if (moved) {
|
|
503
|
-
note(`Promoted '${slug}' from
|
|
698
|
+
note(`Promoted '${slug}' from tasks/backlog to tasks/ready (claimable).`);
|
|
504
699
|
return { moved: true, commitMessage };
|
|
505
700
|
}
|
|
506
701
|
const message = `promote for '${slug}': the arbiter's main kept moving (contended) after ` +
|
|
507
|
-
`${TREELESS_CONTENTION_ATTEMPTS} attempts — item left in
|
|
508
|
-
'move). Try again shortly.';
|
|
702
|
+
`${TREELESS_CONTENTION_ATTEMPTS} attempts — item left in tasks/backlog ` +
|
|
703
|
+
'(no move). Try again shortly.';
|
|
509
704
|
note(message);
|
|
510
705
|
return { moved: false, reasonNotMoved: message };
|
|
511
706
|
}
|
|
@@ -545,7 +740,7 @@ export async function promoteFromPreSpec(options) {
|
|
|
545
740
|
// (spec `prd-to-spec-vocabulary-cutover-and-migration-command`): the lock
|
|
546
741
|
// identity is `spec:${slug}` to match the `spec-<slug>` entry the tasking/apply
|
|
547
742
|
// path now acquires (`tasking.ts` releases under `spec:${slug}`); a stale
|
|
548
|
-
//
|
|
743
|
+
// ''prd:${slug}'' here would key a DIFFERENT ref and break the mutual exclusion.
|
|
549
744
|
// Loss / crash semantics mirror the task case.
|
|
550
745
|
const item = `spec:${slug}`;
|
|
551
746
|
const acquired = await acquireItemLock({
|
|
@@ -622,7 +817,7 @@ export async function promoteFromPreSpec(options) {
|
|
|
622
817
|
}
|
|
623
818
|
/**
|
|
624
819
|
* LIST every staged item awaiting a runner/human promotion — the tasks in
|
|
625
|
-
* `work/
|
|
820
|
+
* `work/tasks/backlog/` and the prds in `work/specs/proposed/` on `<arbiter>/main` (the
|
|
626
821
|
* discovery half of the `promote` verb, so `promote` with no argument answers
|
|
627
822
|
* "what is staged waiting for me?"). It reads the ARBITER's truth (a fetch + a
|
|
628
823
|
* tree read), NOT the local working tree (which may be stale) — the same source
|
|
@@ -785,6 +980,286 @@ function prepareTreelessMoveCommit(params) {
|
|
|
785
980
|
}
|
|
786
981
|
/** The heading that opens an appended requeue handoff note in the item body. */
|
|
787
982
|
const REQUEUE_HEADING_PREFIX = '## Requeue';
|
|
983
|
+
// --- Tree-less SURFACE primitive (PR-1, spec
|
|
984
|
+
// `surface-stuck-as-questions-and-retire-stuck-lock-state`, task
|
|
985
|
+
// `bounce-surfaces-stuck-sidecar-and-releases-lock`) --------------------
|
|
986
|
+
/**
|
|
987
|
+
* PR-1 ADDITIVE primitive (task `bounce-surfaces-stuck-sidecar-and-releases-lock`).
|
|
988
|
+
* The 2-file SIBLING of {@link prepareTreelessMoveCommit}: pure git plumbing
|
|
989
|
+
* (`hash-object` / scratch-index `update-index` / `write-tree` /
|
|
990
|
+
* `commit-tree`, NEVER touches the caller's index/HEAD/working tree) that in
|
|
991
|
+
* ONE commit off {@link base} both
|
|
992
|
+
*
|
|
993
|
+
* 1. writes or appends to the item's `work/questions/<type>-<slug>.md`
|
|
994
|
+
* sidecar (see {@link sidecarPathFor}) a `stuck`-kind entry carrying the
|
|
995
|
+
* bounce {@link reason} plus any agent-surfaced {@link questions}, and
|
|
996
|
+
* 2. sets `needsAnswers: true` on the item body at {@link itemPath} (via
|
|
997
|
+
* {@link setNeedsAnswersMarker}).
|
|
998
|
+
*
|
|
999
|
+
* The current sidecar (if any) and the current item body are read as BLOBS off
|
|
1000
|
+
* `<arbiter>/main` (via {@link catBlob}) — no working tree required and no
|
|
1001
|
+
* dependency on the cwd's `HEAD` matching `main`. That is what makes this the
|
|
1002
|
+
* tree-less path's surface primitive: a `applyTreelessNeedsAttentionTransition`
|
|
1003
|
+
* caller (`continue-push-failure` / rebase-conflict) has NO writable-`main`
|
|
1004
|
+
* checkout, so `persistSurfacedQuestions` (working-tree bound) cannot be reused;
|
|
1005
|
+
* the pure CONTENT builders (`newSidecar` / `appendQuestions` /
|
|
1006
|
+
* `serialiseSidecar` / `setNeedsAnswersMarker`) ARE reused, only the commit
|
|
1007
|
+
* mechanism differs (spec decision #7).
|
|
1008
|
+
*
|
|
1009
|
+
* Returns the throwaway ref + commit sha, exactly like {@link
|
|
1010
|
+
* prepareTreelessMoveCommit}, so the SAME {@link runTreelessLedgerMove} CAS loop
|
|
1011
|
+
* publishes the surface commit through the shared write seam. The
|
|
1012
|
+
* surface-first / release-second ordering + `main`-authoritative crash-safety
|
|
1013
|
+
* (spec decision #4) come FREE from routing through that loop — the caller wires
|
|
1014
|
+
* the release into a `finally` AFTER a successful publish (see the harness
|
|
1015
|
+
* {@link surfaceStuckToNeedsAttention}).
|
|
1016
|
+
*
|
|
1017
|
+
* DECISION — sidecar entry shape for a reason-only bounce (build-time, PR-1):
|
|
1018
|
+
* every bounce always appends ONE engine-authored `stuck`-kind envelope entry
|
|
1019
|
+
* whose `question` names the item and whose `context` is the {@link reason}
|
|
1020
|
+
* prose, THEN any {@link questions} the agent surfaced. So a reason-only bounce
|
|
1021
|
+
* (no agent questions) still surfaces exactly ONE entry a human can answer, and
|
|
1022
|
+
* the agent's own questions (when present) are appended AFTER it, verbatim. The
|
|
1023
|
+
* envelope entry is what turns a raw exit reason into a human-drainable
|
|
1024
|
+
* question; the extra entries are the LLM prose the spec's decision #2 keeps
|
|
1025
|
+
* untouched. Alternative considered — treating an empty `questions` array as a
|
|
1026
|
+
* NO-OP surface — REJECTED because that is the spec's `stuck` retirement
|
|
1027
|
+
* problem all over again (a bounced item with no on-`main` outcome).
|
|
1028
|
+
* Alternative considered — dropping the envelope when agent questions are given
|
|
1029
|
+
* — REJECTED because the reason is engine-authored ground truth; the agent's
|
|
1030
|
+
* questions are advisory prose ABOVE it, not a replacement for it.
|
|
1031
|
+
*
|
|
1032
|
+
* PR-1 boundary: this primitive is EXERCISED BY TESTS ONLY; it is NOT yet
|
|
1033
|
+
* called from any bounce seam. Wiring `applyNeedsAttentionTransition` /
|
|
1034
|
+
* `applyTreelessNeedsAttentionTransition` to it — and migrating the existing
|
|
1035
|
+
* `stuckLockOnArbiter(...).toBe(true)` assertions — is the follow-up PR-2 task
|
|
1036
|
+
* `bounce-atomic-cutover-retire-stuck-lock`.
|
|
1037
|
+
*/
|
|
1038
|
+
export function prepareTreelessSurfaceCommit(params) {
|
|
1039
|
+
const { cwd, slug, item, itemPath, base, reason, questions, envelope: envelopeOverride, commitMessage, refNamespace, env, } = params;
|
|
1040
|
+
const sidecarPath = sidecarPathFor(item);
|
|
1041
|
+
// Read the item body off `main` as a BLOB — never off the cwd working tree,
|
|
1042
|
+
// which the tree-less caller does not have on `main`. `catBlob` throws when
|
|
1043
|
+
// the path is not tracked, which is the honest signal: the tree-less bounce
|
|
1044
|
+
// only fires against an item whose body already rests on `main` (the caller's
|
|
1045
|
+
// plan should short-circuit `missing` beforehand).
|
|
1046
|
+
const itemBody = catBlob(`${base}:${itemPath}`, cwd, env);
|
|
1047
|
+
const flagged = setNeedsAnswersMarker(itemBody, true);
|
|
1048
|
+
// Defense-in-depth (the `sidecar-without-needsAnswers` guard, mirrored from
|
|
1049
|
+
// `persistSurfacedQuestions`): if the marker did not actually parse back as
|
|
1050
|
+
// `true`, refuse to write the sidecar rather than tear the
|
|
1051
|
+
// `needsAnswers ⟺ sidecar` invariant.
|
|
1052
|
+
if (parseFrontmatter(flagged).needsAnswers !== true) {
|
|
1053
|
+
throw new Error(`prepareTreelessSurfaceCommit: could not set needsAnswers:true on '${itemPath}' ` +
|
|
1054
|
+
`for '${item}' — refusing to surface without the flag.`);
|
|
1055
|
+
}
|
|
1056
|
+
// Compose the entries: an engine-authored envelope carrying the reason, then
|
|
1057
|
+
// any agent-surfaced questions (stamped `stuck`-kind if the caller left the
|
|
1058
|
+
// kind unset — this IS the stuck-surface path). Callers may OVERRIDE the
|
|
1059
|
+
// envelope (e.g. the empty-diff path swaps in a dispose-defaulted question);
|
|
1060
|
+
// the override still defaults `kind` to `stuck` and `context` to the bounce
|
|
1061
|
+
// reason when the override leaves them unset (the caller can restate the
|
|
1062
|
+
// reason in the envelope prose without duplicating it in `context`).
|
|
1063
|
+
const envelope = envelopeOverride
|
|
1064
|
+
? {
|
|
1065
|
+
kind: envelopeOverride.kind ?? 'stuck',
|
|
1066
|
+
question: envelopeOverride.question,
|
|
1067
|
+
context: envelopeOverride.context ?? reason,
|
|
1068
|
+
...(envelopeOverride.default !== undefined
|
|
1069
|
+
? { default: envelopeOverride.default }
|
|
1070
|
+
: {}),
|
|
1071
|
+
}
|
|
1072
|
+
: {
|
|
1073
|
+
question: `'${item}' was bounced — how should we proceed?`,
|
|
1074
|
+
context: reason,
|
|
1075
|
+
kind: 'stuck',
|
|
1076
|
+
};
|
|
1077
|
+
const surfaced = (questions ?? []).map((q) => ({
|
|
1078
|
+
...q,
|
|
1079
|
+
kind: q.kind ?? 'stuck',
|
|
1080
|
+
}));
|
|
1081
|
+
const additions = [envelope, ...surfaced];
|
|
1082
|
+
// APPEND to an existing sidecar on `main` (never overwrite) or CREATE it
|
|
1083
|
+
// first-pass — the same append-never-overwrite rule the working-tree surface
|
|
1084
|
+
// path enforces. Read the current sidecar off `main` as a BLOB.
|
|
1085
|
+
const sidecarExists = pathInCommit(base, sidecarPath, cwd, env);
|
|
1086
|
+
const model = sidecarExists
|
|
1087
|
+
? appendQuestions(parseSidecar(catBlob(`${base}:${sidecarPath}`, cwd, env)), additions)
|
|
1088
|
+
: newSidecar(item, additions);
|
|
1089
|
+
const sidecarContent = serialiseSidecar(model);
|
|
1090
|
+
// Hash both blobs INTO the cwd's object store (no working tree write).
|
|
1091
|
+
const itemBlob = hashObject(flagged, cwd, env);
|
|
1092
|
+
const sidecarBlob = hashObject(sidecarContent, cwd, env);
|
|
1093
|
+
// A scratch index so `read-tree` / `update-index` never touch the caller's
|
|
1094
|
+
// index. `--add --cacheinfo` both ADDS a new entry and REPLACES an existing
|
|
1095
|
+
// one (so a re-surface that rewrites the sidecar path is a no-op replace).
|
|
1096
|
+
const scratchIndex = join(tmpdir(), `dorfl-${refNamespace}-${process.pid}-${Date.now()}.index`);
|
|
1097
|
+
const withIndex = {
|
|
1098
|
+
...(env ?? process.env),
|
|
1099
|
+
GIT_INDEX_FILE: scratchIndex,
|
|
1100
|
+
};
|
|
1101
|
+
try {
|
|
1102
|
+
gitHard(['read-tree', base], cwd, withIndex);
|
|
1103
|
+
gitHard([
|
|
1104
|
+
'update-index',
|
|
1105
|
+
'--add',
|
|
1106
|
+
'--cacheinfo',
|
|
1107
|
+
`100644,${itemBlob},${itemPath}`,
|
|
1108
|
+
], cwd, withIndex);
|
|
1109
|
+
gitHard([
|
|
1110
|
+
'update-index',
|
|
1111
|
+
'--add',
|
|
1112
|
+
'--cacheinfo',
|
|
1113
|
+
`100644,${sidecarBlob},${sidecarPath}`,
|
|
1114
|
+
], cwd, withIndex);
|
|
1115
|
+
const tree = runHard(['write-tree'], cwd, withIndex).stdout.trim();
|
|
1116
|
+
const commit = runHard(['commit-tree', tree, '-p', base, '-m', commitMessage], cwd, env).stdout.trim();
|
|
1117
|
+
const ref = `refs/dorfl/${refNamespace}/${slug}`;
|
|
1118
|
+
gitHard(['update-ref', ref, commit], cwd, env);
|
|
1119
|
+
return { ref, commit };
|
|
1120
|
+
}
|
|
1121
|
+
finally {
|
|
1122
|
+
rmSync(scratchIndex, { force: true });
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* The PR-1 thin HARNESS around {@link prepareTreelessSurfaceCommit}: run it
|
|
1127
|
+
* through the EXISTING {@link runTreelessLedgerMove} CAS loop, then release the
|
|
1128
|
+
* per-item lock. Ordering is LOAD-BEARING and INHERITED from that harness —
|
|
1129
|
+
* the surface commit lands on `<arbiter>/main` FIRST, and the lock release only
|
|
1130
|
+
* fires on a successful publish. `main` is authoritative on crash recovery
|
|
1131
|
+
* (spec decision #4).
|
|
1132
|
+
*
|
|
1133
|
+
* PR-1 boundary: this is EXERCISED BY TESTS ONLY. Wiring the seams to call it
|
|
1134
|
+
* is the PR-2 task `bounce-atomic-cutover-retire-stuck-lock`.
|
|
1135
|
+
*/
|
|
1136
|
+
export async function surfaceStuckToNeedsAttention(options) {
|
|
1137
|
+
const note = options.note ?? (() => { });
|
|
1138
|
+
// Merge of #364 (envelope) + PR-2a (itemPath is re-declared as `let` below for
|
|
1139
|
+
// the D1 probe fallback, so it must NOT be in this const destructure).
|
|
1140
|
+
const { cwd, slug, reason, questions, envelope, arbiter, env } = options;
|
|
1141
|
+
const item = options.item ?? `task:${slug}`;
|
|
1142
|
+
// PR-2a D1 body-path probe: when the caller does not name an on-`main`
|
|
1143
|
+
// body path, PROBE `<arbiter>/main` in a fixed order per namespace
|
|
1144
|
+
// (task/spec/observation). A body-absent probe is a CLEAN NO-OP surface
|
|
1145
|
+
// that STILL releases the lock — never leave a bounce as a dead-end held
|
|
1146
|
+
// lock.
|
|
1147
|
+
let itemPath = options.itemPath;
|
|
1148
|
+
if (itemPath === undefined) {
|
|
1149
|
+
const probed = await resolveBounceItemBodyPathOnMain({
|
|
1150
|
+
cwd,
|
|
1151
|
+
item,
|
|
1152
|
+
arbiter,
|
|
1153
|
+
env,
|
|
1154
|
+
});
|
|
1155
|
+
if (probed === undefined) {
|
|
1156
|
+
// Body-absent: skip the surface commit entirely and STILL release the
|
|
1157
|
+
// lock (idempotent). Signal the distinction on the result so a caller
|
|
1158
|
+
// can tell a body-absent no-op apart from a `missing` plan on a
|
|
1159
|
+
// caller-provided itemPath.
|
|
1160
|
+
const rel = await releaseItemLock({ item, cwd, arbiter, env });
|
|
1161
|
+
const released = rel.outcome === 'released' || rel.outcome === 'not-held';
|
|
1162
|
+
return {
|
|
1163
|
+
surfaced: false,
|
|
1164
|
+
released,
|
|
1165
|
+
bodyAbsent: true,
|
|
1166
|
+
reasonNotSurfaced: `no body for '${item}' on ${arbiter}/main (probed the D1 ` +
|
|
1167
|
+
'candidates in order) — surface skipped as a clean no-op; the ' +
|
|
1168
|
+
'lock was still released to avoid a dead-end held lock.',
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
itemPath = probed;
|
|
1172
|
+
}
|
|
1173
|
+
const resolvedItemPath = itemPath;
|
|
1174
|
+
const surfaced = await runTreelessLedgerMove({
|
|
1175
|
+
cwd,
|
|
1176
|
+
slug,
|
|
1177
|
+
arbiter,
|
|
1178
|
+
kind: 'needs-attention',
|
|
1179
|
+
onContended: 'surface',
|
|
1180
|
+
explicitMainRefspec: true,
|
|
1181
|
+
env,
|
|
1182
|
+
note,
|
|
1183
|
+
plan: (base) => {
|
|
1184
|
+
if (!pathInCommit(base, resolvedItemPath, cwd, env)) {
|
|
1185
|
+
return 'missing';
|
|
1186
|
+
}
|
|
1187
|
+
return prepareTreelessSurfaceCommit({
|
|
1188
|
+
cwd,
|
|
1189
|
+
slug,
|
|
1190
|
+
item,
|
|
1191
|
+
itemPath: resolvedItemPath,
|
|
1192
|
+
base,
|
|
1193
|
+
reason,
|
|
1194
|
+
questions,
|
|
1195
|
+
envelope,
|
|
1196
|
+
commitMessage: `surface ${item} (stuck): ${reason}`,
|
|
1197
|
+
refNamespace: 'surface-stuck',
|
|
1198
|
+
env,
|
|
1199
|
+
});
|
|
1200
|
+
},
|
|
1201
|
+
});
|
|
1202
|
+
if (!surfaced) {
|
|
1203
|
+
return {
|
|
1204
|
+
surfaced: false,
|
|
1205
|
+
released: false,
|
|
1206
|
+
reasonNotSurfaced: `surface for '${item}' did not land on ${arbiter}/main ` +
|
|
1207
|
+
'(item missing on main, or contention exhausted after retries).',
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
// Surface-first / release-second: only reach here on a successful publish.
|
|
1211
|
+
const rel = await releaseItemLock({ item, cwd, arbiter, env });
|
|
1212
|
+
const released = rel.outcome === 'released' || rel.outcome === 'not-held';
|
|
1213
|
+
return { surfaced: true, released };
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* The D1 body-path probe (PR-2a task `bounce-atomic-cutover-retire-stuck-lock`,
|
|
1217
|
+
* spec `surface-stuck-as-questions-and-retire-stuck-lock-state`, decision D1):
|
|
1218
|
+
* resolve the item's on-`main` body path by probing `<arbiter>/main` in a
|
|
1219
|
+
* FIXED order per namespace:
|
|
1220
|
+
* - task: `work/tasks/ready/<slug>.md` then `work/tasks/backlog/<slug>.md`.
|
|
1221
|
+
* - spec: `work/specs/ready/<slug>.md` then `work/specs/proposed/<slug>.md`.
|
|
1222
|
+
* - observation: `work/notes/observations/<slug>.md`.
|
|
1223
|
+
*
|
|
1224
|
+
* Returns the first candidate that EXISTS on `<arbiter>/main`, or `undefined`
|
|
1225
|
+
* when no candidate exists (a bounce for an item whose body never landed on
|
|
1226
|
+
* `main`). The caller MUST handle `undefined` by STILL releasing the lock —
|
|
1227
|
+
* never leave a held lock over a body-absent item.
|
|
1228
|
+
*
|
|
1229
|
+
* The probe fetches `<arbiter>/main` with an EXPLICIT refspec (the
|
|
1230
|
+
* `runTreelessLedgerMove` pattern) so a job-worktree with a narrower default
|
|
1231
|
+
* fetch refspec still resolves `<arbiter>/main` reliably.
|
|
1232
|
+
*/
|
|
1233
|
+
export async function resolveBounceItemBodyPathOnMain(params) {
|
|
1234
|
+
const { cwd, item, arbiter, env } = params;
|
|
1235
|
+
const { type, slug } = resolveSidecarIdentity(item);
|
|
1236
|
+
await gitSoftAsync([
|
|
1237
|
+
'fetch',
|
|
1238
|
+
'--quiet',
|
|
1239
|
+
arbiter,
|
|
1240
|
+
`+refs/heads/main:refs/remotes/${arbiter}/main`,
|
|
1241
|
+
], cwd, env);
|
|
1242
|
+
const base = `${arbiter}/main`;
|
|
1243
|
+
const folders = BOUNCE_BODY_PROBE_ORDER[type];
|
|
1244
|
+
for (const folder of folders) {
|
|
1245
|
+
const rel = workItemRel(folder, `${slug}.md`);
|
|
1246
|
+
if (pathInCommit(base, rel, cwd, env)) {
|
|
1247
|
+
return rel;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return undefined;
|
|
1251
|
+
}
|
|
1252
|
+
/** The FIXED per-namespace probe order for {@link resolveBounceItemBodyPathOnMain}
|
|
1253
|
+
* (D1): the FIRST candidate that exists on `<arbiter>/main` is the item's body
|
|
1254
|
+
* path. The ordering encodes the working assumption that a claimed/in-flight
|
|
1255
|
+
* item is in `ready/` and a not-yet-promoted item is in `backlog/`
|
|
1256
|
+
* (spec-tasked/proposed for a spec); an observation has one location.
|
|
1257
|
+
* Callers with an explicit `itemPath` bypass this probe entirely. */
|
|
1258
|
+
const BOUNCE_BODY_PROBE_ORDER = {
|
|
1259
|
+
task: ['tasks-ready', 'tasks-backlog'],
|
|
1260
|
+
spec: ['specs-ready', 'specs-proposed'],
|
|
1261
|
+
observation: ['observations'],
|
|
1262
|
+
};
|
|
788
1263
|
/**
|
|
789
1264
|
* Append a dated `## Requeue YYYY-MM-DD` handoff section to an item body's TEXT
|
|
790
1265
|
* (append-only — never overwrites; repeated requeues accumulate a handoff log).
|