@hegemonart/get-design-done 1.39.5 → 1.40.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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Get Design Done — 5-stage agent-orchestrated design pipeline with 9 connections, handoff-first workflow, bidirectional Figma write-back, 22+ specialized agents, queryable knowledge layer (intel store, dependency analysis, learnings extraction), and a self-improvement loop (reflector, frontmatter + budget feedback, global-skills layer). v1.20.0 ships the SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream, and resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) for rate-limit + 429 + context-overflow recovery. Full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation (auto-tag + GitHub Release + release-time smoke test).",
8
- "version": "1.39.5"
8
+ "version": "1.40.0"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "get-design-done",
13
13
  "source": "./",
14
14
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), Claude Design handoff, bidirectional Figma write-back, and a queryable intel store (.design/intel/) for dependency and learnings queries. Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation. Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
15
- "version": "1.39.5",
15
+ "version": "1.40.0",
16
16
  "author": {
17
17
  "name": "hegemonart"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "get-design-done",
3
3
  "short_name": "gdd",
4
- "version": "1.39.5",
4
+ "version": "1.40.0",
5
5
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), handoff-first workflow via Claude Design bundles, bidirectional Figma write-back (annotations, Code Connect), queryable intel store (`.design/intel/`) for O(1) design surface lookups, and self-improvement loop (reflector agent, frontmatter + budget feedback, global-skills layer at `~/.claude/gdd/global-skills/`). Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings, reflect, apply-reflections. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows, lint + schema + frontmatter + stale-ref + shellcheck + gitleaks + injection-scan + blocking size-budget) and release automation (auto-tag + GitHub Release + release-time smoke test). Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain. v1.27.7 ships gdd-mcp (Phase 27.7): 12 read-only MCP tools for sub-3s priming. v1.28.0 (Phase 28): Foundational References Tier 2 — 5 new reference files (color-theory, composition, proportion-systems, i18n, contrast-advanced), 2 verifier i18n probes + 1 explore i18n-readiness probe, 12 additive cross-link insertions across 10 existing references, 2 orthogonal audit-scoring lens-tags (composition_alignment + i18n_readiness).",
6
6
  "author": {
7
7
  "name": "hegemonart",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@ All notable changes to get-design-done are documented here. Versions follow [sem
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.40.0] - 2026-06-01
8
+
9
+ ### Phase 40 — Team Collaboration Mode
10
+
11
+ The largest architectural phase: GDD's single-operator baseline (Phase 20 lockfile + atomic STATE.md) now extends to **multiple developers**. Multi-writer STATE.md merges per-section, decisions are attributed + reviewed through an async queue with hard locks, a conflict-resolver reconciles parallel branches, decisions export to a read-only shared journal, and per-section permissions + sectional handoff gate who writes what. Everything is **git-native + advisory** — no server, no live multiplayer, no SSO. **No new runtime dependency, no new egress.**
12
+
13
+ ### Breaking changes
14
+
15
+ **None.** Every Phase 40 surface is additive and **off by default**: `gdd_cycle_mode` defaults to `full` (current behavior), `permissions` absent = everyone is `owner`, `collab.multi_writer_enabled` defaults to `false`, `collab.sync_backend` defaults to `git`, and the decision attribution suffix is optional + backward-compatible. A single-operator project upgrading to v1.40.0 behaves exactly as it did on v1.39.5.
16
+
17
+ ### Added
18
+
19
+ - **`reference/multi-author-model.md`** — the team-collaboration contract (merge model, attribution, review queue + locks, lock policy, sectional handoff, permissions, journal export + PR threading, opt-in sync). Registered.
20
+ - **`scripts/lib/collab/`** — 7 pure, dep-free cores: `attribution` (decision `[author= co-author=]` suffix), `section-merge` (git-merge-driver per-section semantic merge — union by D-id, conflict only on same-id divergence), `lock-policy` (team-mode advisory-lock backoff), `review-queue` (proposed→reviewing→approved→locked + audited unlock), `cycle-mode` (sectional handoff stage gate), `permissions` (per-section `can()`), `sync-backend` (git/s3/git-lfs selector).
21
+ - **`agents/conflict-resolver.md`** — three-way STATE.md merge, per-section + human-confirmed; never auto-picks or drops a decision.
22
+ - **`agents/decision-journal-exporter.md`** — `<decisions>` → `pseudonymize` → read-only Notion/Confluence on cycle close (write-only; degrades to local markdown).
23
+ - **`skills/review-decisions/SKILL.md`** (`/gdd:review-decisions`) + **`skills/unlock-decision/SKILL.md`** (`/gdd:unlock-decision <id> --approver`, the only audited escape from a hard lock).
24
+
25
+ ### Changed
26
+
27
+ - **`reference/schemas/config.schema.json`** — + `gdd_cycle_mode` (designer|dev|full) + `permissions` + `collab` (multi_writer_enabled / lock_timeout_ms / sync_backend); `generated.d.ts` regenerated.
28
+ - **`reference/STATE-TEMPLATE.md`** — document the optional decision attribution suffix.
29
+ - **`agents/design-reflector.md`** — a "Per-author patterns" note (reads attribution).
30
+ - **`agents/pr-commenter.md`** — thread PR comments on `D-XX` decisions (team-mode).
31
+
32
+ ### Notes
33
+
34
+ - **No new runtime dependency, no new egress** — 7 pure cores + reference/agent/skill prose + an additive config-schema; the live S3/git-LFS sync client is explicitly out of scope (the selector ships, the backend is pluggable).
35
+ - 6-manifest lockstep at **v1.40.0** + `OFF_CADENCE_VERSIONS.add('1.40.0')` (a minor bump) + the 33 live-pinned `manifests-version.txt` baselines forward-propagated 1.39.5 → 1.40.0.
36
+ - Inventory relock: registry-diff 158 → 159 (+`multi-author-model`), skill-list 80 → 82 (+`review-decisions`, +`unlock-decision`), agent-list +`conflict-resolver` +`decision-journal-exporter` + both frontmatter-snapshots, skill-length-distribution relocked, tarball golden 709 → 721 (+12). Root `SKILL.md` command table += both skills.
37
+
38
+ ---
39
+
7
40
  ## [1.39.5] - 2026-06-01
8
41
 
9
42
  ### Phase 39.5 — GDD Self-Migration Tooling
package/README.md CHANGED
@@ -190,6 +190,10 @@ GDD already tracks cost per task and per runtime — now it **forecasts** it, **
190
190
 
191
191
  GDD migrates *your* design systems (above) — now it migrates **itself**. When GDD ships a breaking path change (like the Phase 31.5 `scripts/lib/**` → `sdk/**` reorg), a machine-readable registry in [`reference/DEPRECATIONS.md`](reference/DEPRECATIONS.md) records `Since · Removed in · Old · New · hint`. After an upgrade, [`/gdd:update`](skills/update/SKILL.md) flags anything that crossed into deprecated/removed, and [`/gdd:migrate`](skills/migrate/SKILL.md) rewrites your project's stale references — **previewing a diff first, never silent** — via the pure [`deprecation-registry`](scripts/lib/deprecation-registry.cjs). Two CI gates keep the system honest: a completeness check (every entry has a shim or a confirmed removal) and `lint-changelog` (every future minor must declare a `### Breaking changes` section). **No new runtime dependency, no new egress.**
192
192
 
193
+ ### Team collaboration mode (v1.40.0)
194
+
195
+ GDD's single-operator baseline now extends to **teams** — git-native + advisory, no server. Two developers working a cycle on parallel branches merge their `.design/STATE.md` **per section** ([`section-merge`](scripts/lib/collab/section-merge.cjs) unions decisions by `D-id`; a real conflict is only a same-id divergence, which [`conflict-resolver`](agents/conflict-resolver.md) reconciles with human confirmation — never auto-picking or dropping a decision). Decisions carry optional `[author= co-author=]` attribution, move through an async review queue (`proposed → reviewing → approved → locked`) with **hard locks** (the only escape is an audited [`/gdd:unlock-decision`](skills/unlock-decision/SKILL.md)), and [`/gdd:review-decisions`](skills/review-decisions/SKILL.md) surfaces what's pending. [`decision-journal-exporter`](agents/decision-journal-exporter.md) publishes a **pseudonymized, read-only** Notion/Confluence journal for stakeholders who don't run GDD. `gdd_cycle_mode` (designer/dev/full) partitions a cycle by role, a `permissions` model gates per-section writes, and `collab.multi_writer_enabled` switches the advisory lock to a team-mode backoff. The full contract: [`reference/multi-author-model.md`](reference/multi-author-model.md). **Everything is off by default** — a single-operator project is unaffected. **No new runtime dependency.**
196
+
193
197
  ### Previous releases
194
198
 
195
199
  - **v1.26.0** — Headless Model Resolver (per-runtime tier→model map, `resolved_models` router field, per-runtime price tables, `reasoning-class` runtime-neutral alias).
package/SKILL.md CHANGED
@@ -105,6 +105,8 @@ Each stage produces artifacts in `.design/` inside the current project.
105
105
  | `budget [--cycles N] [--scenario best\|typical\|worst]` | `get-design-done:gdd-budget` | Phase 39.2 — forecast design-cycle spend (best/typical/worst from telemetry variance) via `cost-forecaster`; "at the current rate you'll hit your $X project cap in Y cycles." Read-only — never spends, edits `budget.json`, or halts (the budget-enforcer hook halts) |
106
106
  | `roi [--since <date>] [--window-days 14]` | `get-design-done:gdd-roi` | Phase 39.2 — ROI table joining per-cycle cost with commits that shipped (survived ≥14d) vs reverted → cost-per-shipped-commit + stick rate. Read-only markdown report |
107
107
  | `migrate [--yes] [--dry-run]` | `get-design-done:gdd-migrate` | Phase 39.5 — migrate a project off GDD's own deprecated paths after an upgrade; reads `reference/DEPRECATIONS.md` via `deprecation-registry.cjs`, previews a diff, applies on confirm. Preview-first; never edits silently |
108
+ | `review-decisions [<id>] [--pending]` | `get-design-done:gdd-review-decisions` | Phase 40 — surface the async decision-review queue (`proposed → reviewing → approved → locked`); `--pending` shows decisions still awaiting action. Read-only |
109
+ | `unlock-decision <id> --approver <who> [--reason <text>] [--dry-run]` | `get-design-done:gdd-unlock-decision` | Phase 40 — reopen a LOCKED decision (the only escape hatch); requires an approver + writes an audit entry; previews before writing |
108
110
 
109
111
  ## Handoff Routing
110
112
 
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: conflict-resolver
3
+ description: Resolves design-artifact merge conflicts when two developers worked a cycle on parallel branches. Reads the conflicted STATE.md from both sides (ours/theirs) plus the merge base, runs the pure scripts/lib/collab/section-merge.cjs per-section semantic merge (union the <decisions> by D-id; a real conflict is only a same-id divergence), and proposes a resolution PER SECTION for explicit human confirmation. Never auto-commits a conflicted section — decisions are durable. Spawned during a team-mode merge.
4
+ tools: Read, Bash, Grep, Glob
5
+ color: green
6
+ default-tier: sonnet
7
+ tier-rationale: "Three-way semantic merge of a structured STATE.md via a pure helper + a human-confirmed per-section proposal; bounded reconciliation, not open design judgment — sonnet-tier."
8
+ size_budget: M
9
+ size_budget_rationale: "Honest tier sized to the ~95-line body. DELEGATES the merge math to scripts/lib/collab/section-merge.cjs + the attribution parse to scripts/lib/collab/attribution.cjs, and the model to reference/multi-author-model.md."
10
+ parallel-safe: false
11
+ typical-duration-seconds: 45
12
+ reads-only: false
13
+ required_reading:
14
+ - "reference/multi-author-model.md"
15
+ writes:
16
+ - ".design/STATE.md (only the sections the human confirms)"
17
+ ---
18
+
19
+ # conflict-resolver
20
+
21
+ Two developers ran a GDD cycle on parallel branches; their `.design/STATE.md` now conflicts. You
22
+ reconcile it **per section, with human confirmation** — you never silently pick a winner, and you
23
+ never drop a decision. **Read `reference/multi-author-model.md` first** (the merge model).
24
+
25
+ ## Inputs
26
+
27
+ A git merge conflict in `.design/STATE.md`. Recover the three versions:
28
+
29
+ ```bash
30
+ git show :1:.design/STATE.md > /tmp/state.base # merge base (common ancestor)
31
+ git show :2:.design/STATE.md > /tmp/state.ours # ours (current branch)
32
+ git show :3:.design/STATE.md > /tmp/state.theirs # theirs (incoming)
33
+ ```
34
+
35
+ (If a stage isn't available — e.g. an add/add conflict with no base — treat the base as empty.)
36
+
37
+ ## Procedure
38
+
39
+ 1. **Parse each side's `<decisions>` block** with `scripts/lib/collab/attribution.cjs`
40
+ (`parseDecisionsBlock`), preserving the `[author= co-author=]` attribution.
41
+ 2. **Three-way merge** via the pure helper — never merge by hand:
42
+
43
+ ```bash
44
+ node -e '
45
+ const fs = require("fs");
46
+ const at = require("./scripts/lib/collab/attribution.cjs");
47
+ const sm = require("./scripts/lib/collab/section-merge.cjs");
48
+ const block = (f) => { const m = fs.readFileSync(f,"utf8").match(/<decisions>([\s\S]*?)<\/decisions>/); return m ? at.parseDecisionsBlock(m[1]) : []; };
49
+ const r = sm.mergeDecisions(block("/tmp/state.base"), block("/tmp/state.ours"), block("/tmp/state.theirs"));
50
+ console.log(JSON.stringify(r, null, 2));
51
+ '
52
+ ```
53
+
54
+ 3. **Report the proposal per section.** For `<decisions>`:
55
+ - the clean union (auto-mergeable D-NN, both new adds kept) — show it,
56
+ - each **conflict** (same D-id, divergent text/status) — show ours vs theirs side by side and ask
57
+ the human which to keep (or to author a merged text). Never decide unilaterally.
58
+ Repeat for any other conflicted section (status, `<prototyping>`, `<rollout_status>`) using
59
+ `mergeStatusScalar` for single-value sections.
60
+ 4. **Apply only confirmed sections.** Write the merged `<decisions>` (re-formatted via
61
+ `attribution.formatDecisionLine`, attribution preserved) back to `.design/STATE.md`, leaving any
62
+ unconfirmed conflict marked for the human. Then `git add .design/STATE.md` is the human's call.
63
+ 5. **Never drop a decision.** A `D-NN` removed on one side but unchanged on the other is kept — surface
64
+ it, don't delete it.
65
+
66
+ ## Record
67
+
68
+ Print a `## Conflict resolution` summary: sections merged, D-NN auto-unioned, conflicts surfaced +
69
+ how each was resolved, and any section left for the human. Append one JSONL line to
70
+ `.design/intel/insights.jsonl` recording the merge outcome. Close with:
71
+
72
+ ```
73
+ ## CONFLICT RESOLUTION COMPLETE
74
+ ```
75
+
76
+ ## Boundaries
77
+
78
+ - Per-section, human-confirmed; no unilateral winner, no silent deletion.
79
+ - Reconciles design artifacts (STATE.md); it does not run `git commit` or resolve source-code conflicts.
80
+ - No network.
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: decision-journal-exporter
3
+ description: Exports a cycle's design decisions to a read-only shared journal (Notion/Confluence) so PMs, designers, and stakeholders who don't run GDD locally can see what was decided and why. Reads STATE.md <decisions>, pipes every line through scripts/lib/pseudonymize.cjs (git identity + stable hash) so author names are masked before leaving the repo, and writes a read-only page via the connections/notion.md interface on cycle close. Write-only (non-GDD users read; they never write back). Degrades to a local markdown file when no Notion connection is available.
4
+ tools: Read, Write, Bash, Grep, Glob, ToolSearch
5
+ color: green
6
+ default-tier: sonnet
7
+ tier-rationale: "Reads a STATE block, redacts via a pure helper, and renders a page through an existing connection; bounded transform + publish, no design judgment — sonnet-tier."
8
+ size_budget: M
9
+ size_budget_rationale: "Honest tier sized to the ~90-line body. DELEGATES redaction to scripts/lib/pseudonymize.cjs, the Notion write to connections/notion.md, and the contract to reference/multi-author-model.md."
10
+ parallel-safe: false
11
+ typical-duration-seconds: 45
12
+ reads-only: false
13
+ required_reading:
14
+ - "reference/multi-author-model.md"
15
+ - "connections/notion.md"
16
+ writes:
17
+ - "a read-only Notion/Confluence page (external) OR .design/exports/decisions-<cycle>.md (fallback)"
18
+ ---
19
+
20
+ # decision-journal-exporter
21
+
22
+ You publish a cycle's decisions to a **read-only shared journal** so non-GDD stakeholders can follow
23
+ the "what + why" without the repo. You run on cycle close. **Read `reference/multi-author-model.md`
24
+ and `connections/notion.md` first.** Every author name is **pseudonymized before it leaves the repo**.
25
+
26
+ ## Procedure
27
+
28
+ 1. **Read the decisions.** Parse `.design/STATE.md` `<decisions>` (with
29
+ `scripts/lib/collab/attribution.cjs` `parseDecisionsBlock`), capturing id, text, status, and
30
+ attribution.
31
+ 2. **Pseudonymize.** Pipe the rendered decision text through `scripts/lib/pseudonymize.cjs` BEFORE any
32
+ outbound call — R1 (git identity) + R8 (stable pseudonym) mask `author=`/`co-author=` into a stable
33
+ hash. Never publish a raw git identity.
34
+
35
+ ```bash
36
+ node -e '
37
+ const fs = require("fs");
38
+ const { pseudonymize } = require("./scripts/lib/pseudonymize.cjs");
39
+ const raw = fs.readFileSync(0, "utf8");
40
+ process.stdout.write(pseudonymize(raw, {}));
41
+ ' < /tmp/decisions.md > /tmp/decisions.redacted.md
42
+ ```
43
+
44
+ 3. **Render** a read-only page: cycle id, the decision table (id · text · status · pseudonymous
45
+ author), and a "generated by GDD on cycle close — read-only" header. Regenerate each cycle (replace,
46
+ don't append duplicates).
47
+ 4. **Publish.** If a Notion connection is `available` (probe via `connections/notion.md`), write the
48
+ page through its write interface (redaction already applied; the connection's own redact pass is a
49
+ second belt). Otherwise **degrade**: write `.design/exports/decisions-<cycle>.md` locally and tell
50
+ the user to share it manually. Confluence follows the same shape if its connection is present.
51
+ 5. **Write-only.** You publish; you never read stakeholder edits back into STATE.md (bidirectional sync
52
+ is out of scope this phase).
53
+
54
+ ## Record
55
+
56
+ Print a `## Decision journal export` summary: cycle, decisions exported, destination (Notion page /
57
+ local fallback), and confirmation that redaction ran. Append one JSONL line to
58
+ `.design/intel/insights.jsonl`. Close with:
59
+
60
+ ```
61
+ ## DECISION JOURNAL EXPORT COMPLETE
62
+ ```
63
+
64
+ ## Boundaries
65
+
66
+ - **Pseudonymize before publish** — no raw git identity ever leaves the repo.
67
+ - Write-only, read-only-for-readers; no bidirectional sync; degrade to local markdown on no connection.
68
+ - Only the `<decisions>` block — not the full STATE.md, not telemetry.
@@ -92,6 +92,8 @@ After listing standard surprises, apply the **Four Principles Checks** from `ref
92
92
 
93
93
  Scan STATE.md `<decisions>` block for D-XX codes. Cross-reference `.design/learnings/` files from prior cycles if present. Flag decisions that: (a) appeared in multiple sessions of the same cycle, or (b) appear under the same keyword in learnings from ≥2 prior cycles. These are candidates for `reference/` additions.
94
94
 
95
+ **Per-author patterns (Phase 40, team mode).** When decisions carry the `[author= co-author=]` attribution suffix (see `reference/multi-author-model.md`), parse it with `scripts/lib/collab/attribution.cjs` (`parseDecisionsBlock` + `groupByAuthor`) and add a brief **Per-author patterns** sub-note: who locks decisions early, whose decisions get reverted or unlocked most, and any author whose decisions cluster around a recurring keyword. Skip silently when no decision is attributed (single-author projects).
96
+
95
97
  ### 3. Agent Performance
96
98
 
97
99
  Read `.design/agent-metrics.json`. For each agent:
@@ -67,6 +67,7 @@ const safeBody = redact(commentBody);
67
67
  1. **Inline review comments** — for each verify/audit finding that maps to a changed file+line, post an inline comment via `gh api repos/{owner}/{repo}/pulls/{n}/comments` (path + line + redacted body: the finding, the rule/pillar, and a one-line suggested fix). Findings with no changed-line locus go into a single summary review comment, not scattered.
68
68
  2. **Screenshot pairs (degrade, D-04)** — when `.design/STATE.md` `<connections>` shows `preview: available` or `chromatic: available` AND a before-after pair exists for a changed surface, attach the image refs in the comment/PR timeline. When absent → text-only; never a precondition.
69
69
  3. **`gdd/design-review` check-run (D-03)** — `gh api repos/{owner}/{repo}/check-runs` with `name: "gdd/design-review"`, a `conclusion` (`success` if verify passed + no blocker pillars, `failure` if verify failed or a11y-gate failed, else `neutral`), and an `output.summary` carrying the audit pillar scores + verify pass/fail + a11y result. This is the gate a teammate's branch-protection rule can require — see the reference for the required-check setup (`scripts/apply-branch-protection.sh`); you **register** the check, you never edit branch protection.
70
+ 4. **Decision threading (Phase 40, team mode)** — for each `D-XX` decision referenced in the PR's `DESIGN.md` / `DESIGN-VERIFICATION.md`, thread a PR comment keyed to that decision (one comment per `D-XX`, body = the decision text + its `proposed/reviewing/approved/locked` review state from `reference/multi-author-model.md`), so decision discussion persists as part of the PR history. Redacted like every other body; degrade-to-noop when `gh` is absent. This makes a decision's rationale reviewable inline by teammates who don't run GDD.
70
71
 
71
72
  ---
72
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.39.5",
3
+ "version": "1.40.0",
4
4
  "description": "A design-quality pipeline for AI coding agents: brief, plan, implement, and verify UI work against your design system.",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -47,6 +47,9 @@ skipped_stages: ""
47
47
  <decisions>
48
48
  <!-- Filled by discover stage. Format: -->
49
49
  <!-- D-01: [decision text] (locked | tentative) -->
50
+ <!-- Phase 40 (team mode): an optional attribution suffix records provenance for multi-author merges: -->
51
+ <!-- D-01: [decision text] (locked | tentative) [author=<git-user> co-author=<gdd-instance-id>] -->
52
+ <!-- The suffix is optional + backward-compatible; see reference/multi-author-model.md. -->
50
53
  </decisions>
51
54
 
52
55
  <must_haves>
@@ -376,3 +376,22 @@ If `.design/cache-manifest.json` is missing when `hooks/budget-enforcer.js` read
376
376
  - `.design/budget.json` — provides `cache_ttl_seconds` default.
377
377
  - `.design/telemetry/costs.jsonl` (Plan 10.1-05) — records `cache_hit: true` rows with zero tokens and zero cost when the short-circuit fires.
378
378
  - D-05, D-08, D-09 in `.planning/phases/10.1-optimization-layer-cost-governance/10.1-CONTEXT.md` — decision lineage.
379
+
380
+ ## Team collaboration (Phase 40)
381
+
382
+ Three optional top-level `.design/config.json` keys enable team mode. All are absent/off by default —
383
+ single-operator projects are unaffected. Full contract: `reference/multi-author-model.md`.
384
+
385
+ - **`gdd_cycle_mode`** (`designer` | `dev` | `full`, default `full`) — sectional handoff. `designer`
386
+ permits Brief + Explore writes; `dev` permits Plan + Design + Verify; `full` = all stages.
387
+ `scripts/lib/collab/cycle-mode.cjs` `stagePermitted(mode, stage)` gates STATE writes by stage.
388
+ - **`permissions`** — per-section write permissions (`scripts/lib/collab/permissions.cjs`). Permissive
389
+ by default (absent = everyone `owner`). Shape: `{ default, actors: {<actor>: <role>}, rules:
390
+ [{section, action, roles}] }`, roles in `owner|contributor|reviewer|viewer`. A rule restricts a
391
+ `(section, action)` to its listed roles; an unruled pair is allowed. `viewer` never mutates. A CI
392
+ gate calls `can(config, actor, section, action)` to enforce on PRs.
393
+ - **`collab`** — `{ multi_writer_enabled (bool), lock_timeout_ms (int), sync_backend
394
+ (git|s3|git-lfs) }`. `multi_writer_enabled: true` switches the gdd-state advisory lock to the
395
+ team-mode policy (`scripts/lib/collab/lock-policy.cjs` — 30 s wait + 100 ms backoff);
396
+ `sync_backend` selects the cross-machine `.design/` backend (`scripts/lib/collab/sync-backend.cjs`,
397
+ default `git`; `s3`/`git-lfs` are opt-in declarations — a live client is not bundled this phase).
@@ -0,0 +1,112 @@
1
+ # Multi-Author Model — Team Collaboration Contract
2
+
3
+ Phase 40 contract. GDD's single-process baseline (Phase 20 lockfile + atomic STATE.md RMW) is safe for
4
+ one operator. This file is the contract for **multiple developers** working a design in parallel: how
5
+ STATE.md merges, how decisions are attributed and reviewed, how writes coordinate across processes,
6
+ and who is allowed to write what. Everything here is **git-native + advisory** — no server, no live
7
+ multiplayer, no SSO.
8
+
9
+ ## 1. Multi-writer STATE.md — git-merge-driver with per-section semantic merge
10
+
11
+ GDD does **not** ship a CRDT or OT engine. The chosen model (lowest risk, highest familiarity) is a
12
+ **git merge driver with per-section semantic conflict detection**. The append-mostly `<decisions>`
13
+ block is the common case: two branches each adding a new `D-NN` must merge cleanly. The pure core
14
+ `scripts/lib/collab/section-merge.cjs` implements the rule:
15
+
16
+ - **Union by id.** A `D-NN` present on only one side (vs the merge base) is kept.
17
+ - **Equal duplicates collapse.** The same `D-NN` with identical text/status/attribution on both sides
18
+ is kept once.
19
+ - **Divergence is the only conflict.** The same `D-NN` with different text or status on the two sides
20
+ is a genuine conflict — surfaced to `agents/conflict-resolver.md`, never auto-resolved.
21
+ - **Decisions are durable.** A `D-NN` removed on one side but unchanged on the other is **kept** — a
22
+ decision is never silently deleted by a merge; removal goes through the unlock flow (§3).
23
+
24
+ A single-value section (e.g. `status`) merges with `mergeStatusScalar`: if only one side changed, take
25
+ that side; if both changed differently, it's a conflict.
26
+
27
+ ## 2. Attribution
28
+
29
+ A decision line carries an **optional** attribution suffix:
30
+
31
+ ```
32
+ D-07: Use OKLCH design tokens (locked) [author=alice co-author=gdd-3f9a]
33
+ ```
34
+
35
+ - `author` — the git user who made the decision.
36
+ - `co-author` — the GDD instance id (which machine/agent run produced it).
37
+
38
+ The suffix is backward-compatible: a plain `D-01: text (locked)` parses with `author`/`co-author` =
39
+ null. `scripts/lib/collab/attribution.cjs` parses + formats the line and groups decisions by author.
40
+ `agents/design-reflector.md` reads attribution to surface **per-author patterns** (who tends to lock
41
+ early, whose decisions get reverted). This is the SC#5 "attribution field" — encoded in the line, not
42
+ a schema-breaking change to the `Decision` type.
43
+
44
+ ## 3. Async review queue + decision locks
45
+
46
+ Each decision under review lives at `.design/reviews/<decision-id>/`, moving through a state machine
47
+ (`scripts/lib/collab/review-queue.cjs`):
48
+
49
+ ```
50
+ proposed → reviewing → approved → locked
51
+ ```
52
+
53
+ - **`locked` is terminal and hard.** A locked decision cannot be amended. `canAmend(state)` is true
54
+ only for `proposed`/`reviewing`.
55
+ - **Unlock is explicit + audited.** `/gdd:unlock-decision <id> --approver <who>` moves `locked →
56
+ reviewing` and appends an audit entry (`{ from:'locked', to:'reviewing', approver, reason }`). There
57
+ is no silent unlock.
58
+ - **`/gdd:review-decisions`** surfaces every queue entry by state (and `--pending` for the ones
59
+ awaiting action).
60
+
61
+ ## 4. Multi-writer lock policy
62
+
63
+ `scripts/lib/collab/lock-policy.cjs` derives the `acquire()` options from config. In single-process
64
+ mode the Phase 20 defaults apply (`maxWaitMs: 5000`). When `collab.multi_writer_enabled: true`, a
65
+ teammate's write may be queued, so the policy waits up to **30 s** with a **100 ms** backoff poll and a
66
+ 2-minute stale threshold. `collab.lock_timeout_ms` overrides the wait. The gdd-state MCP write path
67
+ passes these options to the existing advisory lock — coordinated multi-process writes, no new lock
68
+ mechanism.
69
+
70
+ ## 5. Sectional handoff — `gdd_cycle_mode`
71
+
72
+ `.design/config.json#gdd_cycle_mode` partitions a cycle by role (`scripts/lib/collab/cycle-mode.cjs`):
73
+
74
+ | Mode | Permitted stages |
75
+ |---|---|
76
+ | `designer` | Brief, Explore |
77
+ | `dev` | Plan, Design, Verify |
78
+ | `full` | all (the default / current behavior) |
79
+
80
+ `stagePermitted(mode, stage)` gates STATE writes by stage — a designer-mode cycle cannot write Plan/Design
81
+ state, and vice versa. This lets a designer hand a brief to a dev without either overwriting the other's
82
+ sections.
83
+
84
+ ## 6. Permissions
85
+
86
+ `.design/config.json#permissions` declares who can write which sections
87
+ (`scripts/lib/collab/permissions.cjs` `can(config, actor, section, action)`). The default policy is
88
+ permissive (`owner` can do everything); an override can restrict, e.g. "only `@lead-designer` can
89
+ `lock` decisions". A CI gate asserts the permission model is enforced on PRs.
90
+
91
+ ## 7. Decision-journal export + PR threading
92
+
93
+ - **`agents/decision-journal-exporter.md`** writes a **read-only** Notion/Confluence page from the
94
+ `<decisions>` block on cycle close (write-only this phase; non-GDD stakeholders read it). Every
95
+ decision is piped through `scripts/lib/pseudonymize.cjs` first (R1 git-identity + R8 stable hash), so
96
+ author names are masked before they leave the repo. Degrades to a local markdown file when no Notion
97
+ connection is available.
98
+ - **`agents/pr-commenter.md`** threads PR review comments on the `D-XX` decisions referenced in
99
+ DESIGN.md / DESIGN-VERIFICATION.md, so decision discussion persists as part of the PR history.
100
+
101
+ ## 8. Cross-machine sync (opt-in)
102
+
103
+ `scripts/lib/collab/sync-backend.cjs` `resolveBackend(config)` selects the `.design/` sync backend:
104
+ `git` (the **default** — existing behavior), or an opt-in `s3` / `git-lfs` for orgs whose git
105
+ push/pull cadence is too slow. Sync is **opt-in and off by default**; a live S3/LFS client is out of
106
+ scope for this phase — the selector + contract ship, the backend is pluggable.
107
+
108
+ ## Boundaries
109
+
110
+ No real-time multiplayer, no SSO/SAML, no GDD-side user accounts (users are git authors), no
111
+ bidirectional Notion, no merge-queue automation beyond the conflict-resolver. Everything is advisory
112
+ and git-native: a team that already uses git + PRs gets collaboration without new infrastructure.
@@ -1028,6 +1028,13 @@
1028
1028
  "type": "heuristic",
1029
1029
  "phase": 39.2,
1030
1030
  "description": "Phase 39.2 cost-governance contract: the per-cycle forecast model (best/typical/worst from mean ± k·σ, cyclesToCap) via scripts/lib/budget/cost-forecast.cjs; the project_cap hard-halt (disabled by default, graceful PreToolUse:Agent block, warn 50/80 + halt 100) via scripts/lib/budget/project-cap.cjs + hooks/budget-enforcer.ts; the ROI dashboard (shipped = surviving >=14d, cost-per-shipped-commit) via scripts/lib/budget/roi.cjs; and the budget_forecast/project_cap_warning/project_cap_halt events. Agent agents/cost-forecaster.md; skills /gdd:budget + /gdd:roi. Read/report-only — the hook only blocks, never spends."
1031
+ },
1032
+ {
1033
+ "name": "multi-author-model",
1034
+ "path": "reference/multi-author-model.md",
1035
+ "type": "meta-rules",
1036
+ "phase": 40,
1037
+ "description": "Phase 40 team-collaboration contract: multi-writer STATE.md via a git-merge-driver with per-section semantic merge (scripts/lib/collab/section-merge.cjs — union by D-id, conflict only on same-id divergence); decision attribution line-suffix [author= co-author=] (attribution.cjs); the async review queue proposed->reviewing->approved->locked with hard locks + audited /gdd:unlock-decision (review-queue.cjs); the multi-writer advisory-lock policy (lock-policy.cjs, 30s/100ms backoff in team mode); sectional handoff gdd_cycle_mode designer|dev|full (cycle-mode.cjs); the permission model (permissions.cjs) + CI gate; decision-journal-exporter (pseudonymized, write-only Notion) + pr-commenter D-XX threading; opt-in cross-machine sync (sync-backend.cjs, defaults to git). Agents conflict-resolver + decision-journal-exporter; skills /gdd:review-decisions + /gdd:unlock-decision."
1031
1038
  }
1032
1039
  ]
1033
1040
  }
