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
@@ -6,9 +6,11 @@
6
6
  * STRUCTURALLY VALIDATES it, mirroring the snapshot-assertion style of
7
7
  * `advance-lifecycle-template.ts` / `advance-ci-template.ts` (the package depends on
8
8
  * NO YAML lib, so the checks are presence/shape assertions over the raw text). It
9
- * ALSO carries the PURE author-trust → per-outcome-flags DERIVATION
10
- * ({@link deriveIntakeFlags}) — CI's merge-vs-propose POLICY, the load-bearing
11
- * testable logic the workflow encodes at runtime.
9
+ * ALSO carries the PURE intake-flags DERIVATION ({@link deriveIntakeFlags}) —
10
+ * CI's merge-vs-propose POLICY, the load-bearing testable logic the workflow
11
+ * encodes at runtime. The DOCUMENT mode is the resolved `intakeIntegration`
12
+ * (operator/config); author-trust drives ONLY the `--origin-trust` stamp +
13
+ * placement, never the mode.
12
14
  *
13
15
  * SCOPE FENCE (spec Out-of-Scope): the issue→artifact TRANSFORM engine is
14
16
  * `issue-intake`'s (`intake <N>` + its four-outcome dispatch + the per-outcome
@@ -27,14 +29,23 @@
27
29
  * (re-)evaluation; an EDITED comment is NOT a trigger (the ID-based
28
30
  * `seen=<ids>` watermark suffices). The "post a NEW comment to signal an edit"
29
31
  * convention is documented in the workflow so a human knows to re-trigger.
