@ulysses-ai/create-workspace 0.18.0-beta.0 → 0.20.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/init.mjs +4 -1
- package/lib/payload.mjs +18 -1
- package/lib/payload.test.mjs +55 -0
- package/lib/scaffold.mjs +23 -6
- package/lib/scaffold.test.mjs +59 -0
- package/lib/upgrade.mjs +20 -0
- package/lib/upgrade.test.mjs +119 -0
- package/package.json +3 -3
- package/template/CLAUDE.md.tmpl +3 -0
- package/template/{.claude → _claude}/lib/freshness.mjs +20 -7
- package/template/_claude/lib/registry-check.mjs +172 -0
- package/template/{.claude → _claude}/rules/forge-operations.md +6 -0
- package/template/{.claude → _claude}/rules/memory-guidance.md +4 -0
- package/template/{.claude → _claude}/rules/task-list-mirroring.md +6 -0
- package/template/{.claude → _claude}/scripts/build-workspace-context.mjs +25 -14
- package/template/{.claude → _claude}/scripts/chat-record.mjs +62 -21
- package/template/_claude/scripts/classify-update.mjs +117 -0
- package/template/{.claude → _claude}/scripts/cleanup-work-session.mjs +4 -2
- package/template/{.claude → _claude}/scripts/context-footprint.mjs +139 -30
- package/template/{.claude → _claude}/scripts/forges/github.mjs +2 -1
- package/template/{.claude → _claude}/scripts/forges/interface.mjs +5 -4
- package/template/_claude/scripts/merge-mode.mjs +61 -0
- package/template/{.claude → _claude}/scripts/migrate-canonical-priority.mjs +7 -1
- package/template/_claude/scripts/migrate-claude-md-freshness-include.mjs +55 -0
- package/template/{.claude → _claude}/scripts/migrate-session-layout.mjs +19 -8
- package/template/{.claude → _claude}/scripts/migrate-sessions.mjs +444 -121
- package/template/_claude/scripts/task-pr.mjs +555 -0
- package/template/{.claude → _claude}/scripts/task-worktree.mjs +26 -16
- package/template/{.claude → _claude}/scripts/trackers/github-issues.mjs +11 -0
- package/template/{.claude → _claude}/scripts/trackers/interface.mjs +8 -0
- package/template/{.claude → _claude}/skills/braindump/SKILL.md +1 -0
- package/template/{.claude → _claude}/skills/complete-work/SKILL.md +25 -77
- package/template/{.claude → _claude}/skills/context-placement/SKILL.md +8 -5
- package/template/{.claude → _claude}/skills/goal-driven-work/SKILL.md +1 -1
- package/template/{.claude → _claude}/skills/handoff/SKILL.md +1 -0
- package/template/{.claude → _claude}/skills/maintenance/SKILL.md +49 -17
- package/template/{.claude → _claude}/skills/migrate-sessions/SKILL.md +17 -5
- package/template/{.claude → _claude}/skills/release/SKILL.md +6 -2
- package/template/{.claude → _claude}/skills/start-work/SKILL.md +5 -5
- package/template/{.claude → _claude}/skills/workspace-init/SKILL.md +20 -14
- package/template/_claude/skills/workspace-update/SKILL.md +177 -0
- package/template/_gitignore +3 -0
- package/template/workspace.json.tmpl +1 -1
- package/template/.claude/lib/registry-check.mjs +0 -106
- package/template/.claude/scripts/migrate-claude-md-freshness-include.mjs +0 -30
- package/template/.claude/skills/workspace-update/SKILL.md +0 -134
- /package/template/{.claude → _claude}/agents/aside-researcher.md +0 -0
- /package/template/{.claude → _claude}/agents/implementer.md +0 -0
- /package/template/{.claude → _claude}/agents/researcher.md +0 -0
- /package/template/{.claude → _claude}/agents/reviewer.md +0 -0
- /package/template/{.claude → _claude}/hooks/_utils.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/bash-output-advisory.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/post-compact.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/pre-compact.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/repo-write-detection.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/session-end.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/session-start.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/subagent-start.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/version-freshness-check.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/workspace-update-check.mjs +0 -0
- /package/template/{.claude → _claude}/lib/require-node.mjs +0 -0
- /package/template/{.claude → _claude}/lib/session-frontmatter.mjs +0 -0
- /package/template/{.claude → _claude}/recipes/migrate-from-notion.md +0 -0
- /package/template/{.claude → _claude}/rules/agent-rules.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/cloud-infrastructure.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/coherent-revisions.md +0 -0
- /package/template/{.claude → _claude}/rules/config-review.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/documentation.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/git-conventions.md +0 -0
- /package/template/{.claude → _claude}/rules/goal-driven-work.md +0 -0
- /package/template/{.claude → _claude}/rules/honest-pushback.md +0 -0
- /package/template/{.claude → _claude}/rules/local-dev-environment.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/product-integrity.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/scope-guard.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/superpowers-workflow.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/token-economics.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/work-item-tracking.md +0 -0
- /package/template/{.claude → _claude}/rules/workspace-structure.md +0 -0
- /package/template/{.claude → _claude}/scripts/add-repo-to-session.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/capture-context.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/create-work-session.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/forges/gitlab.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/generate-claude-local.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-open-work.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-to-workspace-context.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/sweep-references.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/sync-tasks.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/workspace-diagnostics.mjs +0 -0
- /package/template/{.claude → _claude}/settings.json +0 -0
- /package/template/{.claude → _claude}/skills/aside/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/framing.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/pitfalls.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/review.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/bulk-fill-migration.py +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/forbidden-word-grep.mjs +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/leak-grep.mjs +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/custom.css.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Arrow.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Box.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Region.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/SectionTitle.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/tokens.ts +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/sidebars.ts.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/spec.md.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/pause-work/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/promote/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/setup-tracker/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/sync-work/SKILL.md +0 -0
- /package/template/{.mcp.json → _mcp.json} +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
// Tracker adapter interface. Skills import only from this module.
|
|
2
2
|
// See design-tracker-abstraction.md for the full Issue shape and method contracts.
|
|
3
|
+
//
|
|
4
|
+
// Method contract added alongside task-pr.mjs (gh:163):
|
|
5
|
+
//
|
|
6
|
+
// issueRef(issueId, { fromRepo })
|
|
7
|
+
// → the closing reference for a PR body: "#N" when fromRepo is (or
|
|
8
|
+
// defaults to) this adapter's own repo, "owner/repo#N" otherwise —
|
|
9
|
+
// only the first form closes an issue in the PR's own repo, so a PR
|
|
10
|
+
// in any other repo must name the tracker's repo explicitly.
|
|
3
11
|
|
|
4
12
|
import '../../lib/require-node.mjs';
|
|
5
13
|
import { createGithubAdapter } from './github-issues.mjs';
|
|
@@ -30,6 +30,7 @@ Under the task model — `workspace.sessionModel` is `"task"` in `workspace.json
|
|
|
30
30
|
|
|
31
31
|
- Default behavior: write `braindump_{topic}.md` directly into that chat's drawer at `workspace-scratchpad/chats/{chat}/` — the drawer sits outside `workspace-context/`, so `capture-context.mjs` is not involved
|
|
32
32
|
- No commit for drawer writes: the drawer is gitignored and machine-local; `/complete-work` lists it and asks what to promote into `workspace-context/`
|
|
33
|
+
- While a task is active, add `workItem: {id}` to the file's frontmatter: `/complete-work` offers a drawer item only to the task that owns it, so the tag keeps this capture out of another task's promotion list
|
|
33
34
|
|
|
34
35
|
When called from the workspace root with no active session — every other case, including a `sessionModel: "session"` workspace (the `Chat record:` line is injected in every chat, so it alone does not select the drawer):
|
|
35
36
|
|
|
@@ -20,7 +20,7 @@ node "{launcher-root}/.claude/scripts/task-worktree.mjs" --root "{launcher-root}
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
- `{launcher-root}` is the absolute path on the `Workspace root:` line the SessionStart hook injects. If that line is absent, derive it from git: run `git rev-parse --git-common-dir` (when it prints a relative path, resolve it against the cwd) and take its parent directory. That derivation lands on the source clone `…/repos/{repo}` when run from inside a **project** task worktree — there the launcher is two levels up; from inside a `.` worktree (`.claude/worktrees/{slug}`) the parent already is the launcher.
|
|
23
|
-
- `{chat}` is the name from the `Chat record:` line the SessionStart hook injects. If that line is absent, omit `--chat` — detection then relies on cwd alone.
|
|
23
|
+
- `{chat}` is the name from the `Chat record:` line the SessionStart hook injects. If that line is absent, run `node .claude/scripts/chat-record.mjs --whoami --root "{launcher-root}"` first — compaction can drop the hook line, and this recovers the name by matching the chat's session id against the records. When that too prints nothing (exit 1), omit `--chat` — detection then relies on cwd alone.
|
|
24
24
|
- `model: session` → continue with this flow (read the session tracker as below), taking `{session-name}` from the detect result's `sessionName`.
|
|
25
25
|
- `model: task` → go to **Task completion (session model v2)**. The result's `tasks` come from the chat record; if several are open, ask the user which one to complete — group by branch, a multi-repo task is several entries sharing a branch.
|
|
26
26
|
- `model: none` → "No active work session. Nothing to complete."
|
|
@@ -368,25 +368,26 @@ Ask: "These changes weren't part of a formal work session. What do you want to d
|
|
|
368
368
|
Reached from Step 1 when detection says `model: task`. The state is the branch, the chat record's task entries, and the linked issue — there is no session folder and no `session.md`. This chat normally runs at the workspace root (the launcher) but may be running inside one of the worktrees; either way, every input comes from the detect result, never from cwd. Run `cd "{launcher-root}"` first — steps 2–5 and every relative path in them are anchored there:
|
|
369
369
|
|
|
370
370
|
- `{chat}` — the `Chat record:` line injected by the SessionStart hook (the same value Step 1 passed as `--chat`)
|
|
371
|
-
- `{tasks}` — the detect result's task entries from the chat record, each carrying `{workItem}
|
|
371
|
+
- `{tasks}` — the detect result's task entries from the chat record, each carrying `{workItem}` (null without a tracker), `{branch}`, `{repo}`; `repo: "."` is the workspace repo itself. When detection came from cwd alone (`source: 'worktree'`, no chat record entry — e.g. a chat whose record was never written) there are no task entries: take `{repo}` and `{branch}` from the detect result itself and treat `{workItem}` as absent
|
|
372
372
|
- `{worktree}` — `{launcher-root}/repos/{repo}/.claude/worktrees/{slug}` for a project repo, or `{launcher-root}/.claude/worktrees/{slug}` for the workspace repo (`.`), where `{slug}` is the branch with `/` replaced by `-`
|
|
373
373
|
- `{workspace-worktree}` — `{launcher-root}/.claude/worktrees/{slug}`: the workspace repo's own task worktree, created in step 2 only when drawer items are promoted (Claude Code's native worktree location — the two converge on it)
|
|
374
|
-
- `{defaultBranch}` — `workspace.json` → `repos.{repo}.branch`, default `main`; for `.` it is the workspace origin's HEAD with `main` as the fallback — exactly what `task-worktree.mjs`'s `defaultBranchFor` resolves
|
|
374
|
+
- `{defaultBranch}` — `workspace.json` → `repos.{repo}.branch`, default `main`; for `.` it is the workspace origin's HEAD with `main` as the fallback — or, when the launcher has no origin, the branch it sits on when that is `main`/`master` — exactly what `task-worktree.mjs`'s `defaultBranchFor` resolves
|
|
375
|
+
- `{mode}` per repo — `forge` or `local`, resolved exactly as `task-pr.mjs` resolves it: `local` when the repo has no origin remote or its override says so (`repos.{repo}.merge`, or `workspace.merge` for `.`) — the override for a clone whose origin is a third-party upstream nobody here may push to; `forge` when its origin is a forge remote. A local repo is never pushed: it merges into its source clone in step 4.
|
|
375
376
|
|
|
376
377
|
If several tasks are open, ask the user which one to complete — group by branch; a multi-repo task is several entries sharing a branch — and complete one branch at a time.
|
|
377
378
|
|
|
378
379
|
0. **Pre-flight: the worktrees must be clean.** For each `{worktree}` of the chosen task, `git -C "{worktree}" status --porcelain` must be empty. If it is not, stop here — before the rebase — and ask the user to commit or discard: rebasing over uncommitted work silently invalidates it.
|
|
379
380
|
|
|
380
|
-
1. **Rebase each task worktree onto `origin/{defaultBranch}
|
|
381
|
+
1. **Rebase each task worktree onto its base** — `origin/{defaultBranch}` for a `forge` repo (`git -C "{worktree}" fetch origin`, then `git -C "{worktree}" rebase "origin/{defaultBranch}"`), the local `{defaultBranch}` for a `local` repo (`git -C "{worktree}" rebase "{defaultBranch}"`; no fetch — there is no remote to fetch from). Freshness first: the PR in step 3 must describe the branch as it will merge. If conflicts arise, STOP and present them — do not auto-resolve.
|
|
381
382
|
|
|
382
|
-
2. **Route durable thinking.** List
|
|
383
|
+
2. **Route durable thinking.** List the chat drawer `{launcher-root}/workspace-scratchpad/chats/{chat}/` and ask which items should graduate into `workspace-context/`. The offer is filtered by task: an item whose frontmatter carries a `workItem:` is listed only when it names this task's `{workItem}`, an item with no `workItem:` is always listed, and an item tagged for another open task is not this completion's to promote — it belongs to that task's completion. Leftovers of an earlier completion attempt are never offered: the `pr-{slug}-*.md` bodies and the `prs-{slug}.json` state file are step 3–4 machinery, not thinking. Drafting skills (`/braindump`, `/handoff`, designs and plans) tag their drawer writes with `workItem:` while a task is active so this filter has something to filter on. If nothing is chosen, skip this step — no `.` worktree is needed yet. Do NOT invoke `/promote`: it writes relative to cwd and commits per item, which at the launcher lands on the default branch — exactly the hole this flow closes, and it does not know about drawer items. For the chosen items, first create the workspace worktree on the task's branch and record it alongside the project entries:
|
|
383
384
|
|
|
384
385
|
```bash
|
|
385
386
|
node "{launcher-root}/.claude/scripts/task-worktree.mjs" --root "{launcher-root}" --create --repo "." --branch "{branch}"
|
|
386
387
|
node "{launcher-root}/.claude/scripts/chat-record.mjs" --root "{launcher-root}" --add-task --chat "{chat}" --work-item "{workItem}" --branch "{branch}" --repo "."
|
|
387
388
|
```
|
|
388
389
|
|
|
389
|
-
|
|
390
|
+
Omit `--work-item` from the record line when the task has none — without a work item the entry is keyed by repo + branch. Then copy each chosen item to `{workspace-worktree}/workspace-context/shared/{item}` or `{workspace-worktree}/workspace-context/team-member/{user}/{item}` — ask which level; `shared/locked/` only if explicitly requested — keeping the filename and making sure the file carries a `description:` frontmatter (add one if the drawer item has none). Never write under `{launcher-root}/workspace-context/` at the launcher. Rebuild the indexes and commit once, and only when something is staged:
|
|
390
391
|
|
|
391
392
|
```bash
|
|
392
393
|
node "{launcher-root}/.claude/scripts/build-workspace-context.mjs" --write --root "{workspace-worktree}"
|
|
@@ -396,58 +397,18 @@ If several tasks are open, ask the user which one to complete — group by branc
|
|
|
396
397
|
|
|
397
398
|
The drawer is per-chat, so it survives task teardown — but it is machine-local and backed up nowhere, and this review, with the work fresh in mind, is the moment to decide what graduates. Items left behind are not lost, only unreviewed.
|
|
398
399
|
|
|
399
|
-
3. **
|
|
400
|
+
3. **Write one PR body per forge repo into the drawer, then create every PR with `task-pr.mjs --create`** — never `gh pr` directly. For each **forge**-mode repo of the task, `{workspace-worktree}` included when it exists, write `{launcher-root}/workspace-scratchpad/chats/{chat}/pr-{slug}-{repo}.md` (with `{repo}` rendered as `workspace` for `.`). Each body carries a short summary of what changed and why, then a `## Verification` section stating how the change was checked — the commands run and their results. The drawer is machine-local and gitignored, so these files never touch a branch; `--create` requires one for every forge repo whose branch has commits, and a local repo needs none (one given for it is accepted and ignored). Then one command does the rest:
|
|
400
401
|
|
|
401
402
|
```bash
|
|
402
|
-
|
|
403
|
+
node "{launcher-root}/.claude/scripts/task-pr.mjs" --create --root "{launcher-root}" --branch "{branch}" \
|
|
404
|
+
--work-item "{workItem}" --chat "{chat}" \
|
|
405
|
+
--body-file "{repo}={launcher-root}/workspace-scratchpad/chats/{chat}/pr-{slug}-{repo}.md" \
|
|
406
|
+
--out "{launcher-root}/workspace-scratchpad/chats/{chat}/prs-{slug}.json"
|
|
403
407
|
```
|
|
404
408
|
|
|
405
|
-
and
|
|
409
|
+
Omit `--work-item` when the task has none, and repeat `--body-file` once per forge repo. `--chat` resolves the task's repos from this chat's record entries for the branch; when detection came from cwd alone, pass `--repo "{repo}"` once per repo instead. The script resolves each repo's merge mode before doing anything else: a forge repo is pushed `-u origin {branch}` and gets one PR through a per-repo forge aimed at that worktree's own origin, never the launcher's; a local repo (no origin, or `merge: "local"`) is neither pushed nor PR'd — it is recorded as a local entry for step 4. The workspace repo (`.`) is handled exactly like a project repo. It skips repos whose branch has no commits over the base (reporting them as `empty` — no push, no PR, no body file needed, torn down in step 5 like any other; a workspace branch that collected no promotions gets no PR), and stops **before pushing anything** when a repo's origin exists but is not forge-hosted and has no `"local"` override — the error names the `workspace.json` setting (`repos.{repo}.merge`, or `workspace.merge` for `.`) that opts the repo into local mode. The PR title is the linked issue's title, or the branch's first commit subject without a `{workItem}`; the body is the drawer file with `Closes <ref>` appended, where `<ref>` is `#N` when the PR's repo is the tracker's repo and `{tracker-repo}#N` otherwise — only the first form closes an issue in the PR's own repo. If the push is rejected as non-fast-forward — the branch already existed on origin and step 1's rebase rewrote it — the script stops and says so; ask the user, and only on explicit confirmation re-run the same command with `--force-with-lease` (the script never forces on its own). If `workspace.forge` is `false` in `workspace.json` and any repo resolved to forge, the script refuses before pushing anything: forge operations are disabled in this workspace and the PR is opened by hand. `--out` writes the run's JSON to `prs-{slug}.json` for step 4 — `{ prs: [{ repo, mode, … }], empty: [repo…], pushed: [repo…] }`, printed to stdout as well — instead of shell redirection, so the command is identical on every shell. Every entry carries `commits`; a forge entry also carries `owner`, `name`, `number`, `id`, `url`, `isWorkspace`, and a local entry `branch`, `base`, `worktree`. The run is idempotent: a repo whose branch already has an open PR against `{defaultBranch}` gets that PR reused, never duplicated, and if the run fails partway the file still records what was pushed and opened so far — fix the cause and re-run the same command; it completes the set.
|
|
406
410
|
|
|
407
|
-
|
|
408
|
-
git -C "{worktree}" remote get-url origin # → parse {owner}/{name} FIRST
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
If the URL does not parse into `{owner}/{name}` (a local/bare remote) or is a forge the adapter does not support, STOP before pushing anything: the task path supports forge-hosted repos only in this stage — complete that repo under the session model.
|
|
412
|
-
|
|
413
|
-
```bash
|
|
414
|
-
git -C "{worktree}" push -u origin "{branch}"
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
If the push is rejected as non-fast-forward — the branch already existed on origin and step 1 rebased it — ask the user before retrying with `git -C "{worktree}" push --force-with-lease`. Never force without asking.
|
|
418
|
-
|
|
419
|
-
If `workspace.forge` is `false` in `workspace.json`, STOP here: forge operations are disabled in this workspace — the push above is done, the PR is opened by hand.
|
|
420
|
-
|
|
421
|
-
```javascript
|
|
422
|
-
// Run from {launcher-root} (see above). Imports stay relative: an absolute
|
|
423
|
-
// path is not a valid ESM specifier on Windows.
|
|
424
|
-
import { createForge } from './.claude/scripts/forges/interface.mjs';
|
|
425
|
-
import { createTracker } from './.claude/scripts/trackers/interface.mjs';
|
|
426
|
-
import { readFileSync } from 'node:fs';
|
|
427
|
-
const ws = JSON.parse(readFileSync('{launcher-root}/workspace.json', 'utf-8'));
|
|
428
|
-
|
|
429
|
-
// Constructed per repo, so the adapter never resolves the launcher's own remote.
|
|
430
|
-
const forge = createForge({ ...ws.workspace?.forge, repo: '{owner}/{name}' });
|
|
431
|
-
// For the workspace repo (.): from the workspace worktree's own origin.
|
|
432
|
-
const wsForge = createForge({ ...ws.workspace?.forge, repo: '{ws-owner}/{ws-name}' });
|
|
433
|
-
|
|
434
|
-
// PR title: the linked issue's title — tracker.getIssue(workItem).title —
|
|
435
|
-
// or, with no workItem, the subject of the branch's first commit beyond
|
|
436
|
-
// the base: git log "origin/{defaultBranch}..HEAD" --reverse --format=%s
|
|
437
|
-
// (run in "{worktree}").
|
|
438
|
-
const title = workItem
|
|
439
|
-
? (await createTracker(ws.workspace.tracker).getIssue(workItem)).title
|
|
440
|
-
: firstCommitSubject;
|
|
441
|
-
|
|
442
|
-
// PR body: one line per commit from
|
|
443
|
-
// git -C "{worktree}" log "origin/{defaultBranch}..HEAD" --oneline,
|
|
444
|
-
// then a blank line and `Closes {workItem}` when a workItem exists.
|
|
445
|
-
const pr = await forge.prCreate({ title, body, head: '{branch}', base: '{defaultBranch}' });
|
|
446
|
-
// The workspace PR, when step 3 did not skip "." as empty:
|
|
447
|
-
const wsPr = await wsForge.prCreate({ title: `context: {branch} task`, body: workspacePrBody, head: '{branch}', base: '{defaultBranch}' });
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
4. **Ask before merging, then merge, then close the linked issue.** Present a summary per repo that got a PR — the workspace repo included — and ask once:
|
|
411
|
+
4. **Ask before merging, then run `task-pr.mjs --merge`, which also closes the linked issue.** When step 3 reported `prs: []` — every repo was empty — there is nothing to merge: skip this step, say so, and leave the linked issue open for the user to decide (close it by hand, or keep the task going); `--merge` itself refuses an empty PRs file for exactly that reason. Otherwise present a summary per repo that got an entry — forge and local alike, the workspace repo included — built from `--create`'s output, and ask once. A local repo shows `(local merge)` in place of a PR line; a mixed task asks `Merge all?`, an all-local task `Merge all locally?`:
|
|
451
412
|
|
|
452
413
|
```
|
|
453
414
|
Task complete:
|
|
@@ -455,9 +416,12 @@ If several tasks are open, ask the user which one to complete — group by branc
|
|
|
455
416
|
PROJECT: {owner}/{name}
|
|
456
417
|
PR: {pr.url}
|
|
457
418
|
Branch: {branch} → {defaultBranch}
|
|
458
|
-
Commits: {n} #
|
|
419
|
+
Commits: {n} # from the entry's `commits` count
|
|
459
420
|
|
|
460
|
-
|
|
421
|
+
PROJECT: {repo} (local merge)
|
|
422
|
+
Branch: {branch} → {defaultBranch} # merged in repos/{repo}
|
|
423
|
+
|
|
424
|
+
WORKSPACE: {ws-owner}/{ws-name} # from the workspace worktree's origin, or "(local merge)"
|
|
461
425
|
PR: {ws-pr.url}
|
|
462
426
|
Branch: {branch} → {defaultBranch}
|
|
463
427
|
|
|
@@ -466,39 +430,23 @@ If several tasks are open, ask the user which one to complete — group by branc
|
|
|
466
430
|
|
|
467
431
|
On "n", stop: the PRs stay open and the worktrees, branches, and record entries stay in place — say so.
|
|
468
432
|
|
|
469
|
-
On "y"
|
|
470
|
-
|
|
471
|
-
```javascript
|
|
472
|
-
for (const pr of projectPrs) {
|
|
473
|
-
await forge.prMerge({ id: pr.id, strategy: 'squash', deleteBranch: true });
|
|
474
|
-
}
|
|
475
|
-
if (wsPr) await wsForge.prMerge({ id: wsPr.id, strategy: 'squash', deleteBranch: true }); // workspace PR, last
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
Pull the launcher only after the workspace PR actually merged — it is still on its default branch, waiting on that merge; when `.` had no PR, pull after the project merges instead:
|
|
433
|
+
On "y":
|
|
479
434
|
|
|
480
435
|
```bash
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
Then close the linked issue — only if a `{workItem}` exists — with a one-line comment naming the merged PR URL(s):
|
|
485
|
-
|
|
486
|
-
```javascript
|
|
487
|
-
const tracker = createTracker(ws.workspace.tracker);
|
|
488
|
-
const comment = `Merged: ${prUrls.join(' ')}`; // prUrls = the .url of each PR merged above
|
|
489
|
-
await tracker.closeIssue(workItem, { comment });
|
|
436
|
+
node "{launcher-root}/.claude/scripts/task-pr.mjs" --merge --root "{launcher-root}" \
|
|
437
|
+
--prs "{launcher-root}/workspace-scratchpad/chats/{chat}/prs-{slug}.json" --work-item "{workItem}"
|
|
490
438
|
```
|
|
491
439
|
|
|
492
|
-
|
|
440
|
+
The script checks each entry's state first, then finishes the project entries — forge PRs (squash, delete branch) and local branches as `git merge --ff-only {branch}` in the repo's source clone (`repos/{repo}`, or the launcher for `.`), which must sit clean on its default branch — then the workspace entry only when every project merge succeeded: the workspace branch's promoted context describes the project merges and must never merge ahead of them. A PR the forge already reports as merged and a local branch already contained in the default branch both count as done, which is what makes a re-run safe: the run that finally gets every entry merged also does the pull and the close. A local branch that will not fast-forward stops the run and says to rebase the task branch onto the local `{defaultBranch}` and re-run. The launcher is pulled `--ff-only` only when it sits on the workspace default branch, `.` was not itself merged locally (that merge ran in the launcher — `pullSkipped: "workspace merged locally"`), the workspace repo is not local (no forge merge of `.` happened that a pull could fetch — `pullSkipped: "workspace repo is local"`), and the launcher branch has an upstream to pull from (`pullSkipped: "launcher has no upstream"`); a launcher on some other branch reports `pullSkipped: "launcher on <branch>"`, and a failed pull is reported as `pullFailed: true` rather than an error, because the merges stand and the issue still closes — a failed pull is the one case that needs the launcher pulled by hand before step 5. The close comes last, with a one-line comment naming each merged PR URL or local `{repo} {branch}->{base}`: merge precedes close because an issue closed before its PR merges points at work that never landed. It happens only when `{workItem}` is given AND a tracker is configured — without a work item or without a tracker, `closed` is `null` and the JSON says why (`closeSkipped`) — say so to the user. On any merge failure the script stops, names what is still open, and exits non-zero — nothing is torn down, and re-running `--merge` once the failure is fixed is safe. After a successful merge, delete the drawer's `pr-{slug}-*.md` body files and the spent `prs-{slug}.json`.
|
|
493
441
|
|
|
494
|
-
5. **Tear down only what finished — worktree first, then the record entry**, and only for a repo whose PR merged in step 4, or whose branch was empty and never pushed in step 3; the workspace repo included (`--repo "."`). If a repo's push, PR, or merge failed, or the user declined the merge, leave that repo's worktree, branch, and record entry exactly in place and say so: `--delete-branch` would otherwise `branch -D` commits that exist nowhere but the local worktree. For `.` the script never deletes the branch checked out at the launcher root.
|
|
442
|
+
5. **Tear down only what finished — worktree first, then the record entry**, and only for a repo whose PR merged or whose local merge succeeded in step 4, or whose branch was empty and never pushed in step 3; the workspace repo included (`--repo "."`). If a repo's push, PR, or merge failed, or the user declined the merge, leave that repo's worktree, branch, and record entry exactly in place and say so: `--delete-branch` would otherwise `branch -D` commits that exist nowhere but the local worktree. For `.` the script never deletes the branch checked out at the launcher root.
|
|
495
443
|
|
|
496
444
|
```bash
|
|
497
445
|
node "{launcher-root}/.claude/scripts/task-worktree.mjs" --root "{launcher-root}" --remove --repo "{repo}" --branch "{branch}" --delete-branch
|
|
498
446
|
node "{launcher-root}/.claude/scripts/chat-record.mjs" --root "{launcher-root}" --remove-task --chat "{chat}" --work-item "{workItem}" --repo "{repo}"
|
|
499
447
|
```
|
|
500
448
|
|
|
501
|
-
|
|
449
|
+
When the task has no `{workItem}`, drop `--work-item` and pass `--branch "{branch}"` instead — the record entry is keyed by repo + branch. Worktree first because a refusal (dirty worktree, slug collision) then leaves both the worktree and its record entry in place — nothing orphaned, safe to retry. `--delete-branch` also removes the local branch: the forge's `deleteBranch` removed only the remote one, so post-merge teardown passes it to clean the local clone; this is the only step that ever passes it. Never pass `--force` without asking the user.
|
|
502
450
|
|
|
503
451
|
## Notes
|
|
504
452
|
- The session tracker's body is the primary source for PR-body synthesis — it captures the full session history alongside specs and plans
|
|
@@ -70,7 +70,7 @@ does not need to be in context while you are editing documentation.
|
|
|
70
70
|
---
|
|
71
71
|
paths:
|
|
72
72
|
- ".claude/scripts/**/*.mjs"
|
|
73
|
-
- "repos/*/template
|
|
73
|
+
- "repos/*/template/_claude/scripts/**/*.mjs"
|
|
74
74
|
---
|
|
75
75
|
```
|
|
76
76
|
|
|
@@ -189,11 +189,14 @@ Gitignored files (anything matching `local-only-*`) are excluded automatically,
|
|
|
189
189
|
`workspace-context/.indexignore` adds path-prefix excludes for tracked files that should
|
|
190
190
|
not appear in the shared index.
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
The canonical byte budget is opt-in: `workspace.canonicalBudgetBytes` is off by default
|
|
193
|
+
(absent or `null`), and the whole always-loaded set is measured by
|
|
194
|
+
`workspace.alwaysLoadedBudgetBytes` instead. When a byte count is set and `canonical.md`
|
|
195
|
+
exceeds it, the builder honours per-file `priority` and section-level
|
|
196
|
+
`<!-- canonical:trim --> ... <!-- canonical:end-trim -->`
|
|
194
197
|
markers to fit: `priority: reference` files are trimmed, then stubbed; `priority: critical`
|
|
195
|
-
files are always included in full. `/maintenance` audits the budget and offers
|
|
196
|
-
over.
|
|
198
|
+
files are always included in full. `/maintenance` audits the budget when on and offers
|
|
199
|
+
triage when over.
|
|
197
200
|
|
|
198
201
|
Hand edits to `index.md`, `canonical.md`, or any per-user index are overwritten. Change the
|
|
199
202
|
source file or its `description:` instead.
|
|
@@ -17,7 +17,7 @@ the work at hand. This skill covers everything after that decision.
|
|
|
17
17
|
- One `goal-{topic}.md` artifact per effort. Session model: at the top of the session worktree, alongside `session.md`. Task model: in the chat drawer at `workspace-scratchpad/chats/{chat}/`, alongside its phase outputs. One goal per worktree or task either way.
|
|
18
18
|
- The artifact's frontmatter holds machine state; its body holds the human-readable goal statement, per-phase intent, and a mandatory `## Start command` section (see "Kicking off the goal") with the literal `/goal "..."` invocation the user runs to start the loop.
|
|
19
19
|
- Phase output artifacts live as siblings. `research-*.md` and `crossref-*.md` are goal-native (produced by `parallel-research` and `crossref` phase types). `design-*.md` and `plan-*.md` are pre-existing session-artifact patterns that `type: skill` phases reuse when the wrapped skill is `superpowers:brainstorming` or `superpowers:writing-plans`; they are not goal-specific.
|
|
20
|
-
- Session model: the artifact is tracked on the session branch and lives there until `/complete-work` runs, which strips it before the final PR. Task model: the drawer is machine-local and untracked; `/complete-work` routes the artifact (promote into `workspace-context/` or discard) at completion.
|
|
20
|
+
- Session model: the artifact is tracked on the session branch and lives there until `/complete-work` runs, which strips it before the final PR. Task model: the drawer is machine-local and untracked; `/complete-work` routes the artifact (promote into `workspace-context/` or discard) at completion, so while a task is active give the artifact's frontmatter a `workItem: {id}` line — the offer is scoped to the task that owns the artifact.
|
|
21
21
|
|
|
22
22
|
## Frontmatter schema
|
|
23
23
|
|
|
@@ -30,6 +30,7 @@ Under the task model — `workspace.sessionModel` is `"task"` in `workspace.json
|
|
|
30
30
|
|
|
31
31
|
- Default behavior: write `handoff_{topic}.md` directly into that chat's drawer at `workspace-scratchpad/chats/{chat}/` — the drawer sits outside `workspace-context/`, so `capture-context.mjs` is not involved
|
|
32
32
|
- No commit for drawer writes: the drawer is gitignored and machine-local; `/complete-work` lists it and asks what to promote into `workspace-context/`
|
|
33
|
+
- While a task is active, add `workItem: {id}` to the file's frontmatter: `/complete-work` offers a drawer item only to the task that owns it, so the tag keeps this capture out of another task's promotion list
|
|
33
34
|
|
|
34
35
|
When called from the workspace root with no active session — every other case, including a `sessionModel: "session"` workspace (the `Chat record:` line is injected in every chat, so it alone does not select the drawer):
|
|
35
36
|
|
|
@@ -59,9 +59,9 @@ node .claude/scripts/build-workspace-context.mjs --check --root .
|
|
|
59
59
|
|
|
60
60
|
The script reports per-artifact status as JSON and uses three exit codes to distinguish what's wrong:
|
|
61
61
|
|
|
62
|
-
- `0` — all artifacts current and the rendered canonical fits inside `workspace.canonicalBudgetBytes`.
|
|
62
|
+
- `0` — all artifacts current and, when a canonical budget is set, the rendered canonical fits inside `workspace.canonicalBudgetBytes`.
|
|
63
63
|
- `1` — at least one artifact is `missing` or `stale`. Run `--write` to regenerate. `missing` means the artifact does not exist yet; `stale` means it exists but no longer matches its sources (a file was added or deleted, a `description:` changed, a `shared/locked/` file was edited, an `.indexignore` rule was added).
|
|
64
|
-
- `2` — artifacts are current but canonical body bytes exceed the budget after the trim and stub stages have already run. Regeneration cannot fix this; the locked content itself needs triage. Stale wins over over-budget when both apply, so a `1` can hide an over-budget condition until you regen.
|
|
64
|
+
- `2` — artifacts are current but canonical body bytes exceed the budget after the trim and stub stages have already run. Only reachable when a budget is set. Regeneration cannot fix this; the locked content itself needs triage. Stale wins over over-budget when both apply, so a `1` can hide an over-budget condition until you regen.
|
|
65
65
|
|
|
66
66
|
The JSON payload always includes a `canonical` block summarizing the budget outcome:
|
|
67
67
|
|
|
@@ -83,11 +83,40 @@ The JSON payload always includes a `canonical` block summarizing the budget outc
|
|
|
83
83
|
|
|
84
84
|
`selectionStatus` walks `ok` → `trimmed` → `stubbed` → `over-budget` as the script gives up progressively more reference content trying to fit the budget. `trimmedFiles` lists reference files whose `<!-- canonical:trim --> ... <!-- canonical:end-trim -->` spans were dropped; `stubbedFiles` lists reference files whose entire body was replaced with a one-line breadcrumb. `overBy` is present only when `selectionStatus === 'over-budget'` and reports the bytes still over after stubbing.
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
The canonical budget is opt-in. `workspace.canonicalBudgetBytes` is off unless workspace.json sets it — absent or `null` means no budget. When off, `canonical.md` ships every locked file in full, the `canonical` block reports `"budget": null` with `selectionStatus: "ok"`, exit `2` cannot occur, and the audit reports one informational line in place of the budget OK/warning line:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
• Canonical budget: off (alwaysLoadedBudgetBytes covers the total)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
No warning accompanies it. To turn the budget back on, set a byte count in workspace.json (e.g. `"canonicalBudgetBytes": 40960`) and regenerate.
|
|
93
|
+
|
|
94
|
+
Audit mode reports the status verbatim. When a budget is set and `selectionStatus` is `over-budget`, audit emits the budget violation and recommends `/maintenance cleanup` to triage — regeneration will not resolve it. Cleanup mode runs `--write` when `missing` or `stale`, re-checks, and then enters the budget triage flow described in cleanup step 11 if the post-regen check still reports `over-budget`.
|
|
87
95
|
|
|
88
96
|
While the indexes are being read, also flag entries with weak fallbacks: filename-slug-only descriptions (e.g., "project status" with no period) usually indicate the underlying file is missing a `description:` or has no usable opening sentence. Suggest adding `description:` to those source files — the index will pick it up on the next regeneration.
|
|
89
97
|
|
|
90
|
-
### 6.
|
|
98
|
+
### 6. Always-loaded context budget
|
|
99
|
+
|
|
100
|
+
Everything Claude reads at launch — CLAUDE.md, its @-imports, and the active rules — is measured against `workspace.alwaysLoadedBudgetBytes`:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
node .claude/scripts/context-footprint.mjs --root .
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Rules carrying `paths:` frontmatter are conditional (they load only when a matching file is touched); the script lists them in a separate conditional section and excludes them from the total. With no `alwaysLoadedBudgetBytes` in workspace.json there is no budget and this check passes trivially.
|
|
107
|
+
|
|
108
|
+
Within budget → an OK line: `✓ Always-loaded context: 43 KB / 64 KB`. Over budget → a Warning (the workspace still functions; this is drift, not breakage) naming the top contributors and the fixes:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
⚠ Always-loaded context exceeds budget: 78 KB / 64 KB. Top contributors:
|
|
112
|
+
.claude/rules/git-conventions.md (12 KB), CLAUDE.md (9 KB),
|
|
113
|
+
.claude/rules/workspace-structure.md (8 KB). Scope situational rules with
|
|
114
|
+
paths: frontmatter, or move reference content to shared/.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The script itself exits `1` when over budget; `/maintenance` reports that as the warning above, not as a failed run.
|
|
118
|
+
|
|
119
|
+
### 7. Template freshness
|
|
91
120
|
|
|
92
121
|
Compare the workspace's pinned template version against the latest published on npm.
|
|
93
122
|
|
|
@@ -112,7 +141,7 @@ Report one of:
|
|
|
112
141
|
|
|
113
142
|
Active recommendations. Flags problems and suggests fixes, but asks before acting.
|
|
114
143
|
|
|
115
|
-
###
|
|
144
|
+
### 8. Component age check
|
|
116
145
|
|
|
117
146
|
Scan the following file sets for a YAML frontmatter `updated:` field:
|
|
118
147
|
- `.claude/rules/*.md` (active rules only — `.md.skip` files are included too, since the rule content can still drift)
|
|
@@ -126,7 +155,7 @@ Files without an `updated:` field are skipped — the check is opt-in and activa
|
|
|
126
155
|
|
|
127
156
|
When stale candidates are found, surface them as warnings in the output format and link to `config-review.md.skip` (in `.claude/rules/`) as the opt-in rule that documents the review cadence and rationale.
|
|
128
157
|
|
|
129
|
-
###
|
|
158
|
+
### 9. Stale context
|
|
130
159
|
- Ephemeral files not updated in 7+ days — suggest resolve, update, or archive
|
|
131
160
|
- `work-sessions/{name}/` folders whose worktrees are gone — suggest cleanup
|
|
132
161
|
- Session trackers whose branches have been merged — suggest `/complete-work` post-flight cleanup
|
|
@@ -136,15 +165,15 @@ When stale candidates are found, surface them as warnings in the output format a
|
|
|
136
165
|
- Braindumps that overlap significantly — suggest merging (e.g., "workspace-branching.md and persistent-work-sessions.md cover the same topic")
|
|
137
166
|
- Handoffs referencing deleted branches — suggest resolve or remove
|
|
138
167
|
|
|
139
|
-
###
|
|
168
|
+
### 10. Context reconciliation
|
|
140
169
|
- Read recent workspace-context writes (last session or last N files by updated date)
|
|
141
170
|
- For each, scan other workspace-context files for references that are now stale
|
|
142
171
|
- Surface: "{file} says X but {newer-file} now says Y. Update {file}?"
|
|
143
172
|
- This is the capture-time cross-check, run retroactively instead of inline
|
|
144
173
|
|
|
145
|
-
###
|
|
174
|
+
### 11. Canonical budget triage
|
|
146
175
|
|
|
147
|
-
This step runs only when the post-regen `--check` from step 9 still reports `selectionStatus: 'over-budget'`.
|
|
176
|
+
This step runs only when a canonical budget is set (`workspace.canonicalBudgetBytes` holds a number) and the post-regen `--check` from the cleanup regen pass (Flow step 9) still reports `selectionStatus: 'over-budget'`. With the budget off — absent or `null` in workspace.json — `--check` can never report over-budget, so this step is unreachable. Skip it too if the regular regen pass cleared the budget, or if `--check` was already `ok`, `trimmed`, or `stubbed` after that pass.
|
|
148
177
|
|
|
149
178
|
The rest of cleanup is suggestion-list-with-confirmation: surface a candidate, ask before applying, move on. Triage is the one meaningfully more interactive surface in `/maintenance`. It runs as a small REPL: present the budget state and a triage menu, take one action, re-run `--check`, present the menu again with the new state. No suggestion is auto-applied; every action is the user's choice.
|
|
150
179
|
|
|
@@ -189,7 +218,7 @@ For each chosen action:
|
|
|
189
218
|
|
|
190
219
|
Trim markers and demotions only matter for `priority: reference` files — `<!-- canonical:trim -->` spans on a `priority: critical` file are inert until the file is demoted. The triage flow never auto-decides which file to demote or which section to wrap; it surfaces the data, presents options, and waits.
|
|
191
220
|
|
|
192
|
-
###
|
|
221
|
+
### 12. Forge configuration
|
|
193
222
|
|
|
194
223
|
Read `workspace.json`. If `workspace.tracker?.type === 'github-issues'` and `workspace.forge` is unset, emit a notice (not an error):
|
|
195
224
|
|
|
@@ -201,8 +230,9 @@ Read `workspace.json`. If `workspace.tracker?.type === 'github-issues'` and `wor
|
|
|
201
230
|
|
|
202
231
|
This is migration guidance for workspaces created before the `forge` field landed — the field is back-compat with a sensible default, so the unset case is not a bug, just an opportunity to make the implicit explicit. If `workspace.forge.type` is set to a value with no adapter at `.claude/scripts/forges/{type}.mjs`, that IS an error and goes in the Issues section.
|
|
203
232
|
|
|
204
|
-
###
|
|
205
|
-
- Canonical budget — read from the same `--check` invocation as step 5.
|
|
233
|
+
### 13. Health metrics
|
|
234
|
+
- Canonical budget — read from the same `--check` invocation as step 5. When a budget is set, reported as `current / budget` bytes with the selection status (e.g., `full`, `2 reference files trimmed`); over-budget cases are deferred to the cleanup triage flow rather than re-reported here. When off, report the step 5 one-liner: `• Canonical budget: off (alwaysLoadedBudgetBytes covers the total)`.
|
|
235
|
+
- Always-loaded context — read from the same `context-footprint.mjs` invocation as audit step 6, reported the same way (`current / budget` bytes); over-budget is already surfaced as a warning there.
|
|
206
236
|
- Number of ephemeral files — flag if accumulating without resolution
|
|
207
237
|
- Session log stats (if `workspace-scratchpad/session-log.jsonl` exists):
|
|
208
238
|
- Sessions without capture
|
|
@@ -231,11 +261,12 @@ Cleanup suggestions (2):
|
|
|
231
261
|
⊕ migration-recipes.md still says "/sync handles dogfood" but
|
|
232
262
|
/sync was replaced by /sync-work — update?
|
|
233
263
|
|
|
234
|
-
OK (
|
|
264
|
+
OK (6):
|
|
235
265
|
✓ All CLAUDE.md skill references valid
|
|
236
266
|
✓ Workspace structure matches rule
|
|
237
267
|
✓ workspace.json repos all present
|
|
238
268
|
✓ Canonical: 17 KB / 40 KB (full)
|
|
269
|
+
✓ Always-loaded context: 43 KB / 64 KB
|
|
239
270
|
✓ Template is up to date (v0.14.0)
|
|
240
271
|
```
|
|
241
272
|
|
|
@@ -246,10 +277,11 @@ OK (5):
|
|
|
246
277
|
3. Read workspace.json — extract repo manifest
|
|
247
278
|
4. Check `.claude/rules/`, `.claude/skills/`, `.claude/agents/` against references
|
|
248
279
|
5. Check git state (worktrees, branches, remotes)
|
|
249
|
-
6. Run `node .claude/scripts/build-workspace-context.mjs --check --root .` — capture status. Exit `0` = clean and within budget, `1` = artifact missing or stale, `2` = artifacts current but canonical body over budget. The `canonical` block in the JSON output drives both the audit budget line and the cleanup triage decision.
|
|
250
|
-
7.
|
|
251
|
-
8.
|
|
252
|
-
9.
|
|
280
|
+
6. Run `node .claude/scripts/build-workspace-context.mjs --check --root .` — capture status. Exit `0` = clean (and within budget when one is set), `1` = artifact missing or stale, `2` = artifacts current but canonical body over budget — only possible with a budget set. The `canonical` block in the JSON output drives both the audit budget line and the cleanup triage decision; `"budget": null` means the canonical budget is off.
|
|
281
|
+
7. Run `node .claude/scripts/context-footprint.mjs --root .` — capture the total and the `BUDGET` line. Exit `0` = within budget or no budget set; exit `1` = over budget, reported as a warning with the top contributors (audit step 6).
|
|
282
|
+
8. Read session-log.jsonl if it exists
|
|
283
|
+
9. If cleanup mode: regenerate the workspace-context auto-files if stale (index.md, canonical.md, per-user team-member indexes); compare files pairwise for overlap; scan for stale cross-references. If post-regen `--check` reports `over-budget`, enter the canonical-budget triage flow described in cleanup step 11.
|
|
284
|
+
10. Compile and present findings grouped by severity
|
|
253
285
|
|
|
254
286
|
## Notes
|
|
255
287
|
- Audit mode is always read-only — never modifies files
|
|
@@ -17,16 +17,21 @@ Drain a workspace's accumulated session entries and switch new work to the task
|
|
|
17
17
|
node .claude/scripts/migrate-sessions.mjs --inventory
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Read-only. Present the stderr table plus each session's proposal with its reasons and warnings. Say plainly that the proposals are proposals — evidence and a starting point, not decisions. Pay particular attention to the per-remote state shown per worktree (`same`, `ahead +N`, `behind -N`, `diverged +N/-M`, `not-fetched`, `unknown`) and to `unbacked` warnings: they change what Finish and Archive mean for that session. Entries shown as `foreign` (symlinked) are never acted on — surface them for manual reconciliation.
|
|
20
|
+
Read-only. Present the stderr table plus each session's proposal with its reasons and warnings. Say plainly that the proposals are proposals — evidence and a starting point, not decisions. Pay particular attention to the per-remote state shown per worktree (`same`, `ahead +N`, `behind -N`, `diverged +N/-M`, `not-fetched`, `unknown`) and to `unbacked` warnings: they change what Finish and Archive mean for that session. Each worktree also lists every configured remote with its exact URL — read those before any backup decision: `origin:none` means only that the remote holds no copy of this branch, never that the repo has no remote, and an `origin` that is really a third-party upstream shows its URL right there. Entries shown as `foreign` (symlinked) are never acted on — surface them for manual reconciliation.
|
|
21
21
|
|
|
22
22
|
## 2. Decide per session, with the operator — one at a time
|
|
23
23
|
|
|
24
24
|
For each session, lay out its evidence and ask the operator which way to go. Never infer the decision from the proposal. The options:
|
|
25
25
|
|
|
26
26
|
- **Finish** (typical for MERGEABLE) — resume the session with `/start-work`, then run `/complete-work`; its own merge confirmation applies there. But if the inventory shows a **diverged** remote for that session, say so *before* the operator chooses Finish: `/complete-work`'s plain push will be rejected, and pushing the rewritten history needs `--force-with-lease` — which you run only on the operator's explicit yes naming the branch. Never force silently.
|
|
27
|
-
- **Archive** (typical for ABANDONED, a broken shell, or a MERGEABLE the operator gives up on) — take it out of the active lifecycle without destroying anything.
|
|
28
|
-
1. **
|
|
29
|
-
2. **
|
|
27
|
+
- **Archive** (typical for ABANDONED, a broken shell, or a MERGEABLE the operator gives up on) — take it out of the active lifecycle without destroying anything. Three steps, in this order, each its own decision:
|
|
28
|
+
1. **Clear uncommitted work.** `--archive` refuses when any of the session's worktrees has uncommitted or untracked changes — an edited `session.md` counts — and names them, because edits buried uncommitted in an archive are invisible to every later merge or PR. Offer the operator: commit them to the session branch first (`git -C {worktree} add -A`, then `git -C {worktree} commit -m "…"` — per dirty worktree), discard them explicitly (`git -C {worktree} restore …` / `git -C {worktree} clean …`), or, on an explicit yes, re-run with `--allow-uncommitted` to archive them mid-edit. Do this before the backup: the backup tags committed tips only, so committing first brings those edits under the backup, while anything archived with `--allow-uncommitted` is NOT in it.
|
|
29
|
+
2. **Offer a backup.** Archiving keeps everything on this machine; a backup adds an off-machine copy of the session's commits, and it is what makes a later deletion safe. It covers the committed tips as they stand after step 1. Plain `--backup` creates `drain/{session}/…` tags locally and pushes nothing — for a repo whose only remote is one the operator does not own, that local tag IS the backup. Pushing is a separate, explicitly allowed step:
|
|
30
|
+
```bash
|
|
31
|
+
node .claude/scripts/migrate-sessions.mjs --backup --session {name} --remote
|
|
32
|
+
```
|
|
33
|
+
With no allow flag this pushes nothing and exits non-zero, listing per repo the tag and the exact URL(s) it would push to. Approve against the push URL, not the fetch URL: a remote whose push URL differs from its fetch URL shows both, and the script refuses to push there on `--remote-allow-all` — only an explicit `--remote-allow {repo}={remote}` naming it proceeds. Walk the operator through every URL and ask per repo. **Never push backup tags to a remote the operator does not own — a third-party upstream, a read-only mirror, an unfamiliar push URL; pushing `drain/*` tags there publishes the session's commits somewhere foreign.** On yes for repos they do own, re-run adding `--remote-allow {repo}={remote}` per repo (`.` is the workspace repo; `--remote-allow-all` only when every listed URL is their own) and show what was pushed. Declining is fine — the archive still keeps everything locally.
|
|
34
|
+
3. **Archive after an explicit yes naming the session:** `node .claude/scripts/migrate-sessions.mjs --archive --session {name}`. The whole session folder moves to `{sessions}/.archived/{name}--{timestamp}/` and git's worktree links are repaired to follow it — every commit, uncommitted edit, untracked or ignored file, and embedded repository comes along. If the move or the repair fails, the session is put back and the result says whether every link was verified. The session's branches stay checked out in the archived worktrees, so a new task cannot reuse those branch names until the archive is deleted. The archive refuses, touching nothing, when a directory in the folder cannot be read, when the folder holds a worktree of a repository outside this workspace, or when it holds a submodule checkout (its link cannot be repaired) — surface the reason; for a submodule the options are Finish or Keep. Relay any `warnings` (relative symlinks that pointed outside the session no longer resolve after the move).
|
|
30
35
|
- **Keep** (typical for ACTIVE, and the only sane answer for UNKNOWN) — leave it; it completes later under the session lifecycle.
|
|
31
36
|
|
|
32
37
|
Unbacked commits (present on no remote) are safe in an archive — they are only ever at risk when someone deletes one. Say so when you archive such a session, and offer the backup.
|
|
@@ -43,7 +48,14 @@ The switch procedure:
|
|
|
43
48
|
```bash
|
|
44
49
|
node .claude/scripts/migrate-sessions.mjs --enable-task-model --root .claude/worktrees/chore-enable-task-model
|
|
45
50
|
```
|
|
46
|
-
3.
|
|
51
|
+
3. Land the change. With a forge-hosted remote, commit there, open a PR through the workspace's normal flow, and pull the launcher after merge. Without one (`git -C . remote -v` shows no remote this workspace can open PRs against), land it locally — commit in the worktree, fast-forward the launcher's default branch, remove the worktree:
|
|
52
|
+
```bash
|
|
53
|
+
git -C .claude/worktrees/chore-enable-task-model add workspace.json
|
|
54
|
+
git -C .claude/worktrees/chore-enable-task-model commit -m "chore: switch to the task lifecycle"
|
|
55
|
+
git -C . merge --ff-only chore/enable-task-model
|
|
56
|
+
node .claude/scripts/task-worktree.mjs --root . --remove --repo . --branch chore/enable-task-model --delete-branch
|
|
57
|
+
```
|
|
58
|
+
Either way the launcher root never commits to its default branch directly. A workspace with neither a remote nor a tracker can use the task model's local mode (gh:173); until that ships, recommend such workspaces stay on sessions.
|
|
47
59
|
|
|
48
60
|
The switch output reports `remainingSessions: null` when run from the worktree — the real remaining-sessions list comes from a separate `--inventory` at the launcher root. Remaining sessions are fine either way: they keep resuming and completing under the session lifecycle after the switch.
|
|
49
61
|
|
|
@@ -52,7 +52,7 @@ Push the branch and open a PR through the forge adapter — per-repo `createForg
|
|
|
52
52
|
|
|
53
53
|
**Step 5: Tag and publish**
|
|
54
54
|
|
|
55
|
-
Pull the merge, tag it, push the tag
|
|
55
|
+
Pull the merge, tag it, and push the tag:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
git -C repos/{repo} pull --ff-only
|
|
@@ -60,11 +60,15 @@ git -C repos/{repo} tag v{version}
|
|
|
60
60
|
git -C repos/{repo} push origin v{version}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
Who creates the release depends on the repo. If `.github/workflows/publish.yml` exists and itself creates the release (it contains `gh release create`, `softprops/action-gh-release`, or `actions/create-release`), the workflow owns the release — do not call `releaseCreate`; racing it duplicates the release or fails. Instead find and watch its run with `workflowRunFind` / `workflowRunWatch` — retry the find up to 5 times with 3 s backoff (the run may not be registered the moment the tag lands); a failed run is reported to the operator, not thrown — then confirm the release exists with `forge.releaseView({ tag: 'v{version}', repo })`.
|
|
64
|
+
|
|
65
|
+
Otherwise the skill creates the release itself:
|
|
66
|
+
|
|
63
67
|
```js
|
|
64
68
|
await forge.releaseCreate({ tag: 'v{version}', repo, generateNotes: true });
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
If
|
|
71
|
+
If a `publish.yml` without release creation exists, still find and watch its run the same way.
|
|
68
72
|
|
|
69
73
|
**Step 6: Tear down and report**
|
|
70
74
|
|
|
@@ -19,9 +19,9 @@ Begin or resume a persistent work session. Each session lives in its own `work-s
|
|
|
19
19
|
|
|
20
20
|
New work as a task: one tracker issue, one branch, one worktree per repo the work touches. This flow creates no `work-sessions/` folder, no `session.md`, and seeds no task list — the issue, the branch, and the chat record are the entire state. The chat stays at the workspace root — `{launcher-root}`, the absolute path on the `Workspace root:` line the SessionStart hook injects (at /start-work time you are normally already there); the worktrees are reached by path.
|
|
21
21
|
|
|
22
|
-
If `workspace.tracker` is absent, say tracking is off and skip step 1 — but still ask for the type (`bug` / `feat` / `chore`) and a one-line description, because the type picks the branch prefix — then continue with steps 2–6.
|
|
22
|
+
If `workspace.tracker` is absent, say tracking is off and skip step 1 — but still ask for the type (`bug` / `feat` / `chore`) and a one-line description, because the type picks the branch prefix — then continue with steps 2–6. What that costs: without a tracker there is no `workItem` and no issue to close at completion — the task is still recorded on the chat record (with no work item, keyed by repo + branch), so `/complete-work` finds it from the launcher like any other task.
|
|
23
23
|
|
|
24
|
-
1. **Identify or create the tracker issue and claim it
|
|
24
|
+
1. **Identify or create the tracker issue and claim it.** If the invocation's arguments already name an issue — `gh:N`, `#N`, or an issue URL — normalize it to the adapter's id (`#42` and a `.../issues/42` URL both mean `gh:42`), fetch it with `tracker.getIssue(id)`, claim it when it is not yet assigned to you (with the same `ALREADY_ASSIGNED` handling as the fallback pick below), and skip the candidate list entirely. Otherwise, list the candidates — the same adapter calls as Flow: Blank steps 3–6:
|
|
25
25
|
|
|
26
26
|
```javascript
|
|
27
27
|
import { createTracker } from './.claude/scripts/trackers/interface.mjs';
|
|
@@ -54,13 +54,13 @@ If `workspace.tracker` is absent, say tracking is off and skip step 1 — but st
|
|
|
54
54
|
```bash
|
|
55
55
|
node .claude/scripts/task-worktree.mjs --root . --create --repo "{repo}" --branch "{branch}"
|
|
56
56
|
```
|
|
57
|
-
The script fetches origin best-effort (offline is fine) before choosing the base. A project repo's worktree lands at `repos/{repo}/.claude/worktrees/{slug}/` — Claude Code's native worktree location — based on `origin/{defaultBranch}` when that ref exists, and never tracking it. For `.` the worktree lands at `.claude/worktrees/{slug}/` — the same native location, one level up — based on the workspace origin's HEAD (falling back to `main`); the workspace's own `.gitignore` already covers the path.
|
|
57
|
+
The script fetches origin best-effort (offline is fine) before choosing the base. A project repo's worktree lands at `repos/{repo}/.claude/worktrees/{slug}/` — Claude Code's native worktree location — based on `origin/{defaultBranch}` when that ref exists and the local `{defaultBranch}` otherwise — a local-mode repo (`merge: "local"`) bases on the local `{defaultBranch}` itself, since its merges land there and its origin ref never advances — and never tracking it. For `.` the worktree lands at `.claude/worktrees/{slug}/` — the same native location, one level up — based on the workspace origin's HEAD (falling back to `main`); the workspace's own `.gitignore` already covers the path. A repo without a forge remote — no origin at all, or `merge: "local"` in `workspace.json` — runs the task in local mode: nothing is pushed for it, and `/complete-work` merges it into its source clone instead.
|
|
58
58
|
|
|
59
|
-
5. **Record the task on this chat's record
|
|
59
|
+
5. **Record the task on this chat's record:**
|
|
60
60
|
```bash
|
|
61
61
|
node .claude/scripts/chat-record.mjs --root . --add-task --chat "{chat}" --work-item "{workItem}" --branch "{branch}" --repo "{repo}"
|
|
62
62
|
```
|
|
63
|
-
`{chat}` is the name from the `Chat record:` line the SessionStart hook injected into this conversation. If there is no such line, say so and skip recording rather than guessing a name.
|
|
63
|
+
Omit `--work-item` when the task has none — the entry is recorded with a null work item, keyed by repo + branch. `{chat}` is the name from the `Chat record:` line the SessionStart hook injected into this conversation. If there is no such line, run `node .claude/scripts/chat-record.mjs --whoami --root .` first — compaction can drop the hook line, and this recovers the name by matching the chat's session id. When that too prints nothing, say so and skip recording rather than guessing a name.
|
|
64
64
|
|
|
65
65
|
6. **Tell the user where the work happens:** the worktree path(s) above — edits belong there, not in the source clones at `repos/{repo}/`. Work continues from this chat by path. A chat started inside a **project** worktree would not load the workspace's conventions or hooks (a worktree is a context boundary), so staying here is the default. A `.` worktree does load a copy of the workspace's `CLAUDE.md`/`.claude/` — but with the worktree as root, so its chat records land in the worktree's own scratchpad rather than the launcher's; the task still belongs to this chat.
|
|
66
66
|
|