orchestrix-skills 0.10.0 → 0.11.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-skills",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Capability-first AI development skill graph (Anthropic-native): plan → build with a warm-context orchestrator, contract-wired skills, and independent verification.",
5
5
  "author": "Orchestrix",
6
6
  "homepage": "https://orchestrix-mcp.youlidao.ai",
package/README.md CHANGED
@@ -23,12 +23,36 @@ intent
23
23
  │ (objective) ↑ accept (batched)
24
24
  ├─ (verify failing, cause unknown?) ──→ investigate (root cause → rework)
25
25
  ├─ (runnable app?) ──→ smoke-test (drive real flows, evidence captured)
26
+ ├─ (team mode?) ──→ pull-request after commit (PR on GitHub, MR on GitLab; CI red re-enters rework)
26
27
  └─ (accepted + ship it?) ──→ deploy (inline gate, rollback-first)
27
28
  ```
28
29
 
29
30
  Human gates are front-loaded (planning = direction) and at the end (acceptance);
30
31
  the build loop runs lights-out, gated only by objective `verify`.
31
32
 
33
+ ## Team mode
34
+
35
+ Uncomment the `collaboration:` block in `core-config.yaml` and the same loop
36
+ runs for a team. Planning happens once, on a planning branch, and reaches the
37
+ base branch through a PR. Each story is then one run on its own branch
38
+ (`story/<origin>/<slug>`) in its own workspace — a `git worktree` when several
39
+ sessions share a machine, a clone elsewhere — and ends with `pull-request`
40
+ instead of a local commit. Merging stays a reviewer's act in the forge.
41
+
42
+ There is no board file. A story's state is derived from git and the forge each
43
+ time it is needed: a remote branch means claimed, an open PR means in review, a
44
+ merged PR means done. Claiming is one push that the remote rejects if the
45
+ branch already exists, so two people cannot take the same story. The PR body
46
+ carries the run's evidence (acceptance criteria → code → test → verify log),
47
+ because `.orchestrate/` never leaves the machine that produced it.
48
+
49
+ GitHub and GitLab are supported through `adapters/forge/`; the exact CLI
50
+ commands are inlined in the `pull-request` skill and kept identical by test.
51
+ Team mode needs that forge's CLI (`gh` or `glab`) installed and logged in on
52
+ each machine. `install` and `doctor` check for it once the block is active;
53
+ without it a builder run still pushes its branch, then stops and asks a human
54
+ to open the PR — it never pretends the PR exists.
55
+
32
56
  ## Install
33
57
 
34
58
  ```bash
@@ -51,7 +75,7 @@ Re-running `install` refreshes the skills in place. It also writes a stamp at
51
75
  `<skills-dir>/.orchestrix-skills.json`:
52
76
 
53
77
  ```json
54
- { "version": "0.10.0", "ide": "claude", "skills": ["brainstorm", "commit", "…"] }
78
+ { "version": "0.11.0", "ide": "claude", "skills": ["brainstorm", "commit", "…"] }
55
79
  ```
56
80
 
57
81
  Two things read it. The installer prunes skills a previous version placed that
@@ -93,11 +117,16 @@ The root skill is where the interesting engineering lives. Beyond wiring:
93
117
  - **Model tiers are declared, not guessed.** Each skill states
94
118
  `requires.model: frontier | capable | cheap`; the adapter maps the tier to a
95
119
  model, and the resolved model is recorded on every ledger step.
120
+ - **A team adds no shared state.** In team mode a run lives on a branch in
121
+ its own workspace, story state is derived from git and the forge, and an
122
+ epic is accepted on base with `smoke-test` — N green PRs do not prove the
123
+ composition.
96
124
 
97
125
  ## Runtime adapters
98
126
 
99
127
  `skills/` is the runtime-neutral source of truth. `adapters/` describes how a
100
- runtime maps generic capabilities to its tools. During a Codex install the CLI
128
+ runtime maps generic capabilities to its tools, and how a forge (GitHub,
129
+ GitLab) maps the generic pull-request operations to its CLI. During a Codex install the CLI
101
130
  removes Claude-only `allowed-tools`, preserves the contract, and adds Codex
102
131
  runtime guidance. If the target has an unmanaged `AGENTS.md`, its content is left
103
132
  untouched and the guidance is placed at `.codex/orchestrix/AGENTS.md` for manual
@@ -9,7 +9,8 @@
9
9
  "shell.execute": "Bash",
10
10
  "web.read": "WebSearch, WebFetch",
11
11
  "agent.spawn": "Task",
12
- "design.canvas": "the bundled design skill (Claude Design canvas) + Artifact publish"
12
+ "design.canvas": "the bundled design skill (Claude Design canvas) + Artifact publish",
13
+ "forge.pr": "Bash + the forge CLI (gh | glab) named by core-config collaboration.forge; op table in the pull-request skill"
13
14
  },
