@openthink/team 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # open-team
2
2
 
3
- Source-agnostic vault-driven role pipeline for spawning Claude agents against tickets. Lifts the "Assign to agent" + role-pipeline flow out of agentic-desktop's Swift code into a standalone npm CLI.
3
+ Source-agnostic workspace-driven role pipeline for spawning Claude agents against tickets. Lifts the "Assign to agent" + role-pipeline flow out of agentic-desktop's Swift code into a standalone npm CLI.
4
4
 
5
5
  ## Install
6
6
 
@@ -42,7 +42,7 @@ Flags:
42
42
 
43
43
  ## Workspace setup
44
44
 
45
- `open-team` reads from a workspace directory ("vault" is Obsidian's word; oteam doesn't depend on Obsidian). Layout:
45
+ `open-team` reads from a workspace directory (no Obsidian required). Layout:
46
46
 
47
47
  ```
48
48
  openteam/
@@ -56,7 +56,7 @@ openteam/
56
56
 
57
57
  A ticket's `state:` frontmatter must always match its containing folder under `tickets/`.
58
58
 
59
- For the simplest single-workspace setup, run `oteam init` (creates and registers `~/openteam/`). To use an existing tree, register it via `oteam config vault add <path>` or set `PRODUCT_VAULT_PATH`. For multiple workspaces (personal + work, etc.) see [Config & multiple vaults](#config--multiple-vaults).
59
+ For the simplest single-workspace setup, run `oteam init` (creates and registers `~/openteam/`). To use an existing tree, register it via `oteam config workspace add <path>` or set `PRODUCT_VAULT_PATH`. For multiple workspaces (personal + work, etc.) see [Config & multiple workspaces](#config--multiple-workspaces).
60
60
 
61
61
  ## Subcommands
62
62
 
@@ -65,12 +65,11 @@ oteam pull <source> <ref> # ingest external item → tickets/triage/
65
65
  oteam pull --project <name> ... # tag the new ticket with a project
66
66
  oteam assign <ticket-or-id> # drive role pipeline (full path or AGT-NNN)
67
67
  oteam assign --inline <path> # … or run inline in current terminal
68
- oteam assign --no-stamp <id> # one-shot override of stamp.enforce (clones from GitHub)
69
68
  oteam list [--state <state>] # list active tickets
70
69
  oteam list --project <name> # filter by project frontmatter
71
- oteam archive <ticket-id> # move done ticket to archive/YYYY-MM/
72
- oteam config vault add <path> # register a vault under a name
73
- oteam config vault list # show registered vaults + default
70
+ oteam archive <ticket-id> # move done ticket to archive/YYYY-MM/ + reap workspace
71
+ oteam config workspace add <path> # register a workspace under a name
72
+ oteam config workspace list # show registered workspaces + default
74
73
  oteam config stamp set --host <url> # configure stamp host post-init
75
74
  oteam config stamp set --enforce on # require repos be stamp-registered
76
75
  oteam config stamp clear # remove the stamp block
@@ -84,7 +83,7 @@ oteam telemetry summary [--days N] [--phase X] [--model Y]
84
83
  oteam telemetry tail [-n 20] # last N telemetry lines (raw JSONL)
85
84
  ```
86
85
 
87
- Most commands accept `--vault <name-or-path>` to operate on a specific vault.
86
+ Most commands accept `--workspace <name-or-path>` (or the back-compat alias `--vault`) to operate on a specific workspace.
88
87
 
89
88
  ### Tagging tickets by project
90
89
 