@@ -40,6 +40,43 @@
40
40
  "update_dismissed": {
41
41
  "type": "string",
42
42
  "description": "Latest plugin tag (e.g. \"v1.0.7.3\") whose update nudge the user has dismissed. Set by /gdd:check-update --dismiss and by hooks/update-check.sh on the --dismiss code path. When a newer tag ships, the nudge reappears."
43
+ },
44
+ "gdd_cycle_mode": {
45
+ "type": "string",
46
+ "enum": ["designer", "dev", "full"],
47
+ "description": "Phase 40 sectional handoff. designer = Brief + Explore only; dev = Plan + Design + Verify; full = all stages (default). scripts/lib/collab/cycle-mode.cjs gates STATE writes by stage so a designer and a dev can hand a cycle back and forth without overwriting each other's sections."
48
+ },
49
+ "permissions": {
50
+ "type": "object",
51
+ "additionalProperties": true,
52
+ "description": "Phase 40 per-section write permissions (scripts/lib/collab/permissions.cjs). Permissive by default (absent = everyone is owner). A team narrows it, e.g. only @lead-designer may lock decisions. A CI gate enforces on PRs.",
53
+ "properties": {
54
+ "default": { "type": "string", "enum": ["owner", "contributor", "reviewer", "viewer"], "description": "Role for any actor not listed in `actors`. Default owner." },
55
+ "actors": { "type": "object", "additionalProperties": { "type": "string", "enum": ["owner", "contributor", "reviewer", "viewer"] }, "description": "Per-actor role map (git user/handle -> role)." },
56
+ "rules": {
57
+ "type": "array",
58
+ "items": {
59
+ "type": "object",
60
+ "additionalProperties": true,
61
+ "properties": {
62
+ "section": { "type": "string", "description": "STATE.md section (decisions/prototyping/rollout_status/status/progress/blockers) or '*'." },
63
+ "action": { "type": "string", "enum": ["write", "lock", "unlock", "approve", "*"] },
64
+ "roles": { "type": "array", "items": { "type": "string", "enum": ["owner", "contributor", "reviewer", "viewer"] } }
65
+ }
66
+ },
67
+ "description": "Restrictive rules: a (section, action) is limited to the listed roles. No matching rule = allowed (permissive)."
68
+ }
69
+ }
70
+ },
71
+ "collab": {
72
+ "type": "object",
73
+ "additionalProperties": true,
74
+ "description": "Phase 40 team-collaboration settings.",
75
+ "properties": {
76
+ "multi_writer_enabled": { "type": "boolean", "description": "When true, the gdd-state advisory lock uses the team-mode policy (longer wait + backoff) via scripts/lib/collab/lock-policy.cjs. Default false (single-process)." },
77
+ "lock_timeout_ms": { "type": "integer", "minimum": 0, "description": "Override the team-mode lock acquire maxWaitMs (default 30000)." },
78
+ "sync_backend": { "type": "string", "enum": ["git", "s3", "git-lfs"], "description": "Cross-machine .design/ sync backend (scripts/lib/collab/sync-backend.cjs). Default git (existing push/pull). s3 / git-lfs are opt-in declarations; a live client is not bundled this phase." }
79
+ }
43
80
  }
