@deftai/directive-content 0.90.0 → 0.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/coding/coding.md +8 -0
- package/commands.md +21 -4
- package/contracts/scm-readiness.md +78 -0
- package/docs/openclaw-agent-host.md +2 -2
- package/package.json +1 -1
- package/packs/lessons/lessons-pack-0.1.json +1 -1
- package/packs/patterns/patterns-pack-0.1.json +13 -1
- package/packs/rules/rules-pack-0.1.json +114 -82
- package/packs/skills/skills-pack-0.1.json +2 -2
- package/patterns/in-band-signaling.md +124 -0
- package/scm/github.md +47 -0
- package/skills/deft-directive-build/SKILL.md +44 -1
- package/skills/deft-directive-pre-pr/SKILL.md +4 -2
- package/tasks/pr.yml +7 -8
- package/tasks/roadmap.yml +1 -1
- package/tasks/scope.yml +4 -1
- package/tasks/spec.yml +5 -2
package/coding/coding.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<!-- Regenerate with: task packs:render -->
|
|
5
5
|
<!-- Edit the source, not this file. Slice instead of loading every coding doc: task packs:slice rules by-tier --tier <TIER> (or by-domain, list) -->
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
# Coding Guidelines
|
|
8
9
|
|
|
9
10
|
Software development specific guidelines for AI agents.
|
|
@@ -83,6 +84,13 @@ See [../scm/git.md](../scm/git.md) for:
|
|
|
83
84
|
- ! Comments explain **why**, code shows **what**
|
|
84
85
|
- ⊗ Clever code over clear code
|
|
85
86
|
|
|
87
|
+
**State & Data Modeling (#1695):**
|
|
88
|
+
- ! A field MUST encode exactly one fact. Do NOT overload a field's value — or its presence/absence — to also signal a second orthogonal concern. Smuggling decision-, config-, lifecycle-, or control-state through a data field is *in-band signaling*; give that signal its own out-of-band field.
|
|
89
|
+
- ! "Absence is not a decision." Distinguish "unset / never considered" from "deliberately set to the default." If a workflow must know a human made a choice, record the choice explicitly — never infer it from whether a value-field is present.
|
|
90
|
+
- ~ Orthogonality test: if two facts can vary independently (e.g. value==default while decided ∈ {true,false}), they MUST live in separate slots. If one fact strictly implies the other (true Optional<T>, tombstones), sharing a slot is fine.
|
|
91
|
+
- ⊗ Infer decision / onboarding / configuration state from the presence of a value field. Use an explicit out-of-band marker — cf. the resolver `source` provenance pattern (typed | default | default-on-error) directive already uses for *value*-provenance.
|
|
92
|
+
- See [../patterns/in-band-signaling.md](../patterns/in-band-signaling.md) for the full model, orthogonality procedure, and the wipCap worked example (#1694).
|
|
93
|
+
|
|
86
94
|
## Quality Standards
|
|
87
95
|
|
|
88
96
|
**General:**
|
package/commands.md
CHANGED
|
@@ -90,6 +90,7 @@ Scope xBRIEFs live under `xbrief/{proposed,pending,active,completed,cancelled}/`
|
|
|
90
90
|
Common commands:
|
|
91
91
|
|
|
92
92
|
- `task scope:promote -- xbrief/proposed/<file>.xbrief.json` -- move proposed work to `pending/` and set status to `pending`.
|
|
93
|
+
- `task scope:promote -- --batch` -- batch-promote **all** `xbrief/proposed/` scopes to `pending/` in one command (#3011 / epic #3009). Optional: `--batch <path>…` for an explicit list; `--force` overrides WIP cap (logged). Does **not** activate; implement remains one `scope:activate` at a time.
|
|
93
94
|
- `task scope:activate -- xbrief/pending/<file>.xbrief.json` -- move accepted work to `active/` and set status to `running`.
|
|
94
95
|
- `task scope:complete -- xbrief/active/<file>.xbrief.json` -- move running work to `completed/` and set status to `completed`.
|
|
95
96
|
- `task scope:fail -- xbrief/active/<file>.xbrief.json` -- mark running work failed when the scope cannot complete.
|
|
@@ -109,7 +110,9 @@ deft xbrief:preflight -- xbrief/active/<file>.xbrief.json
|
|
|
109
110
|
|
|
110
111
|
Gate 0 `task verify:story-ready` machine-checks working-tree cleanliness (or `--allow-dirty`), the target xBRIEF in `xbrief/active/` with `plan.status == "running"`, and the dispatch envelope's `## Allocation context` consent token (#1378). A `swarm-cohort` section is ready only when `allocation_plan_id` AND `batching_rationale` are non-null. Complete stories with `task scope:complete -- <active-story-path>`.
|
|
111
112
|
|
|
112
|
-
**Story Start Gate (#1378):** Before starting any new implementation story or switching stories, run `git status --short --branch`. If the working tree is dirty, stop and summarize the current branch, modified/untracked files, and whether the changes appear related to the next story — ask the operator to choose: commit existing work, stash existing work, include existing work in the current story, or stop. ⊗ Do not begin a new story while unrelated dirty work is present without explicit operator approval. When invoked as part of a swarm cohort dispatch, the approved Phase 5 allocation plan satisfies batching consent (#954); between stories checkpoint-commit it and proceed — do not pause to ask the operator mid-cohort. Promote/activate via `task scope:promote -- <path>` / `task scope:activate -- <path>`; preflight with `deft xbrief:preflight -- <active-story-path>`.
|
|
113
|
+
**Story Start Gate (#1378):** Before starting any new implementation story or switching stories, run `git status --short --branch`. If the working tree is dirty, stop and summarize the current branch, modified/untracked files, and whether the changes appear related to the next story — ask the operator to choose: commit existing work, stash existing work, include existing work in the current story, or stop. ⊗ Do not begin a new story while unrelated dirty work is present without explicit operator approval. When invoked as part of a swarm cohort dispatch, the approved Phase 5 allocation plan satisfies batching consent (#954); between stories checkpoint-commit it and proceed — do not pause to ask the operator mid-cohort. Promote/activate via `task scope:promote -- <path>` (or `task scope:promote -- --batch` for multi-scope pins, #3011) / `task scope:activate -- <path>`; preflight with `deft xbrief:preflight -- <active-story-path>`.
|
|
114
|
+
|
|
115
|
+
**Multi-scope turn/cache budget (#3009):** After offline seed (init + pin scopes + session ritual), implement agents ⊗ re-run `directive init`, cold `session:start`, migrate, or re-copy scopes — recover with `session:ready` / re-arm only (#3010). Batch-promote the pin; activate+implement one scope at a time (#3011). Run full `task check` once at end of the multi-scope batch unless the last check failed (#3012). Init seeds a minimal render-ready `PROJECT-DEFINITION`; treat `project:render` as a one-shot lifecycle refresh, not identity research (#3013).
|
|
113
116
|
|
|
114
117
|
The implementation gate succeeds only for active scope xBRIEFs with `plan.status == "running"`. Do not infer implementation intent from lifecycle vocabulary — require explicit action-verb directives (`build`, `implement`, `ship`, `swarm`, `run agents`, `start agent`) per #810.
|
|
115
118
|
|
|
@@ -140,9 +143,11 @@ flowchart TD
|
|
|
140
143
|
|
|
141
144
|
Edit the xBRIEF source, then render the markdown view.
|
|
142
145
|
|
|
143
|
-
- `task spec:render` -- render `xbrief/specification.xbrief.json` to `SPECIFICATION.md`.
|
|
146
|
+
- `task spec:render` -- render `xbrief/specification.xbrief.json` to a **compact** `SPECIFICATION.md` by default (#1566). Defaults: no lifecycle Scope outlook, no `LegacyArtifacts`. Opt in with engine flags passed after `--`:
|
|
147
|
+
- `--include-scopes=off` (default) / `current` (pending+active) / `all` (include completed archive)
|
|
148
|
+
- `--include-legacy-artifacts=on|off` (default off)
|
|
144
149
|
- `task prd:render` -- render a stakeholder PRD view from the specification xBRIEF.
|
|
145
|
-
- `task roadmap:render` -- render `ROADMAP.md` from lifecycle scope xBRIEFs.
|
|
150
|
+
- `task roadmap:render` -- render `ROADMAP.md` from lifecycle scope xBRIEFs (`pending/` + `proposed/` + `active/` forward; `completed/` capped).
|
|
146
151
|
- `task project:render` -- refresh the `PROJECT-DEFINITION.xbrief.json` items registry from lifecycle folders.
|
|
147
152
|
- `deft xbrief:validate` -- validate xBRIEF schema, filenames, folders, statuses, and cross-file consistency.
|
|
148
153
|
- `deft migrate:xbrief` (or `task migrate:xbrief`) -- convert a legacy `vbrief/` project tree to `xbrief/` (v0.6→v0.8 semantic transforms; requires clean working tree unless `--force`). Legacy `vbrief/` and `x-vbrief/` tokens remain read-accepted until this runs.
|
|
@@ -279,6 +284,17 @@ Full always-on contract for the interactive session-start ritual and its gated v
|
|
|
279
284
|
|
|
280
285
|
Agents use this signal to prefer portable syntax and quote zsh-sensitive data such as globs, tildes, `~N`, `!`, and `#`. When a command requires Bash, zsh, PowerShell, or another shell's behavior, invoke that explicit shell rather than relying on implicit execution semantics.
|
|
281
286
|
|
|
287
|
+
### SCM readiness orientation (#2275)
|
|
288
|
+
|
|
289
|
+
`session:start` also reports whether GitHub SCM tooling is usable **in this execution env** (not the install host). Human output includes `[deft scm]` lines; `--json` includes a `scm` object (`ready`, `binary`, `auth_state`, `github_auth_mode`, `runtime_mode`, `injected_token_present`, `skipped_gates`, `detail`, ...). Cold mutation records a `scm_readiness` step in `steps[]`.
|
|
290
|
+
|
|
291
|
+
- Shallow probe (default hot path): PATH ladder `ghx` > `gh`, injected-token env presence, short `gh auth status`.
|
|
292
|
+
- Deep probe when `--with-network` / `DEFT_SESSION_START_NETWORK=1`: full `github-auth-modes` validation (API + optional repo).
|
|
293
|
+
- Session-start never hard-blocks on SCM absence (framework-local gates still run). When not ready it lists skipped SCM-dependent gates (`triage:queue`, `issue:ingest`, `pr:*`, `reconcile:issues`, `cache:fetch-all`, `scm:*`, ...).
|
|
294
|
+
- Explicit probe: `deft scm:status` (alias `scm:readiness`) -- exit `0` ready / `1` not ready / `2` config; flags `--json`, `--deep` / `--shallow`.
|
|
295
|
+
- Credential bridging: host-gh (`gh auth login` in the execution env) or injected-token (`GH_TOKEN` / `GITHUB_TOKEN` / `GH_ENTERPRISE_TOKEN`). Never put token values in prompts or transcripts.
|
|
296
|
+
- Contract: `content/contracts/scm-readiness.md`; operator docs: `content/scm/github.md` § Mismatched/headless SCM readiness.
|
|
297
|
+
|
|
282
298
|
**Pre-`start_agent` gate stack (#1149/#1348):** (0) `deft verify:session-ritual -- --tier=gated` → (1) `deft verify:story-ready` → (2) `deft xbrief:preflight` → (3) `deft verify:cache-fresh` → (4) `deft verify:branch` + hooks → (5) `start_agent`.
|
|
283
299
|
|
|
284
300
|
```mermaid
|
|
@@ -302,7 +318,7 @@ Local ceremony cost signal for WWYSYDH / weekly process rollups. Emits to `.deft
|
|
|
302
318
|
| Session start (cold) | `session:start` | Mutation `session:start` cold path finishes | `ceremony_tier=cold`, `duration_ms`, `exit_code`, `steps[]` |
|
|
303
319
|
| Session re-arm | `session:start` | `session:start --rearm` finishes | `ceremony_tier=rearm`, `duration_ms`, `exit_code`, `steps[]` |
|
|
304
320
|
| PreToolUse ritual deny | `session:ritual-blocked` | Hook blocks write/spawn because gated ritual is not ready | `tool_name`, `code=ritual-not-ready`, `recovery_tier` (`cold`\|`rearm`) |
|
|
305
|
-
| Per-step wall-clock | (field on `session:start`) | Same emit as session start | `steps[].name` + `steps[].duration_ms` (`alignment`, `branch_policy`, `verify_tools`, `triage_welcome`, `release_probe`, `ritual_write`) |
|
|
321
|
+
| Per-step wall-clock | (field on `session:start`) | Same emit as session start | `steps[].name` + `steps[].duration_ms` (`alignment`, `scm_readiness`, `branch_policy`, `verify_tools`, `triage_welcome`, `release_probe`, `ritual_write`) |
|
|
306
322
|
|
|
307
323
|
CLI mirror (no JSONL required): `deft session:start --json` already exposes the same `steps` / `duration_ms` / `ceremony_tier` fields for one-shot inspection.
|
|
308
324
|
|
|
@@ -414,6 +430,7 @@ flowchart TD
|
|
|
414
430
|
|
|
415
431
|
- `task packs:*` -- render and verify content packs.
|
|
416
432
|
- `task pr:*` -- protected issue checks, closing-keyword checks, merge readiness, and merge helpers.
|
|
433
|
+
- `task pr:check-closing-keywords` -- Layer 0 FP lint (#737) **plus intent mode** (#3015 class D). Default `--mode both`: fails on negation/quote/example/code-block hits **and** on any real `Closes|Fixes|Resolves #N` unless allowlisted via `--allow-close N,M` (CLI only; body trailers are not an authorization path). Offline: `--body-file` / `--commits-file`. Prefer `Tracking: #N` / `Refs #N` until full issue DoD.
|
|
417
434
|
- `task release:*` -- release, publish, rollback, and e2e release rehearsal.
|
|
418
435
|
- Step 3 (`Pre-flight vBRIEF lifecycle sync`) fetches GitHub issue states via REST. On HTTP 403 rate-limit exhaustion it sleeps once (capped at 120s) and retries before failing.
|
|
419
436
|
- When Step 3 still fails with rate-limit exhaustion, stderr includes a `gh api rate_limit` probe (`core.remaining`, reset time) and recovery guidance. After local `task vbrief:validate` (or `task xbrief:validate`) exits 0, operators may pass `--allow-vbrief-drift` to skip Step 3 for that cut — reserved for transient SCM bucket stalls, not unreviewed lifecycle drift.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Contract: SCM readiness in mismatched / headless envs (#2275)
|
|
2
|
+
|
|
3
|
+
Legend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.
|
|
4
|
+
|
|
5
|
+
**Parent:** issue #2275 (follow-up to #2203 Decision 7). Sibling of USER.md
|
|
6
|
+
resolution (#2271).
|
|
7
|
+
|
|
8
|
+
## Problem
|
|
9
|
+
|
|
10
|
+
In agentic sandboxes, execution-env often differs from install-env: `gh` /
|
|
11
|
+
`ghx` may be absent or unauthenticated even when the host desktop is fully
|
|
12
|
+
set up. SCM-dependent gates then fail opaquely (spawn errors, hung auth
|
|
13
|
+
prompts) while framework-local gates still work.
|
|
14
|
+
|
|
15
|
+
## Gate classes
|
|
16
|
+
|
|
17
|
+
### Framework-local (no SCM required)
|
|
18
|
+
|
|
19
|
+
`session:start`, `session:ready`, `verify:session-ritual`, `verify:tools`
|
|
20
|
+
(orientation), `verify:branch`, `xbrief:preflight`, `doctor` (offline),
|
|
21
|
+
`scope:*`, local cache-fresh / ritual state.
|
|
22
|
+
|
|
23
|
+
These ! run regardless of SCM readiness.
|
|
24
|
+
|
|
25
|
+
### SCM-dependent (require binary + auth)
|
|
26
|
+
|
|
27
|
+
At minimum: `triage:queue`, `triage:welcome` network hydrate, `issue:ingest`,
|
|
28
|
+
`reconcile:issues`, `pr:*`, `cache:fetch-all`, `scm:*` issue ops,
|
|
29
|
+
`umbrella:current-shape` live fetch, deep `github-auth-modes`.
|
|
30
|
+
|
|
31
|
+
These ! either run when ready or fail loud with a named reason. They ⊗ hang
|
|
32
|
+
on interactive auth prompts in headless envs without a clear diagnostic.
|
|
33
|
+
|
|
34
|
+
## Probe surface
|
|
35
|
+
|
|
36
|
+
| Surface | Depth | Blocks session? |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `session:start` default | shallow (PATH + token + `gh auth status`) | no |
|
|
39
|
+
| `session:start --with-network` | deep (API + optional repo) | no |
|
|
40
|
+
| `deft scm:status` | shallow default; `--deep` opt-in | n/a (exit 0/1/2) |
|
|
41
|
+
| `deft github-auth-modes` | mode validation (#1557) | n/a |
|
|
42
|
+
|
|
43
|
+
JSON field shape (`session:start --json` → `scm`, or `scm:status --json`):
|
|
44
|
+
|
|
45
|
+
- `ready` (bool)
|
|
46
|
+
- `binary` (`ghx` \| `gh` \| null)
|
|
47
|
+
- `binary_path`
|
|
48
|
+
- `auth_state` (`authenticated` \| `unauthenticated` \| `missing-token` \|
|
|
49
|
+
`binary-absent` \| `unknown`)
|
|
50
|
+
- `github_auth_mode` (`host-gh` \| `injected-token`)
|
|
51
|
+
- `runtime_mode`
|
|
52
|
+
- `injected_token_present` (bool; never the value)
|
|
53
|
+
- `depth` (`shallow` \| `deep`)
|
|
54
|
+
- `detail` (one-line human diagnostic)
|
|
55
|
+
- `remediation` (string \| null)
|
|
56
|
+
- `skipped_gates` (string[])
|
|
57
|
+
- `login` (string \| null; deep only)
|
|
58
|
+
- `failure_kind` (string \| null)
|
|
59
|
+
|
|
60
|
+
## Remediation contract
|
|
61
|
+
|
|
62
|
+
When not ready, agents ! prefer one of:
|
|
63
|
+
|
|
64
|
+
1. Install + auth in the **execution** env (`gh` / `ghx`, then
|
|
65
|
+
`gh auth login` for host-gh).
|
|
66
|
+
2. Inject `GH_TOKEN` / `GITHUB_TOKEN` / `GH_ENTERPRISE_TOKEN` for
|
|
67
|
+
injected-token / cloud-headless mode.
|
|
68
|
+
3. Run SCM-dependent gates from a matched authenticated environment.
|
|
69
|
+
|
|
70
|
+
⊗ Put token values into prompts, dispatch envelopes, or logs.
|
|
71
|
+
|
|
72
|
+
## Implementation anchors
|
|
73
|
+
|
|
74
|
+
- Probe: `packages/core/src/scm/readiness.ts`
|
|
75
|
+
- CLI: `packages/core/src/scm/readiness-cli.ts` (`scm:status`)
|
|
76
|
+
- Session orientation: `packages/core/src/session/session-start.ts`
|
|
77
|
+
- Auth modes: `packages/core/src/intake/github-auth-modes.ts` (#1557)
|
|
78
|
+
- Operator docs: `content/scm/github.md` § Mismatched/headless SCM readiness
|
|
@@ -142,9 +142,9 @@ When OpenClaw signals are present (`OPENCLAW` / `DEFT_PROBE_OPENCLAW` / `DEFT_AG
|
|
|
142
142
|
deft doctor --fix
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
Doctor
|
|
145
|
+
Doctor **copies** the four pin directories from the installed content package (`@deftai/directive-content` / `content/skills/…`) into the main OpenClaw skills root as **real directories** (#3008). It does **not** symlink into the npm tree — OpenClaw 2026.7.x skips workspace skills that resolve outside the skills root (`reason=symlink-escape`), which made pre-#3008 symlink installs look healthy in doctor while `openclaw skills list` never loaded the pins. It does **not** delete other user skills (e.g. a local `vbrief` skill stays). Divergent same-named directories **and escaping symlinks** are left alone unless you pass `--force` or confirm on a TTY (then replaced with a real copy).
|
|
146
146
|
|
|
147
|
-
After a successful wire: **restart the OpenClaw gateway or start a new session** so host `available_skills` refreshes.
|
|
147
|
+
After a successful wire: **restart the OpenClaw gateway or start a new session** so host `available_skills` refreshes. Confirm with `openclaw skills list` that the four always-pins are **ready**, not skipped.
|
|
148
148
|
|
|
149
149
|
### Multi-seat / crew workspaces
|
|
150
150
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-content",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.0",
|
|
4
4
|
"description": "Shippable Directive framework content in the consumer .deft/core/ layout (C1 flatten), plus the engine surfaces (.githooks/, Taskfile.yml, tasks/) the deposit wires. Python-free per #2022 Phase 3. Refs #11, #1669, #1967.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -226,7 +226,7 @@
|
|
|
226
226
|
"swarm"
|
|
227
227
|
],
|
|
228
228
|
"source": "PR #697 -- issue #642 (a tracking umbrella for PR #401) was auto-closed on squash merge despite the PR body intentionally avoiding closing keywords. Recurrence record extended in PR #735 (#737 deterministic encoding gap-closer); see the third bullet below.",
|
|
229
|
-
"body": "**Source:** PR #697 -- issue #642 (a tracking umbrella for PR #401) was auto-closed on squash merge despite the PR body intentionally avoiding closing keywords. Recurrence record extended in PR #735 (#737 deterministic encoding gap-closer); see the third bullet below.\n\n**1. GitHub's closing-keyword parser is substring-based -- the literal token `Closes #N` MUST NOT appear in the PR body even inside a negation, quotation, or example**\n\nThe PR body for #697 contained the parenthetical `` (Intentionally not `Closes #642` -- #642 is a tracking umbrella that should remain open until PR #401's full scope is merged or split into linked follow-up issues.) ``. The text was negating the closing keyword in plain English, but GitHub's auto-close parser operates on token presence, not surrounding semantics. The squash merge processed the literal `Closes #642` substring and closed the issue. The same hazard applies to `Fixes #N`, `Resolves #N`, and the past-tense / lowercase variants (`closed`, `fixed`, `resolved`, etc. -- see GitHub's full keyword list).\n\n**Rule:** When a PR body needs to discuss what it intentionally does NOT close (e.g. tracking umbrellas, partial scope, deferred follow-ups), MUST phrase the disclaimer without using any closing keyword token at all. Use phrasings like:\n\n- \"Intentionally NOT using a closing keyword for #N\" (omits the trigger token entirely)\n- \"Refs #N (tracking umbrella; remains open)\" (use `Refs` only)\n- \"#N stays open as the umbrella anchor for follow-up work\" (no trigger token)\n\n\u2297 MUST NOT write `Closes #N`, `Fixes #N`, `Resolves #N`, or any GitHub closing keyword in a PR body even when negating, quoting, or showing as an example -- the parser does not respect surrounding context.\n\n**2. Post-merge issue-state verification MUST run for every squash merge regardless of intent**\n\nThe existing post-merge verification rule (Lesson: PR Merge Hygiene #1, #167) was framed around closing-keyword failures (issues that should have closed but didn't). The opposite failure mode -- issues that should have stayed open but were auto-closed -- is just as real and is caught by the same check. After every squash merge, MUST verify the state of every issue mentioned in the PR body matches intent: closed if a closing keyword was used; open if only `Refs` was used. If an issue was closed in error, reopen it with a comment referencing the PR and explaining why it should remain open (e.g. tracking umbrella, partial scope). (#697)\n\n**3. Recurrence (#735): the squash-commit body for that PR contained a negation-context clause referencing #734 in a way that auto-closed it on merge despite intent (the issue was the parent for the in-flight #737 work and had to be reopened manually). #737 closes this gap structurally with a deterministic pre-PR lint (`scripts/pr_check_closing_keywords.py`, surfaced via `task pr:check-closing-keywords`) that scans both the PR body AND every commit message for closing-keyword tokens followed by `#\\d+` in negation / quotation / example / code-block contexts and refuses to push when findings surface. The lint is wired into `skills/deft-directive-pre-pr/SKILL.md` Phase 4 (Diff) and cross-referenced from `skills/deft-directive-swarm/SKILL.md` Phase 6 Step 1 as the Layer 0 (prevention) surface alongside the existing Layer 3 (recovery) `task pr:check-protected-issues` (#701).**\n\n**Cross-reference:** existing lesson \"PR Merge Hygiene\" #1 (#167); `scm/github.md` PR conventions; `skills/deft-directive-review-cycle/SKILL.md` Post-Merge Verification; `skills/deft-directive-pre-pr/SKILL.md` Phase 4 (Layer 0 prevention, #737); `skills/deft-directive-swarm/SKILL.md` Phase 6 Step 1 (Layer 3 recovery, #701)."
|
|
229
|
+
"body": "**Source:** PR #697 -- issue #642 (a tracking umbrella for PR #401) was auto-closed on squash merge despite the PR body intentionally avoiding closing keywords. Recurrence record extended in PR #735 (#737 deterministic encoding gap-closer); see the third bullet below.\n\n**1. GitHub's closing-keyword parser is substring-based -- the literal token `Closes #N` MUST NOT appear in the PR body even inside a negation, quotation, or example**\n\nThe PR body for #697 contained the parenthetical `` (Intentionally not `Closes #642` -- #642 is a tracking umbrella that should remain open until PR #401's full scope is merged or split into linked follow-up issues.) ``. The text was negating the closing keyword in plain English, but GitHub's auto-close parser operates on token presence, not surrounding semantics. The squash merge processed the literal `Closes #642` substring and closed the issue. The same hazard applies to `Fixes #N`, `Resolves #N`, and the past-tense / lowercase variants (`closed`, `fixed`, `resolved`, etc. -- see GitHub's full keyword list).\n\n**Rule:** When a PR body needs to discuss what it intentionally does NOT close (e.g. tracking umbrellas, partial scope, deferred follow-ups), MUST phrase the disclaimer without using any closing keyword token at all. Use phrasings like:\n\n- \"Intentionally NOT using a closing keyword for #N\" (omits the trigger token entirely)\n- \"Refs #N (tracking umbrella; remains open)\" (use `Refs` only)\n- \"#N stays open as the umbrella anchor for follow-up work\" (no trigger token)\n\n\u2297 MUST NOT write `Closes #N`, `Fixes #N`, `Resolves #N`, or any GitHub closing keyword in a PR body even when negating, quoting, or showing as an example -- the parser does not respect surrounding context.\n\n**2. Post-merge issue-state verification MUST run for every squash merge regardless of intent**\n\nThe existing post-merge verification rule (Lesson: PR Merge Hygiene #1, #167) was framed around closing-keyword failures (issues that should have closed but didn't). The opposite failure mode -- issues that should have stayed open but were auto-closed -- is just as real and is caught by the same check. After every squash merge, MUST verify the state of every issue mentioned in the PR body matches intent: closed if a closing keyword was used; open if only `Refs` was used. If an issue was closed in error, reopen it with a comment referencing the PR and explaining why it should remain open (e.g. tracking umbrella, partial scope). (#697)\n\n**3. Recurrence (#735): the squash-commit body for that PR contained a negation-context clause referencing #734 in a way that auto-closed it on merge despite intent (the issue was the parent for the in-flight #737 work and had to be reopened manually). #737 closes this gap structurally with a deterministic pre-PR lint (`scripts/pr_check_closing_keywords.py`, surfaced via `task pr:check-closing-keywords`) that scans both the PR body AND every commit message for closing-keyword tokens followed by `#\\d+` in negation / quotation / example / code-block contexts and refuses to push when findings surface. The lint is wired into `skills/deft-directive-pre-pr/SKILL.md` Phase 4 (Diff) and cross-referenced from `skills/deft-directive-swarm/SKILL.md` Phase 6 Step 1 as the Layer 0 (prevention) surface alongside the existing Layer 3 (recovery) `task pr:check-protected-issues` (#701).**\n\n**Cross-reference:** existing lesson \"PR Merge Hygiene\" #1 (#167); `scm/github.md` PR conventions; `skills/deft-directive-review-cycle/SKILL.md` Post-Merge Verification; `skills/deft-directive-pre-pr/SKILL.md` Phase 4 (Layer 0 prevention, #737); `skills/deft-directive-swarm/SKILL.md` Phase 6 Step 1 (Layer 3 recovery, #701).\n\n**4. Class D (#3015): premature *real* `Closes #N` with conditional / multi-phase English is a different failure than class A negation FP**\n\nenterprize PR #30 body contained `Closes #29 Phase A intake only if you want intake closed\u2026`. Layer 0 FP mode (#737) exited 0 (no negation/quote/example/code context). GitHub still matched `Closes #29` and completed multi-phase issue #29 while Phase B/C remained undone.\n\n- Class A = keyword in negation/quote/example/code (FP) \u2014 #737\n- Class D = bare or conditional real closing keyword without full DoD \u2014 #3015 intent mode\n\n**Rule:** Default PR body uses `Tracking: #N` / `Related: #N` / `Refs #N`. Use `Closes`/`Fixes`/`Resolves #N` only at full issue DoD, and pass `--allow-close N` on the lint (CLI allowlist only). \u2297 `Closes #N Phase A` / `only if` / partial English around a real closing keyword.\n\n`task pr:check-closing-keywords` default mode is `both` (FP + intent). `--mode fp` is the class-A-only regression path."
|
|
230
230
|
},
|
|
231
231
|
{
|
|
232
232
|
"id": "github-closing-keyword-false-positive-layer-3-persistent-closingissuesreferences-link-2026-04",
|
|
@@ -13,6 +13,18 @@
|
|
|
13
13
|
"path": "patterns/executor-layer-credentials.md",
|
|
14
14
|
"body": null
|
|
15
15
|
},
|
|
16
|
+
{
|
|
17
|
+
"id": "in-band-signaling",
|
|
18
|
+
"title": "No in-band signaling / absence is not a decision (#1695)",
|
|
19
|
+
"description": "Do not overload one field (or its presence/absence) to carry two orthogonal facts. Separate value from decision-provenance. Load when modeling config/policy fields or onboarding/decision/lifecycle state.",
|
|
20
|
+
"triggers": [
|
|
21
|
+
"in-band-signaling",
|
|
22
|
+
"decision-provenance",
|
|
23
|
+
"absence-is-not-a-decision"
|
|
24
|
+
],
|
|
25
|
+
"path": "patterns/in-band-signaling.md",
|
|
26
|
+
"body": null
|
|
27
|
+
},
|
|
16
28
|
{
|
|
17
29
|
"id": "llm-app",
|
|
18
30
|
"title": "LLM application standards (#481)",
|
|
@@ -31,7 +43,7 @@
|
|
|
31
43
|
"multi-agent"
|
|
32
44
|
],
|
|
33
45
|
"path": "patterns/multi-agent.md",
|
|
34
|
-
"body": "# Multi-agent identity separation pattern (#983)\n\n> Status: stable pattern, v1 docs-only. v1 deliberately scopes to a single\n> shared bot/App credential for all workers; per-worker token rotation is\n> deferred to a follow-up. Per #983 non-goals.\n\nThis pattern lays out *whose* GitHub identity dispatched swarm workers\nshould authenticate as, *why* that should not be the maintainer's PAT,\nand *how* operators provision and rotate the worker credential.\n\nIt is the credential-separation track that was carved out of #976\n(`fix(scm,tests): eliminate remaining GraphQL-backed scm.py / live SCM\nsmoke bucket drain after #961`). #976 owns the REST migration; this\npattern owns the orthogonal identity separation. The two tracks are\ncomplementary: REST migration reduces GraphQL demand on the shared\nbucket, identity separation prevents human and machine traffic from\nsharing one bucket in the first place.\n\n## Why this matters\n\nToday, swarm workers and the maintainer share a single GitHub Personal\nAccess Token (PAT). One PAT serves the maintainer's interactive review\nflow AND every worker spawned by `task swarm:*` / `start_agent` /\n`oz agent run`. The operational consequences observed in the\n2026-05-07 multi-agent session and again on 2026-05-08 are concrete:\n\n- **Bucket coupling**: GitHub's GraphQL bucket is **5,000 req/hr per\n identity**. A swarm of N workers + the maintainer's terminal share\n one bucket. When workers exhaust it (the documented failure mode for\n the PR #652 / #561-class merge cascades), the maintainer's\n `gh issue create` and `gh pr view` start failing with\n `GraphQL: API rate limit already exceeded` -- the human is rate-\n limited by their own swarm.\n- **Audit conflation**: every commit, comment, PR open, issue close,\n and review submission attributes to the same `actor.login`. Forensic\n separation (\"which action was the maintainer vs. an autonomous\n worker?\") requires correlating timestamps and dispatch-envelope\n prompts rather than reading the GitHub event stream directly.\n- **Credential blast radius**: a worker's prompt envelope is by\n definition reviewable / loggable / inspectable -- prompts leak more\n readily than secrets. Today a leaked worker prompt acts with the\n full scope of the maintainer's PAT (issue close, PR merge, release\n publish, branch protection bypass), not the narrow scope a worker\n actually needs (issues:write, pulls:write, contents:read).\n\nReducing GraphQL demand (#976 REST migration) only solves bucket\n*pressure*. It does not solve bucket *coupling*, audit conflation, or\nblast radius. Those require partitioning the identity itself.\n\n## The pattern\n\n### Identity model\n\n- ! Workers MUST authenticate as a **distinct GitHub identity** from\n the maintainer. Two acceptable shapes:\n 1. **GitHub App installation token** (preferred). The deft project\n installs a GitHub App granting the minimum scopes; workers\n consume an installation token derived from the App's private key.\n Tokens are short-lived (~1 hour) and auto-rotated by GitHub.\n 2. **Bot account PAT**. A dedicated GitHub user account (e.g.\n `deft-swarm-bot`) with its own PAT. Lower setup cost, but tokens\n are long-lived until manually rotated, and the bot account\n consumes a real GitHub seat.\n- ! Maintainer PATs MUST be reserved for human-driven work: review,\n merge, release publication, manual triage. Worker dispatch envelopes\n MUST NOT inject a maintainer PAT.\n- \u2297 Workers MUST NOT fall back to the host's `gh auth status` token.\n The dispatch envelope is the contract; an implicit fallback re-\n introduces the coupling this pattern eliminates.\n\n### Bucket partitioning model\n\n| Bucket | Owner | Use |\n|----------------------|----------------------|----------------------------------------------------|\n| Maintainer GraphQL | Maintainer PAT | Human review, PR open/merge/ready, manual triage |\n| Maintainer core REST | Maintainer PAT | `gh api` reads outside swarm context |\n| Worker GraphQL | Bot account / App | `markPullRequestReadyForReview`, `addPullRequestReview` (the only GraphQL surfaces a worker should hit; see #976 + #961) |\n| Worker core REST | Bot account / App | All other reads / mutations via `gh_rest.py` REST helpers |\n\nEach bucket is 5,000 req/hr (REST `core` and GraphQL each). With\nidentity separation the maintainer's two buckets are untouched by\nswarm activity, and N workers share *their own* two buckets.\n\nThe bucket partition only delivers value when paired with the #976\nREST migration: workers that still route reads through GraphQL\n(`gh issue view --json`, `gh pr view --json`, `gh pr ready`) will\nexhaust their own GraphQL bucket within minutes under N concurrent\npoll loops. REST-by-default per `templates/agent-prompt-preamble.md`\nS5 keeps the worker GraphQL bucket reserved for the unavoidable PR\nstate-mutation operations.\n\n### Dispatch-envelope credential rule\n\nThe canonical preamble at `templates/agent-prompt-preamble.md` carries\nthis rule verbatim alongside the existing REST-default,\nmax-1-Draft-toggle, and rate-limit-throttle rules:\n\n> Workers MUST consume the GitHub credential injected by the dispatcher\n> (typically `GH_TOKEN` in the prompt-supplied env). Workers MUST NOT\n> fall back to the host's `gh auth status` token. If `GH_TOKEN` is\n> unset and no other dispatcher-supplied credential is present, the\n> worker MUST fail loud with a clear error rather than silently\n> running under the host identity.\n\nThe rule is enforced at two points:\n\n1. **Dispatch time** (orchestrator side). The orchestrator (swarm\n skill, monitor agent, scheduled run) injects the worker\n credential into the dispatch envelope -- usually as `GH_TOKEN` in\n the env, with the maintainer's `gh auth` state untouched. Workers\n inherit this env at spawn time.\n2. **Worker side**. The worker's first action (after AGENTS.md read)\n verifies `GH_TOKEN` is set and matches the expected bot/App\n identity (e.g. `gh api user --jq .login` returns the bot login).\n Mismatch is a hard refusal: stop the tool loop, send `BLOCKED:\n identity mismatch` to the parent.\n\nThe two-sided enforcement means a missing/malformed credential surfaces\nat the earliest possible point rather than mid-dispatch when the worker\nhas already started writing files.\n\n### Permission scoping\n\n! The worker credential MUST be scoped down to the minimum needed:\n\n- `issues:write` -- open / close / comment / label\n- `pulls:write` -- open / merge / review / comment\n- `contents:read` -- read repo contents (for diff context)\n- `metadata:read` -- standard for any GitHub App\n- \u2297 `contents:write` -- workers MUST NOT push commits directly under\n the worker identity. Pushes happen via the maintainer (interactive\n review of the worker's diff) or via the GitHub App's restricted\n contents:write scope on a specific bot-owned ref pattern.\n- \u2297 `admin:*` / `repo` (broad) -- workers MUST NOT have admin scopes.\n Branch protection bypass, label/milestone management, secret\n rotation -- all maintainer-only.\n\nThe maintainer PAT keeps the broader scope it has today (`repo`,\n`workflow`, etc.). The narrowness of the worker scope is half the\nblast-radius mitigation; the other half is short-lived credentials\n(GitHub App installation tokens auto-rotate ~1 hour; bot PATs MUST be\nrotated quarterly per the runbook below).\n\n## Operator runbook\n\n### Provisioning a worker identity\n\n#### Option A: GitHub App (preferred)\n\n1. Create a new GitHub App at\n `https://github.com/organizations/<org>/settings/apps/new`\n (account-level path: `https://github.com/settings/apps/new`).\n2. Grant the **minimum** permissions enumerated in\n \"Permission scoping\" above. Decline every other permission.\n3. Subscribe to no events (workers consume the API; they do not need\n webhook delivery).\n4. Generate and download a private key (`.pem`). Store it in\n `secrets/swarm-app.pem` outside the repo (the deft `secrets/`\n convention is `.gitignore`d by default per the repo conventions).\n5. Install the App on the deft repository.\n6. Note the App ID and Installation ID; both are required to mint\n installation tokens at dispatch time.\n7. The dispatcher mints an installation token via\n `POST /app/installations/<installation-id>/access_tokens` (the\n private-key-signed JWT flow). Each minted token lives ~1 hour;\n spawn-burst refreshes are cheap.\n\n#### Option B: bot account PAT\n\n1. Create a new GitHub user account (e.g. `deft-swarm-bot`). Email\n address can be a `+swarmbot` alias on the maintainer's account.\n2. Add the bot account as a repository collaborator with **Write**\n access (NOT Maintain or Admin).\n3. Generate a fine-grained PAT under the bot account with the scopes\n enumerated in \"Permission scoping\". Set expiry to 90 days.\n4. Store the PAT in `secrets/swarm-bot.env` as\n `SWARM_GH_TOKEN=<value>`. Reference from dispatcher via\n `Get-Content secrets/swarm-bot.env` or equivalent at dispatch\n time; never paste inline.\n\n### Wiring the dispatcher\n\nThe orchestrator (swarm skill, monitor agent, `start_agent`/`oz`\ninvocation, scheduled run) loads the worker credential and injects it\ninto the worker's env:\n\n```pwsh path=null start=null\n# Option A (GitHub App): mint installation token at dispatch time.\n$jwt = uv run python scripts/swarm_mint_jwt.py --app-id $env:DEFT_SWARM_APP_ID --pem secrets/swarm-app.pem\n$inst_token = (gh api -X POST \"/app/installations/$env:DEFT_SWARM_INSTALLATION_ID/access_tokens\" -H \"Authorization: Bearer $jwt\" --jq .token)\nstart_agent ... -e GH_TOKEN=$inst_token\n\n# Option B (bot PAT): inject from secrets/swarm-bot.env.\n$bot_token = (Get-Content secrets/swarm-bot.env | Where-Object { $_ -like 'SWARM_GH_TOKEN=*' }) -replace '^SWARM_GH_TOKEN=', ''\nstart_agent ... -e GH_TOKEN=$bot_token\n```\n\nToken-mint plumbing (`scripts/swarm_mint_jwt.py`) is intentionally not\nshipped in v1 -- the runbook above is operator-facing guidance, not\ndeft-supplied automation. v1 is documentation-only per #983 non-goals.\nOperators MAY automate token minting in their own dispatcher; the\ncontract this pattern pins is the env-var injection.\n\n### Rotation and recovery\n\n#### Routine rotation\n\n- **GitHub App tokens**: auto-rotate ~1 hour. No operator action\n needed unless the App's private key is compromised.\n- **Bot PAT**: quarterly rotation. Update `secrets/swarm-bot.env`\n and revoke the prior PAT. Verify the new token via\n `gh api user --jq .login` returns the bot login.\n\n#### Compromise recovery (leaked worker token)\n\n1. Immediately revoke the leaked credential:\n - GitHub App: rotate the private key (Settings -> Apps -> Your\n deft-swarm App -> Generate a new private key, then delete the\n compromised key).\n - Bot PAT: revoke at\n `https://github.com/settings/tokens` under the bot account.\n2. Audit the bot's recent activity: `gh api\n /users/<bot-login>/events/public --jq\n '.[] | {repo, type, created_at}'`. Investigate any action outside\n the expected swarm workflow.\n3. Provision a replacement credential per the provisioning steps\n above and update the dispatcher.\n4. File an incident vBRIEF in `vbrief/proposed/` documenting the\n compromise, scope of activity, and remediation. The vBRIEF\n becomes the audit artifact.\n\nThe maintainer PAT MUST NOT be used as a fallback during the\ncompromise window -- doing so re-couples the buckets and re-introduces\nthe blast-radius problem this pattern solves.\n\n## Cross-references\n\n- #976 -- remaining GraphQL drain in `scripts/scm.py` + smoke (the\n REST-migration track this pattern complements; this pattern was\n carved out of #976's \"Complementary mitigation\" section)\n- #588 -- agent identity pattern (distinct attestable identity per\n agent; the conceptual frame this pattern operationalises)\n- #806 -- executor-layer-credentials (secrets bound at invocation\n layer, not in agent context; the architectural pattern this\n pattern lands)\n- #585 -- credential hygiene standard for cloud agent dispatch\n (overlapping with v1, but #585 covers the broader cloud-agent\n surface; this pattern scopes specifically to swarm workers)\n- #519 -- rate limiting as a first-class architectural constraint\n for multi-agent guidance (the \"shared bucket is the bottleneck\"\n framing that motivates partitioning by identity)\n- #520 -- multi-provider routing as the production solution for\n large-scale swarms (the LLM-side analogue of this pattern -- LLM\n providers also rate-limit per identity, and the same partitioning\n argument applies upstream)\n- #954 / #966 -- canonical orchestrator preamble; the\n `templates/agent-prompt-preamble.md` rule enforcing the\n dispatch-envelope credential contract is part of #954's preamble\n surface and is updated by this issue (#983)\n- AGENTS.md `## Multi-agent orchestration discipline (#954)` -- the\n REST-default / no-Draft-retoggle / rate-limit-throttle rules this\n pattern complements\n\n## Non-goals (per #983)\n\n- Per-worker token rotation infrastructure -- a single shared\n bot/App credential for all workers is acceptable in v1; per-worker\n identities are deferred to a follow-up.\n- Cross-provider identity (Anthropic, OpenAI, etc.) -- this pattern\n is GitHub-specific; the LLM-side analogue is #520.\n- Replacing the SCM/REST migration -- #976 owns that track; this\n pattern is complementary, not a substitute.\n"
|
|
46
|
+
"body": "# Multi-agent identity separation pattern (#983)\n\n> Status: stable pattern, v1 docs-only. v1 deliberately scopes to a single\n> shared bot/App credential for all workers; per-worker token rotation is\n> deferred to a follow-up. Per #983 non-goals.\n\nThis pattern lays out *whose* GitHub identity dispatched swarm workers\nshould authenticate as, *why* that should not be the maintainer's PAT,\nand *how* operators provision and rotate the worker credential.\n\nIt is the credential-separation track that was carved out of #976\n(`fix(scm,tests): eliminate remaining GraphQL-backed scm.py / live SCM\nsmoke bucket drain after #961`). #976 owns the REST migration; this\npattern owns the orthogonal identity separation. The two tracks are\ncomplementary: REST migration reduces GraphQL demand on the shared\nbucket, identity separation prevents human and machine traffic from\nsharing one bucket in the first place.\n\n## Why this matters\n\nToday, swarm workers and the maintainer share a single GitHub Personal\nAccess Token (PAT). One PAT serves the maintainer's interactive review\nflow AND every worker spawned by `task swarm:*` / `start_agent` /\n`oz agent run`. The operational consequences observed in the\n2026-05-07 multi-agent session and again on 2026-05-08 are concrete:\n\n- **Bucket coupling**: GitHub's GraphQL bucket is **5,000 req/hr per\n identity**. A swarm of N workers + the maintainer's terminal share\n one bucket. When workers exhaust it (the documented failure mode for\n the PR #652 / #561-class merge cascades), the maintainer's\n `gh issue create` and `gh pr view` start failing with\n `GraphQL: API rate limit already exceeded` -- the human is rate-\n limited by their own swarm.\n- **Audit conflation**: every commit, comment, PR open, issue close,\n and review submission attributes to the same `actor.login`. Forensic\n separation (\"which action was the maintainer vs. an autonomous\n worker?\") requires correlating timestamps and dispatch-envelope\n prompts rather than reading the GitHub event stream directly.\n- **Credential blast radius**: a worker's prompt envelope is by\n definition reviewable / loggable / inspectable -- prompts leak more\n readily than secrets. Today a leaked worker prompt acts with the\n full scope of the maintainer's PAT (issue close, PR merge, release\n publish, branch protection bypass), not the narrow scope a worker\n actually needs (issues:write, pulls:write, contents:read).\n\nReducing GraphQL demand (#976 REST migration) only solves bucket\n*pressure*. It does not solve bucket *coupling*, audit conflation, or\nblast radius. Those require partitioning the identity itself.\n\n## The pattern\n\n### Identity model\n\n- ! Workers MUST authenticate as a **distinct GitHub identity** from\n the maintainer. Two acceptable shapes:\n 1. **GitHub App installation token** (preferred). The deft project\n installs a GitHub App granting the minimum scopes; workers\n consume an installation token derived from the App's private key.\n Tokens are short-lived (~1 hour) and auto-rotated by GitHub.\n 2. **Bot account PAT**. A dedicated GitHub user account (e.g.\n `deft-swarm-bot`) with its own PAT. Lower setup cost, but tokens\n are long-lived until manually rotated, and the bot account\n consumes a real GitHub seat.\n- ! Maintainer PATs MUST be reserved for human-driven work: review,\n merge, release publication, manual triage. Worker dispatch envelopes\n MUST NOT inject a maintainer PAT.\n- ⊗ Workers MUST NOT fall back to the host's `gh auth status` token.\n The dispatch envelope is the contract; an implicit fallback re-\n introduces the coupling this pattern eliminates.\n\n### Bucket partitioning model\n\n| Bucket | Owner | Use |\n|----------------------|----------------------|----------------------------------------------------|\n| Maintainer GraphQL | Maintainer PAT | Human review, PR open/merge/ready, manual triage |\n| Maintainer core REST | Maintainer PAT | `gh api` reads outside swarm context |\n| Worker GraphQL | Bot account / App | `markPullRequestReadyForReview`, `addPullRequestReview` (the only GraphQL surfaces a worker should hit; see #976 + #961) |\n| Worker core REST | Bot account / App | All other reads / mutations via `gh_rest.py` REST helpers |\n\nEach bucket is 5,000 req/hr (REST `core` and GraphQL each). With\nidentity separation the maintainer's two buckets are untouched by\nswarm activity, and N workers share *their own* two buckets.\n\nThe bucket partition only delivers value when paired with the #976\nREST migration: workers that still route reads through GraphQL\n(`gh issue view --json`, `gh pr view --json`, `gh pr ready`) will\nexhaust their own GraphQL bucket within minutes under N concurrent\npoll loops. REST-by-default per `templates/agent-prompt-preamble.md`\nS5 keeps the worker GraphQL bucket reserved for the unavoidable PR\nstate-mutation operations.\n\n### Dispatch-envelope credential rule\n\nThe canonical preamble at `templates/agent-prompt-preamble.md` carries\nthis rule verbatim alongside the existing REST-default,\nmax-1-Draft-toggle, and rate-limit-throttle rules:\n\n> Workers MUST consume the GitHub credential injected by the dispatcher\n> (typically `GH_TOKEN` in the prompt-supplied env). Workers MUST NOT\n> fall back to the host's `gh auth status` token. If `GH_TOKEN` is\n> unset and no other dispatcher-supplied credential is present, the\n> worker MUST fail loud with a clear error rather than silently\n> running under the host identity.\n\nThe rule is enforced at two points:\n\n1. **Dispatch time** (orchestrator side). The orchestrator (swarm\n skill, monitor agent, scheduled run) injects the worker\n credential into the dispatch envelope -- usually as `GH_TOKEN` in\n the env, with the maintainer's `gh auth` state untouched. Workers\n inherit this env at spawn time.\n2. **Worker side**. The worker's first action (after AGENTS.md read)\n verifies `GH_TOKEN` is set and matches the expected bot/App\n identity (e.g. `gh api user --jq .login` returns the bot login).\n Mismatch is a hard refusal: stop the tool loop, send `BLOCKED:\n identity mismatch` to the parent.\n\nThe two-sided enforcement means a missing/malformed credential surfaces\nat the earliest possible point rather than mid-dispatch when the worker\nhas already started writing files.\n\n### Permission scoping\n\n! The worker credential MUST be scoped down to the minimum needed:\n\n- `issues:write` -- open / close / comment / label\n- `pulls:write` -- open / merge / review / comment\n- `contents:read` -- read repo contents (for diff context)\n- `metadata:read` -- standard for any GitHub App\n- ⊗ `contents:write` -- workers MUST NOT push commits directly under\n the worker identity. Pushes happen via the maintainer (interactive\n review of the worker's diff) or via the GitHub App's restricted\n contents:write scope on a specific bot-owned ref pattern.\n- ⊗ `admin:*` / `repo` (broad) -- workers MUST NOT have admin scopes.\n Branch protection bypass, label/milestone management, secret\n rotation -- all maintainer-only.\n\nThe maintainer PAT keeps the broader scope it has today (`repo`,\n`workflow`, etc.). The narrowness of the worker scope is half the\nblast-radius mitigation; the other half is short-lived credentials\n(GitHub App installation tokens auto-rotate ~1 hour; bot PATs MUST be\nrotated quarterly per the runbook below).\n\n## Operator runbook\n\n### Provisioning a worker identity\n\n#### Option A: GitHub App (preferred)\n\n1. Create a new GitHub App at\n `https://github.com/organizations/<org>/settings/apps/new`\n (account-level path: `https://github.com/settings/apps/new`).\n2. Grant the **minimum** permissions enumerated in\n \"Permission scoping\" above. Decline every other permission.\n3. Subscribe to no events (workers consume the API; they do not need\n webhook delivery).\n4. Generate and download a private key (`.pem`). Store it in\n `secrets/swarm-app.pem` outside the repo (the deft `secrets/`\n convention is `.gitignore`d by default per the repo conventions).\n5. Install the App on the deft repository.\n6. Note the App ID and Installation ID; both are required to mint\n installation tokens at dispatch time.\n7. The dispatcher mints an installation token via\n `POST /app/installations/<installation-id>/access_tokens` (the\n private-key-signed JWT flow). Each minted token lives ~1 hour;\n spawn-burst refreshes are cheap.\n\n#### Option B: bot account PAT\n\n1. Create a new GitHub user account (e.g. `deft-swarm-bot`). Email\n address can be a `+swarmbot` alias on the maintainer's account.\n2. Add the bot account as a repository collaborator with **Write**\n access (NOT Maintain or Admin).\n3. Generate a fine-grained PAT under the bot account with the scopes\n enumerated in \"Permission scoping\". Set expiry to 90 days.\n4. Store the PAT in `secrets/swarm-bot.env` as\n `SWARM_GH_TOKEN=<value>`. Reference from dispatcher via\n `Get-Content secrets/swarm-bot.env` or equivalent at dispatch\n time; never paste inline.\n\n### Wiring the dispatcher\n\nThe orchestrator (swarm skill, monitor agent, `start_agent`/`oz`\ninvocation, scheduled run) loads the worker credential and injects it\ninto the worker's env:\n\n```pwsh path=null start=null\n# Option A (GitHub App): mint installation token at dispatch time.\n$jwt = uv run python scripts/swarm_mint_jwt.py --app-id $env:DEFT_SWARM_APP_ID --pem secrets/swarm-app.pem\n$inst_token = (gh api -X POST \"/app/installations/$env:DEFT_SWARM_INSTALLATION_ID/access_tokens\" -H \"Authorization: Bearer $jwt\" --jq .token)\nstart_agent ... -e GH_TOKEN=$inst_token\n\n# Option B (bot PAT): inject from secrets/swarm-bot.env.\n$bot_token = (Get-Content secrets/swarm-bot.env | Where-Object { $_ -like 'SWARM_GH_TOKEN=*' }) -replace '^SWARM_GH_TOKEN=', ''\nstart_agent ... -e GH_TOKEN=$bot_token\n```\n\nToken-mint plumbing (`scripts/swarm_mint_jwt.py`) is intentionally not\nshipped in v1 -- the runbook above is operator-facing guidance, not\ndeft-supplied automation. v1 is documentation-only per #983 non-goals.\nOperators MAY automate token minting in their own dispatcher; the\ncontract this pattern pins is the env-var injection.\n\n### Rotation and recovery\n\n#### Routine rotation\n\n- **GitHub App tokens**: auto-rotate ~1 hour. No operator action\n needed unless the App's private key is compromised.\n- **Bot PAT**: quarterly rotation. Update `secrets/swarm-bot.env`\n and revoke the prior PAT. Verify the new token via\n `gh api user --jq .login` returns the bot login.\n\n#### Compromise recovery (leaked worker token)\n\n1. Immediately revoke the leaked credential:\n - GitHub App: rotate the private key (Settings -> Apps -> Your\n deft-swarm App -> Generate a new private key, then delete the\n compromised key).\n - Bot PAT: revoke at\n `https://github.com/settings/tokens` under the bot account.\n2. Audit the bot's recent activity: `gh api\n /users/<bot-login>/events/public --jq\n '.[] | {repo, type, created_at}'`. Investigate any action outside\n the expected swarm workflow.\n3. Provision a replacement credential per the provisioning steps\n above and update the dispatcher.\n4. File an incident vBRIEF in `vbrief/proposed/` documenting the\n compromise, scope of activity, and remediation. The vBRIEF\n becomes the audit artifact.\n\nThe maintainer PAT MUST NOT be used as a fallback during the\ncompromise window -- doing so re-couples the buckets and re-introduces\nthe blast-radius problem this pattern solves.\n\n## Cross-references\n\n- #976 -- remaining GraphQL drain in `scripts/scm.py` + smoke (the\n REST-migration track this pattern complements; this pattern was\n carved out of #976's \"Complementary mitigation\" section)\n- #588 -- agent identity pattern (distinct attestable identity per\n agent; the conceptual frame this pattern operationalises)\n- #806 -- executor-layer-credentials (secrets bound at invocation\n layer, not in agent context; the architectural pattern this\n pattern lands)\n- #585 -- credential hygiene standard for cloud agent dispatch\n (overlapping with v1, but #585 covers the broader cloud-agent\n surface; this pattern scopes specifically to swarm workers)\n- #519 -- rate limiting as a first-class architectural constraint\n for multi-agent guidance (the \"shared bucket is the bottleneck\"\n framing that motivates partitioning by identity)\n- #520 -- multi-provider routing as the production solution for\n large-scale swarms (the LLM-side analogue of this pattern -- LLM\n providers also rate-limit per identity, and the same partitioning\n argument applies upstream)\n- #954 / #966 -- canonical orchestrator preamble; the\n `templates/agent-prompt-preamble.md` rule enforcing the\n dispatch-envelope credential contract is part of #954's preamble\n surface and is updated by this issue (#983)\n- AGENTS.md `## Multi-agent orchestration discipline (#954)` -- the\n REST-default / no-Draft-retoggle / rate-limit-throttle rules this\n pattern complements\n\n## Non-goals (per #983)\n\n- Per-worker token rotation infrastructure -- a single shared\n bot/App credential for all workers is acceptable in v1; per-worker\n identities are deferred to a follow-up.\n- Cross-provider identity (Anthropic, OpenAI, etc.) -- this pattern\n is GitHub-specific; the LLM-side analogue is #520.\n- Replacing the SCM/REST migration -- #976 owns that track; this\n pattern is complementary, not a substitute.\n"
|
|
35
47
|
},
|
|
36
48
|
{
|
|
37
49
|
"id": "prompt-assembly-layer-ordering",
|