dorfl 0.8.1 → 0.10.1

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 (58) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +70 -26
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +82 -7
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +14 -2
  7. package/dist/config.js.map +1 -1
  8. package/dist/do.d.ts +16 -1
  9. package/dist/do.d.ts.map +1 -1
  10. package/dist/do.js +14 -9
  11. package/dist/do.js.map +1 -1
  12. package/dist/env-config.d.ts.map +1 -1
  13. package/dist/env-config.js +25 -3
  14. package/dist/env-config.js.map +1 -1
  15. package/dist/gc.d.ts +17 -0
  16. package/dist/gc.d.ts.map +1 -1
  17. package/dist/gc.js +75 -2
  18. package/dist/gc.js.map +1 -1
  19. package/dist/git.d.ts +20 -0
  20. package/dist/git.d.ts.map +1 -1
  21. package/dist/git.js +155 -8
  22. package/dist/git.js.map +1 -1
  23. package/dist/intake-trigger-template.d.ts +91 -53
  24. package/dist/intake-trigger-template.d.ts.map +1 -1
  25. package/dist/intake-trigger-template.js +213 -114
  26. package/dist/intake-trigger-template.js.map +1 -1
  27. package/dist/intake.d.ts +53 -4
  28. package/dist/intake.d.ts.map +1 -1
  29. package/dist/intake.js +59 -15
  30. package/dist/intake.js.map +1 -1
  31. package/dist/placement.d.ts +42 -33
  32. package/dist/placement.d.ts.map +1 -1
  33. package/dist/placement.js +20 -17
  34. package/dist/placement.js.map +1 -1
  35. package/dist/repo-config.d.ts +1 -1
  36. package/dist/repo-config.d.ts.map +1 -1
  37. package/dist/repo-config.js +35 -0
  38. package/dist/repo-config.js.map +1 -1
  39. package/dist/tasking.d.ts +43 -0
  40. package/dist/tasking.d.ts.map +1 -1
  41. package/dist/tasking.js +39 -19
  42. package/dist/tasking.js.map +1 -1
  43. package/dist/workspace.d.ts.map +1 -1
  44. package/dist/workspace.js +64 -21
  45. package/dist/workspace.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/cli.ts +83 -29
  48. package/src/config.ts +96 -9
  49. package/src/do.ts +30 -10
  50. package/src/env-config.ts +25 -3
  51. package/src/gc.ts +95 -2
  52. package/src/git.ts +180 -8
  53. package/src/intake-trigger-template.ts +263 -143
  54. package/src/intake.ts +136 -18
  55. package/src/placement.ts +42 -36
  56. package/src/repo-config.ts +35 -0
  57. package/src/tasking.ts +66 -41
  58. package/src/workspace.ts +74 -20