14
15
  "models": {
15
16
  "frontier": "fable",
@@ -9,4 +9,5 @@
9
9
  - When isolated agents are available, dispatch independent leaf skills concurrently and await them. Otherwise execute leaf skills sequentially in the current context, reading only their declared inputs before each step.
10
10
  - Independently run every objective verification command. Never accept an agent's success report as proof.
11
11
  - Keep runtime evidence at the fixed `.orchestrate/` path described by the `orchestrate` skill.
12
+ - When `core-config.yaml` has a `collaboration:` block, follow the `orchestrate` skill's Collaboration section: one story per branch per workspace (`git worktree add`, then `cd` into it — Codex has no worktree tool), builder runs end with `pull-request`, and forge operations come from that skill's op table.
12
13
  <!-- orchestrix:end -->
@@ -9,7 +9,8 @@
9
9
  "shell.execute": "runtime shell tool",
10
10
  "web.read": "runtime web tools when enabled",
11
11
  "agent.spawn": "optional collaboration tools; otherwise sequential fallback",
12
- "design.canvas": "not available; design skills write standalone HTML files"
12
+ "design.canvas": "not available; design skills write standalone HTML files",
13
+ "forge.pr": "runtime shell tool + the forge CLI (gh | glab) named by core-config collaboration.forge; op table in the pull-request skill"
13
14
  },
