dorfl 0.7.0 → 0.8.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.
Files changed (71) hide show
  1. package/dist/advance-drivers.d.ts.map +1 -1
  2. package/dist/advance-drivers.js +11 -2
  3. package/dist/advance-drivers.js.map +1 -1
  4. package/dist/bootstrap-forward.d.ts +224 -0
  5. package/dist/bootstrap-forward.d.ts.map +1 -0
  6. package/dist/bootstrap-forward.js +247 -0
  7. package/dist/bootstrap-forward.js.map +1 -0
  8. package/dist/cli.d.ts +9 -0
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +21 -1
  11. package/dist/cli.js.map +1 -1
  12. package/dist/config.d.ts +50 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +42 -0
  15. package/dist/config.js.map +1 -1
  16. package/dist/cwd-section.d.ts.map +1 -1
  17. package/dist/cwd-section.js +11 -2
  18. package/dist/cwd-section.js.map +1 -1
  19. package/dist/env-config.d.ts.map +1 -1
  20. package/dist/env-config.js +8 -0
  21. package/dist/env-config.js.map +1 -1
  22. package/dist/github.d.ts +32 -1
  23. package/dist/github.d.ts.map +1 -1
  24. package/dist/github.js +115 -0
  25. package/dist/github.js.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/install-ci-core.d.ts.map +1 -1
  31. package/dist/install-ci-core.js +20 -48
  32. package/dist/install-ci-core.js.map +1 -1
  33. package/dist/integration-core.d.ts.map +1 -1
  34. package/dist/integration-core.js +9 -0
  35. package/dist/integration-core.js.map +1 -1
  36. package/dist/integrator.d.ts +48 -0
  37. package/dist/integrator.d.ts.map +1 -1
  38. package/dist/integrator.js +12 -0
  39. package/dist/integrator.js.map +1 -1
  40. package/dist/item-lock.d.ts +26 -0
  41. package/dist/item-lock.d.ts.map +1 -1
  42. package/dist/item-lock.js +39 -0
  43. package/dist/item-lock.js.map +1 -1
  44. package/dist/repo-config.d.ts +1 -1
  45. package/dist/repo-config.d.ts.map +1 -1
  46. package/dist/repo-config.js +20 -1
  47. package/dist/repo-config.js.map +1 -1
  48. package/dist/scan.d.ts +24 -2
  49. package/dist/scan.d.ts.map +1 -1
  50. package/dist/scan.js +28 -4
  51. package/dist/scan.js.map +1 -1
  52. package/dist/skills/setup/SKILL.md +3 -0
  53. package/dist/tasking.d.ts.map +1 -1
  54. package/dist/tasking.js +124 -65
  55. package/dist/tasking.js.map +1 -1
  56. package/package.json +1 -1
  57. package/src/advance-drivers.ts +17 -8
  58. package/src/bootstrap-forward.ts +359 -0
  59. package/src/cli.ts +21 -1
  60. package/src/config.ts +75 -0
  61. package/src/cwd-section.ts +21 -2
  62. package/src/env-config.ts +8 -0
  63. package/src/github.ts +149 -0
  64. package/src/index.ts +24 -0
  65. package/src/install-ci-core.ts +20 -48
  66. package/src/integration-core.ts +10 -0
  67. package/src/integrator.ts +62 -0
  68. package/src/item-lock.ts +50 -0
  69. package/src/repo-config.ts +20 -0
  70. package/src/scan.ts +32 -8
  71. package/src/tasking.ts +154 -68
