dorfl 0.8.0 → 0.10.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 (44) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +63 -25
  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/intake-trigger-template.d.ts +91 -53
  16. package/dist/intake-trigger-template.d.ts.map +1 -1
  17. package/dist/intake-trigger-template.js +213 -114
  18. package/dist/intake-trigger-template.js.map +1 -1
  19. package/dist/intake.d.ts +53 -4
  20. package/dist/intake.d.ts.map +1 -1
  21. package/dist/intake.js +59 -15
  22. package/dist/intake.js.map +1 -1
  23. package/dist/placement.d.ts +42 -33
  24. package/dist/placement.d.ts.map +1 -1
  25. package/dist/placement.js +20 -17
  26. package/dist/placement.js.map +1 -1
  27. package/dist/repo-config.d.ts +1 -1
  28. package/dist/repo-config.d.ts.map +1 -1
  29. package/dist/repo-config.js +35 -0
  30. package/dist/repo-config.js.map +1 -1
  31. package/dist/tasking.d.ts +46 -1
  32. package/dist/tasking.d.ts.map +1 -1
  33. package/dist/tasking.js +48 -21
  34. package/dist/tasking.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/cli.ts +75 -28
  37. package/src/config.ts +96 -9
  38. package/src/do.ts +30 -10
  39. package/src/env-config.ts +25 -3
  40. package/src/intake-trigger-template.ts +263 -143
  41. package/src/intake.ts +136 -18
  42. package/src/placement.ts +42 -36
  43. package/src/repo-config.ts +35 -0
  44. package/src/tasking.ts +75 -43
package/src/intake.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs';
2
2
  import {dirname, join} from 'node:path';
3
3
  import {runAsync, type RunResult} from './git.js';
4
- import {workFolderRel, workFolderPrefix, workItemRel} from './work-layout.js';
4
+ import {workFolderRel, workFolderPrefix} from './work-layout.js';
5
5
  import {paramCase} from './brand.js';