14
15
  "models": {
15
16
  "frontier": "the most capable model the session can select; else the session model, recorded in the ledger",
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "github",
3
+ "term": "pull request",
4
+ "cli": "gh",
5
+ "ops": {
6
+ "auth": "gh auth status",
7
+ "pr_for_branch": "gh pr list --head {branch} --state all --json number,url,state,isDraft,headRefOid,mergedAt",
8
+ "pr_create_draft": "gh pr create --draft --base {base} --head {branch} --title {title} --body-file {body_file}",
9
+ "pr_view": "gh pr view {pr} --json number,url,state,isDraft,headRefOid,baseRefName",
10
+ "pr_update_body": "gh pr edit {pr} --body-file {body_file}",
11
+ "pr_ready": "gh pr ready {pr}",
12
+ "ci_status": "gh pr checks {pr}",
13
+ "ci_watch": "gh pr checks {pr} --watch --fail-fast"
14
+ },
15
+ "notes": {
16
+ "pr_for_branch": "An empty JSON array means no PR exists for the branch.",
17
+ "ci_status": "Exit code 0 = all checks passed, 8 = pending, 1 = at least one failed (gh 2.63).",
18
+ "protection": "Enforce on the base branch via rulesets: require a pull request, require status checks, require the branch to be up to date before merging."
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "gitlab",
3
+ "term": "merge request",
4
+ "cli": "glab",
5
+ "ops": {
6
+ "auth": "glab auth status",
7
+ "pr_for_branch": "glab mr list --source-branch {branch} --all --output json",
8
+ "pr_create_draft": "glab mr create --draft --yes --source-branch {branch} --target-branch {base} --title {title} --description \"$(cat {body_file})\"",
9
+ "pr_view": "glab mr view {pr} --output json",
10
+ "pr_update_body": "glab mr update {pr} --description \"$(cat {body_file})\"",
11
+ "pr_ready": "glab mr update {pr} --ready",
12
+ "ci_status": "glab ci status --branch {branch} --compact",
13
+ "ci_watch": "glab ci status --branch {branch} --live"
14
+ },
15
+ "notes": {
16
+ "pr_for_branch": "An empty JSON array means no MR exists for the branch. The JSON carries state (opened | merged | closed), sha, and web_url.",
17
+ "ci_status": "Read the pipeline status word from the output (success | failed | running | pending); the exit code alone does not encode it in glab 1.50.",
18
+ "protection": "Enforce on the target branch: protected branch (no direct push), merge requests required, pipelines must succeed, and the merge method setting that requires the source to be up to date (fast-forward or semi-linear)."
19
+ }
20
+ }
package/bin/install.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Copies skills into the runtime's skills dir + scaffolds knowledge/.
4
4
  // No MCP, no license, no network calls. The hosted layer is a separate opt-in.
5
5
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
- import { dirname, join } from "node:path";
6
+ import { delimiter, dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
9
  const PKG = join(dirname(fileURLToPath(import.meta.url)), "..");
@@ -45,6 +45,7 @@ What it does:
45
45
  1. Copies the skills into your runtime's skills dir (default: .claude/skills/)
46
46
  2. Scaffolds knowledge/ + core-config.yaml (never overwrites an existing brain)
47
47
  3. Stamps <skills-dir>/.orchestrix-skills.json so upgrades and pruning are exact
48
+ 4. Adds .orchestrate/ to .gitignore (once; ledger and verify logs are per-run state)
48
49
 
49
50
  Hosted orchestration, knowledge hosting, and team features:
50
51
  see https://orchestrix-mcp.youlidao.ai`);
@@ -115,6 +116,18 @@ function installSkills(dir, runtimeName, runtime) {
115
116
  return { count: names.length, pruned };
116
117
  }
117
118
 
119
+ // The ledger and verify logs are per-run, per-workspace state. Committed, they
120
+ // conflict on every PR in team mode, so the ignore line is written once and
121
+ // left alone if the project already has it.
122
+ function ignoreRuntimeDir(dir) {
123
+ const path = join(dir, ".gitignore");
124
+ const current = existsSync(path) ? readFileSync(path, "utf8") : "";
125
+ if (current.split("\n").some((line) => /^\.orchestrate\/?\s*$/.test(line))) return false;
126
+ const separator = current.length === 0 || current.endsWith("\n") ? "" : "\n";
127
+ writeFileSync(path, `${current}${separator}.orchestrate/\n`);
128
+ return true;
129
+ }
130
+
118
131
  function installRuntimeGuidance(dir, runtimeName) {
119
132
  if (runtimeName !== "codex") return;
120
133
  const source = join(PKG, "adapters", "codex", "AGENTS.md");
@@ -160,6 +173,37 @@ function nonemptyFile(path) {
160
173
  return isFile(path) && readFileSync(path, "utf8").trim().length > 0;
161
174
  }
162
175
 
176
+ // Team mode is opt-in: an uncommented `collaboration:` block in core-config.
177
+ // Returns the forge name it declares, or null when the block is absent.
178
+ function activeForge(configContent) {
179
+ const block = configContent.match(/^collaboration:\s*(?:#.*)?\n((?:[ \t]+.*\n?)*)/m);
180
+ if (!block) return null;
181
+ return block[1].match(/^[ \t]+forge:\s*([A-Za-z0-9_-]+)/m)?.[1] ?? "";
182
+ }
183
+
184
+ function forgeCli(forge) {
185
+ const path = join(PKG, "adapters", "forge", `${forge}.json`);
186
+ return isFile(path) ? JSON.parse(readFileSync(path, "utf8")).cli : null;
187
+ }
188
+
189
+ function onPath(bin) {
190
+ const exts = process.platform === "win32" ? ["", ".exe", ".cmd"] : [""];
191
+ return (process.env.PATH ?? "").split(delimiter).some((dir) => dir && exts.some((ext) => isFile(join(dir, bin + ext))));
192
+ }
193
+
194
+ // The pull-request skill drives the forge through its CLI (gh | glab). Without
195
+ // it, builder runs stop at "push the branch, open the PR by hand" — better to
196
+ // hear that from install/doctor than mid-run.
197
+ function forgeCheck(configContent) {
198
+ const forge = activeForge(configContent);
199
+ if (forge === null) return null;
200
+ const cli = forgeCli(forge);
201
+ if (!cli) return { ok: false, detail: `collaboration.forge "${forge}" has no adapter (known: github, gitlab)` };
202
+ return onPath(cli)
203
+ ? { ok: true, detail: `${cli} on PATH (collaboration.forge: ${forge})` }
204
+ : { ok: false, detail: `${cli} not on PATH — team mode needs it installed and logged in (collaboration.forge: ${forge})` };
205
+ }
206
+
163
207
  function validSkill(path, runtimeName, expectedName) {
164
208
  if (!nonemptyFile(path)) return false;
165
209
  const content = readFileSync(path, "utf8");
@@ -203,6 +247,14 @@ function install() {
203
247
  console.log("✓ core-config.yaml scaffolded");
204
248
  }
205
249
 
250
+ // 4. Runtime dir — ledger + verify logs never belong in history.
251
+ if (ignoreRuntimeDir(dir)) console.log("✓ .orchestrate/ added to .gitignore");
252
+ else console.log("• .gitignore already ignores .orchestrate/");
253
+
254
+ // 5. Team mode, if the project turned it on: say now whether the forge CLI is there.
255
+ const forge = forgeCheck(nonemptyFile(cfgTarget) ? readFileSync(cfgTarget, "utf8") : "");
256
+ if (forge) console.log(`${forge.ok ? "✓" : "✗"} forge CLI: ${forge.detail}`);
257
+
206
258
  if (ide === "cursor" || ide === "windsurf") {
207
259
  console.log(`\nNote: ${ide} does not auto-load Anthropic skills yet — copied as reference rules.`);
208
260
  }
@@ -234,6 +286,8 @@ function doctor() {
234
286
  ["core config valid", /(^|\n)knowledge:\s*(#.*)?\n/.test(configContent) && /(^|\n)work:\s*(#.*)?\n/.test(configContent), configPath],
235
287
  ["knowledge brain", isDirectory(join(dir, "knowledge")), join(dir, "knowledge")],
236
288
  ];
289
+ const forge = forgeCheck(configContent);
290
+ if (forge) checks.push(["forge CLI (team mode)", forge.ok, forge.detail]);
237
291
  if (ide === "codex") {
238
292
  const reference = join(dir, ".codex", "orchestrix", "AGENTS.md");
239
293
  const rootInstructions = join(dir, "AGENTS.md");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-skills",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Capability-first AI development skill graph — Anthropic-native skills that run in any agent runtime.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,20 @@ work: # work products — skills WRITE these (outputs:), humans review at gates
18
18
  # here for transparency only.
19
19
  runtime: .orchestrate # ledger + verify logs (ephemeral, gitignored)
20
20
 
21
+ # Team mode (opt-in). Uncomment to make orchestrate run each story on its own
22
+ # branch in its own workspace and end builder runs with a PR/MR instead of a
23
+ # local commit. Absent = solo behaviour, unchanged. See the orchestrate skill,
24
+ # "Collaboration".
25
+ #
26
+ # collaboration:
27
+ # forge: github # github | gitlab
28
+ # base: main
29
+ # branch: story/{origin}/{slug}
30
+ # plan_branch: plan/{origin}
31
+ # workspace: worktree # worktree (same machine) | in-place (a clone elsewhere)
32
+ # pr:
33
+ # reviewers: []
34
+
21
35
  # Org-level cascade (shared taste/standards across many products) is intentionally
22
36
  # NOT enabled yet (YAGNI). When needed: add an `extends:` base that project paths
23
37
  # override. The logical namespaces above do not change when cascade is added.
package/skills/README.md CHANGED
@@ -61,7 +61,7 @@ intent
61
61
  ├─ brainstorm ──(needs facts?)─→ research
62
62
  ├─ (has UI?) ──→ design-directions (human picks a rendered direction) → design-system (once) → design-ui
63
63
  ├─ (arch decision?) ──→ design-architecture
64
- └─ draft-story → implement → run-tests → review-code → commit
64
+ └─ draft-story → implement → run-tests → review-code → commit ──(team mode)─→ pull-request
65
65
  ↑ verify ↑ design-review (UI only)
66
66
  (objective) ↑ accept (batched)
67
67
  ```
@@ -86,6 +86,7 @@ Human gates are front-loaded (planning = direction) and at the very end
86
86
  | `review-code` | build | deferred |
87
87
  | `design-review` | build (UI only) | deferred |
88
88
  | `commit` | build | deferred |
89
+ | `pull-request` | build (team mode) | inline (first open) |
89
90
 
90
91
  ## Attribution
91
92
 
@@ -12,7 +12,7 @@ metadata:
12
12
  reads: [taste/coding-standards, registry/api, registry/db, "front-end-spec?"]
13
13
  outputs: [stories/<slug>.md]
14
14
  authority: "Write one flat story file in the stories namespace (physical path from core-config.yaml; default docs/stories/). No folders. No source code. No production. No spend."
15
- verify: "Every requirement maps to at least one acceptance criterion; constraints are copied verbatim; no placeholders (no TBD/TODO/'handle edge cases')."
15
+ verify: "Every requirement maps to at least one acceptance criterion; constraints are copied verbatim; no placeholders (no TBD/TODO/'handle edge cases'); depends_on names only stories of the same origin whose Interfaces produce what this one consumes; touches equals the File Map."
16
16
  accept:
17
17
  when: "Always — this output sets the direction the whole build rests on."
18
18
  timing: inline
@@ -36,6 +36,8 @@ these sections (with frontmatter) and nothing more.
36
36
  ```markdown
37
37
  ---
38
38
  origin: <stable short handle of the requirement this story came from>
39
+ depends_on: [] # slugs of same-origin stories whose Produces this story Consumes
40
+ touches: [] # every path in File Map (Create + Modify), as written there
39
41
  ---
40
42
 
41
43
  # <Story title>
@@ -85,6 +87,12 @@ What this story deliberately does NOT do.
85
87
  - **Flat, no hierarchy.** One file per story in the stories dir. Grouping is
86
88
  the `origin` field (a query the AI runs), never a folder. Order comes from
87
89
  Interfaces (dependencies), never from a number.
90
+ - **Dependencies and footprint are explicit.** `depends_on` is derived from
91
+ Interfaces: a story that Consumes a signature another same-origin story
92
+ Produces depends on it — nothing else qualifies. `touches` repeats the
93
+ File Map so a team run can detect two stories that would edit the same
94
+ file without opening either. Both are empty lists when there is nothing
95
+ to list, never omitted.
88
96
  - **No placeholders.** "Add validation", "handle errors", "TBD" are failures.
89
97
  State the actual condition.
90
98
  - **Acceptance criteria are observable.** "Works well" is not an AC. "Returns
@@ -102,6 +110,8 @@ What this story deliberately does NOT do.
102
110
  2. Are all hard constraints copied verbatim, not paraphrased?
103
111
  3. Any placeholder text? Replace with the real condition.
104
112
  4. Do the names in Interfaces match across sections?
113
+ 5. Does `depends_on` name exactly the same-origin stories whose Produces
114
+ this story Consumes, and does `touches` equal the File Map?
105
115
 
106
116
  ## Done
107
117
 
@@ -4,9 +4,9 @@ description: Use when a goal must be delivered end-to-end by composing skills, w
4
4
  license: MIT
5
5
  allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, Task, Skill, Artifact]
6
6
  metadata:
7
- version: 7
7
+ version: 8
8
8
  requires:
9
- capabilities: [filesystem.read, filesystem.write, shell.execute, "agent.spawn?", "design.canvas?"]
9
+ capabilities: [filesystem.read, filesystem.write, shell.execute, "agent.spawn?", "design.canvas?", "forge.pr?"]
10
10
  model: frontier
11
11
  contract:
12
12
  inputs: [intent, "constraints?"]
@@ -146,6 +146,121 @@ CANNOT reach — wire these by rule, not by match:
146
146
  gate, hand the human's pick to `design-system` as `chosen_direction`,
147
147
  then `design-ui`.
148
148
  A direction the human has not seen rendered is not a direction.
149
+ 3. **An epic is accepted on base, not story by story.** In collaboration
150
+ mode (below), when every story of an origin is `done`, offer the
151
+ integration run: on base, `smoke-test` with flows from the spec's
152
+ acceptance criteria, final acceptance against the original intent, then
153
+ `deploy` if asked. N green PRs prove N stories; they do not prove the
154
+ composition. No output→input match reaches this step.
155
+
156
+ ## Collaboration (team mode, opt-in)
157
+
158
+ Enabled only when `core-config.yaml` has a `collaboration:` block. Without it,
159
+ nothing in this section applies and the run behaves exactly as described
160
+ above. With it, the same loop runs; what changes is where a run lives (a
161
+ branch in its own workspace), how a run ends (a PR/MR, not a local commit),
162
+ and one preflight.
163
+
164
+ ```yaml
165
+ collaboration:
166
+ forge: github # github | gitlab — op table in the pull-request skill (and adapters/forge/<forge>.json)
167
+ base: main
168
+ branch: story/{origin}/{slug} # one story, one branch, one run
169
+ plan_branch: plan/{origin} # planning runs (specs, stories, knowledge)
170
+ workspace: worktree # worktree (same machine) | in-place (a clone elsewhere)
171
+ pr:
172
+ reviewers: []
173
+ ```
174
+
175
+ **Principle: no new shared state.** Which stories are open, claimed, in
176
+ review, or done is derived from git and the forge every time it is needed,
177
+ never written to a board file that would need its own merge strategy.
178
+
179
+ | Story state | Derived from |
180
+ | --- | --- |
181
+ | `open` | no remote branch for the story (`git ls-remote --heads origin <branch>` is empty) and no merged PR/MR |
182
+ | `claimed` | remote branch exists, no PR/MR (`pr_for_branch` is empty) |
183
+ | `in_review` | an open PR/MR for the branch |
184
+ | `done` | the forge reports a merged PR/MR for the branch (squash merges leave no ancestry, so `git branch --merged` cannot be the source) |
185
+ | `blocked` | any story in `depends_on` is not `done` |
186
+ | `stale` | `claimed`, and the branch's last commit is older than 7 days |
187
+ | `unknown` | the forge could not be queried — never treated as `open` |
188
+
189
+ ### Preflight (deterministic, after binding intent)
190
+
191
+ 1. `git fetch origin`, then bring the base checkout up to date with
192
+ `git merge --ff-only origin/<base>`. If that fails, stop: the checkout
193
+ has diverged from base and the brain it would read is stale.
194
+ 2. Run the forge `auth` op (`gh auth status` | `glab auth status`; the full
195
+ op table is in the `pull-request` skill). Not authenticated → stop;
196
+ logging in is the human's act on this machine.
197
+ 3. `git worktree prune`, then remove worktrees whose story is `done`.
198
+ 4. `.orchestrate/` must be ignored (`git check-ignore -q .orchestrate`). If
199
+ not, add the line to `.gitignore` before anything else — a committed
200
+ ledger conflicts on every PR.
201
+
202
+ ### Planning runs end in a planning PR
203
+
204
+ A run that wires any skill writing to the specs, stories, or knowledge
205
+ namespaces (`brainstorm`, `research`, `design-*`, `draft-story`,
206
+ `map-codebase`) runs on `plan_branch` and ends with `commit` →
207
+ `pull-request`. The backlog exists for the team only once that PR is merged
208
+ to base: a story that is not on base cannot be claimed. Do not build in the
209
+ same run that planned; the builder run starts from base.
210
+
211
+ ### Builder runs: pick → workspace → claim → build → pull-request
212
+
213
+ 1. **Pick.** Compute the board for the origin. The intent names a story, or
214
+ you propose the first `open`, not `blocked` story in dependency order.
215
+ Two `open` stories whose `touches` globs overlap are serialized: name the
216
+ conflict and pick only one. The human confirms the pick at the front gate
217
+ you already hold. Picking is a human act; there is no assign skill.
218
+ 2. **Workspace.** Create the branch without checking it out, then give it a
219
+ working tree. Order matters: `git worktree add` refuses a branch that is
220
+ already checked out anywhere, so the branch must not be switched to in
221
+ the base checkout first.
222
+
223
+ ```
224
+ git branch <branch> origin/<base>
225
+ git worktree add ../<repo>--<slug> <branch> # workspace: worktree
226
+ git switch <branch> # workspace: in-place
227
+ ```
228
+
229
+ From here on, the workspace directory is the working directory for every
230
+ dispatch and every namespace resolution. One story per workspace is
231
+ enforced by git itself, not by this skill.
232
+ 3. **Claim — atomic on the remote.** Inside the workspace, make one empty
233
+ commit (`git commit --allow-empty -m "chore(story): claim <slug>"`) so
234
+ the claim has a SHA and an author, then push it so that the push fails
235
+ if the branch already exists on the remote:
236
+
237
+ ```
238
+ git push -u --force-with-lease=refs/heads/<branch>: origin <branch>
239
+ ```
240
+
241
+ The empty expectation after the colon means "the ref must not exist". A
242
+ rejected push (`stale info`) means someone else claimed it: remove the
243
+ workspace (`git worktree remove`, `git branch -D`), recompute the board,
244
+ pick again. Only after the push succeeds, write `run_start` followed by
245
+ `workspace` in the workspace's own `.orchestrate/ledger.jsonl`.
246
+ 4. **Build — unchanged.** The loop above, gated by `verify`. Namespace paths
247
+ resolve inside the workspace.
248
+ 5. **Tail.** `commit` → `pull-request`. Its `ci_status` is a verify result:
249
+ `red` re-enters the rework loop with the failing job's log as
250
+ `qa_feedback` (investigate first when the cause is not understood; the
251
+ 3-attempt cap applies); `pending` at the ceiling is a gate, not a pass.
252
+ 6. **Deliver** when the PR/MR is open and `ci_status` is `green`. Write
253
+ `run_end` with the `pr` URL. Merging is the reviewer's act in the forge;
254
+ this run does not wait for it.
255
+
256
+ ### Parallel hazards on one machine
257
+
258
+ Two workspaces sharing one local database, port, or container project
259
+ corrupt each other's `smoke-test` and `design-review`. Unless `registry/app`
260
+ declares the app parallel-safe (per-workspace database name, port from
261
+ `$PORT` or `0`, distinct compose project), run those two skills under a lock
262
+ file in the git common dir (`git rev-parse --git-common-dir`) so only one
263
+ workspace drives an app at a time.
149
264
 
150
265
  ## Accept gate
151
266
 
@@ -233,11 +348,12 @@ Events and when to write them:
233
348
 
234
349
  | Event | When | Shape |
235
350
  | ----- | ---- | ----- |
236
- | `run_start` | right after binding intent | `{"e":"run_start","run":"r-<yyyymmdd>-<slug>","intent":"...","ts":"..."}` |
351
+ | `run_start` | right after binding intent (in collaboration mode: as the first line of the workspace's ledger) | `{"e":"run_start","run":"r-<yyyymmdd>-<slug>","intent":"...","actor":"<git user.email>","ts":"..."}` |
352
+ | `workspace` | collaboration mode only, right after `run_start` | `{"e":"workspace","run":"...","branch":"story/<origin>/<slug>","path":"<absolute workspace path>","base":"<origin/base sha at claim>","ts":"..."}` |
237
353
  | `plan` | after wiring the graph, and EVERY time the graph changes | `{"e":"plan","run":"...","steps":[{"n":1,"skill":"research","title":"..."}, …]}` — full current plan; latest `plan` line wins; steps may be added, never removed |
238
354
  | `step` | immediately BEFORE each dispatch, and again after its verify | `{"e":"step","run":"...","n":3,"skill":"implement","status":"dispatched\|done\|failed\|skipped","attempt":1,"model":"<resolved model, or session>","evidence":"<file or one-line result>","ts":"..."}` — rework = same `n`, next `attempt`; a step a replan made obsolete gets `skipped` with the reason in `evidence` (plan lines are never removed, so this is how an obsolete step closes) |
239
355
  | `gate` | when stopping at a human gate | `{"e":"gate","run":"...","kind":"inline_accept","question":"...","shows":"<artboard URL or path, visual gates only>","ts":"..."}` |
240
- | `run_end` | at delivery or abandonment | `{"e":"run_end","run":"...","result":"delivered\|paused\|abandoned","ts":"..."}` |
356
+ | `run_end` | at delivery or abandonment | `{"e":"run_end","run":"...","result":"delivered\|paused\|abandoned","pr":"<PR/MR URL, collaboration mode only>","ts":"..."}` |
241
357
 
242
358
  A step recorded `done` is done — do not re-dispatch it. `evidence` on a `done`
243
359
  step is required and should be the step's verify log path
@@ -261,6 +377,11 @@ session, or a wake-up — do NOT continue from what you remember. Replay:
261
377
  Fail → re-dispatch as the next attempt.
262
378
  4. Continue the loop from the first open step. If the run was stopped at a
263
379
  `gate`, re-ask that gate's question — never assume it was answered.
380
+ 5. In collaboration mode, the `workspace` line says which branch and
381
+ directory the run lives in. Resume from inside that workspace; the base
382
+ checkout's ledger never holds a builder run. A `workspace` line whose
383
+ path no longer exists means the worktree was removed: the run is
384
+ `abandoned`, write `run_end` and say so.
264
385
 
265
386
  ## Context discipline (stay lean)
266
387
 
@@ -312,3 +433,10 @@ implementer missed; mechanical skills are `cheap`.
312
433
  - Running a `frontier` step on a cheaper model without recording it in the
313
434
  ledger
314
435
  - Marking the run complete without every step's `verify` evidence
436
+ - Collaboration mode: building on base, or in the same run that planned
437
+ - Collaboration mode: claiming with a plain push (two claims both succeed),
438
+ or switching to the story branch before `git worktree add` (which then
439
+ refuses it)
440
+ - Collaboration mode: treating `ci_status: pending` or `unknown` as green, or
441
+ delivering a builder run with no `pr` on `run_end`
442
+ - Collaboration mode: two workspaces driving one local app with no lock
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: pull-request
3
+ description: Use when a verified story branch (or a planning branch) must be published for team review as a pull request (GitHub) or merge request (GitLab), and its CI result pulled back into the run. Never merges. Not for solo runs with no collaboration block.
4
+ license: MIT
5
+ allowed-tools: [Read, Bash]
6
+ metadata:
7
+ requires:
8
+ capabilities: [filesystem.read, shell.execute, forge.pr]
9
+ model: cheap
10
+ contract:
11
+ inputs: [branch, story, verification_report, "ac_traceability?", "review_report?", "smoke_report?", "design_review_report?", "pr?"]
12
+ reads: [core-config]
13
+ outputs: [pr_url, ci_status]
14
+ authority: "Rebase the current branch onto base, push it, and open or update ONE pull/merge request against base. Never merge, never push to base, never force-push over commits you did not make (--force-with-lease only), never change forge settings."
15
+ verify: "The forge returns a PR/MR for the branch whose head SHA equals local HEAD; ci_status is green, red, or pending — never unknown; the body lists every acceptance criterion with its evidence."
16
+ accept:
17
+ when: "The first open of a PR/MR — the human approves title, body, and reviewers once. Later updates to the same PR/MR need no gate."
18
+ timing: inline
19
+ ---
20
+
21
+ # Pull Request
22
+
23
+ Publish a verified branch for team review. A pull request (GitHub) and a merge
24
+ request (GitLab) are the same object: a request to merge one branch into
25
+ another with review and CI attached. This skill speaks to whichever forge
26
+ `core-config.yaml` names and treats the two identically.
27
+
28
+ **Core principle:** The PR body is the run's evidence, made portable. Verify
29
+ logs live in `.orchestrate/`, which never leaves the machine. A reviewer on
30
+ another machine sees only what this skill puts in the body — so the body is
31
+ assembled from the run's files, never from memory.
32
+
33
+ ## Preconditions — verify each now
34
+
35
+ 1. `core-config.yaml` has a `collaboration:` block. Absent → this skill does
36
+ not apply; stop and say so.
37
+ 2. The forge CLI is authenticated: run the `auth` op. Not authenticated →
38
+ stop; authentication is the human's act on this machine.
39
+ 3. The current branch matches `collaboration.branch` (story) or
40
+ `collaboration.plan_branch` (planning), and is not `collaboration.base`.
41
+ 4. `verification_report` is a fresh green run — not a remembered one.
42
+
43
+ ## Forge operations
44
+
45
+ `collaboration.forge` selects the column. Substitute `{base}`, `{branch}`,
46
+ `{pr}`, `{title}`, `{body_file}`. The same table ships as
47
+ `adapters/forge/<forge>.json` for tools that read it as data; the two are
48
+ kept identical by test.
49
+
50
+ | Op | GitHub (`gh`) | GitLab (`glab`) |
51
+ | --- | --- | --- |
52
+ | `auth` | `gh auth status` | `glab auth status` |
53
+ | `pr_for_branch` | `gh pr list --head {branch} --state all --json number,url,state,isDraft,headRefOid,mergedAt` | `glab mr list --source-branch {branch} --all --output json` |
54
+ | `pr_create_draft` | `gh pr create --draft --base {base} --head {branch} --title {title} --body-file {body_file}` | `glab mr create --draft --yes --source-branch {branch} --target-branch {base} --title {title} --description "$(cat {body_file})"` |
55
+ | `pr_view` | `gh pr view {pr} --json number,url,state,isDraft,headRefOid,baseRefName` | `glab mr view {pr} --output json` |
56
+ | `pr_update_body` | `gh pr edit {pr} --body-file {body_file}` | `glab mr update {pr} --description "$(cat {body_file})"` |
57
+ | `pr_ready` | `gh pr ready {pr}` | `glab mr update {pr} --ready` |
58
+ | `ci_status` | `gh pr checks {pr}` | `glab ci status --branch {branch} --compact` |
59
+ | `ci_watch` | `gh pr checks {pr} --watch --fail-fast` | `glab ci status --branch {branch} --live` |
60
+
61
+ Reading results: an empty JSON array from `pr_for_branch` means no PR/MR
62
+ exists. `gh pr checks` exits 0 when every check passed, 8 while checks are
63
+ pending, 1 when one failed. `glab ci status` encodes the pipeline state in
64
+ its output words (`success`, `failed`, `running`, `pending`), not in its
65
+ exit code — read the text.
66
+
67
+ An unknown forge name, or a CLI missing from PATH → push the branch, print
68
+ the URL a human would use to open the PR/MR, and return `ci_status:
69
+ unknown`. That fails this skill's verify on purpose: the run stops at a gate
70
+ instead of pretending.
71
+
72
+ ## Process
73
+
74
+ 1. **Sync with base.** `git fetch origin`, then rebase onto
75
+ `origin/<base>`. A conflict outside the knowledge namespace → stop and
76
+ hand the conflict to the human; do not resolve source conflicts. A
77
+ conflict inside the knowledge namespace is resolved by **keyed union**:
78
+ keep every entry from both sides, matching on the entry's key (`id` for
79
+ taste rows, `path` for registry endpoints, the ADR id for decisions),
80
+ prefer the base side when the same key differs, and list what you merged
81
+ in the body under Brain changes.
82
+ 2. **Re-verify on the rebased tree.** Run the project's test command fresh
83
+ and capture it to `.orchestrate/verify/pull-request-tests.log`. Red →
84
+ return `ci_status: red` with the failing output as the reason; do not
85
+ push a red branch.
86
+ 3. **Push** with `git push --force-with-lease origin <branch>`.
87
+ `--force-with-lease` protects commits you did not make; plain `--force`
88
+ is forbidden.
89
+ 4. **Find or create.** `pr_for_branch`. None → assemble the body (below),
90
+ write it to `.orchestrate/pr-body.md`, then stop for the inline gate:
91
+ show title, body, and reviewers; create as draft only on sign-off.
92
+ Exists → `pr_update_body` with the re-assembled body; no gate.
93
+ 5. **Wait for CI, bounded.** `ci_watch` with a ceiling of about 15 minutes.
94
+ Green → `pr_ready`, return `ci_status: green`. Red → return
95
+ `ci_status: red` with the failing job name and its log tail as the
96
+ reason. Still pending at the ceiling → return `ci_status: pending`.
97
+ 6. **Verify.** `pr_view`: head SHA must equal `git rev-parse HEAD`. Mismatch
98
+ means the push did not land or someone else pushed; report it as a
99
+ failure, never as done.
100
+
101
+ ## The body
102
+
103
+ Every section is filled from a file the run produced. A missing input leaves
104
+ its section reading `not run this run`, never blank and never invented.
105
+
106
+ ```markdown
107
+ # <story title>
108
+
109
+ `<branch>` → `<base>` · story: `<stories path>` · origin: `<origin>`
110
+
111
+ ## Acceptance criteria → evidence
112
+ | AC | Code | Test | Verify log |
113
+ | --- | --- | --- | --- |
114
+ | AC1 <text> | src/x.ts:42-58 | tests/x.test.ts 'rejects empty email' | step-4-attempt-1.log · exit 0 |
115
+
116
+ ## Verification
117
+ - run-tests: <passed>/<failed> · `<command>`
118
+ - smoke-test: <passed> passed · <failed> failed · <untested> untested (<reasons>)
119
+ - review-code: <verdict>; findings routed back: <n>
120
+ - design-review: <verdict> · coverage <full|partial> (<untested checks>)
121
+
122
+ ## Brain changes — review these; a bad lesson contaminates every later run
123
+ - taste/<file>: <+n rows | none>
124
+ - registry/<file>: <+n entries | none>
125
+ - architecture/decisions: <ids added | none>
126
+
127
+ ## Run
128
+ actor <email> · run <id> · attempts per step: <n:k, …> · models: <skill=model, …>
129
+ ```
130
+
131
+ Title: the story title, prefixed with the conventional-commit type of the
132
+ dominant change (`feat:`, `fix:`, …). Reviewers: `collaboration.pr.reviewers`.
133
+
134
+ ## Output
135
+
136
+ ```yaml
137
+ pr_url: https://…
138
+ ci_status: green # green | red | pending | unknown
139
+ reason: "" # required when red or unknown: failing job + log tail, or what is missing
140
+ head: <sha>
141
+ ```
142
+
143
+ ## Red flags — stop
144
+
145
+ - Pushing a branch whose fresh test run is red
146
+ - `git push --force` without `--force-with-lease`
147
+ - Resolving a source-code conflict yourself during the rebase
148
+ - A body section written from memory instead of from the run's files
149
+ - Creating the PR/MR before the inline gate on first open
150
+ - Reporting `green` from the CI tool's summary without reading which checks ran
151
+ - Merging, or marking ready while CI is red or pending
152
+
153
+ ## Done
154
+
155
+ PR/MR exists, head SHA matches local HEAD, `ci_status` is known. Return
156
+ `pr_url` and `ci_status`; the orchestrator records `pr_url` on `run_end` and
157
+ routes a `red` result into the rework loop.