44
81
  }
45
82
  }
@@ -101,6 +101,56 @@ export interface DesignConfigJson {
101
101
  * Latest plugin tag (e.g. "v1.0.7.3") whose update nudge the user has dismissed. Set by /gdd:check-update --dismiss and by hooks/update-check.sh on the --dismiss code path. When a newer tag ships, the nudge reappears.
102
102
  */
103
103
  update_dismissed?: string;
104
+ /**
105
+ * Phase 40 sectional handoff. designer = Brief + Explore only; dev = Plan + Design + Verify; full = all stages (default). scripts/lib/collab/cycle-mode.cjs gates STATE writes by stage so a designer and a dev can hand a cycle back and forth without overwriting each other's sections.
106
+ */
107
+ gdd_cycle_mode?: 'designer' | 'dev' | 'full';
108
+ /**
109
+ * Phase 40 per-section write permissions (scripts/lib/collab/permissions.cjs). Permissive by default (absent = everyone is owner). A team narrows it, e.g. only @lead-designer may lock decisions. A CI gate enforces on PRs.
110
+ */
111
+ permissions?: {
112
+ /**
113
+ * Role for any actor not listed in `actors`. Default owner.
114
+ */
115
+ default?: 'owner' | 'contributor' | 'reviewer' | 'viewer';
116
+ /**
117
+ * Per-actor role map (git user/handle -> role).
118
+ */
119
+ actors?: {
120
+ [k: string]: 'owner' | 'contributor' | 'reviewer' | 'viewer';
121
+ };
122
+ /**
123
+ * Restrictive rules: a (section, action) is limited to the listed roles. No matching rule = allowed (permissive).
124
+ */
125
+ rules?: {
126
+ /**
127
+ * STATE.md section (decisions/prototyping/rollout_status/status/progress/blockers) or '*'.
128
+ */
129
+ section?: string;
130
+ action?: 'write' | 'lock' | 'unlock' | 'approve' | '*';
131
+ roles?: ('owner' | 'contributor' | 'reviewer' | 'viewer')[];
132
+ [k: string]: unknown;
133
+ }[];
134
+ [k: string]: unknown;
135
+ };
136
+ /**
137
+ * Phase 40 team-collaboration settings.
138
+ */
139
+ collab?: {
140
+ /**
141
+ * When true, the gdd-state advisory lock uses the team-mode policy (longer wait + backoff) via scripts/lib/collab/lock-policy.cjs. Default false (single-process).
142
+ */
143
+ multi_writer_enabled?: boolean;
144
+ /**
145
+ * Override the team-mode lock acquire maxWaitMs (default 30000).
146
+ */
147
+ lock_timeout_ms?: number;
148
+ /**
149
+ * Cross-machine .design/ sync backend (scripts/lib/collab/sync-backend.cjs). Default git (existing push/pull). s3 / git-lfs are opt-in declarations; a live client is not bundled this phase.
150
+ */
151
+ sync_backend?: 'git' | 's3' | 'git-lfs';
152
+ [k: string]: unknown;
153
+ };
104
154
  [k: string]: unknown;
105
155
  }