6
6
  import {
7
7
  performIntegration,
@@ -9,8 +9,9 @@ import {
9
9
  } from './integration-core.js';
10
10
  import type {IntegrateResult, ReviewProvider} from './integrator.js';
11
11
  import {integrationFromFlags} from './complete.js';
12
- import type {IntegrationMode, SpecsLandIn} from './config.js';
12
+ import type {IntegrationMode, SpecsLandIn, TasksLandIn} from './config.js';
13
13
  import type {OriginTrust} from './frontmatter.js';
14
+ import {TASK_PLACEMENT_SLOTS, landingToSide} from './tasking.js';
14
15
  import {
15
16
  placementFolder,
16
17
  resolvePlacement,
@@ -284,11 +285,47 @@ export interface PerformIntakeOptions {
284
285
  * the PR opens normally.
285
286
  */
286
287
  noPR?: boolean;
288
+ /**
289
+ * **The per-repo TASK-PLACEMENT default, passed IN** (spec
290
+ * `untrusted-origin-carries-via-stamp-intake-placement-symmetry-and-ci-gate-resolution`
291
+ * US #2, governing ADR `untrusted-origin-carries-via-stamp-not-forced-staging`).
292
+ * The resolved per-repo default landing for `intake`-authored TASKS emitted
293
+ * DIRECTLY from an issue (`backlog` = staging; `ready` = the agent pool), fed
294
+ * as the CONFIGURED-DEFAULT rung into the shared placement resolver
295
+ * (`src/placement.ts`) via the SAME task slots + `landingToSide` mapping the
296
+ * tasker (`tasking.ts`) uses — so the intake TASK emit reaches PARITY with the
297
+ * intake SPEC emit (both route through the resolver, both stamp origin-trust).
298
+ * Unset ⇒ the resolver's built-in floor applies (`staging` = `tasks/backlog/`,
299
+ * the conservative landing). The TASK twin of {@link specsLandIn}.
300
+ */
301
+ tasksLandIn?: TasksLandIn;
302
+ /**
303
+ * **The per-repo UNTRUSTED-side TASK-PLACEMENT default, passed IN** (spec
304
+ * `untrusted-origin-carries-via-stamp-intake-placement-symmetry-and-ci-gate-resolution`
305
+ * US #5, governing ADR `untrusted-origin-carries-via-stamp-not-forced-staging`).
306
+ * The TWIN of {@link tasksLandIn} selected when the intake stamp is
307
+ * `originTrust: untrusted`: {@link dispatchTask} reads the stamp and feeds THIS
308
+ * default (rather than {@link tasksLandIn}) as the configured-default rung of
309
+ * the shared placement resolver. The resolver no longer has an
310
+ * untrusted-forces-staging rung; DEFAULTS to staging (`backlog`), opt-in
311
+ * `ready` (safety then via the task's carried stamp, not the folder). The TASK
312
+ * twin of {@link untrustedSpecsLandIn}; the SAME knob the tasker consults for
313
+ * an untrusted-origin spec's downstream tasks (one policy, both call sites).
314
+ */
315
+ untrustedTasksLandIn?: TasksLandIn;
316
+ /**
317
+ * **The OPERATOR's EXPLICIT task-placement override** (the TOP precedence
318
+ * rung). When set, the runner-deterministic resolver lands the emitted task
319
+ * HERE regardless of {@link tasksLandIn} / {@link untrustedTasksLandIn} — the
320
+ * TASK twin of {@link explicitSpecsLandIn}. Set ONLY when the operator typed
321
+ * the explicit placement flag; never when the value came from config.
322
+ */
323
+ explicitTasksLandIn?: TasksLandIn;
287
324
  /**
288
325
  * **The per-repo SPEC-PLACEMENT default, passed IN** (spec
289
326
  * `staging-pool-position-gate-and-trust-model` US #2/#5, task
290
327
  * `pre-prd-staging-pool-split-and-untrusted-prd-placement`). The resolved
291
- * per-repo default landing for `intake`-authored prds (`pre-proposed` =
328
+ * per-repo default landing for `intake`-authored prds (`proposed` =
292
329
  * staging; `ready` = the auto-tasking pool), fed as the CONFIGURED-DEFAULT rung
293
330
  * into the shared placement resolver (`src/placement.ts`). The resolver
294
331
  * overlays an EXPLICIT operator flag ({@link explicitSpecsLandIn}, top) and
@@ -298,11 +335,24 @@ export interface PerformIntakeOptions {
298
335
  * path — one resolver, two lifecycles.
299
336
  */
300
337
  specsLandIn?: SpecsLandIn;
338
+ /**
339
+ * **The per-repo UNTRUSTED-side SPEC-PLACEMENT default, passed IN** (spec
340
+ * `untrusted-origin-carries-via-stamp-intake-placement-symmetry-and-ci-gate-resolution`
341
+ * US #7, governing ADR
342
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`). The TWIN of
343
+ * {@link specsLandIn} selected when the intake stamp is
344
+ * `originTrust: untrusted`: the spec dispatch reads the stamp and feeds THIS
345
+ * default (rather than {@link specsLandIn}) as the configured-default rung of
346
+ * the shared placement resolver. The resolver no longer has an
347
+ * untrusted-forces-staging rung; DEFAULTS to staging (`proposed`), opt-in
348
+ * `ready` (safety then via the tasks' carried stamp, not the folder).
349
+ */
350
+ untrustedSpecsLandIn?: SpecsLandIn;
301
351
  /**
302
352
  * **The OPERATOR's EXPLICIT spec-placement override** (the TOP precedence
303
353
  * rung). When set, the runner-deterministic resolver lands the spec HERE
304
- * regardless of `originTrust` or {@link specsLandIn} — the positional
305
- * analogue of `explicitMerge` overriding the untrusted-origin
354
+ * regardless of {@link specsLandIn} / {@link untrustedSpecsLandIn} — the
355
+ * positional analogue of `explicitMerge` overriding the untrusted-origin
306
356
  * build-propose rule ("the operator is present; CLI always wins, no
307
357
  * special force-key"). Set ONLY when the operator typed
308
358
  * `--specs-land-in <where>`; never when the value came from config.
@@ -374,7 +424,7 @@ const SPEC_PLACEMENT_SLOTS: PlacementSlots = {
374
424
  };
375
425
 
376
426
  /**
377
- * Map the `specsLandIn` value spelling (`pre-proposed` | `ready`) onto the
427
+ * Map the `specsLandIn` value spelling (`proposed` | `ready`) onto the
378
428
  * resolver's lifecycle-generic side enum (`staging` | `pool`). Returns
379
429
  * `undefined` when no value is set, so the resolver's next precedence rung
380
430
  * applies (the built-in floor). The spec twin of `landingToSide` on the
@@ -383,7 +433,7 @@ const SPEC_PLACEMENT_SLOTS: PlacementSlots = {
383
433
  function specLandingToSide(
384
434
  landing: SpecsLandIn | undefined,
385
435
  ): 'staging' | 'pool' | undefined {
386
- if (landing === 'pre-proposed') return 'staging';
436
+ if (landing === 'proposed') return 'staging';
387
437
  if (landing === 'ready') return 'pool';
388
438
  return undefined;
389
439
  }
@@ -858,6 +908,17 @@ async function decideAndDispatch(
858
908
  // not laundered. Unset ⇒ unstamped (a local intake ⇒ human/trusted).
859
909
  originTrust: options.originTrust,
860
910
  noPR: options.noPR,
911
+ // RUNNER-DETERMINISTIC PLACEMENT for the DIRECT-from-issue task (ADR
912
+ // `untrusted-origin-carries-via-stamp-not-forced-staging`): the
913
+ // configured-default + explicit-flag rungs, fed into the SHARED placement
914
+ // resolver alongside the `originTrust` stamp above — the TASK twin of the
915
+ // spec dispatch below. `dispatchTask` selects the untrusted-side default
916
+ // (`untrustedTasksLandIn`) when the stamp is `untrusted`, else
917
+ // `tasksLandIn`; the resolver decides `tasks/backlog/` (staging) vs
918
+ // `tasks/ready/` (the pool). `intake` never places itself.
919
+ tasksLandIn: options.tasksLandIn,
920
+ untrustedTasksLandIn: options.untrustedTasksLandIn,
921
+ explicitTasksLandIn: options.explicitTasksLandIn,
861
922
  providerInstance: options.providerInstance,
862
923
  issueProvider,
863
924
  // The bounded lone-task review seam (tests inject a canned verdict;
@@ -893,6 +954,10 @@ async function decideAndDispatch(
893
954
  // `specs/proposed/` (staging) vs `specs/ready/` (the tasking pool); `intake` never
894
955
  // places itself.
895
956
  specsLandIn: options.specsLandIn,
957
+ // The UNTRUSTED-side default, selected in `dispatchSpec` when the
958
+ // `originTrust` stamp is `untrusted` (ADR
959
+ // `untrusted-origin-carries-via-stamp-not-forced-staging`).
960
+ untrustedSpecsLandIn: options.untrustedSpecsLandIn,
896
961
  explicitSpecsLandIn: options.explicitSpecsLandIn,
897
962
  providerInstance: options.providerInstance,
898
963
  issueProvider,
@@ -1065,6 +1130,16 @@ async function dispatchTask(params: {
1065
1130
  /** The origin-trust stamp passed IN (unset ⇒ emit unstamped ⇒ human/trusted). */
1066
1131
  originTrust: OriginTrust | undefined;
1067
1132
  noPR: boolean | undefined;
1133
+ /** The per-repo TRUSTED-side TASK-PLACEMENT default (configured-default rung when the task is trusted/unset). */
1134
+ tasksLandIn: TasksLandIn | undefined;
1135
+ /**
1136
+ * The per-repo UNTRUSTED-side TASK-PLACEMENT default, selected as the
1137
+ * configured-default rung when `originTrust` is `untrusted` (ADR
1138
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`).
1139
+ */
1140
+ untrustedTasksLandIn: TasksLandIn | undefined;
1141
+ /** The OPERATOR's EXPLICIT task-placement override (the TOP rung). */
1142
+ explicitTasksLandIn: TasksLandIn | undefined;
1068
1143
  providerInstance: ReviewProvider | undefined;
1069
1144
  /** The issue seam the completion comment is posted back through (runner-owned). */
1070
1145
  issueProvider: IssueProvider;
@@ -1087,6 +1162,9 @@ async function dispatchTask(params: {
1087
1162
  mergeRetries,
1088
1163
  originTrust,
1089
1164
  noPR,
1165
+ tasksLandIn,
1166
+ untrustedTasksLandIn,
1167
+ explicitTasksLandIn,
1090
1168
  providerInstance,
1091
1169
  issueProvider,
1092
1170
  reviewTask,
@@ -1107,7 +1185,33 @@ async function dispatchTask(params: {
1107
1185
  note(message);
1108
1186
  return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
1109
1187
  }
1110
- const relPath = workItemRel('tasks-ready', `${slug}.md`);
1188
+ // RUNNER-DETERMINISTIC PLACEMENT (task `intake-task-placement-symmetry`; the
1189
+ // untrusted-forces-staging rung RETIRED by ADR
1190
+ // `untrusted-origin-carries-via-stamp-not-forced-staging`). Resolve which
1191
+ // folder the runner writes the intake-authored task into BEFORE handing it to
1192
+ // the shared integrate band — the SAME precedence chain, the SAME shared
1193
+ // resolver, and (via `TASK_PLACEMENT_SLOTS` + `landingToSide` reused from
1194
+ // `tasking.ts`) the SAME task slots the tasker uses; only the intake dispatch
1195
+ // differs from the spec twin (`dispatchSpec`) in its lifecycle SLOTS. THIS
1196
+ // caller selects the trusted-vs-untrusted configured default by reading the
1197
+ // `originTrust` stamp: an untrusted task selects `untrustedTasksLandIn`
1198
+ // (default staging; `ready` when configured), a trusted/unset task selects
1199
+ // `tasksLandIn`. The agent (the intake decider) never influences placement.
1200
+ // Net vs the old hardcoded `tasks-ready`: an untrusted-author task now MERGES
1201
+ // a document to `main` in `backlog` (by default) carrying the stamp instead of
1202
+ // opening a document PR; safety for a `ready`-landed untrusted task flows
1203
+ // through its carried stamp at BUILD time (forces a code PR), not the folder.
1204
+ const configuredTasksLanding =
1205
+ originTrust === 'untrusted' ? untrustedTasksLandIn : tasksLandIn;
1206
+ const placementDecision = resolvePlacement({
1207
+ explicit: landingToSide(explicitTasksLandIn),
1208
+ configuredDefault: landingToSide(configuredTasksLanding),
1209
+ });
1210
+ const placementDir = placementFolder(
1211
+ TASK_PLACEMENT_SLOTS,
1212
+ placementDecision.choice,
1213
+ );
1214
+ const relPath = `${placementDir}/${slug}.md`;
1111
1215
 
1112
1216
  // BOUNDED INTERNAL REVIEW (observation
1113
1217
  // `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C):
@@ -1267,8 +1371,14 @@ async function dispatchSpec(params: {
1267
1371
  /** The origin-trust stamp passed IN (unset ⇒ emit unstamped ⇒ human/trusted). */
1268
1372
  originTrust: OriginTrust | undefined;
1269
1373
  noPR: boolean | undefined;
1270
- /** The per-repo SPEC-PLACEMENT default (configured-default rung of the placement chain). */
1374
+ /** The per-repo TRUSTED-side SPEC-PLACEMENT default (configured-default rung when the spec is trusted/unset). */
1271
1375
  specsLandIn: SpecsLandIn | undefined;
1376
+ /**
1377
+ * The per-repo UNTRUSTED-side SPEC-PLACEMENT default, selected as the
1378
+ * configured-default rung when `originTrust` is `untrusted` (ADR
1379
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`).
1380
+ */
1381
+ untrustedSpecsLandIn: SpecsLandIn | undefined;
1272
1382
  /** The OPERATOR's EXPLICIT spec-placement override (the TOP rung). */
1273
1383
  explicitSpecsLandIn: SpecsLandIn | undefined;
1274
1384
  providerInstance: ReviewProvider | undefined;
@@ -1289,6 +1399,7 @@ async function dispatchSpec(params: {
1289
1399
  originTrust,
1290
1400
  noPR,
1291
1401
  specsLandIn,
1402
+ untrustedSpecsLandIn,
1292
1403
  explicitSpecsLandIn,
1293
1404
  providerInstance,
1294
1405
  issueProvider,
@@ -1308,18 +1419,25 @@ async function dispatchSpec(params: {
1308
1419
  return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
1309
1420
  }
1310
1421
  // RUNNER-DETERMINISTIC PLACEMENT (task
1311
- // `pre-prd-staging-pool-split-and-untrusted-prd-placement`, governing ADR
1312
- // `placement-is-runner-deterministic-humanonly-is-agent-judgement`). Resolve
1313
- // which folder the runner writes the intake-authored spec into BEFORE handing
1314
- // it to the shared integrate band: the SAME precedence chain the tasker uses
1315
- // (`explicit > untrusted-origin ⇒ staging > specsLandIn > built-in (staging)`),
1316
- // the SAME shared resolver — only the lifecycle SLOTS differ. The agent
1422
+ // `pre-prd-staging-pool-split-and-untrusted-prd-placement`; the
1423
+ // untrusted-forces-staging rung RETIRED by ADR
1424
+ // `untrusted-origin-carries-via-stamp-not-forced-staging`). Resolve which
1425
+ // folder the runner writes the intake-authored spec into BEFORE handing it to
1426
+ // the shared integrate band: the SAME precedence chain the tasker uses
1427
+ // (`explicit > configured default > built-in (staging)`), the SAME shared
1428
+ // resolver — only the lifecycle SLOTS differ. THIS caller selects the
1429
+ // trusted-vs-untrusted configured default by reading the `originTrust` stamp:
1430
+ // an untrusted spec selects `untrustedSpecsLandIn` (default staging; `ready`
1431
+ // when configured), a trusted/unset spec selects `specsLandIn`. The agent
1317
1432
  // (the intake decider) never influences placement; it returns the verdict and
1318
- // the runner computes the destination from unforgeable inputs.
1433
+ // the runner computes the destination from unforgeable inputs. Safety for an
1434
+ // untrusted spec landing in `ready` flows through its tasks' carried stamp,
1435
+ // not the folder.
1436
+ const configuredSpecsLanding =
1437
+ originTrust === 'untrusted' ? untrustedSpecsLandIn : specsLandIn;
1319
1438
  const placementDecision = resolvePlacement({
1320
1439
  explicit: specLandingToSide(explicitSpecsLandIn),
1321
- originTrust,
1322
- configuredDefault: specLandingToSide(specsLandIn),
1440
+ configuredDefault: specLandingToSide(configuredSpecsLanding),
1323
1441
  });
1324
1442
  const placementDir = placementFolder(
1325
1443
  SPEC_PLACEMENT_SLOTS,
package/src/placement.ts CHANGED
@@ -6,23 +6,28 @@
6
6
  *
7
7
  * Pure function from UNFORGEABLE inputs to a staging-vs-pool destination:
8
8
  *
9
- * explicit operator flag > untrusted-origin forces STAGING >
10
- * configured default > built-in
9
+ * explicit operator flag > configured default > built-in
11
10
  *
12
- * It is the POSITIONAL twin of the untrusted-origin BUILD-propose rule in
13
- * `integration-core.ts` (which decides MODE: `propose` vs `merge`): the same
14
- * `originTrust:` stamp + the same "explicit operator override beats the trust
15
- * force" shape, reused here to decide POSITION (which folder the runner lands
16
- * the emitted ledger files in). The agent cannot influence either — both
17
- * resolve runner-side from the stamped frontmatter + the resolved policy + the
18
- * operator's explicit flag.
11
+ * Author-trust is NO LONGER a rung here (ADR
12
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`). This resolver was
13
+ * once the POSITIONAL twin of the untrusted-origin BUILD-propose rule in
14
+ * `integration-core.ts` (an `originTrust: untrusted ⇒ staging` rung sitting
15
+ * above the configured default). That rung is REMOVED: it made "untrusted
16
+ * lands in `ready`" inexpressible, and its safety was redundant with the
17
+ * build-time propose rule (an untrusted item in the pool still cannot become
18
+ * merged CODE without human review). The trusted-vs-untrusted destination is
19
+ * now selected BY THE CALLER, which reads the stamp and picks the matching
20
+ * configured default (`untrusted*LandIn` vs `*LandIn`) BEFORE calling — so
21
+ * this resolver stays a pure precedence over caller-supplied inputs. The
22
+ * agent cannot influence placement — it resolves runner-side from the resolved
23
+ * policy + the operator's explicit flag.
19
24
  *
20
25
  * LIFECYCLE-GENERIC. The folder names + the configured-default value are
21
26
  * PARAMETERS (`slots`, `configuredDefault`), so the same resolver serves the
22
27
  * TASK lifecycle (`tasksLandIn`: `backlog`/`ready` — the POOL value was
23
28
  * renamed `'backlog'` → `'todo'` → `'ready'`, ADR
24
29
  * `rename-task-pool-folder-todo-to-ready`) AND the SPEC-
25
- * placement lifecycle (`specsLandIn`: `pre-proposed`/`ready`) without forking. A future
30
+ * placement lifecycle (`specsLandIn`: `proposed`/`ready`) without forking. A future
26
31
  * lifecycle (e.g. intake's lone-task) plugs its own `slots` in and reuses the
27
32
  * exact precedence — no second implementation.
28
33
  */
@@ -54,34 +59,37 @@ export interface PlacementSlots {
54
59
  export interface ResolvePlacementInput {
55
60
  /**
56
61
  * The operator's EXPLICIT override (the TOP of the chain) — when set, it
57
- * wins over the untrusted-origin force AND the configured default. Mirrors
58
- * `integration-core.ts`'s `explicitMerge` "operator is present; CLI always
59
- * wins, no special force-key" shape (the untrusted-origin trust signal
60
- * gates pool entry, not the mode in this case). Unset (`undefined`) ⇒ this
61
- * rung is skipped and the next one applies.
62
+ * wins over the configured default. Mirrors `integration-core.ts`'s
63
+ * `explicitMerge` "operator is present; CLI always wins, no special
64
+ * force-key" shape. Unset (`undefined`) ⇒ this rung is skipped and the next
65
+ * one applies.
62
66
  */
63
67
  explicit?: PlacementSide;
64
68
  /**
65
- * The source's stamped `originTrust:` frontmatter (`trusted` | `untrusted` |
66
- * absent ⇒ trusted by default). When `untrusted`, the resolver FORCES
67
- * `staging` even on a "land in pool" repo — the positional analogue of the
68
- * existing `untrusted-origin-forces-build-propose` rule. Unset / `trusted`
69
- * ⇒ this rung is skipped (zero behaviour change for the normal path).
70
- */
71
- originTrust?: 'trusted' | 'untrusted';
72
- /**
73
- * The repo's resolved configured DEFAULT landing (`tasksLandIn` /
74
- * `specsLandIn`). Caller resolves it like the existing `taskingIntegration`
75
- * (flag > env > per-repo > global > built-in) and passes the result; this
76
- * resolver just consumes it.
69
+ * The repo's resolved configured DEFAULT landing, ALREADY selected by the
70
+ * caller for the item's author-trust: the caller reads the `originTrust:`
71
+ * stamp and passes the `untrusted*LandIn` default for an untrusted item, or
72
+ * the trusted `*LandIn` default otherwise (ADR
73
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`). Resolved like the
74
+ * existing `taskingIntegration` (flag > env > per-repo > global > built-in)
75
+ * and mapped to a {@link PlacementSide}; this resolver just consumes it. Unset
76
+ * ⇒ the built-in floor applies.
77
77
  */
78
78
  configuredDefault?: PlacementSide;
79
79
  }
80
80
 
81
- /** The resolved choice + which precedence rung won (for honest reporting). */
81
+ /**
82
+ * The resolved choice + which precedence rung won (for honest reporting).
83
+ *
84
+ * The `'untrusted-origin'` reason is RETIRED (ADR
85
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`): author-trust no
86
+ * longer decides POSITION inside this resolver, so an untrusted item that lands
87
+ * in staging now does so via `'configured-default'` (its caller selected the
88
+ * `untrusted*LandIn` default), not via a distinct trust rung.
89
+ */
82
90
  export interface PlacementResult {
83
91
  choice: PlacementSide;
84
- reason: 'explicit' | 'untrusted-origin' | 'configured-default' | 'built-in';
92
+ reason: 'explicit' | 'configured-default' | 'built-in';
85
93
  }
86
94
 
87
95
  /**
@@ -97,11 +105,12 @@ const BUILT_IN_FLOOR: PlacementSide = 'staging';
97
105
  * Resolve the staging-vs-pool placement from unforgeable inputs, via the fixed
98
106
  * precedence chain:
99
107
  *
100
- * explicit > untrusted-origin ⇒ staging > configured default > built-in
108
+ * explicit > configured default > built-in
101
109
  *
102
- * Pure: no I/O, no env reads — the caller resolves config + reads the
103
- * frontmatter and passes both in. Reused by every lifecycle (task + spec
104
- * placement + future intake variants) so a precedence change touches ONE place.
110
+ * Pure: no I/O, no env reads, NO trust rung — the caller resolves config
111
+ * (INCLUDING selecting the trusted-vs-untrusted default from the stamp) and
112
+ * passes the result in. Reused by every lifecycle (task + spec placement +
113
+ * future intake variants) so a precedence change touches ONE place.
105
114
  */
106
115
  export function resolvePlacement(
107
116
  input: ResolvePlacementInput,
@@ -109,9 +118,6 @@ export function resolvePlacement(
109
118
  if (input.explicit !== undefined) {
110
119
  return {choice: input.explicit, reason: 'explicit'};
111
120
  }
112
- if (input.originTrust === 'untrusted') {
113
- return {choice: 'staging', reason: 'untrusted-origin'};
114
- }
115
121
  if (input.configuredDefault !== undefined) {
116
122
  return {choice: input.configuredDefault, reason: 'configured-default'};
117
123
  }
@@ -72,6 +72,22 @@ export const REPO_ALLOWED_KEYS = [
72
72
  // intake's per-EMITTED-TYPE `{task, spec}` resolver (front door, author-trust):
73
73
  // this is a per-lifecycle-transition knob, inside the boundary, config-resolved.
74
74
  'taskingIntegration',
75
+ // `intakeIntegration` (the per-TRANSITION INTAKE-DOCUMENT override — the twin of
76
+ // `taskingIntegration` for the intake front door) is a genuine repo property
77
+ // exactly like `taskingIntegration`/`integration`: whether THIS repo's intake
78
+ // front door emits a task/spec DOCUMENT straight onto `main` (merge, no PR) or
79
+ // as a reviewable PR is agreed by all collaborators + travels with the repo.
80
+ // Resolved per-repo through the SAME chain as `taskingIntegration` (flag > env
81
+ // `DORFL_INTAKE_INTEGRATION` > per-repo > global), then falls back to
82
+ // `integration` when unset. DECOUPLED from the autonomy gates
83
+ // (`autoBuild`/`autoTask`): the document PR-mode is operator/config, not a
84
+ // function of autonomy (ADR untrusted-origin-carries-via-stamp-not-forced-staging).
85
+ // A SINGLE value applies to both the task AND the spec document (US #1 chose one
86
+ // knob, not a per-type split). DISTINCT from `taskingIntegration` (a DIFFERENT
87
+ // lifecycle transition) — the third member of the per-transition integration
88
+ // family (`integration` build / `taskingIntegration` tasking / `intakeIntegration`
89
+ // intake), each falling back to `integration`, none tied to a gate.
90
+ 'intakeIntegration',
75
91
  // `tasksLandIn` (the per-repo TASK-PLACEMENT default — staging vs pool, spec
76
92
  // `staging-pool-position-gate-and-trust-model` US #5) is a genuine repo property
77
93
  // exactly like `taskingIntegration`/`integration`: whether THIS repo's tasker
@@ -96,6 +112,25 @@ export const REPO_ALLOWED_KEYS = [
96
112
  // (`src/placement.ts`). The sole spec-placement key after the ''prd'' → `spec` HARD
97
113
  // CUTOVER (the legacy `prdsLandIn` key is GONE — clean break, no accepted alias).
98
114
  'specsLandIn',
115
+ // `untrustedTasksLandIn` (the UNTRUSTED-side TWIN of `tasksLandIn` — spec
116
+ // `untrusted-origin-carries-via-stamp-intake-placement-symmetry-and-ci-gate-resolution`
117
+ // US #5/#6, ADR `untrusted-origin-carries-via-stamp-not-forced-staging`) is a
118
+ // genuine repo property exactly like `tasksLandIn`: whether THIS repo's
119
+ // untrusted-origin tasks land STAGED (`tasks/backlog/`, human promotion) or in
120
+ // the agent POOL (`tasks/ready/`, claimable — safety then via the carried build
121
+ // STAMP) is a repo-wide policy agreed by all collaborators + travels with the
122
+ // repo. Resolved per-repo through the SAME chain as `tasksLandIn` (flag
123
+ // `--untrusted-tasks-land-in` > env `DORFL_UNTRUSTED_TASKS_LAND_IN` > per-repo >
124
+ // global > built-in `backlog`). The caller selects it over the trusted twin by
125
+ // reading the `originTrust` stamp before the placement resolver.
126
+ 'untrustedTasksLandIn',
127
+ // `untrustedSpecsLandIn` (the UNTRUSTED-side TWIN of `specsLandIn` — same spec
128
+ // US #7) is a genuine repo property exactly like `specsLandIn`: whether THIS
129
+ // repo's untrusted intake specs land STAGED (`specs/proposed/`) or in the
130
+ // auto-tasking POOL (`specs/ready/`) is a repo-wide policy. Resolved per-repo
131
+ // through the SAME chain (flag `--untrusted-specs-land-in` > env
132
+ // `DORFL_UNTRUSTED_SPECS_LAND_IN` > per-repo > global > built-in `proposed`).
133
+ 'untrustedSpecsLandIn',
99
134
  // `noPR` (the PR-INTENT axis — push the branch but deliberately skip the PR) is
100
135
  // a genuine repo property exactly like `integration`/`review`: whether this
101
136
  // repo's propose runs open a PR is agreed by all collaborators + travels with
package/src/tasking.ts CHANGED
@@ -271,6 +271,25 @@ export interface PerformTaskOptions {
271
271
  * landing that preserves zero behaviour change for the normal path).
272
272
  */
273
273
  tasksLandIn?: 'backlog' | 'ready';
274
+ /**
275
+ * **The per-repo UNTRUSTED-side TASK-PLACEMENT default** (spec
276
+ * `untrusted-origin-carries-via-stamp-intake-placement-symmetry-and-ci-gate-resolution`
277
+ * US #6/#10, governing ADR
278
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`). The TWIN of
279
+ * {@link tasksLandIn} selected when the SPEC being tasked carries an
280
+ * `originTrust: untrusted` stamp: `performTask` reads the stamp and feeds
281
+ * THIS default (rather than {@link tasksLandIn}) as the configured-default
282
+ * rung of the placement resolver. This is the same knob intake uses for a
283
+ * task emitted directly from an issue (decision X: one policy governs an
284
+ * untrusted task's destination regardless of birth path). Resolved per-repo
285
+ * like {@link tasksLandIn} (flag `--untrusted-tasks-land-in` > env
286
+ * `DORFL_UNTRUSTED_TASKS_LAND_IN` > per-repo > global > built-in `backlog`).
287
+ * Unset ⇒ the resolver's built-in floor (`staging`) applies. The resolver no
288
+ * longer has an untrusted-forces-staging rung; safety for an untrusted task
289
+ * that lands in `ready` here is the carried stamp (its BUILD is forced to a
290
+ * code PR), NOT the folder.
291
+ */
292
+ untrustedTasksLandIn?: 'backlog' | 'ready';
274
293
  /**
275
294
  * **The OPERATOR's EXPLICIT task-placement override** (the TOP precedence
276
295
  * rung). When set, the runner-deterministic resolver lands the tasks HERE
@@ -353,8 +372,14 @@ export const STAGED_TASKS_DIR = workFolderRel('tasks-backlog');
353
372
  */
354
373
  const POOL_TASKS_DIR = workFolderRel('tasks-ready');
355
374
 
356
- /** The placement slots for the TASK lifecycle (folder names). */
357
- const TASK_PLACEMENT_SLOTS = {
375
+ /**
376
+ * The placement slots for the TASK lifecycle (folder names). EXPORTED so the
377
+ * intake TASK emit (`intake.ts` `dispatchTask`) reuses the SAME slots + resolver
378
+ * the tasker uses, reaching parity with the intake SPEC emit (ADR
379
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`) WITHOUT forking a
380
+ * second copy of the task folder names.
381
+ */
382
+ export const TASK_PLACEMENT_SLOTS = {
358
383
  staging: STAGED_TASKS_DIR,
359
384
  pool: POOL_TASKS_DIR,
360
385
  } as const;
@@ -367,9 +392,11 @@ const TASK_PLACEMENT_SLOTS = {
367
392
  * are NOT accepted (clean break — the staging value was renamed
368
393
  * `'pre-backlog'` → `'backlog'`, and the pool value was renamed
369
394
  * `'backlog'` → `'todo'` → `'ready'`, ADR
370
- * `rename-task-pool-folder-todo-to-ready`).
395
+ * `rename-task-pool-folder-todo-to-ready`). EXPORTED alongside
396
+ * {@link TASK_PLACEMENT_SLOTS} so the intake TASK emit reuses this exact mapping
397
+ * (the task twin of `specLandingToSide` on the intake spec path).
371
398
  */
372
- function landingToSide(
399
+ export function landingToSide(
373
400
  landing: 'backlog' | 'ready' | undefined,
374
401
  ): 'staging' | 'pool' | undefined {
375
402
  if (landing === 'backlog') return 'staging';
@@ -593,17 +620,28 @@ export async function performTask(
593
620
  const stagedEmitted = newOrChangedStagedTasks(cwd, before);
594
621
  const emitTasks = collectEmittedTasks(cwd, stagedEmitted);
595
622
  // RUNNER-DETERMINISTIC PLACEMENT (task
596
- // `runner-deterministic-slice-placement-policy-and-precedence`). Resolve which
623
+ // `runner-deterministic-slice-placement-policy-and-precedence`; the
624
+ // untrusted-forces-staging rung was RETIRED by ADR
625
+ // `untrusted-origin-carries-via-stamp-not-forced-staging`). Resolve which
597
626
  // folder the runner lands the emitted tasks in BEFORE handing them to the
598
- // shared integrate band: precedence `explicit > untrusted-origin ⇒ staging >
599
- // tasksLandIn > built-in (staging)`, all from unforgeable inputs (the spec's
600
- // stamped `originTrust:` + the resolved per-repo default + the operator's
601
- // explicit flag). The agent NEVER influences this; it always writes to
602
- // `work/tasks/backlog/`, and the runner redirects at `stage()` time.
627
+ // shared integrate band: precedence `explicit > configured default >
628
+ // built-in (staging)`. THIS caller selects the trusted-vs-untrusted
629
+ // configured default by reading the spec's propagated `originTrust:` stamp:
630
+ // an untrusted-origin spec selects `untrustedTasksLandIn` (default staging;
631
+ // `ready` when configured), a trusted/unset spec selects `tasksLandIn`. One
632
+ // knob (`untrustedTasksLandIn`) governs an untrusted task's destination
633
+ // whether born from an issue directly (intake) or from an untrusted spec
634
+ // here. The agent NEVER influences this; it always writes to
635
+ // `work/tasks/backlog/`, and the runner redirects at `stage()` time. Safety
636
+ // for an untrusted task landing in `ready` is the carried stamp (its BUILD is
637
+ // forced to a code PR), not the folder.
638
+ const configuredTasksLanding =
639
+ specFm.originTrust === 'untrusted'
640
+ ? options.untrustedTasksLandIn
641
+ : options.tasksLandIn;
603
642
  const placementDecision = resolvePlacement({
604
643
  explicit: landingToSide(options.explicitTasksLandIn),
605
- originTrust: specFm.originTrust,
606
- configuredDefault: landingToSide(options.tasksLandIn),
644
+ configuredDefault: landingToSide(configuredTasksLanding),
607
645
  });
608
646
  const placementDir = placementFolder(
609
647
  TASK_PLACEMENT_SLOTS,
@@ -721,7 +759,6 @@ export async function performTask(
721
759
  emitTasks,
722
760
  poolBefore,
723
761
  placementDir,
724
- placementReason: placementDecision.reason,
725
762
  note,
726
763
  env,
727
764
  }),
@@ -1033,32 +1070,19 @@ async function stageTaskingLifecycle(params: {
1033
1070
  /**
1034
1071
  * The runner-resolved destination folder (task
1035
1072
  * `runner-deterministic-slice-placement-policy-and-precedence`). Computed
1036
- * ONCE in `performTask` via the shared {@link resolvePlacement} from the
1037
- * spec's `originTrust:` stamp + the configured `tasksLandIn` default + the
1038
- * operator's explicit override, then passed in here — so the call site sees
1039
- * exactly where the emitted tasks landed (the placement decision is not
1040
- * buried in the stage closure).
1073
+ * ONCE in `performTask` via the shared {@link resolvePlacement}. The caller
1074
+ * selects the configured default from the spec's `originTrust:` stamp
1075
+ * (`untrustedTasksLandIn` when untrusted, else `tasksLandIn`; ADR
1076
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`) and overlays the
1077
+ * operator's explicit override, then passes the result in here — so the call
1078
+ * site sees exactly where the emitted tasks landed (the placement decision is
1079
+ * not buried in the stage closure).
1041
1080
  */
1042
1081
  placementDir: string;
1043
- /** Which precedence rung the resolver took (for honest reporting). */
1044
- placementReason:
1045
- | 'explicit'
1046
- | 'untrusted-origin'
1047
- | 'configured-default'
1048
- | 'built-in';
1049
1082
  note: (message: string) => void;
1050
1083
  env: NodeJS.ProcessEnv | undefined;
1051
1084
  }): Promise<void> {
1052
- const {
1053
- cwd,
1054
- slug,
1055
- emitTasks,
1056
- poolBefore,
1057
- placementDir,
1058
- placementReason,
1059
- note,
1060
- env,
1061
- } = params;
1085
+ const {cwd, slug, emitTasks, poolBefore, placementDir, note, env} = params;
1062
1086
  const spec = workItemRel('specs-ready', `${slug}.md`);
1063
1087
  const specTasked = workItemRel('specs-tasked', `${slug}.md`);
1064
1088
  // PROPAGATE the origin-trust PROVENANCE (task
@@ -1072,13 +1096,14 @@ async function stageTaskingLifecycle(params: {
1072
1096
  const specProvenance = existsSync(specAbs)
1073
1097
  ? parseFrontmatter(readFileSync(specAbs, 'utf8'))
1074
1098
  : {origin: undefined, originTrust: undefined};
1075
- if (placementReason === 'untrusted-origin') {
1076
- note(
1077
- `Untrusted-origin spec '${slug}': forcing the emitted tasks STAGED ` +
1078
- `(${placementDir}/) regardless of tasksLandIn (a human promotes ` +
1079
- 'them into work/tasks/ready/). Pass --tasks-land-in <where> to override.',
1080
- );
1081
- }
1099
+ // NOTE: author-trust no longer FORCES staging here (ADR
1100
+ // `untrusted-origin-carries-via-stamp-not-forced-staging`). An untrusted spec's
1101
+ // emitted tasks land per `untrustedTasksLandIn` (default staging; `ready` when
1102
+ // configured), selected by the caller from the stamp before `resolvePlacement`;
1103
+ // safety for a `ready` landing is the propagated `originTrust: untrusted` stamp
1104
+ // below (its BUILD is forced to a code PR), not the folder. The placement
1105
+ // decision itself is fully computed in `performTask`; this stage just writes to
1106
+ // the resolved `placementDir`.
1082
1107
  // Move the held spec specs/ready/ -> specs/tasked/ (the TASKED resting state — folder =
1083
1108
  // source of truth, like done/ for tasks). This is the DURABLE `specs/ready → specs/tasked`
1084
1109
  // success move, owned by THIS transition's commit (the lock no longer moved the
@@ -1603,7 +1628,9 @@ function collectEmittedTasks(
1603
1628
  * Sections (all suppressed when empty, so the body stays honest):
1604
1629
  * 1. A lead line naming the source spec + emitted task count.
1605
1630
  * 2. **Tasks** — one bullet per task: `slug — title`, plus `covers:` (which
1606
- * spec user stories it maps to), `blockedBy:`, and `needsAnswers` if set.
1631
+ * spec user stories it maps to, as `US-<n>` so GitHub does not autolink
1632
+ * the index as an issue/PR reference), `blockedBy:`, and `needsAnswers`
1633
+ * if set.
1607
1634
  * 3. **Dependency graph** — the KEYSTONE(S) (tasks whose `blockedBy` names no
1608
1635
  * other task in this set, so they land first) and the `blockedBy` edges.
1609
1636
  * 4. **Needs answers** — any open questions the tasker review→edit loop
@@ -1637,7 +1664,12 @@ export function composeTaskingProposeBody(
1637
1664
  for (const t of tasks) {
1638
1665
  lines.push(`- **${t.slug}** — ${t.title}`);
1639
1666
  if (t.covers.length > 0) {
1640
- lines.push(` - covers: ${t.covers.map((c) => `US #${c}`).join(', ')}`);
1667
+ // User-story ids are rendered as `US-<n>` (NOT `US #<n>`): a bare
1668
+ // `#<n>` in a GitHub PR body autolinks to issue/PR #<n>, which is a
1669
+ // confusing false reference (the number is a spec user-story index,
1670
+ // not an issue). The hyphenated form carries the same meaning without
1671
+ // tripping GitHub's autolinker.
1672
+ lines.push(` - covers: ${t.covers.map((c) => `US-${c}`).join(', ')}`);
1641
1673
  }
1642
1674
  if (t.blockedBy.length > 0) {
1643
1675
  lines.push(` - blockedBy: ${t.blockedBy.join(', ')}`);