@@ -141,7 +140,7 @@ Where the clone comes from is governed by oteam config (`~/.open-team/config.jso
141
140
 
142
141
  | `stamp` config | Mode | Clone source | Behaviour |
143
142
  |------------------------------------------------|-----------|--------------------------------------------------------|-------------------------------------------------------------------------------------------------|
144
- | absent / `null` | no-stamp | `git@github.com:<repo>.git` | Default. No stamp config files are read. `oteam` works against any git repo. |
143
+ | absent / `null` | plain | `git@github.com:<repo>.git` | Default. No stamp config files are read. `oteam` works against any git repo. |
145
144
  | `{ host, enforce: false }` | soft | `git@github.com:<repo>.git` | Stamp host is recorded for tooling that asks for it; `oteam assign` does not gate. |
146
145
  | `{ host, enforce: true }` | enforce | `<host>/srv/git/<basename>.git` (the stamp server) | The clone IS the gate: clone failure exits non-zero before any spawn. AGT-050 behaviour. |
147
146
 
@@ -157,11 +156,7 @@ oteam config stamp set --enforce off # … or back off
157
156
  oteam config stamp clear # remove the stamp block entirely
158
157
  ```
159
158
 
160
- `oteam assign --no-stamp` is a per-run override: it forces the github clone path even when `stamp.enforce: true` is set. The persistent setting is `oteam config stamp set --enforce off`; `--no-stamp` is convenient when you want to spawn a one-off agent without touching config.
161
-
162
- > **Migration note.** Earlier `oteam` builds read `~/.stamp/server.yml` directly. This version does not — to keep the AGT-050 stamp gate in place after upgrade, run `oteam init` and paste the host (or `oteam config stamp set --host <url> --enforce on`).
163
-
164
- Stale workspaces from prior assigns are GC'd at spawn time: any `/tmp/open-team-issues/agt-N/` directory whose ticket id has no matching ticket in the active vault is `rm -rf`'d before the new clone. The current run's workspace is also `rm -rf`'d before its clone, so re-assigns are hermetic.
159
+ Stale workspaces from prior assigns are GC'd at spawn time: any `/tmp/open-team-issues/agt-N/` directory whose ticket id has no matching ticket in the active workspace is `rm -rf`'d before the new clone. The current run's workspace is also `rm -rf`'d before its clone, so re-assigns are hermetic.
165
160
 
166
161
  ## Per-phase model selection
167
162
 
@@ -225,33 +220,61 @@ Knobs:
225
220
  - `oteam config telemetry set off` opts out — no JSONL writes occur. Re-enable with `oteam config telemetry set on`. Default is on.
226
221
  - Telemetry is best-effort: a write failure (read-only dir, missing session file, malformed log) writes one line to stderr and does not fail the role-pipeline phase. Token fields the agent SDK doesn't expose are omitted from the line rather than recorded as zero.
227
222
 
228
- ## Config & multiple vaults
223
+ ## Config & multiple workspaces
229
224
 
230
- `open-team` supports any number of named vaults via `~/.open-team/config.json`. Register them with:
225
+ `open-team` supports any number of named workspaces via `~/.open-team/config.json`. The on-disk key is `vaults` for back-compat with earlier builds; conceptually these are workspaces. Register them with:
231
226
 
232
227
  ```sh
233
- oteam config vault add ~/Documents/product-vault # auto-name "product-vault"; first add becomes default
234
- oteam config vault add ~/Documents/work-vault --name work
235
- oteam config vault list
236
- oteam config vault default --set work
237
- oteam config vault remove work # clears default if it pointed here
228
+ oteam config workspace add ~/Documents/my-workspace # auto-name "my-workspace"; first add becomes default
229
+ oteam config workspace add ~/Documents/work-workspace --name work
230
+ oteam config workspace list
231
+ oteam config workspace default --set work
232
+ oteam config workspace remove work # clears default if it pointed here
238
233
  ```
239
234
 
240
- Paths are resolved to absolute at `add` time, so the registration survives `cd`. Removing the default vault clears `default` and forces an explicit `--vault` on every subsequent command until you set a new one — there is no silent promotion.
235
+ The `oteam config vault ...` form also works as a silent back-compat alias (`oteam config vault add` and `oteam config workspace add` register the same thing).
236
+
237
+ Paths are resolved to absolute at `add` time, so the registration survives `cd`. Removing the default workspace clears `default` and forces an explicit `--workspace` on every subsequent command until you set a new one — there is no silent promotion.
241
238
 
242
239
  ### Resolution precedence (most-specific wins)
243
240
 
244
- | # | Source | Notes |
245
- |---|---------------------------------------------------|------------------------------------------------------|
246
- | 1 | `--vault <name-or-path>` flag | Per-command override |
247
- | 2 | `PRODUCT_VAULT_PATH` env var | One-off shell override; also propagated to spawns |
248
- | 3 | `default` in `~/.open-team/config.json` | Set via `oteam config vault default --set <name>` |
249
- | 4 | `~/Documents/product-vault` | Implicit fallback if no config exists |
241
+ | # | Source | Notes |
242
+ |---|-----------------------------------------------------------|---------------------------------------------------------------|
243
+ | 1 | `--workspace <name-or-path>` flag (or `--vault` alias) | Per-command override |
244
+ | 2 | `PRODUCT_VAULT_PATH` env var | One-off shell override; also propagated to spawns |
245
+ | 3 | `default` in `~/.open-team/config.json` | Set via `oteam config workspace default --set <name>` |
246
+ | 4 | `~/Documents/product-vault` | Implicit fallback if no config exists |
250
247
 
251
248
  `oteam assign` adds two niceties on top:
252
249
 
253
- - **AGT-NNN shorthand**: `oteam assign AGT-001` walks `<vault>/tickets/<state>/` for a file whose basename starts with `AGT-001-`.
254
- - **Vault auto-detection from path**: passing a full path that lives inside a registered vault root makes that vault the active one for the run, even if it's not the default. The spawned `_role-run` then inherits `PRODUCT_VAULT_PATH=<that-vault>` so any follow-up `oteam pull/list/...` from the agent lands in the same vault.
250
+ - **AGT-NNN shorthand**: `oteam assign AGT-001` walks `<workspace>/tickets/<state>/` for a file whose basename starts with `AGT-001-`.
251
+ - **Workspace auto-detection from path**: passing a full path that lives inside a registered workspace root makes that workspace the active one for the run, even if it's not the default. The spawned `_role-run` then inherits `PRODUCT_VAULT_PATH=<that-workspace>` so any follow-up `oteam pull/list/...` from the agent lands in the same workspace.
252
+
253
+ ## Claim-on-assign (preventing double-pickup)
254
+
255
+ When multiple operators or agents work the same workspace, two of them can race on the same ticket — both run `oteam assign` and both spin up role pipelines against the same GitHub issue. To prevent that, `oteam assign` can claim the underlying GH issue (sets `assignees`) before driving the pipeline:
256
+
257
+ ```sh
258
+ oteam config bot-identity set <github-login> # e.g. your own login, or a dedicated bot account
259
+ oteam config bot-identity show
260
+ oteam config bot-identity clear # disable claim-on-assign
261
+ ```
262
+
263
+ When `botIdentity` is set, every `oteam assign` run on a github-sourced ticket does the following pre-flight before any expensive setup:
264
+
265
+ 1. GET the issue from `source.url`.
266
+ 2. If state is `closed` → exit 1 (refuses to drive the pipeline on resolved work).
267
+ 3. If already assigned to someone other than `botIdentity` → exit 1 (someone else has it).
268
+ 4. PATCH `assignees: [botIdentity]`. Re-read the response.
269
+ 5. If assignees came back empty, the operator's `gh` token has no push access on the repo (GitHub silently drops assignee changes without it) → exit 1 with a hint.
270
+ 6. If assignees != `[botIdentity]` (race with another writer) → exit 1.
271
+ 7. Otherwise the claim is held; proceed.
272
+
273
+ When `botIdentity` is empty (the default), the pre-flight is skipped — backwards-compatible with configs that predate the field.
274
+
275
+ Per-invocation override: `OTEAM_BOT_IDENTITY=<login> oteam assign …`. Useful when one operator runs occasionally under a different identity (e.g. testing with a personal account) without rewriting the persisted config.
276
+
277
+ Manual tickets (`source.type: manual`) and tickets with no parseable GH URL skip the claim entirely — there's nothing to claim.
255
278
 
256
279
  ## Migration from agentic-desktop
257
280
 
@@ -260,7 +283,7 @@ agentic-desktop now keeps only the PR-side modules (`GitHubPRs`, `AIReview*`, `C
260
283
  Migration steps:
261
284
 
262
285
  1. `npm install -g @openthink/team` (or `npm link` from a local clone).
263
- 2. Either set `PRODUCT_VAULT_PATH` if your vault isn't at `~/Documents/product-vault`, or register it via `oteam config vault add <path>` (see [Config & multiple vaults](#config--multiple-vaults)).
286
+ 2. Either set `PRODUCT_VAULT_PATH` if your workspace isn't at `~/Documents/product-vault`, or register it via `oteam config workspace add <path>` (see [Config & multiple workspaces](#config--multiple-workspaces)).
264
287
  3. Optionally set `OTEAM_MONITORED_ORGS=Org1,Org2` to route those repos' tickets to the "work" kitty socket (preserves the personal/work split agentic-desktop had).
265
288
  4. Delete `~/Library/Application Support/AgenticDesktop/vault-assignments.json` (panel-indicator state, no longer used).
266
289
  5. Use `oteam pull github <ref>` instead of clicking "Assign to agent" on the Issues panel.
@@ -13,7 +13,7 @@ You are working a `product-vault` ticket. The user invoked `oteam assign <path>`
13
13
  3. **No commits, no PRs, no Linear.** Vault tickets do not necessarily map to a code repo. Only act on code if the ticket's `repo:` field is set AND the work demands it.
14
14
  4. **STOP at every role-handoff boundary.** When your role is done, write a STOP marker (visual banner per Output discipline below) and let the human decide whether to continue.
15
15
  5. **3-attempt cap on any failing operation.** If a step fails (e.g., file mv fails, frontmatter parse fails, build/test fails), you get 3 tries before STOPPing.
16
- 6. **Never read or write inside `$HOME/Development/<repo>`.** That tree may have uncommitted in-flight work; entangling with it is a sterile-field violation. For repo-bound tickets, the `oteam` runner has already prepared an isolated agent worktree at `/tmp/open-team-issues/<ticket-id-lowercased>/repo` and spawned you cd'd into it — that's your only valid working directory. The runner clones from the stamp server when `stamp.enforce: true` is set in `~/.open-team/config.json`, otherwise from GitHub directly; either way, the worktree is isolated from your primary, so AC-shaped requirements like "primary's `git remote -v` is byte-equal before/after a spawn" are satisfied by construction. If your `$PWD` is not the prepared workspace (e.g. you invoked the slash command by hand outside of `oteam assign`), set up the workspace yourself before reading any repo file — see Phase 3 Step 0.
16
+ 6. **Never read or write inside `$HOME/Development/<repo>`.** That tree may have uncommitted in-flight work; entangling with it is a sterile-field violation. For repo-bound tickets, the `oteam` runner has already prepared an isolated agent worktree at `/tmp/open-team-issues/<ticket-id-lowercased>/repo` and spawned you cd'd into it — that's your only valid working directory. The runner clones from the URI recorded for the repo in `~/.open-team/config.json` and sets your cwd to it; the worktree is isolated from your primary, so AC-shaped requirements like "primary's `git remote -v` is byte-equal before/after a spawn" are satisfied by construction. If your `$PWD` is not the prepared workspace (e.g. you invoked the slash command by hand outside of `oteam assign`), set up the workspace yourself before reading any repo file — see Phase 3 Step 0.
17
17
 
18
18
  ## Phase 0 — Read the ticket
19
19
 
@@ -65,9 +65,9 @@ If your appended system context flags the **AGT-107 haiku-downshift heuristic**
65
65
 
66
66
  ## Phase 3 — Engineering agent (state: refined → spike phase)
67
67
 
68
- **Step 0 — Workspace is already prepared.** When `oteam assign` spawned you against a repo-bound ticket, it already cloned `/tmp/open-team-issues/<ticket-id-lowercased>/repo` (from the stamp server when `stamp.enforce: true` is set in `~/.open-team/config.json`; from GitHub otherwise, or when `--no-stamp` was passed) and set your cwd to it. Confirm with `pwd` and `git remote -v`; for stamp-governed repos you should see exactly one remote, `origin`, pointing at `ssh://git@<stamp-host>:<port>/srv/git/<basename>.git`.
68
+ **Step 0 — Workspace is already prepared.** When `oteam assign` spawned you against a repo-bound ticket, it already cloned `/tmp/open-team-issues/<ticket-id-lowercased>/repo` from the URI recorded for the repo in `~/.open-team/config.json` and set your cwd to it. Confirm with `pwd` and `git remote -v`; you should see exactly one remote, `origin`, pointing at the URI recorded for this repo.
69
69
 
70
- Cost trade: a fresh stamp clone adds a few seconds vs. the older `git worktree add` fast path. That's an intentional trade for AC-grade isolation — the agent worktree shares no `.git/objects` and no remotes with your primary, and removing or renaming any remote inside the worktree cannot leak back to your daily flow.
70
+ Cost trade: a fresh clone per assign adds a few seconds vs. the older `git worktree add` fast path. That's an intentional trade for AC-grade isolation — the agent worktree shares no `.git/objects` and no remotes with your primary, and removing or renaming any remote inside the worktree cannot leak back to your daily flow.
71
71
 
72
72
  If you invoked `/assign-ticket` by hand (no `oteam assign` wrapper) and the workspace doesn't exist yet, set it up the same way the runner would:
73
73
 
@@ -77,19 +77,19 @@ WORKSPACE="/tmp/open-team-issues/$TICKET_ID_LC"
77
77
  # REPO_SLUG is "<owner>/<name>" from `repo:` if set, else inferred from the
78
78
  # ticket. When inferring, name it explicitly in your spike notes so the human
79
79
  # can correct you.
80
- REPO_BASE=$(basename "$REPO_SLUG")
81
80
  mkdir -p "$WORKSPACE"
82
81
  cd "$WORKSPACE"
83
82
  rm -rf repo
84
- SERVER_HOST=$(awk '/^host:/ { print $2 }' "$HOME/.stamp/server.yml" 2>/dev/null)
85
- SERVER_PORT=$(awk '/^port:/ { print $2 }' "$HOME/.stamp/server.yml" 2>/dev/null)
86
- if [ -n "$SERVER_HOST" ] && [ -n "$SERVER_PORT" ] && \
87
- git clone "ssh://git@${SERVER_HOST}:${SERVER_PORT}/srv/git/${REPO_BASE}.git" repo 2>/dev/null; then
88
- : # cloned from stamp; origin points at the stamp URL
89
- else
90
- # No stamp config or repo not on stamp server fall back to GitHub.
91
- git clone "git@github.com:${REPO_SLUG}.git" repo
83
+ # Look up the recorded clone URI. `oteam config repo show` prints two lines
84
+ # (clone-uri: ..., added: ...) on success, or "(no entry for ...)" on miss.
85
+ CLONE_URI=$(oteam config repo show "$REPO_SLUG" 2>/dev/null | awk '/^clone-uri:/ { print $2 }')
86
+ if [ -z "$CLONE_URI" ]; then
87
+ # No recorded URI yet fall back to the GitHub HTTPS default. Running
88
+ # `oteam assign` interactively will prompt once and record it; hand-running
89
+ # this slash command skips the prompt and uses the public default.
90
+ CLONE_URI="git@github.com:${REPO_SLUG}.git"
92
91
  fi
92
+ git clone -- "$CLONE_URI" repo
93
93
  cd repo
94
94
  ```
95
95
 
@@ -175,8 +175,8 @@ Read `CLAUDE.md`, `AGENTS.md`, `README.md` at the repo root if present.
175
175
 
176
176
  **2. Verify the worktree shape and compute the merge mode.** Run `git remote -v` and check whether `.stamp/` exists in the worktree. Three shapes are valid:
177
177
 
178
- - **Stamp-governed (default).** Exactly one remote, `origin`, pointing at `ssh://git@<stamp-host>:<port>/srv/git/<basename>.git`. The runner clones with that shape on purpose; no rename / re-add is required, and adding a `github` remote here would defeat the AGT-050 invariant. `MODE` will resolve to `stamp` and routing goes through the stamp-protected branch (5a) at the end of Step 5.
179
- - **Local-stamp.** `origin` points at `git@github.com:<owner>/<repo>.git` AND the worktree contains a `.stamp/` directory. The repo carries stamp config but no stamp server is in use (typically a `--no-stamp` run against a stamp-aware repo). `MODE` will resolve to `local-stamp` and routing goes through 5c — `stamp review` + `stamp merge` produce a signed merge commit locally, which is then pushed to GitHub as the PR head for human review. `stamp push` is intentionally not invoked (no stamp server); `stamp verify <merge-sha>` still works against the PR head from any clone with the trusted public keys.
178
+ - **Stamp-governed.** Exactly one remote, `origin`, pointing at a non-GitHub URI (the stamp server). `MODE` will resolve to `stamp` and routing goes through the stamp-protected branch (5a) at the end of Step 5.
179
+ - **Local-stamp.** `origin` points at `git@github.com:<owner>/<repo>.git` AND the worktree contains a `.stamp/` directory. The repo carries stamp config but `origin` is GitHub, not a stamp server. `MODE` will resolve to `local-stamp` and routing goes through 5c — `stamp review` + `stamp merge` produce a signed merge commit locally, which is then pushed to GitHub as the PR head for human review. `stamp push` is intentionally not invoked (no stamp server); `stamp verify <merge-sha>` still works against the PR head from any clone with the trusted public keys.
180
180
  - **Plain GitHub.** `origin` points at `git@github.com:<owner>/<repo>.git` and there is no `.stamp/` directory. `MODE` will resolve to `plain` and routing goes through 5b — `git push origin <feature>` + `gh pr create`.
181
181
 
182
182
  Compute `MODE` once, here, from the worktree's actual state — every subsequent step branches on this single variable:
@@ -241,13 +241,9 @@ When tests pass:
241
241
  ```sh
242
242
  git add -A
243
243
  COMMIT_BODY="Refs <ticket-id>"
244
- # GitHub-bound paths (plain, local-stamp): append a `Fixes <gh-issue-url>`
245
- # trailer so the eventual PR merge auto-closes the GH issue. The
246
- # stamp-governed path skips this trailer — those worktrees have no github
247
- # remote, the merge gets pushed to the stamp server, and GH never sees a
248
- # commit that would trigger auto-close. QA Phase 5 closes the GH issue
249
- # explicitly via `gh issue close`, so behaviour is preserved either way.
250
- if [ "<source.type>" = "github" ] && [ "$MODE" != "stamp" ]; then
244
+ # When the source is a GitHub issue, append a `Fixes <gh-issue-url>` trailer
245
+ # so the PR merge (or stamp push that mirrors to GitHub) auto-closes the issue.
246
+ if [ "<source.type>" = "github" ]; then
251
247
  COMMIT_BODY="$COMMIT_BODY"$'\n'"Fixes <linked-github URL>"
252
248
  fi
253
249
  git commit -m "<one-line summary>
@@ -260,6 +256,14 @@ Never use `--no-verify`. Fix hook failures at the root cause.
260
256
 
261
257
  **5. Route by `$MODE`** (set in Step 2): `stamp` → 5a, `plain` → 5b, `local-stamp` → 5c.
262
258
 
259
+ **Push gate (AGT-099).** If your appended system context includes a `# Push step: disabled by oteam config` block, the operator has set `push: off` in `~/.open-team/config.json`. Run every step in 5a/5b/5c up to (but not including) the outbound push command, then print:
260
+
261
+ ```
262
+ push disabled by oteam config; merge commit is local at <sha>; run 'git push origin' manually when ready
263
+ ```
264
+
265
+ substituting `<sha>` with `git rev-parse HEAD` after the merge (5a/5c) or after the last feature commit (5b). In 5b/5c, also skip `gh pr create` and leave `linked-pr:` empty — there is no pushed branch for the PR to reference. Note the held push in the wrap-up comment. Step 6 (stamp retro routing) still runs because it does not depend on the push.
266
+
263
267
  #### 5a — Stamp-protected repo
264
268
 
265
269
  Run review and merge. Capture the review's combined output (stdout + stderr) to a known tempfile so Step 6 can route any `STAMP-RETRO` candidates the reviewers emit. Re-run the entire `tee` block on every round of the 5-round iteration — `$STAMP_REVIEW_OUT` is reassigned to a fresh `mktemp` each round, so Step 6 reads only the last (gate-opening) run; prior tempfiles are left behind for the OS to reap.