106
156
 
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+ // Phase 40 — attribution.cjs — PURE, dep-free parser/formatter for multi-author decision attribution.
3
+ //
4
+ // A STATE.md decision line carries an OPTIONAL attribution suffix so multiple developers' decisions
5
+ // survive a merge with provenance intact (SC#5). The canonical line form is:
6
+ //
7
+ // D-NN: <text> (<status>) [author=<git-user> co-author=<gdd-instance-id>]
8
+ //
9
+ // The suffix is optional and backward-compatible — a plain `D-01: text (locked)` parses with
10
+ // author/coAuthor = null. This module does ONLY string parse/format + grouping; no fs, no clock.
11
+ //
12
+ // No `require` — pure. Deterministic.
13
+
14
+ const STATUSES = Object.freeze(['locked', 'tentative']);
15
+
16
+ /**
17
+ * Parse a single decision line into its parts. Returns null when the line is not a decision line.
18
+ * @returns {{id, text, status, author, coAuthor} | null}
19
+ */
20
+ function parseDecisionLine(line) {
21
+ const s = String(line).trim().replace(/^[-*]\s+/, ''); // tolerate list-bullet prefixes
22
+ const m = s.match(/^(D-\d+)\s*:\s*(.*)$/);
23
+ if (!m) return null;
24
+ const id = m[1];
25
+ let rest = m[2].trim();
26
+ let author = null;
27
+ let coAuthor = null;
28
+ // Pull a trailing [author=... co-author=...] suffix (order-independent, both optional).
29
+ const attr = rest.match(/\[([^\]]*)\]\s*$/);
30
+ if (attr) {
31
+ const inner = attr[1];
32
+ const am = inner.match(/\bauthor=([^\s\]]+)/);
33
+ const cm = inner.match(/\bco-author=([^\s\]]+)/);
34
+ if (am) author = am[1];
35
+ if (cm) coAuthor = cm[1];
36
+ if (am || cm) rest = rest.slice(0, attr.index).trim();
37
+ }
38
+ // Pull a trailing (status).
39
+ let status = null;
40
+ const st = rest.match(/\(([a-z]+)\)\s*$/i);
41
+ if (st && STATUSES.includes(st[1].toLowerCase())) {
42
+ status = st[1].toLowerCase();
43
+ rest = rest.slice(0, st.index).trim();
44
+ }
45
+ return { id, text: rest, status, author, coAuthor };
46
+ }
47
+
48
+ /** Format a decision object back into the canonical line (omitting absent optional parts). */
49
+ function formatDecisionLine(d) {
50
+ if (!d || !d.id) throw new Error('attribution: formatDecisionLine needs {id}');
51
+ let line = `${d.id}: ${String(d.text || '').trim()}`;
52
+ if (d.status) line += ` (${d.status})`;
53
+ const bits = [];
54
+ if (d.author) bits.push(`author=${d.author}`);
55
+ if (d.coAuthor) bits.push(`co-author=${d.coAuthor}`);
56
+ if (bits.length) line += ` [${bits.join(' ')}]`;
57
+ return line;
58
+ }
59
+
60
+ /** Group an array of decision objects by author → { '<author>': [decision,...], '<unattributed>': [...] }. */
61
+ function groupByAuthor(decisions) {
62
+ if (!Array.isArray(decisions)) throw new Error('attribution: groupByAuthor needs an array');
63
+ const out = {};
64
+ for (const d of decisions) {
65
+ const key = d && d.author ? d.author : '<unattributed>';
66
+ (out[key] = out[key] || []).push(d);
67
+ }
68
+ return out;
69
+ }
70
+
71
+ /** Parse a whole `<decisions>` block body into decision objects (skips blanks/comments). */
72
+ function parseDecisionsBlock(body) {
73
+ const out = [];
74
+ for (const line of String(body).replace(/\r\n/g, '\n').split('\n')) {
75
+ const t = line.trim();
76
+ if (!t || t.startsWith('<!--')) continue;
77
+ const d = parseDecisionLine(t);
78
+ if (d) out.push(d);
79
+ }
80
+ return out;
81
+ }
82
+
83
+ module.exports = { STATUSES, parseDecisionLine, formatDecisionLine, groupByAuthor, parseDecisionsBlock };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+ // Phase 40 — cycle-mode.cjs — PURE, dep-free sectional-handoff gate (SC#8).
3
+ //
4
+ // `.design/config.json#gdd_cycle_mode` partitions a cycle by role so a designer can hand a brief to a
5
+ // dev (and vice versa) without either overwriting the other's stages:
6
+ // designer → Brief, Explore
7
+ // dev → Plan, Design, Verify
8
+ // full → all stages (the default / current single-operator behavior)
9
+ // `stagePermitted(mode, stage)` gates a STATE write by the stage that produced it.
10
+ //
11
+ // No `require` — pure. Deterministic.
12
+
13
+ const MODES = Object.freeze(['designer', 'dev', 'full']);
14
+ const ALL_STAGES = Object.freeze(['brief', 'explore', 'plan', 'design', 'verify']);
15
+
16
+ const STAGES_BY_MODE = Object.freeze({
17
+ designer: Object.freeze(['brief', 'explore']),
18
+ dev: Object.freeze(['plan', 'design', 'verify']),
19
+ full: ALL_STAGES,
20
+ });
21
+
22
+ /** Normalize a mode value; unknown/missing → 'full' (the safe, backward-compatible default). */
23
+ function normalizeMode(mode) {
24
+ const m = String(mode || 'full').toLowerCase().trim();
25
+ return MODES.includes(m) ? m : 'full';
26
+ }
27
+
28
+ /** Resolve the cycle mode from a parsed `.design/config.json` (defaults to 'full'). */
29
+ function resolveMode(config) {
30
+ return normalizeMode(config && config.gdd_cycle_mode);
31
+ }
32
+
33
+ /** True when `stage` is writable under `mode`. Unknown stage → false. */
34
+ function stagePermitted(mode, stage) {
35
+ const m = normalizeMode(mode);
36
+ const s = String(stage || '').toLowerCase().trim();
37
+ if (!ALL_STAGES.includes(s)) return false;
38
+ return STAGES_BY_MODE[m].includes(s);
39
+ }
40
+
41
+ module.exports = { MODES, ALL_STAGES, STAGES_BY_MODE, normalizeMode, resolveMode, stagePermitted };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+ // Phase 40 — lock-policy.cjs — PURE, dep-free advisory-lock policy for gdd-state multi-writer mode (SC#6).
3
+ //
4
+ // Phase 20's sdk/state/lockfile.ts already implements PID+timestamp advisory locks with retry
5
+ // (staleMs / maxWaitMs / pollMs). "Multi-writer mode" is a POLICY layered on top: when a project
6
+ // enables team mode (`.design/config.json#collab.multi_writer_enabled`), the state write path should
7
+ // wait LONGER and poll on a backoff before giving up, because a teammate's write may be in flight.
8
+ // This module derives the acquire-options object from config; the MCP write path passes it to acquire().
9
+ //
10
+ // No `require` — pure. Deterministic.
11
+
12
+ // sdk/state/lockfile.ts defaults (single-process baseline).
13
+ const SINGLE = Object.freeze({ staleMs: 60000, maxWaitMs: 5000, pollMs: 50 });
14
+ // Team mode: a stuck teammate write is rare but a normal queued one is not — wait up to 30s,
15
+ // poll slower (100ms) to reduce contention, and treat a lock older than 2min as stale.
16
+ const TEAM = Object.freeze({ staleMs: 120000, maxWaitMs: 30000, pollMs: 100 });
17
+
18
+ /** True when team multi-writer mode is enabled in config. */
19
+ function isMultiWriter(config) {
20
+ return !!(config && config.collab && config.collab.multi_writer_enabled === true);
21
+ }
22
+
23
+ /**
24
+ * Resolve the acquire() options for the current config. A numeric `collab.lock_timeout_ms`
25
+ * overrides the team-mode maxWaitMs. Returns a fresh object (never the frozen constants).
26
+ */
27
+ function acquireOpts(config) {
28
+ const base = isMultiWriter(config) ? TEAM : SINGLE;
29
+ const out = { staleMs: base.staleMs, maxWaitMs: base.maxWaitMs, pollMs: base.pollMs };
30
+ if (isMultiWriter(config)) {
31
+ const t = Number(config.collab.lock_timeout_ms);
32
+ if (Number.isFinite(t) && t > 0) out.maxWaitMs = t;
33
+ }
34
+ return out;
35
+ }
36
+
37
+ module.exports = { isMultiWriter, acquireOpts, SINGLE, TEAM };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+ // Phase 40 — permissions.cjs — PURE, dep-free section-write permission model (SC#10).
3
+ //
4
+ // `.design/config.json#permissions` declares who may perform which action on which STATE.md section.
5
+ // The model is permissive-by-default (single-operator projects are unaffected): with no `permissions`
6
+ // block, everyone is an `owner` and `can(...)` is always true. A team narrows it, e.g. "only
7
+ // @lead-designer can `lock` decisions". A CI gate calls `can()` to enforce on PRs.
8
+ //
9
+ // Shape of config.permissions:
10
+ // {
11
+ // "default": "owner", // role for any actor not listed
12
+ // "actors": { "@alice": "reviewer", ... },// per-actor role
13
+ // "rules": [ { "section": "decisions", "action": "lock", "roles": ["owner"] }, ... ]
14
+ // }
15
+ // A rule restricts (section, action) to the listed roles. No matching rule ⇒ allowed (permissive).
16
+ //
17
+ // No `require` — pure. Deterministic.
18
+
19
+ const SECTIONS = Object.freeze(['decisions', 'prototyping', 'rollout_status', 'status', 'progress', 'blockers']);
20
+ const ACTIONS = Object.freeze(['write', 'lock', 'unlock', 'approve']);
21
+ const ROLES = Object.freeze(['owner', 'contributor', 'reviewer', 'viewer']);
22
+
23
+ /** The role assigned to `actor` by the config (falls back to config.default, then 'owner'). */
24
+ function roleOf(config, actor) {
25
+ const perms = (config && config.permissions) || {};
26
+ const actors = perms.actors || {};
27
+ if (actor && Object.prototype.hasOwnProperty.call(actors, actor)) return actors[actor];
28
+ return perms.default || 'owner';
29
+ }
30
+
31
+ /**
32
+ * May `actor` perform `action` on `section` under this config?
33
+ * Permissive by default: a (section, action) with no matching rule is allowed. A matching rule
34
+ * allows only its listed roles. `viewer` is denied any mutating action even absent a rule.
35
+ */
36
+ function can(config, actor, section, action) {
37
+ const role = roleOf(config, actor);
38
+ if (role === 'viewer') return false; // viewers never mutate
39
+ const perms = (config && config.permissions) || {};
40
+ const rules = Array.isArray(perms.rules) ? perms.rules : [];
41
+ const matching = rules.filter(
42
+ (r) => r && (r.section === section || r.section === '*') && (r.action === action || r.action === '*'),
43
+ );
44
+ if (matching.length === 0) return true; // no restriction → allowed
45
+ return matching.some((r) => Array.isArray(r.roles) && r.roles.includes(role));
46
+ }
47
+
48
+ /** The default permissive policy (used when config has no permissions block). */
49
+ function defaultPolicy() {
50
+ return { default: 'owner', actors: {}, rules: [] };
51
+ }
52
+
53
+ module.exports = { SECTIONS, ACTIONS, ROLES, roleOf, can, defaultPolicy };
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+ // Phase 40 — review-queue.cjs — PURE, dep-free async-review state machine for decisions (SC#7).
3
+ //
4
+ // Each decision under review lives at .design/reviews/<decision-id>/ and moves through:
5
+ // proposed → reviewing → approved → locked
6
+ // `locked` is terminal and HARD: a locked decision cannot be amended. The only way back is an
7
+ // explicit, AUDITED unlock (/gdd:unlock-decision <id> --approver <who>), which records who reopened
8
+ // it and why. This module is the pure transition core — the skill does the filesystem I/O.
9
+ //
10
+ // No `require` — pure. Deterministic.
11
+
12
+ const STATES = Object.freeze(['proposed', 'reviewing', 'approved', 'locked']);
13
+
14
+ // Allowed forward transitions per state. `locked` has none (terminal — unlock() is the escape hatch).
15
+ const TRANSITIONS = Object.freeze({
16
+ proposed: ['reviewing'],
17
+ reviewing: ['approved', 'proposed'], // can bounce back for revision
18
+ approved: ['locked', 'reviewing'],
19
+ locked: [],
20
+ });
21
+
22
+ /** True while a decision may still be edited (before it is locked). */
23
+ function canAmend(state) {
24
+ return state === 'proposed' || state === 'reviewing';
25
+ }
26
+
27
+ /**
28
+ * Apply a transition. `event` is the TARGET state.
29
+ * @returns the new state string. Throws on an invalid transition.
30
+ */
31
+ function transition(state, event) {
32
+ if (!STATES.includes(state)) throw new Error(`review-queue: unknown state "${state}"`);
33
+ if (!STATES.includes(event)) throw new Error(`review-queue: unknown target "${event}"`);
34
+ const allowed = TRANSITIONS[state];
35
+ if (!allowed.includes(event)) {
36
+ throw new Error(`review-queue: illegal transition ${state} -> ${event} (allowed: ${allowed.join(', ') || 'none'})`);
37
+ }
38
+ return event;
39
+ }
40
+
41
+ /**
42
+ * Explicit audited unlock of a locked decision. Moves locked -> reviewing and returns the new entry
43
+ * with an appended audit record. Requires a non-empty approver. Throws if the entry is not locked.
44
+ * @param {{id, state, audit?: Array}} entry
45
+ * @param {{approver: string, reason?: string}} opts
46
+ */
47
+ function unlock(entry, opts) {
48
+ if (!entry || entry.state !== 'locked') {
49
+ throw new Error('review-queue: unlock() requires a locked decision');
50
+ }
51
+ const approver = opts && opts.approver ? String(opts.approver).trim() : '';
52
+ if (!approver) throw new Error('review-queue: unlock() requires an approver');
53
+ const audit = Array.isArray(entry.audit) ? entry.audit.slice() : [];
54
+ audit.push({ action: 'unlock', from: 'locked', to: 'reviewing', approver, reason: (opts && opts.reason) || '' });
55
+ return Object.assign({}, entry, { state: 'reviewing', audit });
56
+ }
57
+
58
+ /** Filter queue entries that still need a human action (not yet locked). */
59
+ function pending(entries) {
60
+ if (!Array.isArray(entries)) throw new Error('review-queue: pending() needs an array');
61
+ return entries.filter((e) => e && e.state !== 'locked');
62
+ }
63
+
64
+ module.exports = { STATES, TRANSITIONS, canAmend, transition, unlock, pending };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+ // Phase 40 — section-merge.cjs — PURE, dep-free per-section semantic merge for STATE.md (SC#1).
3
+ //
4
+ // The chosen multi-writer model (ROADMAP default) is a git-merge-driver with PER-SECTION semantic
5
+ // conflict detection — strictly safer than a line-based merge for the append-mostly `<decisions>`
6
+ // block. Two developers each adding a new D-NN on parallel branches should merge cleanly (union by
7
+ // id); a real conflict is ONLY when the SAME id diverges in text or status. This module is the merge
8
+ // core: it takes three decision arrays (base/ours/theirs, already parsed by attribution.cjs) and
9
+ // returns the merged set plus any genuine conflicts for the conflict-resolver agent to resolve.
10
+ //
11
+ // No `require` — pure. Deterministic (stable id-sorted output).
12
+
13
+ function byId(list) {
14
+ const m = new Map();
15
+ for (const d of list || []) if (d && d.id) m.set(d.id, d);
16
+ return m;
17
+ }
18
+
19
+ function sameDecision(a, b) {
20
+ return a.text === b.text && (a.status || null) === (b.status || null) &&
21
+ (a.author || null) === (b.author || null) && (a.coAuthor || null) === (b.coAuthor || null);
22
+ }
23
+
24
+ /** Numeric sort by the D-NN id (D-2 before D-10). */
25
+ function idNum(id) {
26
+ const m = String(id).match(/(\d+)/);
27
+ return m ? parseInt(m[1], 10) : 0;
28
+ }
29
+
30
+ /**
31
+ * Three-way merge of `<decisions>` (base = common ancestor; ours/theirs = the two branches).
32
+ * @returns {{merged: Decision[], conflicts: [{id, ours, theirs}], added: string[]}}
33
+ * - id only in one side (vs base) → kept (union).
34
+ * - id in both sides, equal → kept once.
35
+ * - id in both sides, divergent → CONFLICT (and `ours` is kept provisionally so the merged set is
36
+ * still complete; the resolver overwrites after the human picks).
37
+ * - id removed on one side but unchanged on the other → kept (deletions are never auto-applied —
38
+ * decisions are durable; an explicit unlock/removal flow handles that).
39
+ */
40
+ function mergeDecisions(base, ours, theirs) {
41
+ const B = byId(base);
42
+ const O = byId(ours);
43
+ const T = byId(theirs);
44
+ const ids = new Set([...O.keys(), ...T.keys(), ...B.keys()]);
45
+ const merged = [];
46
+ const conflicts = [];
47
+ const added = [];
48
+ for (const id of ids) {
49
+ const o = O.get(id);
50
+ const t = T.get(id);
51
+ const b = B.get(id);
52
+ if (o && t) {
53
+ if (sameDecision(o, t)) { merged.push(o); }
54
+ else { conflicts.push({ id, ours: o, theirs: t }); merged.push(o); }
55
+ } else if (o && !t) {
56
+ merged.push(o);
57
+ if (!b) added.push(id); // ours added it
58
+ } else if (!o && t) {
59
+ merged.push(t);
60
+ if (!b) added.push(id); // theirs added it
61
+ }
62
+ // (!o && !t) — present only in base, removed on both → drop.
63
+ }
64
+ merged.sort((a, x) => idNum(a.id) - idNum(x.id));
65
+ added.sort((a, x) => idNum(a) - idNum(x));
66
+ return { merged, conflicts, added };
67
+ }
68
+
69
+ /** Scalar merge for a single-value section (e.g. status): equal → value; divergent → conflict marker. */
70
+ function mergeStatusScalar(base, ours, theirs) {
71
+ if (ours === theirs) return { value: ours, conflict: false };
72
+ if (ours === base) return { value: theirs, conflict: false }; // only theirs changed
73
+ if (theirs === base) return { value: ours, conflict: false }; // only ours changed
74
+ return { value: ours, conflict: true }; // both changed differently
75
+ }
76
+
77
+ module.exports = { mergeDecisions, mergeStatusScalar, sameDecision };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+ // Phase 40 — sync-backend.cjs — PURE, dep-free cross-machine sync backend selector (SC#9).
3
+ //
4
+ // `.design/` syncs between teammates over git by DEFAULT (existing behavior). Orgs whose git
5
+ // push/pull cadence is too slow can opt into an `s3` or `git-lfs` backend. This module is the
6
+ // SELECTOR + contract: it resolves which backend a config asks for and whether sync is opted in.
7
+ // A live S3/LFS client is explicitly out of scope this phase — the selector ships, the backend is
8
+ // pluggable. Defaulting to `git` means single-operator + most-team projects are unaffected.
9
+ //
10
+ // No `require` — pure. Deterministic.
11
+
12
+ const BACKENDS = Object.freeze(['git', 's3', 'git-lfs']);
13
+ const DEFAULT_BACKEND = 'git';
14
+
15
+ /** True when the project opted into a non-git sync backend. */
16
+ function isOptIn(config) {
17
+ const b = config && config.collab && config.collab.sync_backend;
18
+ return !!b && b !== 'git' && BACKENDS.includes(b);
19
+ }
20
+
21
+ /**
22
+ * Resolve the sync backend. Unknown/missing → 'git' (the safe default).
23
+ * @returns {{backend, optIn, supported}} supported=false for opt-in backends whose live client
24
+ * is not bundled this phase (the caller falls back to git + warns).
25
+ */
26
+ function resolveBackend(config) {
27
+ const raw = config && config.collab && config.collab.sync_backend;
28
+ const backend = BACKENDS.includes(raw) ? raw : DEFAULT_BACKEND;
29
+ const optIn = backend !== DEFAULT_BACKEND;
30
+ // Phase 40 ships the selector only; no live S3/LFS client → opt-in backends are "declared but not
31
+ // yet executable". git is always supported (it's the existing push/pull path).
32
+ const supported = backend === DEFAULT_BACKEND;
33
+ return { backend, optIn, supported };
34
+ }
35
+
36
+ module.exports = { BACKENDS, DEFAULT_BACKEND, isOptIn, resolveBackend };
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: gdd-review-decisions
3
+ description: "Surfaces the async decision-review queue for team mode. Reads .design/reviews/<decision-id>/ entries and reports each decision's state in the proposed → reviewing → approved → locked machine (via scripts/lib/collab/review-queue.cjs), so a team can see what's awaiting review, what's approved, and what's locked. --pending shows only decisions still needing action. Read-only — it reports the queue; it never advances a decision (that's a reviewer's explicit call). Use to run an async design-decision review without a meeting."
4
+ argument-hint: "[<decision-id>] [--pending]"
5
+ user-invocable: true
6
+ tools: Read, Bash, Grep, Glob
7
+ ---
8
+
9
+ # /gdd:review-decisions
10
+
11
+ Closes the async-review gap for team mode: design decisions move through an explicit review queue
12
+ instead of being decided in a single operator's head. This skill reports where each decision is.
13
+ **Read-only** — it surfaces state; advancing a decision is a reviewer's explicit action. Contract:
14
+ `../../reference/multi-author-model.md`.
15
+
16
+ ## Invocation
17
+
18
+ | Command | Behavior |
19
+ |---|---|
20
+ | `/gdd:review-decisions` | Every decision in the queue, grouped by state. |
21
+ | `/gdd:review-decisions <decision-id>` | One decision's state + audit trail. |
22
+ | `/gdd:review-decisions --pending` | Only decisions not yet `locked` (awaiting action). |
23
+
24
+ ## Steps
25
+
26
+ 1. **Find the queue.** List `.design/reviews/*/` (each dir is a `<decision-id>`). No reviews dir →
27
+ print `review-decisions: no review queue yet (team mode not in use).` and exit.
28
+ 2. **Read each entry** (`.design/reviews/<id>/state.json` → `{ id, state, audit }`). Validate the
29
+ state against `scripts/lib/collab/review-queue.cjs` `STATES`.
30
+ 3. **Render** grouped by state: `proposed` / `reviewing` / `approved` / `locked`, each listing the
31
+ decision id + a one-line summary. For `--pending`, use `review-queue.pending(entries)` to show only
32
+ non-locked ones. For a single `<decision-id>`, also print its audit trail (transitions + approvers).
33
+ 4. **Do not advance.** Reporting only — moving a decision forward (or `/gdd:unlock-decision`) is the
34
+ reviewer's explicit call.
35
+
36
+ ## Output
37
+
38
+ End with:
39
+
40
+ ```
41
+ ## REVIEW-DECISIONS COMPLETE
42
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: gdd-unlock-decision
3
+ description: "Reopens a LOCKED design decision — the only escape hatch from the hard lock. Requires an explicit --approver and writes an audit entry, then moves the decision locked → reviewing (via scripts/lib/collab/review-queue.cjs). Previews the audit record before writing; never unlocks silently. Use when a locked decision genuinely must change (a later constraint invalidated it) and a reviewer has signed off."
4
+ argument-hint: "<decision-id> --approver <who> [--reason <text>] [--dry-run]"
5
+ user-invocable: true
6
+ tools: Read, Write, Bash, Grep, Glob
7
+ ---
8
+
9
+ # /gdd:unlock-decision
10
+
11
+ A `locked` decision is hard — it cannot be amended. This skill is the **only** way back, and it is
12
+ deliberately heavyweight: it requires a named approver and records an audit entry, so reopening a
13
+ locked decision is always traceable. Contract: `../../reference/multi-author-model.md`.
14
+
15
+ ## Invocation
16
+
17
+ | Command | Behavior |
18
+ |---|---|
19
+ | `/gdd:unlock-decision <id> --approver <who>` | Unlock `<id>` (locked → reviewing), recording the approver. |
20
+ | `/gdd:unlock-decision <id> --approver <who> --reason <text>` | Same, with a reason in the audit entry. |
21
+ | `/gdd:unlock-decision <id> --dry-run` | Preview the audit record + the resulting state; change nothing. |
22
+
23
+ ## Steps
24
+
25
+ 1. **Validate args.** `<decision-id>` required; `--approver` required (a non-empty name). Missing
26
+ `--approver` → print the usage + exit without changing anything.
27
+ 2. **Load the entry** from `.design/reviews/<decision-id>/state.json`. Not found → report it. Not in
28
+ `locked` state → print `unlock-decision: <id> is not locked (state: <state>); nothing to unlock.`
29
+ 3. **Preview.** Show the audit entry that will be appended (`{ action: unlock, from: locked, to:
30
+ reviewing, approver, reason }`) and the resulting state. If `--dry-run`, stop here.
31
+ 4. **Apply** via the pure helper:
32
+
33
+ ```bash
34
+ node -e '
35
+ const fs = require("fs");
36
+ const rq = require("./scripts/lib/collab/review-queue.cjs");
37
+ const p = process.argv[1], approver = process.argv[2], reason = process.argv[3] || "";
38
+ const entry = JSON.parse(fs.readFileSync(p, "utf8"));
39
+ const next = rq.unlock(entry, { approver, reason });
40
+ fs.writeFileSync(p, JSON.stringify(next, null, 2) + "\n");
41
+ console.log(JSON.stringify(next.audit[next.audit.length - 1]));
42
+ ' ".design/reviews/<id>/state.json" "<who>" "<reason>"
43
+ ```
44
+
45
+ 5. **Report** the new state + the recorded approver. The decision is now `reviewing` and amendable
46
+ again; it must be re-reviewed + re-locked to become durable.
47
+
48
+ ## Output
49
+
50
+ End with:
51
+
52
+ ```
53
+ ## UNLOCK-DECISION COMPLETE
54
+ ```