package/src/tasking.ts CHANGED
@@ -435,6 +435,11 @@ export async function performTask(
435
435
  // with no contention may task on `main` directly WITHOUT the lock.
436
436
  let lockedBlob: string | undefined;
437
437
  const useLock = doer === 'agent';
438
+ // The integrate mode resolved ONCE (`--merge`/`--propose` > default `propose`),
439
+ // used to decide BOTH the tasking lock's lifetime (release on `merge`, HOLD
440
+ // across the PR on `propose`) AND whether a not-landed surface should close a
441
+ // stale open PR (propose only).
442
+ const resolvedMode = options.integration ?? 'propose';
438
443
  if (useLock) {
439
444
  const acquired = await lock.acquire({slug, cwd, arbiter, env, note});
440
445
  if (acquired.outcome === 'lost') {
@@ -545,22 +550,25 @@ export async function performTask(
545
550
  loopDisposition.specQuestions,
546
551
  );
547
552
  if (useLock) {
548
- const routed = await lock.release({
553
+ return await surfaceTaskingBlock({
549
554
  slug,
550
555
  cwd,
551
556
  arbiter,
557
+ reason,
558
+ message: loopDisposition.message,
552
559
  lockedBlob,
553
- routeToNeedsAttention: {reason},
560
+ release: lock.release,
561
+ mode: resolvedMode,
562
+ provider: options.providerInstance,
554
563
  env,
555
564
  note,
556
565
  });
557
- if (routed.outcome !== 'released') {
558
- return releaseFailureToResult(routed, slug);
559
- }
560
566
  }
561
567
  note(loopDisposition.message);
568
+ // Human, no-lock path: nothing to release/surface via the seam; a clean
569
+ // park-for-human is still a success terminal (exit 0).
562
570
  return {
563
- exitCode: 1,
571
+ exitCode: 0,
564
572
  outcome: 'needs-attention',
565
573
  slug,
566
574
  message: loopDisposition.message,
@@ -687,7 +695,7 @@ export async function performTask(
687
695
  // `merge` IS the auto-land mode, so a resolved `merge` is never downgraded.
688
696
  // The task gate family is `--review`/`--no-review`/`--review-model` only
689
697
  // (spec US #6).
690
- mode: options.integration ?? 'propose',
698
+ mode: resolvedMode,
691
699
  noPR: options.noPR,
692
700
  providerInstance: options.providerInstance,
693
701
  // PROPOSE-MODE PR BODY (task `slicing-pr-body-summary-threading`): mirror the
@@ -736,27 +744,22 @@ export async function performTask(
736
744
  // needs-attention THROUGH the lock release — the set never lands.
737
745
  if (core.outcome === 'review-blocked') {
738
746
  const reason = taskGateBlockedReason(slug, core.reviewBlockReason);
739
- const routed = await lock.release({
747
+ return await surfaceTaskingBlock({
740
748
  slug,
741
749
  cwd,
742
750
  arbiter,
751
+ reason,
752
+ message:
753
+ `The task acceptance gate disapproved the set produced for '${slug}'; ` +
754
+ `parked it for your attention (no tasks landed). ` +
755
+ `Resolve the findings, then re-task.`,
743
756
  lockedBlob,
744
- routeToNeedsAttention: {reason},
757
+ release: lock.release,
758
+ mode: resolvedMode,
759
+ provider: options.providerInstance,
745
760
  env,
746
761
  note,
747
762
  });
748
- if (routed.outcome !== 'released') {
749
- return releaseFailureToResult(routed, slug);
750
- }
751
- note(reason);
752
- return {
753
- exitCode: 1,
754
- outcome: 'needs-attention',
755
- slug,
756
- message:
757
- `The task acceptance gate blocked the set produced for '${slug}'; ` +
758
- `marked the per-item lock stuck (needs attention; no tasks landed).`,
759
- };
760
763
  }
761
764
  if (core.outcome === 'sidecar-violation') {
762
765
  // A co-located `<slug>/` asset sidecar sits beside the FLOWING spec item
@@ -768,28 +771,22 @@ export async function performTask(
768
771
  const reason =
769
772
  `The spec '${slug}' carries a co-located asset sidecar: ` +
770
773
  `${core.reviewBlockReason ?? core.reason ?? ''}`;
771
- const routed = await lock.release({
774
+ return await surfaceTaskingBlock({
772
775
  slug,
773
776
  cwd,
774
777
  arbiter,
778
+ reason,
779
+ message:
780
+ `The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
781
+ `rule 8); parked it for your attention (no tasks landed; relocate it ` +
782
+ `to docs/spikes/${slug}/ and reference by path).`,
775
783
  lockedBlob,
776
- routeToNeedsAttention: {reason},
784
+ release: lock.release,
785
+ mode: resolvedMode,
786
+ provider: options.providerInstance,
777
787
  env,
778
788
  note,
779
789
  });
780
- if (routed.outcome !== 'released') {
781
- return releaseFailureToResult(routed, slug);
782
- }
783
- note(reason);
784
- return {
785
- exitCode: 1,
786
- outcome: 'needs-attention',
787
- slug,
788
- message:
789
- `The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
790
- `rule 8); marked the per-item lock stuck (needs attention; no tasks ` +
791
- `landed; relocate it to docs/spikes/${slug}/ and reference by path).`,
792
- };
793
790
  }
794
791
  if (core.outcome === 'review-unparseable') {
795
792
  // The task-set acceptance gate RAN but its verdict was UNPARSEABLE (malformed
@@ -801,41 +798,50 @@ export async function performTask(
801
798
  const reason =
802
799
  `The task acceptance gate for '${slug}' produced an UNPARSEABLE verdict ` +
803
800
  `(re-run — transient): ${core.reason ?? ''}`;
804
- const routed = await lock.release({
801
+ return await surfaceTaskingBlock({
805
802
  slug,
806
803
  cwd,
807
804
  arbiter,
805
+ reason,
806
+ message:
807
+ `The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
808
+ `parked it for your attention (no tasks landed; re-run).`,
808
809
  lockedBlob,
809
- routeToNeedsAttention: {reason},
810
+ release: lock.release,
811
+ mode: resolvedMode,
812
+ provider: options.providerInstance,
810
813
  env,
811
814
  note,
812
815
  });
813
- if (routed.outcome !== 'released') {
814
- return releaseFailureToResult(routed, slug);
815
- }
816
- note(reason);
817
- return {
818
- exitCode: 1,
819
- outcome: 'needs-attention',
820
- slug,
821
- message:
822
- `The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
823
- `marked the per-item lock stuck (needs attention; no tasks landed; re-run).`,
824
- };
825
816
  }
826
817
  if (core.outcome === 'completed') {
827
- // The durable `specs/ready specs/tasked` `main` move landed through the shared integrate
828
- // core (the body moved straight from `work/specs/ready/` — no transient `tasking/`
829
- // marker). The completing commit is owned by the integrate band, NOT
830
- // `releaseTaskingLock`, so the unified per-item lock that `acquireTaskingLock`
831
- // took is released HERE (delete the ref). A
832
- // `propose` (`mode: 'propose'`) is ALSO `completed` (the PR opened, the lock's
833
- // hold over the in-flight tasking is done); the eventual hold-across-the-PR
834
- // crash-safe ordering is the capstone task #7's concern, not this interim
835
- // half. Best-effort + idempotent (`not-held` is fine).
836
- // MIGRATE step: release under the `spec:<slug>` identity (keyed to the
837
- // `spec-<slug>` lock entry the acquire now takes). Idempotent.
838
- if (useLock) {
818
+ // LOCK LIFETIME IS MODE-DEPENDENT (fix
819
+ // `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`;
820
+ // the "hold-across-the-PR" capstone the older interim half deferred).
821
+ //
822
+ // `merge`: the durable `specs/ready specs/tasked` `main` move HAS landed, so
823
+ // residence-in-`specs/tasked/` on `main` now carries tasked-ness and the lock
824
+ // has done its job RELEASE it (delete the ref). Best-effort + idempotent
825
+ // (`not-held` is fine).
826
+ //
827
+ // `propose`: the move lives ONLY on the pushed work branch / open PR — `main`
828
+ // still holds the spec in `work/specs/ready/`, so residence does NOT yet signal
829
+ // tasked-ness. If we released here, the next in-place scan would see the spec
830
+ // eligible again (ready + not-tasked + lock free) and re-task it EVERY tick,
831
+ // force-recreating the branch (`git switch -C`) and force-pushing the SAME PR —
832
+ // regenerating its review forever until a human merges/closes it. So we KEEP
833
+ // the `spec:<slug>` lock HELD across the open PR: the held lock IS the
834
+ // "in-flight tasking" marker, and `scoreSpecs`/`taskableSpecs` subtract held
835
+ // specs from the taskable pool (symmetric to the task-pool held-slug
836
+ // subtraction). The lock is reaped when the PR resolves: a MERGE lands the
837
+ // durable move (which now carries tasked-ness on its own) and the reap/gc path
838
+ // deletes the merged branch + its lock; a human CLOSING the PR unmerged is a
839
+ // recovery the human owns via `release-lock` (the same model as any other
840
+ // abandoned in-flight item — no liveness heartbeat/auto-steal).
841
+ //
842
+ // MIGRATE step: the ref is keyed under the `spec:<slug>` identity (the
843
+ // `spec-<slug>` lock entry the acquire took).
844
+ if (useLock && resolvedMode === 'merge') {
839
845
  await releaseItemLock({item: `spec:${slug}`, cwd, arbiter, env});
840
846
  }
841
847
  }
@@ -1199,21 +1205,101 @@ function taskGateBlockedReason(
1199
1205
  ): string {
1200
1206
  const head =
1201
1207
  `The task acceptance gate (fresh-context review of the produced SET) blocked ` +
1202
- `'${slug}'. The spec is routed to needs-attention with no tasks landed; a human ` +
1203
- `must resolve the blocking findings, then re-task.`;
1208
+ `'${slug}'. The spec is parked for your attention with no tasks landed; ` +
1209
+ `resolve the blocking findings, then re-task.`;
1204
1210
  return findingsReason ? `${head}\n\n${findingsReason}` : head;
1205
1211
  }
1206
1212
 
1213
+ /**
1214
+ * Shared terminal for a tasking transition that DID NOT land — a disapproving
1215
+ * task-set review, a co-located sidecar violation, an unparseable verdict, or a
1216
+ * decomposition-unclear loop exhaustion. It:
1217
+ *
1218
+ * 1. (propose mode + a provider) CLOSES the spec's OPEN PR — if one already
1219
+ * exists (the multi-run artefact) — with the disapproving `reason` as the
1220
+ * CLOSING COMMENT, KEEPING the branch (task
1221
+ * `tasking-disapprove-closes-existing-pr-keeps-branch`). A later approving
1222
+ * re-task REOPENs the same PR via `openRequest`. Only-if-exists: we never
1223
+ * OPEN a PR just to close it, and merge mode has no PR to close.
1224
+ * 2. RELEASES the spec's per-item lock and SURFACES the item for the human as a
1225
+ * `needsAnswers: true` body + a question sidecar on `<arbiter>/main` (via the
1226
+ * release's `routeToNeedsAttention` redirect). The lock is RELEASED, not
1227
+ * held — this is a parked question, not a lock the human must clear.
1228
+ *
1229
+ * A clean surface is a SUCCESSFUL terminal of the loop's "drain toward done OR
1230
+ * surface a question and idle" contract, so it returns `exitCode: 0` (the CI leg
1231
+ * is GREEN — a park-for-human is not a failure). Only a surface that FAILED to
1232
+ * publish maps to a non-zero {@link releaseFailureToResult}.
1233
+ */
1234
+ async function surfaceTaskingBlock(params: {
1235
+ slug: string;
1236
+ cwd: string;
1237
+ arbiter: string;
1238
+ reason: string;
1239
+ message: string;
1240
+ lockedBlob: string | undefined;
1241
+ release: TaskingLockSeam['release'];
1242
+ mode: 'propose' | 'merge';
1243
+ provider: ReviewProvider | undefined;
1244
+ env: NodeJS.ProcessEnv | undefined;
1245
+ note: (message: string) => void;
1246
+ }): Promise<TaskResult> {
1247
+ const {
1248
+ slug,
1249
+ cwd,
1250
+ arbiter,
1251
+ reason,
1252
+ message,
1253
+ lockedBlob,
1254
+ release,
1255
+ mode,
1256
+ provider,
1257
+ env,
1258
+ note,
1259
+ } = params;
1260
+
1261
+ // 1. Close a stale OPEN PR (propose only, only-if-exists) with the review as
1262
+ // the closing comment; keep the branch for a later approving re-task to
1263
+ // reopen. Advisory + never-throw — a close failure never blocks the surface.
1264
+ if (mode === 'propose' && provider !== undefined) {
1265
+ const closed = await provider.closeRequestOnBranch({
1266
+ cwd,
1267
+ branch: workBranchRef('spec', slug),
1268
+ arbiter,
1269
+ comment: reason,
1270
+ env,
1271
+ });
1272
+ note(closed.instruction);
1273
+ }
1274
+
1275
+ // 2. Release the lock + surface the parked question on main.
1276
+ const routed = await release({
1277
+ slug,
1278
+ cwd,
1279
+ arbiter,
1280
+ lockedBlob,
1281
+ routeToNeedsAttention: {reason},
1282
+ env,
1283
+ note,
1284
+ });
1285
+ if (routed.outcome !== 'released') {
1286
+ return releaseFailureToResult(routed, slug);
1287
+ }
1288
+ note(reason);
1289
+ // exit 0: a clean park-for-human is a successful loop terminal, not a failure.
1290
+ return {exitCode: 0, outcome: 'needs-attention', slug, message};
1291
+ }
1292
+
1207
1293
  /**
1208
1294
  * Build the needs-attention REASON for a decomposition-unclear loop verdict (the
1209
- * spec is marked stuck on its per-item lock with these open questions, no guessed
1210
- * tasks). Prose only — recorded as the spec's stuck-lock reason.
1295
+ * spec is parked for the human with these open questions + a question sidecar on
1296
+ * main, no guessed tasks). Prose only — recorded as the parked question's reason.
1211
1297
  */
1212
1298
  function decompositionUnclearReason(slug: string, questions: string[]): string {
1213
1299
  const head =
1214
1300
  `The tasker review→edit loop could not converge on a sound decomposition of ` +
1215
- `'${slug}' (--tasker-loop-max exhausted with unresolved blockers). The spec is routed ` +
1216
- `to needs-attention with no guessed tasks; a human must resolve:`;
1301
+ `'${slug}' (--tasker-loop-max exhausted with unresolved blockers). The spec is parked ` +
1302
+ `for your attention with no guessed tasks; resolve:`;
1217
1303
  const body =
1218
1304
  questions.length > 0
1219
1305
  ? questions.map((q) => `- ${q}`).join('\n')