package/src/cli.ts CHANGED
@@ -400,11 +400,13 @@ function buildRegistrySetAdvanceTick(options: {
400
400
  // The per-TRANSITION TASKING override: the `do spec:` tasking path threads
401
401
  // `taskingIntegration ?? integration`; the build path stays on `integration`.
402
402
  taskingIntegration: config.taskingIntegration,
403
- // The TASK-PLACEMENT configured default (`do spec:` tasking output:
403
+ // The TASK-PLACEMENT configured defaults (`do spec:` tasking output:
404
404
  // `backlog` staged vs `ready` pool). No operator flag on this
405
- // registry-driven advance context, so only the configured default rung is
406
- // threaded (the resolver still layers untrusted-origin force + built-in floor).
405
+ // registry-driven advance context, so only the configured default rungs
406
+ // are threaded (trusted + the untrusted twin the tasker selects from the
407
+ // spec's stamp; the resolver layers the built-in floor).
407
408
  tasksLandIn: config.tasksLandIn,
409
+ untrustedTasksLandIn: config.untrustedTasksLandIn,
408
410
  prepare: config.prepare,
409
411
  verify: config.verify,
410
412
  // Single-job build path: gate the REBASED tip (the default) unconditionally.
@@ -719,13 +721,13 @@ function explicitTasksLandInFromFlag(
719
721
  function explicitSpecsLandInFromFlag(
720
722
  raw: string | undefined,
721
723
  flagName = '--specs-land-in',
722
- ): 'pre-proposed' | 'ready' | undefined {
724
+ ): 'proposed' | 'ready' | undefined {
723
725
  if (raw === undefined) {
724
726
  return undefined;
725
727
  }
726
- if (raw !== 'pre-proposed' && raw !== 'ready') {
728
+ if (raw !== 'proposed' && raw !== 'ready') {
727
729
  throw new Error(
728
- `${flagName} must be 'pre-proposed' or 'ready' (got '${raw}').`,
730
+ `${flagName} must be 'proposed' or 'ready' (got '${raw}').`,
729
731
  );
730
732
  }
731
733
  return raw;
@@ -800,8 +802,10 @@ interface IntakeFlags {
800
802
  * flags. UNSET (a local intake) ⇒ emit unstamped ⇒ human/trusted.
801
803
  */
802
804
  originTrust?: string;
803
- /** `--specs-land-in <pre-proposed|ready>`: the explicit operator spec-placement override (top of the precedence). Resolves into the `specsLandIn` config key. */
805
+ /** `--specs-land-in <proposed|ready>`: the explicit operator spec-placement override (top of the precedence). Resolves into the `specsLandIn` config key. */
804
806
  specsLandIn?: string;
807
+ /** `--tasks-land-in <backlog|ready>`: the explicit operator TASK-placement override for a DIRECT-from-issue task emit (top of the precedence). The task twin of `--specs-land-in`. Resolves into the `tasksLandIn` config key. */
808
+ tasksLandIn?: string;
805
809
  agentCmd?: string;
806
810
  model?: string;
807
811
  harness?: string;
@@ -2592,10 +2596,12 @@ export function buildProgram(): Command {
2592
2596
  explicitMerge: flagMode === 'merge',
2593
2597
  // Per-TRANSITION TASKING override (the `do --remote spec:` tasking path).
2594
2598
  taskingIntegration: remoteConfig.taskingIntegration,
2595
- // TASK-PLACEMENT: the configured default + the EXPLICIT operator override
2596
- // (`--tasks-land-in`), the top of the placement precedence — mirrors
2597
- // `explicitMerge` (set only when the flag was typed).
2599
+ // TASK-PLACEMENT: the configured defaults (trusted + the untrusted twin
2600
+ // the tasker selects from the spec's stamp) + the EXPLICIT operator
2601
+ // override (`--tasks-land-in`), the top of the placement precedence —
2602
+ // mirrors `explicitMerge` (set only when the flag was typed).
2598
2603
  tasksLandIn: remoteConfig.tasksLandIn,
2604
+ untrustedTasksLandIn: remoteConfig.untrustedTasksLandIn,
2599
2605
  explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
2600
2606
  prepare: remoteConfig.prepare,
2601
2607
  verify: remoteConfig.verify,
@@ -2766,12 +2772,13 @@ export function buildProgram(): Command {
2766
2772
  // `taskingIntegration ?? integration`; the task-build path stays on
2767
2773
  // `integration`. Unset ⇒ tasking falls back to `integration` (today's behaviour).
2768
2774
  taskingIntegration: config.taskingIntegration,
2769
- // TASK-PLACEMENT (`do spec:` tasking output): the configured default rung +
2775
+ // TASK-PLACEMENT (`do spec:` tasking output): the configured default rungs
2776
+ // (trusted + the untrusted twin the tasker selects from the spec's stamp) +
2770
2777
  // the EXPLICIT operator override `--tasks-land-in` (top of the precedence).
2771
2778
  // `explicitTasksLandIn` is set ONLY when the flag was typed (mirrors
2772
- // `explicitMerge`), so an untrusted-origin staging force still wins under a
2773
- // config default.
2779
+ // `explicitMerge`).
2774
2780
  tasksLandIn: config.tasksLandIn,
2781
+ untrustedTasksLandIn: config.untrustedTasksLandIn,
2775
2782
  explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
2776
2783
  // In-place divergence guard override (mirrors --ignore-not-ready).
2777
2784
  ignoreDivergedMain: flags.ignoreDivergedMain === true,
@@ -3104,9 +3111,10 @@ export function buildProgram(): Command {
3104
3111
  explicitMerge: flagMode === 'merge',
3105
3112
  // Per-TRANSITION TASKING override (the isolated `do --remote spec:` path).
3106
3113
  taskingIntegration: remoteConfig.taskingIntegration,
3107
- // TASK-PLACEMENT: configured default + EXPLICIT `--tasks-land-in` override
3108
- // (set only when typed, mirroring `explicitMerge`).
3114
+ // TASK-PLACEMENT: configured defaults (trusted + untrusted twin) + EXPLICIT
3115
+ // `--tasks-land-in` override (set only when typed, mirroring `explicitMerge`).
3109
3116
  tasksLandIn: remoteConfig.tasksLandIn,
3117
+ untrustedTasksLandIn: remoteConfig.untrustedTasksLandIn,
3110
3118
  explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
3111
3119
  prepare: remoteConfig.prepare,
3112
3120
  verify: remoteConfig.verify,
@@ -3247,9 +3255,10 @@ export function buildProgram(): Command {
3247
3255
  // Per-TRANSITION TASKING override (the `do spec:` tasking path threads
3248
3256
  // `taskingIntegration ?? integration`; the build path stays on `integration`).
3249
3257
  taskingIntegration: config.taskingIntegration,
3250
- // TASK-PLACEMENT: configured default + EXPLICIT `--tasks-land-in` override
3251
- // (set only when typed, mirroring `explicitMerge`).
3258
+ // TASK-PLACEMENT: configured defaults (trusted + untrusted twin) + EXPLICIT
3259
+ // `--tasks-land-in` override (set only when typed, mirroring `explicitMerge`).
3252
3260
  tasksLandIn: config.tasksLandIn,
3261
+ untrustedTasksLandIn: config.untrustedTasksLandIn,
3253
3262
  explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
3254
3263
  prepare: config.prepare,
3255
3264
  verify: config.verify,
@@ -3643,8 +3652,15 @@ export function buildProgram(): Command {
3643
3652
  ` [retained] ${retained.slug} \u2014 ${RETAIN_REASON_TEXT[retained.reason]}`,
3644
3653
  );
3645
3654
  }
3655
+ for (const orphan of result.sweptOrphans) {
3656
+ console.log(` [swept] ${orphan.dir} \u2014 ${orphan.kind}`);
3657
+ }
3658
+ const orphanNote =
3659
+ result.sweptOrphans.length > 0
3660
+ ? `, ${result.sweptOrphans.length} orphan(s) swept`
3661
+ : '';
3646
3662
  console.log(
3647
- `Summary: ${result.reaped.length} reaped, ${result.retained.length} retained.`,
3663
+ `Summary: ${result.reaped.length} reaped, ${result.retained.length} retained${orphanNote}.`,
3648
3664
  );
3649
3665
  });
3650
3666
 
@@ -4277,7 +4293,11 @@ export function buildProgram(): Command {
4277
4293
  )
4278
4294
  .option(
4279
4295
  '--specs-land-in <where>',
4280
- 'where an intake-authored spec lands: `pre-proposed` (staged, not auto-taskable) or `ready` (the auto-tasking pool). The EXPLICIT operator override at the top of the placement precedence (explicit flag > untrusted-origin forces staging > specsLandIn default > built-in). Resolved flag > env (DORFL_SPECS_LAND_IN) > per-repo > global > built-in.',
4296
+ 'where an intake-authored spec lands: `proposed` (staged, not auto-taskable) or `ready` (the auto-tasking pool). The EXPLICIT operator override at the top of the placement precedence (explicit flag > untrusted-origin forces staging > specsLandIn default > built-in). Resolved flag > env (DORFL_SPECS_LAND_IN) > per-repo > global > built-in.',
4297
+ )
4298
+ .option(
4299
+ '--tasks-land-in <where>',
4300
+ 'where an intake-authored TASK (emitted DIRECTLY from an issue) lands: `backlog` (staged, not agent-eligible) or `ready` (the agent POOL). The task twin of --specs-land-in: the EXPLICIT operator override at the top of the placement precedence (explicit flag > configured default > built-in staging; the caller selects the trusted-vs-untrusted default from the origin-trust stamp). Resolved flag > env (DORFL_TASKS_LAND_IN) > per-repo > global > built-in.',
4281
4301
  )
4282
4302
  .option('--agent-cmd <cmd>', 'command to run the decision agent')
4283
4303
  .option(
@@ -4327,14 +4347,21 @@ export function buildProgram(): Command {
4327
4347
  // Resolve the PER-OUTCOME integration modes (spec US #9): `intake` decides
4328
4348
  // the artifact TYPE at runtime, so a single --merge/--propose can't express
4329
4349
  // a type-conditional policy. The granular flags override the aggregate; an
4330
- // UNSET type falls back to the per-repo/global `integration` (the SAME chain
4331
- // `do`/`complete` use — flag > per-repo > global > default propose). `intake`
4332
- // is GATE-FREE, so autoTask/autoBuild are NOT consulted (the explicit
4333
- // invocation is its own authorization). `intake` owns only these KNOBS; WHICH
4334
- // knobs CI sets is CI's POLICY (`runner-in-ci`), NOT here.
4350
+ // UNSET type falls back to the per-repo/global `intakeIntegration ??
4351
+ // integration` (the INTAKE-DOCUMENT knob, twin of the tasking path's
4352
+ // `taskingIntegration ?? integration`; spec
4353
+ // `intake-integration-knob-and-specs-land-in-proposed-rename`). DECOUPLED
4354
+ // from the autonomy gates: `intake` is GATE-FREE, so autoTask/autoBuild are
4355
+ // NOT consulted for the document mode (the explicit invocation is its own
4356
+ // authorization; ADR untrusted-origin-carries-via-stamp-not-forced-staging).
4357
+ // `intake` owns only these KNOBS; WHICH knobs CI sets is CI's POLICY
4358
+ // (`runner-in-ci`), NOT here.
4335
4359
  let modes;
4336
4360
  try {
4337
- modes = resolveIntakeIntegrationModes(flags, config.integration);
4361
+ modes = resolveIntakeIntegrationModes(
4362
+ flags,
4363
+ config.intakeIntegration ?? config.integration,
4364
+ );
4338
4365
  } catch (err) {
4339
4366
  console.error(
4340
4367
  `error: ${err instanceof Error ? err.message : String(err)}`,
@@ -4367,7 +4394,7 @@ export function buildProgram(): Command {
4367
4394
  // The OPERATOR's EXPLICIT spec-placement override (`--specs-land-in`), the
4368
4395
  // TOP of the placement precedence — mirrors `explicitTasksLandInFromFlag` on
4369
4396
  // the `do spec:` path. Fails loudly on a bad value.
4370
- let explicitSpecsLandIn: 'pre-proposed' | 'ready' | undefined;
4397
+ let explicitSpecsLandIn: 'proposed' | 'ready' | undefined;
4371
4398
  try {
4372
4399
  explicitSpecsLandIn = explicitSpecsLandInFromFlag(flags.specsLandIn);
4373
4400
  } catch (err) {
@@ -4376,6 +4403,19 @@ export function buildProgram(): Command {
4376
4403
  );
4377
4404
  process.exit(1);
4378
4405
  }
4406
+ // The OPERATOR's EXPLICIT task-placement override (`--tasks-land-in`), the
4407
+ // TOP of the placement precedence for a DIRECT-from-issue task emit — the
4408
+ // task twin of `explicitSpecsLandIn` above (reuses the same `do`-path
4409
+ // helper). Fails loudly on a bad value.
4410
+ let explicitTasksLandIn: 'backlog' | 'ready' | undefined;
4411
+ try {
4412
+ explicitTasksLandIn = explicitTasksLandInFromFlag(flags.tasksLandIn);
4413
+ } catch (err) {
4414
+ console.error(
4415
+ `error: ${err instanceof Error ? err.message : String(err)}`,
4416
+ );
4417
+ process.exit(1);
4418
+ }
4379
4419
  const result = await performIntake({
4380
4420
  issueNumber,
4381
4421
  cwd,
@@ -4391,11 +4431,25 @@ export function buildProgram(): Command {
4391
4431
  // The origin-trust stamp the CI shell passes IN (unset ⇒ unstamped).
4392
4432
  originTrust,
4393
4433
  noPR: config.noPR,
4394
- // SPEC-PLACEMENT: the configured-default `specsLandIn` rung + the EXPLICIT
4395
- // `--specs-land-in` override (top of the precedence). The shared placement
4396
- // resolver in `intake.ts` overlays the untrusted-origin staging force.
4434
+ // SPEC-PLACEMENT: the TRUSTED-side configured default (`specsLandIn`) +
4435
+ // the UNTRUSTED-side twin (`untrustedSpecsLandIn`, selected in
4436
+ // `dispatchSpec` when the stamp is untrusted) + the EXPLICIT
4437
+ // `--specs-land-in` override (top of the precedence). The placement
4438
+ // resolver no longer has an untrusted-forces-staging rung (ADR
4439
+ // `untrusted-origin-carries-via-stamp-not-forced-staging`); the caller
4440
+ // selects the trusted-vs-untrusted default from the stamp.
4397
4441
  specsLandIn: config.specsLandIn,
4442
+ untrustedSpecsLandIn: config.untrustedSpecsLandIn,
4398
4443
  explicitSpecsLandIn,
4444
+ // TASK-PLACEMENT (parity with the spec-placement block above; ADR
4445
+ // `untrusted-origin-carries-via-stamp-not-forced-staging`): the
4446
+ // TRUSTED-side default (`tasksLandIn`) + the UNTRUSTED-side twin
4447
+ // (`untrustedTasksLandIn`, selected in `dispatchTask` when the stamp is
4448
+ // untrusted) + the EXPLICIT `--tasks-land-in` override. Replaces the old
4449
+ // hardcoded `tasks-ready` path; the resolver picks backlog vs ready.
4450
+ tasksLandIn: config.tasksLandIn,
4451
+ untrustedTasksLandIn: config.untrustedTasksLandIn,
4452
+ explicitTasksLandIn,
4399
4453
  harness,
4400
4454
  agentCmd: config.agentCmd,
4401
4455
  model: config.model,
package/src/config.ts CHANGED
@@ -44,21 +44,23 @@ export type TasksLandIn = 'backlog' | 'ready';
44
44
  * `pre-prd-staging-pool-split-and-untrusted-prd-placement`, governing ADR
45
45
  * `placement-is-runner-deterministic-humanonly-is-agent-judgement`). Which
46
46
  * folder the runner lands `intake`-authored spec files in BY DEFAULT —
47
- * `'pre-proposed'` (staging — durable + readable but NOT in the auto-tasking
47
+ * `'proposed'` (staging — durable + readable but NOT in the auto-tasking
48
48
  * POOL; a runner/human promotion is needed to make the spec auto-taskable; the
49
49
  * on-disk folder for this value is `work/specs/proposed/`) or `'ready'` (the
50
50
  * auto-tasking POOL — the trusted fast-path landing, on-disk
51
51
  * `work/specs/ready/`). The same runner-deterministic placement RESOLVER
52
52
  * (`src/placement.ts`) layers on top:
53
- * `explicit operator flag > untrusted-origin ⇒ pre-proposed > specsLandIn
54
- * default > built-in (pre-proposed)`. An untrusted-origin intake spec is FORCED
55
- * to staging even in a `'ready'` repo (the positional analogue of the existing
56
- * `untrusted-origin-forces-build-propose` rule). The SPEC twin of
53
+ * `explicit operator flag > configured default > built-in (proposed)`.
54
+ * Author-trust is NO LONGER a rung in the resolver (ADR
55
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`): the CALLER reads the
56
+ * `originTrust:` stamp and selects the untrusted twin `untrustedSpecsLandIn`
57
+ * (default `'proposed'`; opt-in `'ready'`) as the configured default for an
58
+ * untrusted intake spec. The SPEC twin of
57
59
  * {@link TasksLandIn}; the SAME shape, the SAME precedence chain. The value
58
60
  * spellings mirror the live spec folders (`specs/proposed/` staging,
59
61
  * `specs/ready/` pool), exactly as {@link TasksLandIn} mirrors the task folders.
60
62
  */
61
- export type SpecsLandIn = 'pre-proposed' | 'ready';
63
+ export type SpecsLandIn = 'proposed' | 'ready';
62
64
 
63
65
  /**
64
66
  * The observation-triage gate (ADR `ci-config-policy-and-gate-family` §2): a
@@ -357,6 +359,37 @@ export interface Config {
357
359
  * per-LIFECYCLE-TRANSITION knob, inside the trust boundary, operator/config-only.
358
360
  */
359
361
  taskingIntegration?: IntegrationMode;
362
+ /**
363
+ * **Per-TRANSITION override for the INTAKE DOCUMENT emit only** — the twin of
364
+ * {@link taskingIntegration} for the intake front door. When set, an
365
+ * `intake`-emitted DOCUMENT (a task file `work/tasks/*` OR a spec file
366
+ * `work/specs/*`) integrates with THIS mode instead of the flat
367
+ * {@link integration}; the task-BUILD transition is unaffected (it always reads
368
+ * {@link integration}), and neither is the tasking transition (it reads
369
+ * {@link taskingIntegration}). UNSET (the default) ⇒ intake falls back to
370
+ * {@link integration} — byte-for-byte today's behaviour for any repo that does
371
+ * not set it, so a single `integration: 'merge'` merges documents across BOTH
372
+ * the tasking and intake transitions with no extra key.
373
+ *
374
+ * A SINGLE value applies to both the task AND the spec document (spec
375
+ * `intake-integration-knob-and-specs-land-in-proposed-rename` US #1 chose a
376
+ * single knob, NOT a per-type `{task, spec}` split). Decoupled from the
377
+ * AUTONOMY GATES (`autoBuild`/`autoTask`): the intake document PR-mode is now an
378
+ * operator/config choice, NOT a function of "may an agent act autonomously" —
379
+ * so a repo can have `autoBuild: true`/`autoTask: true` (autonomy) AND intake
380
+ * documents merging to `main` at the same time (ADR
381
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`). Untrusted safety is
382
+ * unchanged: it rests entirely on placement (`untrusted*LandIn`) + the
383
+ * build-time `originTrust: untrusted` stamp (the code PR), never a forced
384
+ * document PR. Resolved per-repo like {@link taskingIntegration}: flag
385
+ * (`--merge`/`--propose`) > env (`DORFL_INTAKE_INTEGRATION`) > per-repo >
386
+ * global > (fall back to) `integration` > default `propose`. The intake CLI's
387
+ * explicit `--merge-task`/`--merge-spec`/`--merge`/`--propose` flags still win
388
+ * (operator-present, top of precedence). DISTINCT from `taskingIntegration` (a
389
+ * DIFFERENT lifecycle transition) — each is its own `merge|propose` mode that
390
+ * falls back to `integration`, none tied to an autonomy gate.
391
+ */
392
+ intakeIntegration?: IntegrationMode;
360
393
  /**
361
394
  * **Per-repo DEFAULT landing for the TASKER's emitted tasks** (spec
362
395
  * `staging-pool-position-gate-and-trust-model` US #5, task
@@ -377,7 +410,7 @@ export interface Config {
377
410
  * `staging-pool-position-gate-and-trust-model` US #2/#5/#6/#12). The SPEC twin
378
411
  * of {@link tasksLandIn}: resolved per-repo EXACTLY like it (flag
379
412
  * `--specs-land-in` > env `DORFL_SPECS_LAND_IN` > per-repo > global >
380
- * built-in `'pre-proposed'`). `intake`'s spec dispatch reads it and passes it as
413
+ * built-in `'proposed'`). `intake`'s spec dispatch reads it and passes it as
381
414
  * the CONFIGURED-DEFAULT rung into the shared placement resolver
382
415
  * (`src/placement.ts`); the resolver overlays an EXPLICIT operator flag
383
416
  * (top) and the UNTRUSTED-ORIGIN force (staging) on top, in that order.
@@ -391,6 +424,48 @@ export interface Config {
391
424
  * `prdsLandIn` config key + `--prds-land-in` flag are GONE (clean break).
392
425
  */
393
426
  specsLandIn: SpecsLandIn;
427
+ /**
428
+ * **Per-repo DEFAULT landing for an UNTRUSTED-origin TASK** (spec
429
+ * `untrusted-origin-carries-via-stamp-intake-placement-symmetry-and-ci-gate-resolution`
430
+ * US #5/#6, governing ADR
431
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`). The exact TWIN of
432
+ * {@link tasksLandIn} for items whose `originTrust` stamp is `untrusted`:
433
+ * SAME value shape ({@link TasksLandIn}), SAME resolution chain (flag
434
+ * `--untrusted-tasks-land-in` > env `DORFL_UNTRUSTED_TASKS_LAND_IN` >
435
+ * per-repo > global > built-in `'backlog'`). The ADR removes the
436
+ * untrusted-forces-staging RUNG from the placement resolver; instead the
437
+ * CALLER reads the stamp and selects THIS key (vs the trusted
438
+ * {@link tasksLandIn}) as the configured-default rung. Governs EVERY
439
+ * untrusted-stamped task — intake-authored directly from an issue OR emitted
440
+ * by the tasker from an untrusted-origin spec (decision X: one policy, both
441
+ * call sites; no third knob). DEFAULTS to STAGING (`'backlog'`), the
442
+ * conservative human-admission landing: safety for an untrusted item that
443
+ * opts into the pool (`'ready'`) is then the carried build STAMP (a code PR),
444
+ * not the folder. CONSUMED at both task call sites: the tasker
445
+ * (`performTask`) selects it for an untrusted-origin spec's emitted tasks, and
446
+ * intake's direct task emit (`dispatchTask`, a sibling task) selects it for a
447
+ * task born straight from an issue.
448
+ */
449
+ untrustedTasksLandIn: TasksLandIn;
450
+ /**
451
+ * **Per-repo DEFAULT landing for an UNTRUSTED-origin intake SPEC** (spec
452
+ * `untrusted-origin-carries-via-stamp-intake-placement-symmetry-and-ci-gate-resolution`
453
+ * US #7, governing ADR
454
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`). The exact TWIN of
455
+ * {@link specsLandIn} for untrusted-stamped intake specs: SAME value shape
456
+ * ({@link SpecsLandIn} — `proposed` staging vs `ready` pool), SAME
457
+ * resolution chain (flag `--untrusted-specs-land-in` > env
458
+ * `DORFL_UNTRUSTED_SPECS_LAND_IN` > per-repo > global > built-in
459
+ * `'proposed'`). The caller reads the `originTrust` stamp and selects THIS
460
+ * key (vs the trusted {@link specsLandIn}) as the placement resolver's
461
+ * configured-default rung. DEFAULTS to STAGING (`'proposed'`) — the
462
+ * conservative human-admission landing, preserving today's effective
463
+ * behaviour for a repo that configures nothing; a repo opts an untrusted spec
464
+ * into the pool (`'ready'`) explicitly, safety then via the carried stamp.
465
+ * CONSUMED at the intake SPEC call site (`dispatchSpec`), which selects it
466
+ * over {@link specsLandIn} when the intake stamp is `originTrust: untrusted`.
467
+ */
468
+ untrustedSpecsLandIn: SpecsLandIn;
394
469
  /**
395
470
  * **The PR-INTENT axis** (ADR §6): on the `propose` path, do NOT open a review
396
471
  * request even on a GitHub arbiter with auth — push the branch (the
@@ -825,14 +900,26 @@ export const DEFAULT_CONFIG: Config = {
825
900
  // The runner-deterministic resolver overlays explicit-flag + untrusted-origin
826
901
  // force on top of this default (`src/placement.ts`).
827
902
  tasksLandIn: 'backlog',
828
- // `intake`-authored specs land STAGED (`pre-proposed/`) by default — the
903
+ // `intake`-authored specs land STAGED (`proposed/`) by default — the
829
904
  // conservative landing that mirrors `tasksLandIn`'s built-in floor: a spec is
830
905
  // durable + readable but NOT in the auto-tasking POOL until a human/runner
831
906
  // promotes it. A repo opts into the trusted fast-path with `specsLandIn: 'ready'`
832
907
  // (or `--specs-land-in ready` / `DORFL_SPECS_LAND_IN=ready`). The same
833
908
  // runner-deterministic resolver overlays explicit-flag + untrusted-origin
834
909
  // force on top of this default (`src/placement.ts`).
835
- specsLandIn: 'pre-proposed',
910
+ specsLandIn: 'proposed',
911
+ // The untrusted-side placement TWINS default to STAGING (`backlog` /
912
+ // `proposed`) — the conservative human-admission landing (ADR
913
+ // `untrusted-origin-carries-via-stamp-not-forced-staging`). A repo that trusts
914
+ // its stamp-based pipeline opts an untrusted item into the pool (`ready`)
915
+ // explicitly; safety is then the carried build STAMP (a code PR), not the
916
+ // folder. Unset ⇒ both resolve to staging, so a repo configuring nothing keeps
917
+ // today's effective behaviour. NO call site consumes these yet (resolver +
918
+ // intake/tasker wiring are later tasks); resolved flag > env
919
+ // (`DORFL_UNTRUSTED_TASKS_LAND_IN` / `DORFL_UNTRUSTED_SPECS_LAND_IN`) > per-repo
920
+ // > global > built-in, exactly like their trusted twins.
921
+ untrustedTasksLandIn: 'backlog',
922
+ untrustedSpecsLandIn: 'proposed',
836
923
  agentCmd: '',
837
924
  // Gate 2 (PR/code review) defaults OFF — it puts a model on the merge path, so
838
925
  // it is opt-in (ADR §8). On an `approve` a resolved `merge` lands automatically
package/src/do.ts CHANGED
@@ -416,11 +416,21 @@ export interface DoOptions {
416
416
  * lifecycle concern).
417
417
  */
418
418
  tasksLandIn?: 'backlog' | 'ready';
419
+ /**
420
+ * **The per-repo UNTRUSTED-side TASK-PLACEMENT default** (spec
421
+ * `untrusted-origin-carries-via-stamp-intake-placement-symmetry-and-ci-gate-resolution`,
422
+ * ADR `untrusted-origin-carries-via-stamp-not-forced-staging`). Threaded into
423
+ * {@link performTask}, which selects it (over {@link tasksLandIn}) as the
424
+ * configured-default rung when the tasked spec carries an
425
+ * `originTrust: untrusted` stamp. See {@link performTask}'s
426
+ * `untrustedTasksLandIn`.
427
+ */
428
+ untrustedTasksLandIn?: 'backlog' | 'ready';
419
429
  /**
420
430
  * **The OPERATOR's EXPLICIT task-placement override** (the TOP precedence
421
431
  * rung in the placement resolver). Set ONLY when the operator typed
422
432
  * `--tasks-land-in <where>` on this invocation; never when the value came
423
- * from config. Wins over `originTrust: untrusted` (the operator is present;
433
+ * from config. Wins over the configured default (the operator is present;
424
434
  * CLI always wins, no special force-key) — the positional analogue of
425
435
  * `explicitMerge` overriding the untrusted-origin build-propose rule.
426
436
  */
@@ -704,6 +714,11 @@ export interface DoRemoteOptions extends DoAgentLaunchOptions {
704
714
  * configured-default rung. See {@link DoOptions.tasksLandIn}.
705
715
  */
706
716
  tasksLandIn?: 'backlog' | 'ready';
717
+ /**
718
+ * **The per-repo UNTRUSTED-side TASK-PLACEMENT default** on the `do --remote
719
+ * spec:` path. See {@link DoOptions.untrustedTasksLandIn}.
720
+ */
721
+ untrustedTasksLandIn?: 'backlog' | 'ready';
707
722
  /**
708
723
  * **The OPERATOR's EXPLICIT task-placement override** on the `do --remote
709
724
  * spec:` path. See {@link DoOptions.explicitTasksLandIn}.
@@ -942,13 +957,16 @@ export async function performDo(options: DoOptions): Promise<DoResult> {
942
957
  // same per-repo cap the build path uses (task
943
958
  // `thread-merge-retries-cross-task-and-ratify-default`).
944
959
  mergeRetries: options.mergeRetries,
945
- // The per-repo TASK-PLACEMENT default + the operator's explicit
946
- // override (task `runner-deterministic-slice-placement-policy-and-
947
- // precedence`). The tasker reads them as the configured-default + the
948
- // top rung of the runner-deterministic placement resolver; the
949
- // `originTrust: untrusted` force is read inside the tasker from the
950
- // spec's stamped frontmatter.
960
+ // The per-repo TASK-PLACEMENT defaults (trusted + untrusted twin) + the
961
+ // operator's explicit override (task `runner-deterministic-slice-
962
+ // placement-policy-and-precedence`; ADR `untrusted-origin-carries-via-
963
+ // stamp-not-forced-staging`). The tasker reads the spec's stamped
964
+ // `originTrust:` to SELECT the trusted-vs-untrusted configured default,
965
+ // then feeds it (with the explicit override on top) into the
966
+ // runner-deterministic placement resolver (which no longer has a trust
967
+ // rung).
951
968
  tasksLandIn: options.tasksLandIn,
969
+ untrustedTasksLandIn: options.untrustedTasksLandIn,
952
970
  explicitTasksLandIn: options.explicitTasksLandIn,
953
971
  noPR: options.noPR,
954
972
  providerInstance: options.providerInstance,
@@ -2372,10 +2390,12 @@ export async function performDoRemote(
2372
2390
  // threaded on the no-checkout `do --remote spec:` tasking path too,
2373
2391
  // mirroring the in-place site above.
2374
2392
  mergeRetries: options.mergeRetries,
2375
- // The per-repo TASK-PLACEMENT default + the operator's explicit
2376
- // override (task `runner-deterministic-slice-placement-policy-and-
2377
- // precedence`). Same threading as the in-place `do spec:` path.
2393
+ // The per-repo TASK-PLACEMENT defaults (trusted + untrusted twin) + the
2394
+ // operator's explicit override. Same stamp-selected threading as the
2395
+ // in-place `do spec:` path (ADR `untrusted-origin-carries-via-stamp-
2396
+ // not-forced-staging`).
2378
2397
  tasksLandIn: options.tasksLandIn,
2398
+ untrustedTasksLandIn: options.untrustedTasksLandIn,
2379
2399
  explicitTasksLandIn: options.explicitTasksLandIn,
2380
2400
  noPR: options.noPR,
2381
2401
  providerInstance: options.providerInstance,
package/src/env-config.ts CHANGED
@@ -98,6 +98,14 @@ const KEY_COERCIONS: {[K in keyof Config]?: Coercion} = {
98
98
  // works and a typo FAILS LOUDLY. Unset ⇒ the tasking transition falls back to
99
99
  // `integration` (the flat value). It NEVER touches the build transition or intake.
100
100
  taskingIntegration: {enum: ['propose', 'merge']},
101
+ // `intakeIntegration` (the per-TRANSITION INTAKE-DOCUMENT override — the twin of
102
+ // `taskingIntegration` for the intake front door) coerces as the SAME
103
+ // `propose`/`merge` enum as `integration`, so `DORFL_INTAKE_INTEGRATION`
104
+ // works and a typo FAILS LOUDLY. Unset ⇒ the intake document emit falls back to
105
+ // `integration` (the flat value). Decoupled from the autonomy gates
106
+ // (`autoBuild`/`autoTask`) — the intake document PR-mode is operator/config, not
107
+ // a function of autonomy (ADR untrusted-origin-carries-via-stamp-not-forced-staging).
108
+ intakeIntegration: {enum: ['propose', 'merge']},
101
109
  // `tasksLandIn` (the per-repo TASK-PLACEMENT default — spec
102
110
  // `staging-pool-position-gate-and-trust-model` US #5) coerces as the
103
111
  // `backlog`/`ready` enum, so `DORFL_TASKS_LAND_IN=ready` works and a
@@ -108,13 +116,27 @@ const KEY_COERCIONS: {[K in keyof Config]?: Coercion} = {
108
116
  tasksLandIn: {enum: ['backlog', 'ready']},
109
117
  // `specsLandIn` (the per-repo SPEC-PLACEMENT default — spec
110
118
  // `staging-pool-position-gate-and-trust-model` US #2/#5) coerces as the
111
- // `pre-proposed`/`ready` enum, so `DORFL_SPECS_LAND_IN=ready` works and a typo
119
+ // `proposed`/`ready` enum, so `DORFL_SPECS_LAND_IN=ready` works and a typo
112
120
  // FAILS LOUDLY. Same precedence chain as `tasksLandIn` (flag > env > per-repo
113
- // > global > built-in `pre-proposed`); fed into the shared placement resolver
121
+ // > global > built-in `proposed`); fed into the shared placement resolver
114
122
  // (`src/placement.ts`) as the configured-default rung for the spec lifecycle.
115
123
  // The legacy `prdsLandIn` key / `DORFL_PRDS_LAND_IN` env are GONE after the
116
124
  // ''prd'' → `spec` hard cutover (clean break).
117
- specsLandIn: {enum: ['pre-proposed', 'ready']},
125
+ specsLandIn: {enum: ['proposed', 'ready']},
126
+ // `untrustedTasksLandIn` (the UNTRUSTED-side TWIN of `tasksLandIn` — spec
127
+ // `untrusted-origin-carries-via-stamp-intake-placement-symmetry-and-ci-gate-resolution`
128
+ // US #5/#6, ADR `untrusted-origin-carries-via-stamp-not-forced-staging`)
129
+ // coerces as the SAME `backlog`/`ready` enum as `tasksLandIn`, so
130
+ // `DORFL_UNTRUSTED_TASKS_LAND_IN=ready` works and a typo FAILS LOUDLY. Same
131
+ // precedence chain (flag > env > per-repo > global > built-in `backlog`); the
132
+ // caller selects it (over the trusted `tasksLandIn`) by reading the
133
+ // `originTrust` stamp before feeding the placement resolver.
134
+ untrustedTasksLandIn: {enum: ['backlog', 'ready']},
135
+ // `untrustedSpecsLandIn` (the UNTRUSTED-side TWIN of `specsLandIn` — same spec
136
+ // US #7) coerces as the SAME `proposed`/`ready` enum as `specsLandIn`, so
137
+ // `DORFL_UNTRUSTED_SPECS_LAND_IN=ready` works and a typo FAILS LOUDLY. Same
138
+ // precedence chain (flag > env > per-repo > global > built-in `proposed`).
139
+ untrustedSpecsLandIn: {enum: ['proposed', 'ready']},
118
140
  // `noPR` (the PR-INTENT axis) is a BOOLEAN coercion (like `review`), so
119
141
  // `DORFL_NO_PR=true|false` works and a typo FAILS LOUDLY. The removed
120
142
  // `provider` override has NO env var (a stale `DORFL_PROVIDER` is ignored
package/src/gc.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {existsSync, readdirSync, statSync} from 'node:fs';
1
+ import {existsSync, lstatSync, readdirSync, rmSync, statSync} from 'node:fs';
2
2
  import {join} from 'node:path';
3
3
  import {git, run} from './git.js';
4
4
  import {
@@ -261,11 +261,29 @@ export interface GcOptions {
261
261
  env?: NodeJS.ProcessEnv;
262
262
  }
263
263
 
264
+ /**
265
+ * An ORPHAN `<workspacesDir>/work/*` entry `gc` swept: a path git never
266
+ * registered as a worktree and that carries NO job record — a dangling symlink,
267
+ * or a bare directory left by a run that crashed BEFORE `git worktree add`
268
+ * registered it (an early `spawn git ENOENT`). It holds no durable work, so it is
269
+ * always safe to remove; sweeping it self-heals a half-set-up claim instead of
270
+ * leaving it to wedge the next `worktree add` ("already exists") until a human
271
+ * `rm`s it. Reported SEPARATELY from reaped jobs (it was never a real job).
272
+ */
273
+ export interface SweptOrphan {
274
+ /** Absolute path to the orphan entry that was removed. */
275
+ dir: string;
276
+ /** Whether the orphan was a dangling symlink or an un-registered directory. */
277
+ kind: 'dangling-symlink' | 'orphan-dir';
278
+ }
279
+
264
280
  export interface GcResult {
265
281
  /** The worktrees reaped this sweep (provably safe, or forced). */
266
282
  reaped: ReapedJob[];
267
283
  /** The worktrees retained, each with a clear reason. */
268
284
  retained: RetainedJob[];
285
+ /** Record-less orphan `work/*` entries swept (dangling symlinks / orphan dirs). */
286
+ sweptOrphans: SweptOrphan[];
269
287
  }
270
288
 
271
289
  /**
@@ -284,6 +302,11 @@ export function gc(options: GcOptions): GcResult {
284
302
  const reaped: ReapedJob[] = [];
285
303
  const retained: RetainedJob[] = [];
286
304
 
305
+ // First, self-heal any record-less ORPHAN `work/*` entry (a dangling symlink
306
+ // or a dir git never registered) so a half-set-up claim does not linger unseen
307
+ // by the job loop below and wedge the next `worktree add`.
308
+ const sweptOrphans = sweepOrphans(options.workspacesDir, note);
309
+
287
310
  for (const job of discoverJobs(options.workspacesDir)) {
288
311
  const mirrorPath = resolveMirrorPath(options.workspacesDir, job);
289
312
  const result = reapJob({
@@ -310,7 +333,77 @@ export function gc(options: GcOptions): GcResult {
310
333
  note(`Retained ${job.slug}: ${reasonText}.`);
311
334
  }
312
335
 
313
- return {reaped, retained};
336
+ return {reaped, retained, sweptOrphans};
337
+ }
338
+
339
+ /**
340
+ * Sweep record-less ORPHAN entries under `<workspacesDir>/work/*`: a DANGLING
341
+ * SYMLINK (its target gone) or a directory with NO job record at either the
342
+ * sibling or legacy in-tree location. These are the residue of a run that
343
+ * crashed BETWEEN creating the `work/<id>` path and registering it as a git
344
+ * worktree (or writing its record) — e.g. an early `spawn git ENOENT`. They hold
345
+ * no durable work, are invisible to {@link discoverJobs} (which requires a
346
+ * record), and block the next same-id `worktree add`. Removing them is a bounded
347
+ * `rmSync` of ONE path each (never a registered worktree — those carry a record
348
+ * and go through the reap predicate). Best-effort per entry.
349
+ */
350
+ function sweepOrphans(
351
+ workspacesDir: string,
352
+ note: (message: string) => void,
353
+ ): SweptOrphan[] {
354
+ const workDir = join(workspacesDir, 'work');
355
+ if (!existsSync(workDir)) {
356
+ return [];
357
+ }
358
+ const swept: SweptOrphan[] = [];
359
+ for (const entry of readdirSync(workDir)) {
360
+ if (entry.endsWith('.json')) {
361
+ continue; // a sibling record file, not a work-id entry
362
+ }
363
+ const dir = join(workDir, entry);
364
+ let link;
365
+ try {
366
+ link = lstatSync(dir);
367
+ } catch {
368
+ continue; // vanished under us
369
+ }
370
+ const isSymlink = link.isSymbolicLink();
371
+ const targetExists = existsSync(dir); // follows the link; false ⇒ dangling
372
+ const hasRecord =
373
+ existsSync(jobRecordPath(dir)) ||
374
+ (targetExists && existsSync(join(dir, JOB_RECORD_FILENAME)));
375
+ // Orphan iff: a dangling symlink (target gone), OR a record-less entry that
376
+ // is not a live directory git could own (a stray symlink-to-elsewhere, or a
377
+ // dir with no record). A record-bearing entry is a real job → leave it to the
378
+ // reap predicate above.
379
+ const danglingSymlink = isSymlink && !targetExists;
380
+ if (hasRecord) {
381
+ continue;
382
+ }
383
+ if (!danglingSymlink) {
384
+ // A record-less real directory: only sweep it if git does not track it as a
385
+ // worktree here. `discoverJobs` already skips it (no record), and a
386
+ // registered worktree always has our record, so a record-less dir is an
387
+ // orphan. But be conservative: skip a NON-symlink dir that is not empty of
388
+ // a `.git` pointer only when it looks like a crashed pre-register dir.
389
+ if (isSymlink && targetExists) {
390
+ // symlink to a live path but no record → still an orphan link
391
+ } else if (!link.isDirectory()) {
392
+ continue; // not a dir, not a dangling link — leave alone
393
+ } else if (existsSync(join(dir, '.git'))) {
394
+ continue; // has a git pointer but no record: leave for a human (rare)
395
+ }
396
+ }
397
+ try {
398
+ rmSync(dir, {recursive: true, force: true});
399
+ const kind = danglingSymlink ? 'dangling-symlink' : 'orphan-dir';
400
+ swept.push({dir, kind});
401
+ note(`Swept orphan work entry ${entry} (${kind}).`);
402
+ } catch {
403
+ // best-effort: a permission error surfaces on the next add attempt
404
+ }
405
+ }
406
+ return swept;
314
407
  }
315
408
 
316
409
  /**