30
- * - AUTHOR-TRUST → per-outcome FLAGS (Decision 1, {@link deriveIntakeFlags}): an
31
- * UNTRUSTED author (`author_association` not in OWNER/MEMBER/COLLABORATOR)
32
- * forces `--propose-task` REGARDLESS of the `autoBuild` gate, while
33
- * `--merge-spec` stays allowed (a human still tasks a spec before anything
34
- * autonomous acts — the checkpoint is intact). A TRUSTED author gets the plain
35
- * gate-derived mode for both. The fully-gateless "all gates on + merge
36
- * everywhere" path is a LOUD, NON-DEFAULT opt-in — the default is conservative
37
- * (propose / human-in-the-loop).
32
+ * - INTAKE-INTEGRATION → the file-emit MODE; AUTHOR-TRUST → PLACEMENT + the
33
+ * STAMP (ADR `untrusted-origin-carries-via-stamp-not-forced-staging`; spec
34
+ * `intake-integration-knob-and-specs-land-in-proposed-rename`; {@link
35
+ * deriveIntakeFlags}): the intake DOCUMENT mode (merge-vs-propose for a
36
+ * task/spec FILE) is the resolved `intakeIntegration ?? integration` — an
37
+ * operator/config choice, a SINGLE value applied to BOTH the task and the spec
38
+ * document, DECOUPLED from the autonomy gates. `autoBuild`/`autoTask` no
39
+ * longer feed the document mode (they mean ONLY "may an agent act
40
+ * autonomously"). Author-trust feeds exactly (1) the `--origin-trust` STAMP
41
+ * (`author_association` not in OWNER/MEMBER/COLLABORATOR ⇒ `untrusted`) and
42
+ * (2) — via that stamp, read by `intake`'s dispatch — which PLACEMENT default
43
+ * (`untrusted*LandIn` vs `*LandIn`) the emitted document lands in. Untrusted
44
+ * safety is the CARRIED stamp (it forces the BUILD transition to a code PR)
45
+ * plus the placement default, NOT a forced document PR. A document therefore
46
+ * merges to `main` regardless of who filed the issue OR whether the gates are
47
+ * on; whether it is reviewed-as-a-PR is purely the `intakeIntegration`
48
+ * operator config, not a trust or autonomy consequence.
38
49
  * - INSERTION POINT E (the issue-thread review surface): the review verdict over
39
50
  * intake's generated specs/tasks is surfaced into the ISSUE THREAD via the
40
51
  * `IssueProvider.postIssueComment` seam (issue thread, by NUMBER — NOT the PR
@@ -98,77 +109,101 @@ export const TRUSTED_AUTHOR_ASSOCIATIONS = [
98
109
  * The per-outcome integration flags CI passes to `intake <N>` — the GRANULAR
99
110
  * per-type pair (the aggregates `--merge`/`--propose` are not needed because CI
100
111
  * always resolves BOTH types explicitly). Each is `'merge'` or `'propose'`.
112
+ *
113
+ * Both {@link spec} and {@link task} are the SAME resolved `intakeIntegration`
114
+ * value (spec `intake-integration-knob-and-specs-land-in-proposed-rename` US #1
115
+ * chose a SINGLE intake-document knob, NOT a per-type split): the pair is kept
116
+ * only because `intake`'s CLI consumes `--merge-task`/`--merge-spec` on two flag
117
+ * axes, not because task and spec can differ. Both are DECOUPLED from the autonomy
118
+ * gates and trust-INDEPENDENT (ADR
119
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`): `autoBuild`/`autoTask`
120
+ * no longer compose into the document mode, and author-trust never did. The trust
121
+ * signal rides only {@link originTrust} (the stamp), which `intake`'s dispatch
122
+ * reads to select the untrusted-side PLACEMENT default. So `spec`/`task` here
123
+ * answer "does the DOCUMENT merge or open a PR", derived purely from the resolved
124
+ * `intakeIntegration` operator config — never from who filed the issue nor whether
125
+ * an agent may act autonomously.
101
126
  */
102
127
  export interface IntakeIntegrationFlags {
103
- /** The spec outcome's mode → `--merge-spec` / `--propose-spec`. */
128
+ /**
129
+ * The spec outcome's mode → `--merge-spec` / `--propose-spec`. The resolved
130
+ * `intakeIntegration ?? integration` (operator/config), IDENTICAL to
131
+ * {@link task} — the single intake-document knob applied to the spec axis.
132
+ */
104
133
  spec: 'merge' | 'propose';
105
- /** The task outcome's mode → `--merge-task` / `--propose-task`. */
134
+ /**
135
+ * The task outcome's mode → `--merge-task` / `--propose-task`. The resolved
136
+ * `intakeIntegration ?? integration` (operator/config), IDENTICAL to
137
+ * {@link spec} — the single intake-document knob applied to the task axis.
138
+ * Neither the autonomy gates nor author-trust force this: an untrusted author's
139
+ * task DOCUMENT merges to `main` exactly like a trusted one when
140
+ * `intakeIntegration` is `merge` (the untrusted safety is the carried
141
+ * {@link originTrust} stamp — which forces the BUILD to a code PR — plus the
142
+ * placement default, NOT a forced document PR).
143
+ */
106
144
  task: 'merge' | 'propose';
107
145
  /**
108
146
  * The ORIGIN-TRUST verdict CI passes to `intake <N>` via `--origin-trust`
109
147
  * (task `untrusted-origin-forces-build-propose`) so the emitted spec/task is
110
- * STAMPED with how it was born. Derived from the SAME `author_association` case
111
- * as the integration flags (it IS `authorTrusted` collapsed to the wire value),
112
- * so the stamp and the integration mode CANNOT desync. `intake.ts` writes it
113
- * verbatim onto the frontmatter — it never re-resolves trust (the `intake.ts`
114
- * ~L296 boundary: author-trust is CI's POLICY, passed IN, not resolved here).
148
+ * STAMPED with how it was born. This is now the SOLE thing author-trust drives
149
+ * on the wire: `intake`'s dispatch reads this stamp to (1) select the
150
+ * untrusted-side PLACEMENT default (`untrusted*LandIn`) and (2) force the later
151
+ * BUILD transition of an untrusted task to a code PR. Derived from the SAME
152
+ * `author_association` case the (gate-derived) modes above see, so it cannot
153
+ * desync. `intake.ts` writes it verbatim onto the frontmatter — it never
154
+ * re-resolves trust (the `intake.ts` ~L296 boundary: author-trust is CI's
155
+ * POLICY, passed IN, not resolved here).
115
156
  */
116
157
  originTrust: 'trusted' | 'untrusted';
117
158
  }
118
159
 
119
- /** The gate state CI composes with author-trust to derive the per-outcome flags. */
120
- export interface IntakeGateState {
121
- /**
122
- * `autoBuild` — whether an agent will AUTO-BUILD an undeclared task next. ON ⇒
123
- * a task needs a human PR checkpoint NOW (`--propose-task`); OFF ⇒ a human
124
- * must build it, so it may `--merge-task` (when the author is also trusted).
125
- */
126
- autoBuild: boolean;
127
- /**
128
- * `autoTask` — whether an agent will AUTO-TASK an undeclared spec next. ON ⇒ a
129
- * spec needs a human PR checkpoint NOW (`--propose-spec`); OFF ⇒ a human must
130
- * task it, so it may `--merge-spec`.
131
- */
132
- autoTask: boolean;
133
- }
134
-
135
160
  /**
136
- * DERIVE the per-outcome merge-vs-propose flags from the gate state COMPOSED with
137
- * author-trust — CI's merge-vs-propose POLICY (spec "merge-vs-propose POLICY" +
138
- * "Composed with AUTHOR-TRUST"; task Decision 1). This is the load-bearing pure
139
- * logic the workflow encodes at runtime (it reads `author_association` off the
140
- * event payload and sets the flags accordingly):
161
+ * DERIVE the per-outcome file-emit modes + the origin-trust stamp — CI's intake
162
+ * POLICY (ADR `untrusted-origin-carries-via-stamp-not-forced-staging`; spec
163
+ * `intake-integration-knob-and-specs-land-in-proposed-rename` US #1/#2). This is
164
+ * the load-bearing pure logic the workflow encodes at runtime (it reads the
165
+ * resolved `intakeIntegration ?? integration` via `dorfl config --json` for the
166
+ * MODE, and `author_association` off the event payload for the STAMP):
141
167
  *
142
- * - **SPEC** — gate-derived ONLY (author-trust does NOT bite): `--merge-spec` iff
143
- * `autoTask` is OFF (a human must task the spec before anything autonomous
144
- * acts on it — the human checkpoint stays AHEAD even for an untrusted author),
145
- * else `--propose-spec`.
146
- * - **TASK** — `--propose-task` iff (`autoBuild` ON) OR (author UNTRUSTED);
147
- * `--merge-task` ONLY iff (`autoBuild` OFF AND author TRUSTED). An untrusted
148
- * author can never auto-merge a task from a public-front-door issue.
168
+ * - **DOCUMENT mode (spec = task)** — the resolved `intakeIntegration` value, a
169
+ * SINGLE mode applied to BOTH the spec and task document: `merge` ⇒
170
+ * `--merge-spec` + `--merge-task`, `propose` ⇒ `--propose-spec` +
171
+ * `--propose-task`. DECOUPLED from the autonomy gates: `autoBuild`/`autoTask`
172
+ * no longer bite the document mode (they mean ONLY "may an agent act
173
+ * autonomously"). So a repo with `autoBuild: true`/`autoTask: true` +
174
+ * `intakeIntegration` (or `integration`) `merge` MERGES intake documents to
175
+ * `main` — previously impossible (the gates forced a document PR).
176
+ * - **ORIGIN-TRUST stamp** — the ONLY thing author-trust drives: `untrusted`
177
+ * iff the author is not trusted, else `trusted`. `intake`'s dispatch reads
178
+ * this stamp to select the untrusted-side PLACEMENT default and to force the
179
+ * later BUILD transition of an untrusted task to a code PR.
149
180
  *
150
- * So the only way to `--merge-task` is a TRUSTED author with `autoBuild` OFF —
151
- * and the fully-gateless "merge everything" path additionally needs `autoTask`
152
- * OFF; both gates off + a trusted author is the LOUD, NON-DEFAULT opt-in. The
153
- * conservative case (untrusted author, or any gate on) keeps a human in the loop.
181
+ * So author-trust changes ONLY (a) which folder the document lands in (via the
182
+ * stamp → `untrusted*LandIn`) and (b) the carried stamp — NEVER whether the
183
+ * DOCUMENT is a PR. Whether a document merges or is proposed is now purely the
184
+ * `intakeIntegration` operator config, independent of the gates AND of who filed
185
+ * the issue. The untrusted safety is the stamp (the build-time code PR) plus the
186
+ * placement default, not a forced document PR (the ADR's core move).
154
187
  */
155
188
  export function deriveIntakeFlags(options: {
156
- gate: IntakeGateState;
189
+ intakeIntegration: 'merge' | 'propose';
157
190
  authorTrusted: boolean;
158
191
  }): IntakeIntegrationFlags {
159
- const {gate, authorTrusted} = options;
160
- // SPEC: gate-derived only — a human-tasks-it checkpoint stays ahead regardless
161
- // of author-trust, so an untrusted author may still --merge-spec.
162
- const spec: 'merge' | 'propose' = gate.autoTask ? 'propose' : 'merge';
163
- // TASK: propose if the agent will auto-build it (gate ON) OR the author is
164
- // untrusted; merge ONLY when both are safe (gate OFF AND author trusted).
165
- const task: 'merge' | 'propose' =
166
- gate.autoBuild || !authorTrusted ? 'propose' : 'merge';
167
- // ORIGIN-TRUST stamp (task `untrusted-origin-forces-build-propose`): the SAME
168
- // author-trust verdict, collapsed to the wire value `intake` stamps onto the
169
- // emitted artifact. Derived HERE — next to the integration flags, off the SAME
170
- // `authorTrusted` input — so the stamp and the task/spec modes cannot desync.
171
- // It is NOT a re-resolution of trust (CI already resolved it); it is the verdict
192
+ const {intakeIntegration, authorTrusted} = options;
193
+ // DOCUMENT mode: the resolved `intakeIntegration ?? integration` (operator/config),
194
+ // a SINGLE value applied to BOTH the spec and the task document. Decoupled from
195
+ // the autonomy gates (ADR untrusted-origin-carries-via-stamp-not-forced-staging;
196
+ // spec intake-integration-knob-and-specs-land-in-proposed-rename): autoBuild/
197
+ // autoTask no longer bite the mode, and author-trust never did. The two axes are
198
+ // kept only because intake's CLI takes --merge-spec/--merge-task separately.
199
+ const spec: 'merge' | 'propose' = intakeIntegration;
200
+ const task: 'merge' | 'propose' = intakeIntegration;
201
+ // ORIGIN-TRUST stamp — the SOLE thing author-trust drives on the wire (task
202
+ // `untrusted-origin-forces-build-propose`): the author-trust verdict collapsed
203
+ // to the value `intake` stamps onto the emitted artifact. `intake`'s dispatch
204
+ // reads it to (1) select the untrusted-side placement default (`untrusted*LandIn`)
205
+ // and (2) force the later BUILD transition of an untrusted task to a code PR. It
206
+ // is NOT a re-resolution of trust (CI already resolved it); it is the verdict
172
207
  // being CARRIED so it survives the spec/task merge boundary (the becomes-code
173
208
  // checkpoint is not laundered when the file lands on main).
174
209
  const originTrust: 'trusted' | 'untrusted' = authorTrusted
@@ -204,9 +239,13 @@ export function isAuthorTrusted(
204
239
  * shape itself is config-independent.
205
240
  *
206
241
  * The per-outcome FLAGS are DERIVED AT RUNTIME by a `bash` step that mirrors
207
- * {@link deriveIntakeFlags}: it reads the gate env block + the event's
208
- * `author_association` and sets `--merge-spec`/`--propose-spec` +
209
- * `--merge-task`/`--propose-task` accordingly. The same rule that
242
+ * {@link deriveIntakeFlags}: it reads the resolved `intakeIntegration ??
243
+ * integration` via `dorfl config --json` to set the (config-derived) DOCUMENT
244
+ * mode `--merge-spec`/`--propose-spec` + `--merge-task`/`--propose-task` (a SINGLE
245
+ * mode applied to both), and the event's `author_association` to set ONLY the
246
+ * `--origin-trust` STAMP (which carries the placement + build-PR consequence). The
247
+ * autonomy gates (`autoBuild`/`autoTask`) are NOT read for the document mode (they
248
+ * mean only "may an agent act autonomously"). The same rule that
210
249
  * {@link deriveIntakeFlags} unit-tests is what the workflow executes — they cannot
211
250
  * desync because the test asserts the SHELL derivation matches the function.
212
251
  */
@@ -237,15 +276,22 @@ export function generateIntakeWorkflow(config: ResolvedCIConfig): string {
237
276
  # re-evaluation (a fresh id the watermark catches). There is NO edit-detection /
238
277
  # \`updated_at\` / body-hash tracking.
239
278
  #
240
- # AUTHOR-TRUST → per-outcome FLAGS (the merge-vs-propose POLICY): because ANYBODY
241
- # can file an issue, the merge decision composes with WHO authored it. An UNTRUSTED
242
- # author (\`author_association\` not OWNER/MEMBER/COLLABORATOR) forces
243
- # \`--propose-task\` REGARDLESS of the \`autoBuild\` gate — a task from a public
244
- # front-door issue can never auto-merge — while \`--merge-spec\` stays allowed (a
245
- # human must still task a spec before anything autonomous acts on it, so the human
246
- # checkpoint is intact). A TRUSTED author gets the plain gate-derived mode. The
247
- # fully-gateless "merge everything" path (both gates off + a trusted author) is a
248
- # LOUD, NON-DEFAULT opt-in; the default is conservative (propose).
279
+ # THE DOCUMENT MODE is \`intakeIntegration\`; AUTHOR-TRUST → PLACEMENT + the STAMP:
280
+ # the intake DOCUMENT merge-vs-propose mode is the resolved \`intakeIntegration ??
281
+ # integration\` (an operator/config choice, a SINGLE value applied to BOTH the task
282
+ # and spec document), DECOUPLED from the autonomy gates (spec
283
+ # intake-integration-knob-and-specs-land-in-proposed-rename; ADR
284
+ # untrusted-origin-carries-via-stamp-not-forced-staging). \`autoBuild\`/\`autoTask\`
285
+ # no longer decide the document mode (they mean only "may an agent act
286
+ # autonomously"). Because ANYBODY can file an issue, WHO authored it still matters —
287
+ # but author-trust drives only (1) the \`--origin-trust\` STAMP on the emitted
288
+ # document and (2), via that stamp read by \`intake\`'s dispatch, which PLACEMENT
289
+ # default the document lands in. So an untrusted author's task DOCUMENT MERGES to
290
+ # \`main\` just like a trusted one when \`intakeIntegration\` is \`merge\`; the
291
+ # untrusted safety is the CARRIED stamp (it forces the later BUILD to a code PR)
292
+ # plus the placement default, not a forced document PR. "Merge everything" is
293
+ # \`intakeIntegration: merge\` (or \`integration: merge\`), independent of the gates
294
+ # AND of who filed the issue — so a repo can have autonomy AND merged documents.
249
295
  #
250
296
  # CI runs IN-PLACE (the CI container IS the isolation): NO --isolated/--remote/
251
297
  # registry (laptop-only affordances). The PER-ISSUE concurrency group below
@@ -291,23 +337,25 @@ permissions:
291
337
  pull-requests: write
292
338
  issues: write
293
339
 
294
- env:
295
- # ── The engine GATE FAMILY, surfaced as the DORFL_* env block ─────────
296
- # CI is NOT a special policy surface (ADR ci-config-policy-and-gate-family §5):
297
- # it runs the SAME engine gates, resolved through flag > env > per-repo > global
298
- # > default. The SAME dorfl.json the laptop uses applies here; this env
299
- # block is the optional CI-only override. Change behaviour by editing these
300
- # values (or a GitHub repo variable / dorfl.json key) — NOT by re-running
301
- # install-ci (ADR §6: install-ci is one-time).
302
- #
303
- # \`intake\` itself is GATE-FREE (the explicit invocation is its own
304
- # authorization), so these gates do NOT block it — CI READS them to DERIVE the
305
- # per-outcome merge-vs-propose flags below (the merge-vs-propose POLICY). CALM
306
- # DEFAULTS: both off ⇒ the next step is a HUMAN, so the gate-derived mode is the
307
- # permissive merge side; author-trust then forces propose for a task from an
308
- # untrusted author.
309
- DORFL_AUTO_BUILD: 'false' # gate: will an agent auto-build the emitted task next?
310
- DORFL_AUTO_TASK: 'false' # gate: will an agent auto-task the emitted spec next?
340
+ # ── The intake DOCUMENT mode is resolved FROM CONFIG, not carried here ───────
341
+ # CI is NOT a special policy surface (ADR ci-config-policy-and-gate-family §5):
342
+ # it runs the SAME engine config, resolved through flag > env > per-repo > global
343
+ # > default. The SAME dorfl.json the laptop uses applies here. This workflow emits
344
+ # NO DORFL_AUTO_BUILD / DORFL_AUTO_TASK line (ADR
345
+ # untrusted-origin-carries-via-stamp-not-forced-staging: hardcoding them here made
346
+ # the env layer OUTRANK the committed dorfl.json — the shadowing bug). So the env
347
+ # layer carries NO config default; the policy step below READS the resolved intake
348
+ # DOCUMENT mode \`intakeIntegration ?? integration\` via \`dorfl config --json\` (the
349
+ # mechanism \`advance\` already uses), so your committed dorfl.json wins (then the
350
+ # global config, then the built-in default \`propose\`). To land intake documents on
351
+ # main, set \`intakeIntegration: merge\` (or \`integration: merge\`) in dorfl.json
352
+ # (applies everywhere) — NOT by re-running install-ci (ADR §6: install-ci is
353
+ # one-time). The autonomy gates \`autoBuild\`/\`autoTask\` are DECOUPLED from the
354
+ # document mode (spec intake-integration-knob-and-specs-land-in-proposed-rename):
355
+ # they gate autonomy only and are NOT read here. \`intake\` itself is GATE-FREE (the
356
+ # explicit invocation is its own authorization); CI READS only the resolved
357
+ # \`intakeIntegration\`/\`integration\` to DERIVE the merge-vs-propose document flags
358
+ # below (the merge-vs-propose POLICY).
311
359
 
312
360
  jobs:
313
361
  intake:
@@ -322,16 +370,23 @@ jobs:
322
370
  fetch-depth: 0
323
371
  - uses: ./.github/actions/dorfl-setup${setupWith}
324
372
 
325
- - name: derive the per-outcome merge-vs-propose flags (gate × author-trust)
373
+ - name: derive the intake DOCUMENT mode (intakeIntegration) + the origin-trust stamp (author-trust)
326
374
  id: policy
327
- # The merge-vs-propose POLICY, executed at runtime — the SAME rule
375
+ # The intake POLICY, executed at runtime — the SAME rule
328
376
  # \`deriveIntakeFlags\` unit-tests (they cannot desync; the test asserts this
329
- # shell matches the function):
330
- # * SPEC — gate-derived ONLY: --merge-spec iff autoTask OFF (a human
331
- # tasks it before anything autonomous acts — the checkpoint
332
- # stays ahead even for an UNTRUSTED author), else --propose-spec.
333
- # * TASK — --propose-task iff (autoBuild ON) OR (author UNTRUSTED);
334
- # --merge-task ONLY iff (autoBuild OFF AND author TRUSTED).
377
+ # shell matches the function). The DOCUMENT mode is the resolved
378
+ # \`intakeIntegration ?? integration\` (operator/config), DECOUPLED from the
379
+ # autonomy gates; author-trust drives ONLY the stamp + placement (ADR
380
+ # untrusted-origin-carries-via-stamp-not-forced-staging; spec
381
+ # intake-integration-knob-and-specs-land-in-proposed-rename):
382
+ # * DOCUMENT mode (spec = task) — config-derived: --merge-* iff the
383
+ # resolved intakeIntegration is merge, else --propose-*. A SINGLE
384
+ # value applied to BOTH the spec and task document. autoBuild/
385
+ # autoTask are NOT read (they gate autonomy only).
386
+ # * ORIGIN-TRUST stamp — the ONLY thing author-trust drives: --origin-trust
387
+ # untrusted iff the author is not OWNER/MEMBER/COLLABORATOR, which
388
+ # \`intake\`'s dispatch reads to select the untrusted PLACEMENT
389
+ # default and to force the later BUILD to a code PR.
335
390
  # author_association comes from the COMMENT on an \`issue_comment\` event,
336
391
  # else the ISSUE on an \`issues\` event — read straight off the payload, no
337
392
  # extra API call.
@@ -340,37 +395,53 @@ jobs:
340
395
  run: |
341
396
  set -euo pipefail
342
397
 
343
- # SPEC flag: gate-derived only (author-trust does NOT bite a spec).
344
- if [ "\${DORFL_AUTO_TASK}" = "true" ]; then
345
- spec_flag="--propose-spec"
346
- else
398
+ # Read the RESOLVED intake DOCUMENT mode from the committed config via
399
+ # \`dorfl config --json\` (the mechanism \`advance\` already uses), NOT a
400
+ # hardcoded DORFL_* env (ADR
401
+ # untrusted-origin-carries-via-stamp-not-forced-staging: an env default
402
+ # would OUTRANK dorfl.json — the shadowing bug). In-place, so the
403
+ # resolution chain reads THIS repo's dorfl.json exactly like the laptop.
404
+ # \`intakeIntegration\` is OPTIONAL (unset ⇒ falls back to \`integration\`),
405
+ # so read \`.intakeIntegration // .integration\` in ONE jq expression — the
406
+ # shell twin of the \`intakeIntegration ?? integration\` the CLI applies.
407
+ # The autonomy gates (autoBuild/autoTask) are DECOUPLED from the document
408
+ # mode (spec intake-integration-knob-and-specs-land-in-proposed-rename):
409
+ # they gate autonomy only and are NOT read here.
410
+ config_json="$(dorfl config --json)"
411
+ intake_integration="$(echo "\${config_json}" | jq -r '.intakeIntegration // .integration')"
412
+
413
+ # DOCUMENT mode: config-derived from the single \`intakeIntegration ??
414
+ # integration\` value, applied to BOTH the spec and task document (US #1
415
+ # chose one intake knob, not a per-type split). merge ⇒ --merge-*,
416
+ # else --propose-*. Author-trust does NOT bite the mode (ADR
417
+ # untrusted-origin-carries-via-stamp-not-forced-staging): an untrusted
418
+ # author's DOCUMENT merges just like a trusted one; the untrusted safety
419
+ # is the stamp + placement below, not a document PR.
420
+ if [ "\${intake_integration}" = "merge" ]; then
347
421
  spec_flag="--merge-spec"
422
+ task_flag="--merge-task"
423
+ else
424
+ spec_flag="--propose-spec"
425
+ task_flag="--propose-task"
348
426
  fi
349
427
 
350
428
  # Author-trust: TRUSTED iff OWNER/MEMBER/COLLABORATOR (admin / write-
351
429
  # collaborator — the whole signal). Anything else (incl. empty) is
352
- # UNTRUSTED → the conservative, human-in-the-loop path.
430
+ # UNTRUSTED. It drives ONLY the origin-trust stamp below (NOT the modes).
353
431
  trusted="false"
354
432
  case "\${AUTHOR_ASSOCIATION:-}" in
355
433
  OWNER|MEMBER|COLLABORATOR) trusted="true" ;;
356
434
  esac
357
435
 
358
- # TASK flag: propose if the agent will auto-build it (gate ON) OR the
359
- # author is untrusted; merge ONLY when both are safe.
360
- if [ "\${DORFL_AUTO_BUILD}" = "true" ] || [ "\${trusted}" != "true" ]; then
361
- task_flag="--propose-task"
362
- else
363
- task_flag="--merge-task"
364
- fi
365
-
366
- # ORIGIN-TRUST stamp (task untrusted-origin-forces-build-propose):
367
- # derived from the SAME \${trusted} case above (one author-trust read,
368
- # two consumers — the task/spec modes AND the stamp — so they cannot
369
- # desync). \`intake\` STAMPS this onto the emitted spec/task frontmatter
370
- # (origin: issue + originTrust: <value>); it does NOT re-resolve trust
371
- # (that is CI's policy, passed IN). The stamp SURVIVES the merge boundary
372
- # so a later auto-task/auto-build of an untrusted-origin artifact still
373
- # forces a human becomes-code checkpoint (the laundering gap is closed).
436
+ # ORIGIN-TRUST stamp — the SOLE thing author-trust drives on the wire
437
+ # (task untrusted-origin-forces-build-propose). \`intake\` STAMPS this
438
+ # onto the emitted spec/task frontmatter (origin: issue + originTrust:
439
+ # <value>); its dispatch reads the stamp to (1) select the untrusted-side
440
+ # PLACEMENT default (\`untrusted*LandIn\`) and (2) force the later BUILD of
441
+ # an untrusted task to a code PR. It does NOT re-resolve trust (that is
442
+ # CI's policy, passed IN). The stamp SURVIVES the merge boundary so a
443
+ # later auto-task/auto-build of an untrusted-origin artifact still forces
444
+ # a human becomes-code checkpoint (the laundering gap is closed).
374
445
  if [ "\${trusted}" = "true" ]; then
375
446
  origin_trust_flag="--origin-trust=trusted"
376
447
  else
@@ -380,12 +451,15 @@ jobs:
380
451
  echo "spec_flag=\${spec_flag}" >> "\$GITHUB_OUTPUT"
381
452
  echo "task_flag=\${task_flag}" >> "\$GITHUB_OUTPUT"
382
453
  echo "origin_trust_flag=\${origin_trust_flag}" >> "\$GITHUB_OUTPUT"
383
- echo "intake policy: author_association='\${AUTHOR_ASSOCIATION:-}' trusted=\${trusted} → \${spec_flag} \${task_flag} \${origin_trust_flag}"
454
+ echo "intake policy: intakeIntegration='\${intake_integration}' author_association='\${AUTHOR_ASSOCIATION:-}' trusted=\${trusted} → \${spec_flag} \${task_flag} \${origin_trust_flag}"
384
455
 
385
456
  - name: intake the issue (four-outcome dispatch; surfaces the review verdict into the thread)
386
457
  # In-place in this checkout (no --isolated/--remote): the CI container IS
387
458
  # the isolation. EXPLICIT \`intake <N>\`, never a bare slug. The per-outcome
388
- # flags carry the merge-vs-propose POLICY derived above. \`intake\` runs the
459
+ # flags carry the (intakeIntegration-derived) document modes + the
460
+ # origin-trust stamp derived above (author-trust drives only the stamp +
461
+ # placement, not the document merge-vs-propose; the autonomy gates drive
462
+ # neither). \`intake\` runs the
389
463
  # lone-task review/edit loop and posts its findings as questions back into
390
464
  # THIS issue thread (insertion point E) through the issue-comment seam —
391
465
  # CI surfaces E by invoking intake; it adds no new review mechanism.
@@ -498,29 +572,33 @@ export function validateIntakeWorkflow(text: string): IntakeTriggerValidation {
498
572
  text,
499
573
  ), 'author-trust must be OWNER/MEMBER/COLLABORATOR (admin / write-collaborator ' +
500
574
  '— the whole signal; Decision 1).');
501
- // An untrusted author forces --propose-task; the derivation must emit both
502
- // task modes (so the untrusted path can reach propose and the trusted-safe
503
- // path can reach merge).
575
+ // The DOCUMENT mode is intakeIntegration-DERIVED (spec
576
+ // intake-integration-knob-and-specs-land-in-proposed-rename; the autonomy gates
577
+ // no longer bite it): the derivation must be able to emit both task modes
578
+ // (merge iff the resolved intakeIntegration is merge, else propose).
504
579
  require('derives-propose-task', /--propose-task\b/.test(
505
580
  operative,
506
581
  ), 'the policy derivation must be able to emit `--propose-task` (the ' +
507
- 'untrusted-author / autoBuild-on fallback).');
582
+ 'intakeIntegration-propose path).');
508
583
  require('derives-merge-task', /--merge-task\b/.test(
509
584
  operative,
510
585
  ), 'the policy derivation must be able to emit `--merge-task` (the ' +
511
- 'trusted-author + autoBuild-off path).');
512
- // --merge-spec stays allowed even for an untrusted author (spec checkpoint ahead).
586
+ 'intakeIntegration-merge path; a task DOCUMENT merges regardless of the ' +
587
+ 'autonomy gates or author-trust).');
588
+ // --merge-spec is emitted when the resolved intakeIntegration is merge (the
589
+ // SAME single value as the task; the gates do not bite it).
513
590
  require('derives-merge-spec', /--merge-spec\b/.test(
514
591
  operative,
515
- ), 'the policy derivation must be able to emit `--merge-spec` (a spec stays ' +
516
- 'mergeable even for an untrusted author — the human-tasks-it checkpoint).');
592
+ ), 'the policy derivation must be able to emit `--merge-spec` (the ' +
593
+ 'intakeIntegration-merge path).');
517
594
  require('derives-propose-spec', /--propose-spec\b/.test(
518
595
  operative,
519
- ), 'the policy derivation must be able to emit `--propose-spec` (autoTask on).');
596
+ ), 'the policy derivation must be able to emit `--propose-spec` (the ' +
597
+ 'intakeIntegration-propose path).');
520
598
  // ORIGIN-TRUST stamp (task untrusted-origin-forces-build-propose): the shell
521
- // must derive `--origin-trust <trusted|untrusted>` from the SAME author-trust
522
- // case it uses for the task/SPEC modes, and pass it to `intake` so the emitted
523
- // artifact is stamped (the stamp + the modes cannot desync).
599
+ // must derive `--origin-trust <trusted|untrusted>` from the author-trust case
600
+ // (independent of the document mode) and pass it to `intake` so the emitted
601
+ // artifact is stamped (the stamp is the SOLE thing author-trust drives).
524
602
  require('derives-origin-trust-untrusted', /--origin-trust=untrusted\b/.test(
525
603
  operative,
526
604
  ), 'the policy derivation must emit `--origin-trust=untrusted` for a non-trusted ' +
@@ -533,13 +611,55 @@ export function validateIntakeWorkflow(text: string): IntakeTriggerValidation {
533
611
  operative,
534
612
  ), 'the intake invocation must pass the derived `--origin-trust` flag (the ' +
535
613
  'stamp must reach `dorfl intake`).');
536
- // The derivation must compose the gate env block (it READS the gates to derive).
537
- require('reads-auto-build-gate', /DORFL_AUTO_BUILD\b/.test(
538
- text,
539
- ), 'the policy derivation must read the `DORFL_AUTO_BUILD` gate.');
540
- require('reads-auto-task-gate', /DORFL_AUTO_TASK\b/.test(
541
- text,
542
- ), 'the policy derivation must read the `DORFL_AUTO_TASK` gate.');
614
+ // The derivation must read the RESOLVED intake DOCUMENT mode via `dorfl config
615
+ // --json` (the mechanism `advance` uses), NOT a hardcoded DORFL_* env (ADR
616
+ // untrusted-origin-carries-via-stamp-not-forced-staging — the shadowing bug).
617
+ require('reads-config-json', /dorfl config --json/.test(
618
+ operative,
619
+ ), 'the policy derivation must read the resolved config via ' +
620
+ '`dorfl config --json` (as `advance` does), so a committed `dorfl.json` ' +
621
+ 'mode is honored in CI (not shadowed by a hardcoded env).');
622
+ // The DOCUMENT mode is the resolved `intakeIntegration ?? integration` (spec
623
+ // intake-integration-knob-and-specs-land-in-proposed-rename): the derivation
624
+ // must read `.intakeIntegration` with a `.integration` FALLBACK (the shell twin
625
+ // of `intakeIntegration ?? integration`), NOT the autonomy gates. Pin BOTH the
626
+ // key and the fallback so a regression to a gate-derived mode is caught.
627
+ require('reads-intake-integration', /\.intakeIntegration\b/.test(
628
+ operative,
629
+ ), 'the policy derivation must read the resolved `intakeIntegration` mode (jq ' +
630
+ '`.intakeIntegration` off `dorfl config --json`) for the document mode.');
631
+ require('intake-integration-falls-back-to-integration', /\.intakeIntegration\s*\/\/\s*\.integration/.test(
632
+ operative,
633
+ ), 'the policy derivation must fall back to `.integration` when ' +
634
+ '`intakeIntegration` is unset (jq `.intakeIntegration // .integration`, the ' +
635
+ 'shell twin of `intakeIntegration ?? integration`).');
636
+ // The DOCUMENT mode must NOT be derived from the autonomy gates (spec
637
+ // intake-integration-knob-and-specs-land-in-proposed-rename: autoBuild/autoTask
638
+ // gate autonomy ONLY, never the document PR-mode). The mode branch must not read
639
+ // `.autoBuild` / `.autoTask` off the config json.
640
+ require('mode-not-gate-derived', !/\.auto(?:Build|Task)\b/.test(
641
+ operative,
642
+ ), 'the intake DOCUMENT mode must be derived from `intakeIntegration ?? ' +
643
+ 'integration`, NOT the autonomy gates — the derivation must not read ' +
644
+ '`.autoBuild` / `.autoTask` (they gate autonomy only; spec ' +
645
+ 'intake-integration-knob-and-specs-land-in-proposed-rename).');
646
+ // ANTI-REGRESSION (ADR untrusted-origin-carries-via-stamp-not-forced-staging;
647
+ // spec US #12): the workflow must NOT emit a `DORFL_AUTO_BUILD:` /
648
+ // `DORFL_AUTO_TASK:` env ASSIGNMENT. The env layer OUTRANKS per-repo config, so
649
+ // a hardcoded default here SHADOWS the committed `dorfl.json` gates (the bug this
650
+ // task fixes). Mirrors `advance-lifecycle-template.ts`'s `no-gate-env-auto-build`
651
+ // / `no-gate-env-auto-task`. Checked over the OPERATIVE (non-comment) lines so
652
+ // the header comment that NAMES these keys is not a false positive.
653
+ require('no-gate-env-auto-build', !/DORFL_AUTO_BUILD\s*:/.test(
654
+ operative,
655
+ ), 'the workflow must NOT emit a `DORFL_AUTO_BUILD:` env assignment (env ' +
656
+ 'carries no defaults; the gate is resolved from per-repo config / built-in ' +
657
+ 'default — else the env SHADOWS the committed dorfl.json).');
658
+ require('no-gate-env-auto-task', !/DORFL_AUTO_TASK\s*:/.test(
659
+ operative,
660
+ ), 'the workflow must NOT emit a `DORFL_AUTO_TASK:` env assignment (env ' +
661
+ 'carries no defaults; the gate is resolved from per-repo config / built-in ' +
662
+ 'default — else the env SHADOWS the committed dorfl.json).');
543
663
 
544
664
  // --- Insertion point E: the issue-thread review surface ---------------------
545
665
  // E is REUSED via `intake` (which runs the lone-task review and posts to the