@sabaiway/agent-workflow-kit 14.9.0 → 14.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.
- package/CHANGELOG.md +70 -0
- package/README.md +1 -0
- package/SKILL.md +5 -5
- package/bin/install.mjs +1 -0
- package/capability.json +1 -1
- package/migrations/4.0.0-full-flow-offer.md +33 -0
- package/migrations/README.md +2 -2
- package/package.json +1 -1
- package/references/modes/bootstrap.md +3 -2
- package/references/modes/hook.md +1 -1
- package/references/modes/upgrade.md +18 -6
- package/references/modes/velocity.md +1 -1
- package/references/shared/report-footer.md +2 -2
- package/tools/cheap-agents-read.mjs +1 -1
- package/tools/direct-run.mjs +2 -0
- package/tools/profile-gaps.mjs +86 -0
- package/tools/reference-profile.mjs +69 -0
- package/tools/tier-preview.mjs +248 -0
- package/tools/upgrade-runlist.mjs +6 -0
- package/tools/velocity-profile.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,76 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
|
|
|
4
4
|
is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
|
|
5
5
|
every `migrations/<version>-<slug>.md` newer than it, in semver order.
|
|
6
6
|
|
|
7
|
+
## 14.11.0 — the upgrade offers the epic and task settings and the epic store in one preview, and remembers a no (AD-157)
|
|
8
|
+
|
|
9
|
+
**An upgraded project is now offered the epic tier's settings and store instead of being left to find them.** Up to
|
|
10
|
+
14.10.0 an upgrade seeded no epic or task setting in `docs/ai/orchestration.json` and no epic store, so a project
|
|
11
|
+
reached the epic, story and task tier only by hand. 14.11.0 moves the deployment-lineage head from `3.0.0` to
|
|
12
|
+
`4.0.0`: every project stamped below it takes the upgrade route once more, and the upgrade's step 3 gains the item
|
|
13
|
+
`tier`, which shows every profile gap the kit can detect — the Story sessions section, the epic and task settings
|
|
14
|
+
and the epic store — in one preview and asks once. Story S3 of the epic `CONSUMERS-MOVE-ONTO-THE-FULL-FLOW`;
|
|
15
|
+
contract `docs/ai/specs/kit/tier/tier-offer/` (new).
|
|
16
|
+
|
|
17
|
+
- **`tools/tier-preview.mjs --cwd <project> [--apply | --decline]`.** Without a flag it writes nothing and prints
|
|
18
|
+
one line per profile gap the kit knows — `present`, `offered`, `declined` with the lineage it was declined at, or
|
|
19
|
+
`undecidable` with its reason — and under each offered gap the preview command that closes it when re-run with
|
|
20
|
+
`--apply`. Under an offered epic and task settings gap it prints one line per setting: `present`, or the value it
|
|
21
|
+
would write, with the reason for every better value it skipped. With `--apply` it writes only its own two items:
|
|
22
|
+
the settings that are absent (with the `_README` note refresh `set-recipe --write` also makes), and
|
|
23
|
+
`docs/ai/epics/.gitkeep`. With `--decline` it records every offered gap as declined. Exit 0 done, 1 one named
|
|
24
|
+
refusal on stderr, 2 usage, raised before anything is read.
|
|
25
|
+
- **The value written is the best one this environment can run.** `epic.author` tries subagent then solo,
|
|
26
|
+
`epic.review` reviewed then solo, `task.author` and `task.execute` delegated, subagent, then solo; the first one
|
|
27
|
+
the kit's own recipe check does not degrade wins, and each skipped one is shown with the kit's own reason (a
|
|
28
|
+
missing executor names `/agent-workflow-kit agents`). A setting you already have, whatever its value, is reported
|
|
29
|
+
present and never changed; the file is written by the same chain as `set-recipe --set … --write`, so the bytes are
|
|
30
|
+
the ones that command would write.
|
|
31
|
+
- **A no is remembered.** `--decline` writes `docs/ai/profile-declines.json`, each gap with the shipped profile's
|
|
32
|
+
lineage (`4.0.0`). Later runs report it `declined` and do not offer it again until a later lineage step changes
|
|
33
|
+
the shipped profile's lineage. A record the tool cannot read or parse is refused by name — never overwritten and
|
|
34
|
+
never ignored. In a hidden deployment the record is local to this machine, like the rest of `docs/ai/`.
|
|
35
|
+
- **Nothing is guessed about a broken config.** The config is checked without following a symlink before it is
|
|
36
|
+
loaded; a missing, symlinked, unreadable, unparseable or invalid file is reported by one name, and nothing is
|
|
37
|
+
written to it. The epic store is judged by itself: a regular file or a symlink at `docs/ai/epics` is reported,
|
|
38
|
+
never replaced.
|
|
39
|
+
- **The offer asks once.** The new `tier` item runs after the agent-rules lens item. The ask is put only when a
|
|
40
|
+
gap is `offered`; a yes runs the offer and every other offered command with `--apply`, a no runs `--decline`. The
|
|
41
|
+
lens step's own ask for the Story sessions section folds into it while that section is offered; a Communication
|
|
42
|
+
section missing while the Story sessions section is present or declined keeps its own ask, shown with the insert's
|
|
43
|
+
full preview, since one insert adds every missing section. A refusal, or a failed command the ask runs, stops the
|
|
44
|
+
upgrade before the re-stamp.
|
|
45
|
+
- **The note.** `migrations/4.0.0-full-flow-offer.md` says why the step exists, how to check its outcome and how to
|
|
46
|
+
undo it, and tells a project that came from kit 14.1.0 to close its open task threads before opening a new one.
|
|
47
|
+
`migrations/README.md` now says a note's version is the lineage step, not a kit release.
|
|
48
|
+
- **Two new gaps in the registry.** `tools/profile-gaps.mjs` gains `epic-task-slots` and `epic-store-seeded`, each
|
|
49
|
+
answering present, absent or undecidable from the project's own files, never from a recorded no; the new read-only
|
|
50
|
+
`tools/reference-profile.mjs` reads the shipped profile and the decline record.
|
|
51
|
+
|
|
52
|
+
## 14.10.0 — every place a user first looks names the epic, story and task tier and the guide that explains it (AD-156)
|
|
53
|
+
|
|
54
|
+
**A kit user now meets the tier without running the help.** Until 14.10.0 only the help named the tier and its
|
|
55
|
+
guide. Six more places now carry the phrase `epic, story and task` and the command `/agent-workflow-kit tier` on one
|
|
56
|
+
line, each in its own words: the skill's description, a read-only row of the kit README's command table directly after
|
|
57
|
+
`now`, a short paragraph after step 3 of the root README's start section, a fixed line of the welcome mat in the
|
|
58
|
+
bootstrap and upgrade report footer, a read-only entry of the bootstrap's optional block, and a bullet of the install's `Next —` lines.
|
|
59
|
+
Story S4 of the epic `KIT-USERS-LEARN-THE-TIER-AND-WALK-IT`, its last; contract
|
|
60
|
+
`docs/ai/specs/kit/tier/tier-discovery.md` (new).
|
|
61
|
+
|
|
62
|
+
- **The welcome mat.** The tier line is printed right after the help line, in every footer, whichever rung fires or
|
|
63
|
+
none; it is not a rung, since a rung prints only when every rung above it is silent and a user with no backend
|
|
64
|
+
ready would never reach it. The six rungs and their order are unchanged; with no rung, the two lines stand alone.
|
|
65
|
+
- **The bootstrap block.** The lead-in now says every writer previews first, since the new tier entry writes
|
|
66
|
+
nothing; every other entry is unchanged.
|
|
67
|
+
- **Proof on the installed copies.** The repository walk fixture `test/tier-walk-e2e.test.mjs` checks each
|
|
68
|
+
installed surface in both of its states — the installer's recorded output, the installed files and the help
|
|
69
|
+
rendered from the installed `tools/commands.mjs` — and the root README once, from the repository. Each region
|
|
70
|
+
is cut by asserted anchors (a missing one fails naming the surface and the anchor, from the new test helper
|
|
71
|
+
`test/doc-region-harness.test.mjs`), and cells that remove, move or case-change a token assert the surface is named.
|
|
72
|
+
- **No budget raised.** The router byte budgets hold unedited; `bin/install.mjs` grows by its one bullet line, a
|
|
73
|
+
recorded raise to 585 lines. The help's own text, which the 14.9.0 entry expected with this story, stays as it is:
|
|
74
|
+
its rendered `tier` line already names the tier and the guide. The command catalog, the rungs and `upgrade.md` are
|
|
75
|
+
unchanged.
|
|
76
|
+
|
|
7
77
|
## 14.9.0 — one read-only guide says what an epic, a story and a task are, and names the next step of the walk (AD-155)
|
|
8
78
|
|
|
9
79
|
**`/agent-workflow-kit tier` answers "what is this tier, and what do I do next?" from the disk alone.** Until 14.9.0
|
package/README.md
CHANGED
|
@@ -234,6 +234,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
234
234
|
| `/agent-workflow-kit setup [backend]` | opt-in, any time | **link-only** auto-setup of a bridge: places the bundled bridge skill (only into an absent / empty / managed dir — never overwrites an unmanaged one) + links its wrappers onto `PATH` via managed symlinks (idempotent; refuses to clobber a non-symlink; try `--dry-run` to preview). The binary install + the one-time subscription login stay **manual**: it prints the exact **login** command and points the binary install at each bridge's `setup/README.md`. POSIX wrappers — on Windows use WSL. Never commits, never runs a subscription CLI. |
|
|
235
235
|
| `/agent-workflow-kit status` | any time | **read-only** single view of **versions + deployment + settings + bridges**: which members (kit / memory / engine / the two bridges) are installed and at what version (with an honest "installed on this machine" note when one is behind) and — in a project — what's deployed (`docs/ai`, the version stamps, and the **visibility**: visible / hidden / unclear), and the **feature-spec adoption state** (`not adopted` / `adopting (N draft)` / `adopted (N live, M draft)` / `could not be read`, plus `declined` when recorded), plus your settings (orchestration recipes, attribution, velocity) and the bridges' readiness. The two version axes (package number vs deployment-structure head) stay decoupled. Never writes, never commits, never runs a subscription CLI. |
|
|
236
236
|
| `/agent-workflow-kit now` | any time | **read-only** answer to where work stands in four blocks: **NOW** names every plan in flight, its phase, current and next step, and the tree evidence; **STEPS** gives every ledger row its evidence-derived status; **QUEUE** folds named rows under their full heading paths and calls position priority only in the judged backlog; **CAMPAIGN** shows the source-size practice figures and config state. The plan's own text never sets a status; a contradicted claim prints beside it. A block that cannot be derived is WITHHELD with its cause; NOW has two halves, plans and tree, each independently a value or WITHHELD, and NOW itself is WITHHELD only when both are. Never writes, never commits, never runs a subscription CLI. |
|
|
237
|
+
| `/agent-workflow-kit tier` | any time | **read-only** guide to the epic, story and task tier: what each level is, and the next step of the walk, with the command that runs it wherever a tool does. Never writes, never commits, never runs a subscription CLI. |
|
|
237
238
|
| `/agent-workflow-kit recipes` | any time | **read-only** orchestration advisor: presents five named recipes for composing the carriers of a step — the bridges and the full-tool executor subagent — into plan → execute → review — **Solo / Reviewed / Council / Delegated / Subagent** — plans + recommends one for your environment (degrading with a stated reason when a backend isn't ready, or when the executor vehicle is missing/unusable), and offers the choice. The activity/slot registry (six activities) is rendered by `procedures` and by `set-recipe --help`. The orchestrator runs it via the bridge skills or the executor vehicle and **always commits**; the kit never executes a recipe, never runs a subscription CLI, never commits. |
|
|
238
239
|
| `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution` / `routine` / `feedback-triage` / `epic` / `task`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + carrier readiness (`plan-authoring`: author, fold, review; default Reviewed when a backend is ready, Council on request; slot-aware incl. Delegated and Subagent) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same) — except `feedback-triage`, whose record-bound block carries the check with `--excerpts`, the resolved member lines (ready bridge commands, the ready lens brief, or explicit skipped members), and either the record round-table command for a bridge obligation or the no-receipt fact otherwise: the record is the artifact. `--override <slot>=<value>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
|
|
239
240
|
| `/agent-workflow-kit set-recipe` | any time | **config writer** for `docs/ai/orchestration.json`: tell the agent your preference in plain language and it maps it to explicit `--set <activity>.<slot>=<value>` / `--unset` ops; the kit validates, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe), resolving the effective recipe vs live readiness. Writes **only** that file — **never runs a backend or a subagent, never commits**; hand-editing stays fully supported. |
|
package/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-workflow-kit
|
|
3
|
-
description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
|
|
3
|
+
description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow". New to the epic, story and task tier? Run `/agent-workflow-kit tier`.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
|
-
version: '14.
|
|
6
|
+
version: '14.11.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
|
@@ -71,14 +71,14 @@ Pick the mode from the user's invocation — the mapping is pinned by `tools/com
|
|
|
71
71
|
Before acting, read `docs/ai/.workflow-version` (the project's stamp) — via the host's file-read tool, else ONE plain undecorated command (no compounds/redirects/pipes) — to decide the route — an **internal** routing decision, **not** a line you print (version display: *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Route:
|
|
72
72
|
|
|
73
73
|
- **absent** → bootstrap (a fresh deployment).
|
|
74
|
-
- **stamp < `
|
|
75
|
-
- **stamp == `
|
|
74
|
+
- **stamp < `4.0.0`** (the deployment-lineage head) → `upgrade`.
|
|
75
|
+
- **stamp == `4.0.0`** → already current; only the stamp-independent reconciles may run — the FULL set lives in `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` step 3; run step 3 (never enumerate the reconciles from memory).
|
|
76
76
|
- **stamp > head / unparseable** → STOP — never-downgrade gate (see `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` step 2).
|
|
77
77
|
- **Exempt:** `autonomy-doctor` provisions the OS, not the deployment — stamp-independent (stated in its mode file).
|
|
78
78
|
|
|
79
79
|
**Two independent version axes — never conflate them:**
|
|
80
80
|
|
|
81
|
-
1. **Project deployment** — `docs/ai/.workflow-version` vs the lineage head (`
|
|
81
|
+
1. **Project deployment** — `docs/ai/.workflow-version` vs the lineage head (`4.0.0`). This is the **only** axis this skill compares.
|
|
82
82
|
2. **Kit freshness** — this skill's own files vs the published npm package. That is the **npx installer's** job: `npx @sabaiway/agent-workflow-kit@latest init` (it refuses a stale-cache downgrade by comparing the version on disk — **no network**). This skill never checks npm, and the package version is **not** the lineage head.
|
|
83
83
|
|
|
84
84
|
**Refreshed the kit but nothing changed?** After the npx installer updates `~/.claude/skills/agent-workflow-kit/`, **restart the session** so the agent reloads the new skill files.
|
package/bin/install.mjs
CHANGED
|
@@ -551,6 +551,7 @@ const main = async () => {
|
|
|
551
551
|
Next — open your agent inside a project and run the skill:
|
|
552
552
|
• first time in this project -> /agent-workflow-kit
|
|
553
553
|
• project already has the kit -> /agent-workflow-kit upgrade
|
|
554
|
+
• new to the epic, story and task tier -> /agent-workflow-kit tier
|
|
554
555
|
• per agent: Claude Code -> /agent-workflow-kit · Devin Local -> /agent-workflow-kit ·
|
|
555
556
|
Codex -> its /skills menu -> agent-workflow-kit (Codex may also auto-trigger it)
|
|
556
557
|
|
package/capability.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Migration 4.0.0-full-flow-offer
|
|
2
|
+
|
|
3
|
+
**From:** versions < 4.0.0 **To:** 4.0.0
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The full flow is recommended to every kit consumer (AD-151), and the reference profile
|
|
8
|
+
`full-flow` announces it. This lineage step is the one that offers it: the epic and task
|
|
9
|
+
settings, resolved to what this environment can run or to solo with the reason, and the epic
|
|
10
|
+
store `docs/ai/epics/`.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
1. **Task threads (a project that came from kit 14.1.0):** close every task thread that was open
|
|
15
|
+
when this upgrade started before opening a new one (AD-146). Undoing a stray change follows the
|
|
16
|
+
"Before you upgrade" paragraph of the installed kit's `CHANGELOG.md` entry 14.2.0.
|
|
17
|
+
2. **The offer:** it already ran as this upgrade's step-3 `tier` item. Re-run the preview
|
|
18
|
+
`node <installed-kit>/tools/tier-preview.mjs --cwd <project>` to verify, and relay its lines;
|
|
19
|
+
never ask again. An item still `offered` waits for the next upgrade.
|
|
20
|
+
3. **Re-stamp:** the upgrade re-stamps `docs/ai/.workflow-version` to 4.0.0.
|
|
21
|
+
|
|
22
|
+
## Verification
|
|
23
|
+
|
|
24
|
+
The preview reports each of `epic-task-slots` and `epic-store-seeded` `present`, `declined`, or
|
|
25
|
+
`offered` when the step-3 ask got no answer; an `undecidable` item names its reason.
|
|
26
|
+
|
|
27
|
+
## Rollback
|
|
28
|
+
|
|
29
|
+
Only what this upgrade changed: remove each setting it added with
|
|
30
|
+
`node <installed-kit>/tools/set-recipe.mjs --unset <activity>.<slot> --write`; remove
|
|
31
|
+
`docs/ai/epics/.gitkeep`, then the `docs/ai/epics` directory when it holds nothing else; restore
|
|
32
|
+
`docs/ai/profile-declines.json` to its bytes before this upgrade, or delete it when this upgrade
|
|
33
|
+
created it.
|
package/migrations/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Migrations
|
|
2
2
|
|
|
3
3
|
Each upgrade step is one file: `migrations/<version>-<slug>.md`, where `<version>` is the
|
|
4
|
-
|
|
4
|
+
deployment-lineage step the note moves a project to (not a skill release) and `<slug>`
|
|
5
5
|
is a short kebab-case name. Empty until the first change that needs migrating — most
|
|
6
6
|
releases add files/templates, which `upgrade` reconciles without a migration.
|
|
7
7
|
|
|
@@ -15,7 +15,7 @@ releases add files/templates, which `upgrade` reconciles without a migration.
|
|
|
15
15
|
The upgrade must relay those stdout lines verbatim and apply exactly the listed notes.
|
|
16
16
|
A named refusal stops the upgrade, except `stamp-absent`, which routes it back to the upgrade's step 1.
|
|
17
17
|
3. Apply them in the order the tool listed them (**ascending semver order**).
|
|
18
|
-
4. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`
|
|
18
|
+
4. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`4.0.0` today — the
|
|
19
19
|
shared lineage, **not** this skill's npm package version). A stamp greater than the head → STOP.
|
|
20
20
|
|
|
21
21
|
## Authoring rules
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.11.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -37,13 +37,14 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
37
37
|
template copy do (the agent-rules lens,
|
|
38
38
|
`${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md`); it is idempotent, so an
|
|
39
39
|
untouched tree simply reports `already-current`. Then write the **deployment-lineage head** into
|
|
40
|
-
`docs/ai/.workflow-version` (one semver line). The lineage head is **`
|
|
40
|
+
`docs/ai/.workflow-version` (one semver line). The lineage head is **`4.0.0`** — the shared
|
|
41
41
|
`agent-workflow` deployment lineage, **NOT** this kit's npm package version (see
|
|
42
42
|
`package.json` / `CHANGELOG.md`). The two are
|
|
43
43
|
independent axes: a packaging-only release bumps the package but leaves the lineage head until a
|
|
44
44
|
migration actually changes the deployed `docs/ai` structure. A stamp greater than the head →
|
|
45
45
|
STOP (never downgrade).
|
|
46
|
-
11. **Report & ask.** Show `tree docs/ai/`, 2–3 lines on what was filled with real data vs left as TODO, then print the **report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, rendered from the helpers, same host-can't-run skip-with-reason). The welcome mat closes on **one** caveat-aware next step (a behind member first, else `setup` / `recipes` / `velocity` / `agents` / `hook`). **After the footer, present ONE compact optional-accelerators block** —
|
|
46
|
+
11. **Report & ask.** Show `tree docs/ai/`, 2–3 lines on what was filled with real data vs left as TODO, then print the **report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, rendered from the helpers, same host-can't-run skip-with-reason). The welcome mat closes on **one** caveat-aware next step (a behind member first, else `setup` / `recipes` / `velocity` / `agents` / `hook`). **After the footer, present ONE compact optional-accelerators block** — a one-line why each; every writer previews first, and nothing runs without a yes:
|
|
47
|
+
- `/agent-workflow-kit tier` — read-only: explains the epic, story and task tier and names the next step of the walk;
|
|
47
48
|
- `/agent-workflow-kit velocity` — routine read-only commands stop prompting (incl. the `--kit-tools` tier for the kit's own read-only tools; `${CLAUDE_SKILL_DIR}/references/modes/velocity.md`);
|
|
48
49
|
- `/agent-workflow-kit agents` — cheap-model subagents take the mechanical work (sweeps, changelog skeletons, gate triage; `${CLAUDE_SKILL_DIR}/references/modes/agents.md`);
|
|
49
50
|
- **gates seeding + hook** — offer to seed `docs/ai/gates.json` from the commands recon already recorded (step 1), via the consent-gated preview in `${CLAUDE_SKILL_DIR}/references/modes/gates.md` — this block is where that offer fires; then, once gates are declared, `/agent-workflow-kit hook` auto-approves exactly those declared commands (`${CLAUDE_SKILL_DIR}/references/modes/hook.md`);
|
package/references/modes/hook.md
CHANGED
|
@@ -24,7 +24,7 @@ The opt-in **gate-approval PreToolUse hook** — the family's third `.claude/` w
|
|
|
24
24
|
|
|
25
25
|
**The read-lane (opt-in, AD-055 Part II).** The toggle lives in `docs/ai/lanes.json` — a **SEPARATE kit-owned strict-JSON file** (`{ "readLane": true }`); `docs/ai/gates.json`, both its validators and the byte-mirrored template are **untouched** (the gates schema has no lane/model/routing fields — that claim stays true). The hook reads `lanes.json` LIVE per call, **fail-closed**: an absent / malformed / non-object file, or a `readLane` that is not the boolean `true`, leaves the lane dark (gate auto-approval and the residual ask are unaffected). `lanes.json` becomes a **privileged file** exactly like `gates.json` — an auto-approved read chain runs **unattended** and can read any file you can (the same trust boundary as the audited read-only core velocity seeds, extended to compounds (and singles) of that core; **prompt-bypass only, never a sandbox bypass**). Enable it with `--read-lane` (below), which runs a **currency check** first: it refuses unless the PLACED hook is byte-identical to the current bundle — a **pre-1.48 hook never reads `lanes.json`**, so enabling the lane against a stale placed copy would be a silent no-op the user paid consent for. The stale-hook silent-blackout class is dead **by construction** (an old hook merely lacks the rung; nothing existing changes behavior); the currency refusal names the **delete-to-reseed** recovery (`rm .claude/hooks/agent-workflow-gates.mjs` + the `--apply` one-liner) — there is **no refresh-only hook lane** this release (a diverged wired copy stays preserved, unchanged).
|
|
26
26
|
|
|
27
|
-
**Version-status routing** like the other writer modes (stamp head `
|
|
27
|
+
**Version-status routing** like the other writer modes (stamp head `4.0.0`; `--apply` enforces it in code).
|
|
28
28
|
|
|
29
29
|
Run `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs [--dry-run | --apply] [--cwd <dir>]`:
|
|
30
30
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md · ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
|
|
7
7
|
|
|
8
8
|
1. Read `docs/ai/.workflow-version` (the project's stamped lineage). If missing, treat as a pre-versioned deployment and offer to re-bootstrap conservatively.
|
|
9
|
-
2. **Never-downgrade gate — FIRST, before any write.** Compare the stamp to the **deployment-lineage head** (`
|
|
10
|
-
3. **The step-3 run-list — stamp-independent reconciles, BEFORE the equal-head short-circuit.** Reached only when the stamp **≤ head**. Run the **
|
|
9
|
+
2. **Never-downgrade gate — FIRST, before any write.** Compare the stamp to the **deployment-lineage head** (`4.0.0` — NOT this kit's package version). If the stamp is **greater than the head** or unparseable → **STOP and report**; do not touch a newer / unknown deployment at all (not even the methodology slot). This STOP is one of the few places the number is actionable (*Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`): show the user **the `docs/ai` structure version** their deployment carries versus the one this kit expects, plus the plain one-line two-axes note — naming it the structure version, **never** "lineage head".
|
|
10
|
+
3. **The step-3 run-list — stamp-independent reconciles, BEFORE the equal-head short-circuit.** Reached only when the stamp **≤ head**. Run the **eight operations below in order, on every upgrade** (equal-head too) — no lineage-head bump, no migration file. Each backticked id anchors its rationale block below the checklist; a consent marker is a CONDITIONAL gate — an explicit yes is required exactly where the row's clause states one:
|
|
11
11
|
|
|
12
12
|
1. `pointers` — `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md` → per pointer: added · already present · skipped (reported) · a hard STOP.
|
|
13
13
|
2. `footprint` — `node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --reconcile --dry-run` → visibility: visible · ambiguous · hidden — consent (conditional): ambiguous → ask which it is BEFORE anything; hidden → the conditional re-run without `--dry-run` (its surfaced paths ask per bootstrap step 9).
|
|
@@ -15,7 +15,8 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
15
15
|
4. `gates-migration` — `node ${CLAUDE_SKILL_DIR}/references/scripts/migrate-gates.mjs --kit-tools ${CLAUDE_SKILL_DIR}/tools --cwd <project>` → the preview plan · INERT checker · CUSTOMIZED entries, each named — consent: apply only on an explicit yes, re-run with `--apply`.
|
|
16
16
|
5. `bridges` — `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs --refresh-placed` → per-bridge lines: refreshed · already current · skipped — with its stated reason (not placed / newer than the bundle / unsupported host) · `skipped-readonly` · could not refresh.
|
|
17
17
|
6. `lens` — `node ${CLAUDE_SKILL_DIR}/tools/lens-region.mjs reconcile <project>/docs/ai/agent_rules.md` → per section: refreshed · already current · custom edit preserved · file absent / engine too old — skipped · over the line cap — refused · section absent — noted · a fully absent/invalid engine → hard STOP.
|
|
18
|
-
7. `
|
|
18
|
+
7. `tier` — `node ${CLAUDE_SKILL_DIR}/tools/tier-preview.mjs --cwd <project>` → per gap: present · offered · declined · undecidable · applied · recorded · refused — consent: one ask, only when offered.
|
|
19
|
+
8. `bridge-settings` — `node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs --reconcile` → outcome line(s), pasted verbatim: ok · absent · flagged · duplicates · unusable.
|
|
19
20
|
|
|
20
21
|
**`pointers` — the bounded pointers.** ONE call reconciles **three** pointers — the **workflow-methodology** pointer, the **orchestration-recipes** pointer (Solo / Reviewed / Council / Delegated / Subagent, routing to `/agent-workflow-kit recipes`) right below it, and the **autonomy-policy** pointer (the `docs/ai/autonomy.json` read contract, routing to `/agent-workflow-kit set-autonomy`) below that — and is filled on **every** upgrade, idempotently (zero-diff when all are already present + filled), so even a legacy or current equal-head deployment gains them **without a lineage-head bump or a migration** (the `agent-workflow-memory` **package** template did get docs-only headroom trims for the chained pointers, but no deployed-`docs/ai` structure changed). Per slot it inserts an empty pair at its anchor if absent, preserves a customized pair verbatim, and STOPs (never edits) on a malformed pair, a missing / duplicate anchor, or **when a fill is needed but the installed `agent-workflow-engine` is absent/invalid** (the fragments are read live from it — see the distinct outcomes below).
|
|
21
22
|
|
|
@@ -87,7 +88,18 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
87
88
|
A doubled heading in any of the three sections is preserved with a note and no write.
|
|
88
89
|
The **Communication and story-sessions** absent-section notes already carry the preview
|
|
89
90
|
command: relay the printed note verbatim as the offer.
|
|
90
|
-
Run the insert with `--apply` only on the user's explicit yes
|
|
91
|
+
Run the insert with `--apply` only on the user's explicit yes (the `tier` ask while `tier`
|
|
92
|
+
offers `story-sessions-section`).
|
|
93
|
+
|
|
94
|
+
**`tier` — the full-flow offer.** Relay every preview line. Ask ONCE, only when an entry is
|
|
95
|
+
`offered`: yes re-runs it and every other offered `apply:` line with `--apply`; no re-runs it
|
|
96
|
+
with `--decline`. `story-sessions-section` `offered`: run its preview line and relay its lines
|
|
97
|
+
(Communication `planned` too) before the ask; `declined`: relay the lens story-sessions absent note
|
|
98
|
+
as the recorded decline, without its preview command, and put no ask for it. A Communication absent
|
|
99
|
+
note beside a `present` or `declined` one keeps its own ask, after the insert's full preview
|
|
100
|
+
(the insert is indivisible). In an upgrade this governs the composition handoff's lens ask. A
|
|
101
|
+
`refused` item, or a non-zero exit of any command the ask runs, STOPs the upgrade before the
|
|
102
|
+
re-stamp.
|
|
91
103
|
|
|
92
104
|
**`bridge-settings` — bridge settings reconcile.** **Paste its outcome line(s)
|
|
93
105
|
verbatim**: it validates the deployed host settings file's keys against the bundled manifests and
|
|
@@ -96,7 +108,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
96
108
|
**The LATE navigator finalizer — the AUTHORITATIVE run, after the last `docs/ai` mutation.** `lens` above rewrites `docs/ai/agent_rules.md`, so the step-3 index ensure is already behind by the time the run-list ends. Re-run that ONE op here, before the step-4 report: `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only index --cwd <project>`. Relay **this** line in the report (it supersedes the early one; an untouched tree reports `already-current`, a failure STOPs the upgrade like any other ensure). On the migrated path the same rung runs again at the END of step 7 — after the migrations, before the step-8 re-stamp.
|
|
97
109
|
4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
|
|
98
110
|
- **A skip line that contradicts the observed tree is a FINDING, never pasted as neutral.** Every ensure skip names the fact it proved (the probes that answered absent, the layout it read); if the tree you can see disproves that reason — a "no Node" skip beside deployed Node scripts, a "not deployed" skip beside a stamped `docs/ai` — do not relay the line as an outcome: raise it in the report as a defect of the tool, with the contradicting fact named, and stop short of any step that would build on the skip.
|
|
99
|
-
- **Report step 3's outcome in plain language** — for **each** `pointers` slot (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); the **eight project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (the ensures and their outcome tokens are enumerated in step 3), each rendered in plain language: what was created, what was left exactly as the user wrote it, and — for a `failed` line — what stopped it; for the navigator, relay the **late** `--only index` line, not the early one; the **`gates-migration`** result — *nothing to migrate*, the shown plan *applied* on your explicit yes, or the plan *left unapplied* (consent not given), with any INERT checker or CUSTOMIZED entries named; the **placed-bridge refresh** (`bridges`) outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` with its re-scan verdict / *could not refresh* + recovery); the **agent-rules lens** (`lens`) outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*) and the **Communication-section** outcome (its own set: refreshed / already current / custom preserved + note / section absent — noted / over the cap — refused) and the **story-sessions section** outcome (its own set: refreshed / already current / custom edit preserved + note / section absent — noted / over the file's line cap — refused); the **bridge-settings reconcile** (`bridge-settings`) outcome (paste the tool's line(s) verbatim); and, for a hidden deployment, whether the hidden-mode footprint (`footprint`) was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
|
|
111
|
+
- **Report step 3's outcome in plain language** — for **each** `pointers` slot (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); the **eight project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (the ensures and their outcome tokens are enumerated in step 3), each rendered in plain language: what was created, what was left exactly as the user wrote it, and — for a `failed` line — what stopped it; for the navigator, relay the **late** `--only index` line, not the early one; the **`gates-migration`** result — *nothing to migrate*, the shown plan *applied* on your explicit yes, or the plan *left unapplied* (consent not given), with any INERT checker or CUSTOMIZED entries named; the **placed-bridge refresh** (`bridges`) outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` with its re-scan verdict / *could not refresh* + recovery); the **agent-rules lens** (`lens`) outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*) and the **Communication-section** outcome (its own set: refreshed / already current / custom preserved + note / section absent — noted / over the cap — refused) and the **story-sessions section** outcome (its own set: refreshed / already current / custom edit preserved + note / section absent — noted / over the file's line cap — refused); the **full-flow offer** (`tier`) lines; the **bridge-settings reconcile** (`bridge-settings`) outcome (paste the tool's line(s) verbatim); and, for a hidden deployment, whether the hidden-mode footprint (`footprint`) was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
|
|
100
112
|
- **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
|
|
101
113
|
- **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent-gated applies: the user picks items in plain language; surface each picked item's posture note, get the explicit confirm, then run EXACTLY the rendered one-liners (a HAND-APPLY item is never run by you) — the full lane in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
|
|
102
114
|
- **Live host/session facts are tool-composed only.** Any claim this report makes about the current host or session state — prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts — must trace to **live tool output** from **this session** (the lines you just composed, or a probe you ran this run); a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection. Full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`.
|
|
@@ -105,4 +117,4 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
105
117
|
5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
|
|
106
118
|
6. **Collect the migration answers FIRST, then apply.** Run `node ${CLAUDE_SKILL_DIR}/tools/migration-notes.mjs --cwd <project>` and relay its stdout lines verbatim, in order; a non-zero exit relays its one named line and STOPs, except `stamp-absent`, which routes back to step 1. Then run the preview `node ${CLAUDE_SKILL_DIR}/tools/migration-blocks.mjs --cwd <project>`. If its lines say both blocks are missing (pre-1.1.0), ask the two questions as ONE structured multi-question prompt; record each answer individually, write nothing until ALL are answered, and carry the answers into the migrations below: a migration whose answer was already collected never re-asks; a single missing block keeps its single ask (step 7). Then apply exactly the notes the tool listed, in its order ("only those newer than the project's stamp" is why, never a second selection). Migrations are **idempotent** — safe to re-run.
|
|
107
119
|
7. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. For a block the preview names as needing its answer, **ask the user their conversational language** (`--language`) or **whether the agent may attribute work to itself / AI** (`--attribution`, defaulting to `off`), never re-asking a step-6 answer; then re-run the preview with `--apply` and the answers, relaying its lines. Exit 1 `answer-missing`: ask exactly what its lines name and re-run `--apply` once; a second one STOPs. Any other exit 1 is a named refusal: relay it with its recovery (`cap`: trim `AGENTS.md`, re-run) and STOP before the finalizer and the stamp. **Then, before the stamp, re-run the LATE navigator finalizer:** the migrations of steps 6–7 may have changed `docs/ai` files, so run the one op again — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only index --cwd <project>` — and relay THAT line in the step-8 report (idempotent: an untouched tree reports `already-current`; a failure STOPs this upgrade like any other ensure).
|
|
108
|
-
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`
|
|
120
|
+
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`4.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 run-list outcomes, each as step 4 words it: the `pointers` slots, the `footprint` visibility verdict (and what a consented re-run migrated), the **eight project-configuration ensure** (`configs`) lines (enumerated in step 3) — the navigator one being the late `--only index` run at the end of step 7, the **`gates-migration`** result, the **placed-bridge refresh** (`bridges`) lines (pasted verbatim), the **agent-rules lens** (`lens`) + **Communication-section** + **story-sessions section** outcomes (same outcome sets as step 4), the **full-flow offer** (`tier`) outcome, and the **bridge-settings reconcile** (`bridge-settings`) outcome (line(s) pasted verbatim) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language (every fact, count and item, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). **Every current host/session claim in this report is tool-composed only** — prompts fired, sandbox scope, whether a bypass was needed, network reachability and approval counts must trace to **live tool output** from **this session**, a memory/handover snapshot is **context, never report facts**, and an unbacked claim is **omitted or explicitly marked unverified** (full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
|
|
@@ -11,7 +11,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
|
|
|
11
11
|
|
|
12
12
|
The opt-in onboarding **velocity profile** — it seeds a fixed, audited **read-only** Claude Code allowlist into `.claude/settings.json` so an agent stops idling on approval prompts for routine read-only commands while the maintainer is away. It is the family's **first programmatic `.claude/settings.json` writer** (attribution stayed an agent-driven prose merge). **In-agent, opt-in, writes only `.claude/settings.json`**, on one hard rule: **it never allowlists `commit`/`push`/`publish`** — so a direct commit/push/publish still ASKs; the only caveat is the trust-posture residual (below) — its closure is **shipped, opt-in: `${CLAUDE_SKILL_DIR}/references/modes/hook.md`**.
|
|
13
13
|
|
|
14
|
-
**Version-status routing (like the other writer modes):** read `docs/ai/.workflow-version` first — not-deployed → bootstrap; stamp < `
|
|
14
|
+
**Version-status routing (like the other writer modes):** read `docs/ai/.workflow-version` first — not-deployed → bootstrap; stamp < `4.0.0` → `upgrade`; stamp > head / unparseable → STOP. The tool enforces this in code too (`--apply` STOPs unless the stamp is the lineage head).
|
|
15
15
|
|
|
16
16
|
Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs [--dry-run | --apply] [--kit-tools] [--bridge-tier] [--accept-edits] [--cwd <dir>]`:
|
|
17
17
|
|
|
@@ -77,7 +77,7 @@ skipping the installed-versions block."* — never a silent skip (Hard Constrain
|
|
|
77
77
|
the rest of the report — and the commit gate — proceeds.
|
|
78
78
|
|
|
79
79
|
**Welcome mat — the last line(s) of the footer.** After the version block and the backend-status
|
|
80
|
-
line, print *"Run `/agent-workflow-kit help` to see every command."* then **one** recommended next
|
|
80
|
+
line, print *"Run `/agent-workflow-kit help` to see every command."* and *"New to the epic, story and task tier? Run `/agent-workflow-kit tier`."* then **one** recommended next
|
|
81
81
|
step, chosen **caveat-aware** from signals already in hand (the version block's notes, the settings
|
|
82
82
|
areas of the same `--json` envelope, and the backend-status line — no new helper call) in this
|
|
83
83
|
priority order:
|
|
@@ -99,7 +99,7 @@ priority order:
|
|
|
99
99
|
placed) → the optional *`/agent-workflow-kit agents`* opt-in (never run it without a yes);
|
|
100
100
|
6. else **gates are declared but the approval hook is not wired** (the envelope's hook settings:
|
|
101
101
|
at least one declared gate, wired = no) → the optional *`/agent-workflow-kit hook`* opt-in
|
|
102
|
-
(never run it without a yes). If no rung applies, the
|
|
102
|
+
(never run it without a yes). If no rung applies, the two lines above stand alone.
|
|
103
103
|
|
|
104
104
|
Keep it compact — a few short lines, plain language, no kit-internal terms.
|
|
105
105
|
|
|
@@ -15,7 +15,7 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
15
15
|
export const AGENTS_DIR = '.claude/agents';
|
|
16
16
|
export const CLAUDE_DIR = '.claude';
|
|
17
17
|
export const WORKFLOW_STAMP = 'docs/ai/.workflow-version';
|
|
18
|
-
export const EXPECTED_WORKFLOW_VERSION = '
|
|
18
|
+
export const EXPECTED_WORKFLOW_VERSION = '4.0.0';
|
|
19
19
|
export const BUNDLED_AGENTS_DIR = resolve(HERE, '..', 'references', 'agents');
|
|
20
20
|
|
|
21
21
|
export const UTF8 = 'utf8';
|
package/tools/direct-run.mjs
CHANGED
|
@@ -65,6 +65,8 @@ export const LIBRARY_ONLY_MODULES = Object.freeze({
|
|
|
65
65
|
'review-roster.mjs': '/agent-workflow-kit recipes',
|
|
66
66
|
'review-roster-resolve.mjs': '/agent-workflow-kit recipes',
|
|
67
67
|
'set-recipe-roster.mjs': '/agent-workflow-kit set-recipe',
|
|
68
|
+
// The read half of the tier offer: the upgrade's step-3 `tier` item runs the offer that uses it.
|
|
69
|
+
'reference-profile.mjs': '/agent-workflow-kit upgrade',
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
// The frozen refusal line. One line, names the module, names the command.
|
package/tools/profile-gaps.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { CONFIG_REL, validateConfig } from './orchestration-config.mjs';
|
|
4
|
+
import { TIER_TARGETS } from './reference-profile.mjs';
|
|
3
5
|
import {
|
|
4
6
|
RULES_REGIONS,
|
|
5
7
|
extractRegionBy,
|
|
@@ -11,6 +13,11 @@ import {
|
|
|
11
13
|
} from './rules-regions.mjs';
|
|
12
14
|
|
|
13
15
|
const GAP_ID = 'story-sessions-section';
|
|
16
|
+
const SLOTS_ID = 'epic-task-slots';
|
|
17
|
+
const STORE_ID = 'epic-store-seeded';
|
|
18
|
+
const STORE_PATH = 'docs/ai/epics';
|
|
19
|
+
const DEPLOYMENT_CHAIN = ['.', 'docs', 'docs/ai'];
|
|
20
|
+
const TIER_PREVIEW_PATH = fileURLToPath(new URL('./tier-preview.mjs', import.meta.url));
|
|
14
21
|
const STORY_ID = 'story-sessions';
|
|
15
22
|
const TEMPLATE_CANON = 'template';
|
|
16
23
|
const ENCODING = 'utf8';
|
|
@@ -28,6 +35,15 @@ const REASONS = Object.freeze({
|
|
|
28
35
|
fileAbsent: 'file-absent',
|
|
29
36
|
fileSymlink: 'file-symlink',
|
|
30
37
|
fileUnreadable: 'file-unreadable',
|
|
38
|
+
noDeployment: 'no-deployment',
|
|
39
|
+
configAbsent: 'config-absent',
|
|
40
|
+
configSymlink: 'config-symlink',
|
|
41
|
+
configUnreadable: 'config-unreadable',
|
|
42
|
+
configMalformed: 'config-malformed',
|
|
43
|
+
configInvalid: 'config-invalid',
|
|
44
|
+
storeNotDirectory: 'store-not-directory',
|
|
45
|
+
storeSymlink: 'store-symlink',
|
|
46
|
+
storeUnreadable: 'store-unreadable',
|
|
31
47
|
});
|
|
32
48
|
const TEMPLATE_REGIONS = RULES_REGIONS.filter(({ canon }) => canon === TEMPLATE_CANON);
|
|
33
49
|
const STORY_REGION = RULES_REGIONS.find(({ id }) => id === STORY_ID);
|
|
@@ -81,6 +97,76 @@ const detectStorySessions = ({ root, deps }) => {
|
|
|
81
97
|
return { verdict: VERDICTS.absent };
|
|
82
98
|
};
|
|
83
99
|
|
|
100
|
+
const isDeployed = (root, deps) => {
|
|
101
|
+
try {
|
|
102
|
+
return DEPLOYMENT_CHAIN.every((path, index) => {
|
|
103
|
+
const stat = deps.lstatSync(resolve(root, path));
|
|
104
|
+
if (stat.isSymbolicLink()) return false;
|
|
105
|
+
return index < DEPLOYMENT_CHAIN.length - 1 || stat.isDirectory();
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const parseConfig = (bytes) => {
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = JSON.parse(decodeUtf8Strict(bytes));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return error instanceof SyntaxError ? { reason: REASONS.configMalformed } : { reason: REASONS.configUnreadable };
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return { config: validateConfig(parsed) };
|
|
121
|
+
} catch {
|
|
122
|
+
return { reason: REASONS.configInvalid };
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const readTierConfig = (root, deps) => {
|
|
127
|
+
const path = resolve(root, CONFIG_REL);
|
|
128
|
+
try {
|
|
129
|
+
const stat = deps.lstatSync(path);
|
|
130
|
+
if (stat.isSymbolicLink()) return { reason: REASONS.configSymlink };
|
|
131
|
+
if (!stat.isFile()) return { reason: REASONS.configUnreadable };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return { reason: error?.code === MISSING_FILE_CODE ? REASONS.configAbsent : REASONS.configUnreadable };
|
|
134
|
+
}
|
|
135
|
+
let bytes;
|
|
136
|
+
try {
|
|
137
|
+
bytes = deps.readFileSync(path);
|
|
138
|
+
} catch {
|
|
139
|
+
return { reason: REASONS.configUnreadable };
|
|
140
|
+
}
|
|
141
|
+
return parseConfig(bytes);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const detectSlots = ({ root, deps }) => {
|
|
145
|
+
if (!isDeployed(root, deps)) return reportUndecidable(REASONS.noDeployment);
|
|
146
|
+
const read = readTierConfig(root, deps);
|
|
147
|
+
if (read.reason) return reportUndecidable(read.reason);
|
|
148
|
+
const declared = TIER_TARGETS.every(({ activity, slot }) => Object.hasOwn(read.config[activity] ?? {}, slot));
|
|
149
|
+
return { verdict: declared ? VERDICTS.present : VERDICTS.absent };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const detectStore = ({ root, deps }) => {
|
|
153
|
+
if (!isDeployed(root, deps)) return reportUndecidable(REASONS.noDeployment);
|
|
154
|
+
let stat;
|
|
155
|
+
try {
|
|
156
|
+
stat = deps.lstatSync(resolve(root, STORE_PATH));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error?.code === MISSING_FILE_CODE) return { verdict: VERDICTS.absent };
|
|
159
|
+
return reportUndecidable(REASONS.storeUnreadable);
|
|
160
|
+
}
|
|
161
|
+
if (stat.isSymbolicLink()) return reportUndecidable(REASONS.storeSymlink);
|
|
162
|
+
if (!stat.isDirectory()) return reportUndecidable(REASONS.storeNotDirectory);
|
|
163
|
+
return { verdict: VERDICTS.present };
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const tierPreview = (root) => buildInsertPreview(root, TIER_PREVIEW_PATH);
|
|
167
|
+
|
|
84
168
|
export const PROFILE_GAPS = Object.freeze([
|
|
85
169
|
Object.freeze({ id: GAP_ID, detect: detectStorySessions, apply: (root) => buildInsertPreview(root) }),
|
|
170
|
+
Object.freeze({ id: SLOTS_ID, detect: detectSlots, apply: tierPreview }),
|
|
171
|
+
Object.freeze({ id: STORE_ID, detect: detectStore, apply: tierPreview }),
|
|
86
172
|
]);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// reference-profile.mjs — the READ-ONLY leaf over the running kit's reference profile and a
|
|
2
|
+
// project's decline record: the frozen epic/task target table, the shipped profile reader, the
|
|
3
|
+
// decline record reader and the currency rule. Governing contract: docs/ai/specs/kit/tier/tier-offer/.
|
|
4
|
+
// No CLI (refuseDirectRun); every read goes through injected deps; nothing here writes.
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
8
|
+
import { refuseDirectRun } from './direct-run.mjs';
|
|
9
|
+
|
|
10
|
+
const PROFILE_PATH = fileURLToPath(new URL('../references/reference-profile.json', import.meta.url));
|
|
11
|
+
const DECLINES_REL = 'docs/ai/profile-declines.json';
|
|
12
|
+
const PROFILE_KEYS = ['items', 'lineage', 'name', 'schema'];
|
|
13
|
+
const ITEM_KEYS = ['has', 'id', 'story'];
|
|
14
|
+
const RECORD_KEYS = ['declined', 'schema'];
|
|
15
|
+
const SCHEMA = 1;
|
|
16
|
+
const OK = 'ok';
|
|
17
|
+
const ABSENT = 'absent';
|
|
18
|
+
const SYMLINK_CLASS = 'symlink';
|
|
19
|
+
|
|
20
|
+
const target = (activity, slot, candidates) => Object.freeze({ activity, slot, candidates: Object.freeze(candidates) });
|
|
21
|
+
|
|
22
|
+
export const TIER_TARGETS = Object.freeze([
|
|
23
|
+
target('epic', 'author', ['subagent', 'solo']),
|
|
24
|
+
target('epic', 'review', ['reviewed', 'solo']),
|
|
25
|
+
target('task', 'author', ['delegated', 'subagent', 'solo']),
|
|
26
|
+
target('task', 'execute', ['delegated', 'subagent', 'solo']),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
const hasKeys = (value, keys) => isPlainObject(value) && Object.keys(value).sort().join() === keys.join();
|
|
31
|
+
const isText = (value) => typeof value === 'string' && value.length > 0;
|
|
32
|
+
const parseJson = (text) => {
|
|
33
|
+
try {
|
|
34
|
+
return { value: JSON.parse(text) };
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const isProfile = (value) => hasKeys(value, PROFILE_KEYS) && value.schema === SCHEMA
|
|
41
|
+
&& isText(value.name) && isText(value.lineage) && Array.isArray(value.items)
|
|
42
|
+
&& value.items.every((item) => hasKeys(item, ITEM_KEYS) && isText(item.id) && isText(item.story) && isText(item.has));
|
|
43
|
+
|
|
44
|
+
const isRecord = (value) => hasKeys(value, RECORD_KEYS) && value.schema === SCHEMA && isPlainObject(value.declined)
|
|
45
|
+
&& Object.entries(value.declined).every(([id, lineage]) => id.length > 0 && typeof lineage === 'string');
|
|
46
|
+
|
|
47
|
+
export const readShippedProfile = (deps = {}) => {
|
|
48
|
+
const read = readRegularFileNoFollow(deps.profilePath ?? PROFILE_PATH, deps);
|
|
49
|
+
if (read.outcome !== OK) return { refusal: 'profile-unreadable' };
|
|
50
|
+
const parsed = parseJson(read.content);
|
|
51
|
+
if (parsed === null || !isProfile(parsed.value)) return { refusal: 'profile-malformed' };
|
|
52
|
+
return { profile: parsed.value };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const readDeclines = (root, deps = {}) => {
|
|
56
|
+
const read = readRegularFileNoFollow(join(root, DECLINES_REL), deps);
|
|
57
|
+
if (read.outcome === ABSENT) return { absent: true };
|
|
58
|
+
if (read.outcome !== OK) {
|
|
59
|
+
return { refusal: read.className === SYMLINK_CLASS ? 'declines-symlink' : 'declines-unreadable' };
|
|
60
|
+
}
|
|
61
|
+
const parsed = parseJson(read.content);
|
|
62
|
+
if (parsed === null || !isRecord(parsed.value)) return { refusal: 'declines-malformed' };
|
|
63
|
+
return { declined: parsed.value.declined };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const isDeclineCurrent = (declined, id, lineage) =>
|
|
67
|
+
isPlainObject(declined) && Object.hasOwn(declined, id) && declined[id] === lineage;
|
|
68
|
+
|
|
69
|
+
refuseDirectRun(import.meta.url);
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// tier-preview.mjs — the offer of lineage step 4.0.0: one preview over the whole profile-gap
|
|
3
|
+
// registry; on an explicit yes (--apply) the absent epic and task slots and the epic store seed;
|
|
4
|
+
// on an explicit no (--decline) the decline record. Governing contract:
|
|
5
|
+
// docs/ai/specs/kit/tier/tier-offer/ (part tier-preview). main(argv, deps) returns, never exits,
|
|
6
|
+
// never asks; every read and write goes through deps.
|
|
7
|
+
import { linkSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { assertDocsAiDeployment, writeDocsAiFileAtomic, writeProjectFileCreateOnly } from './atomic-write.mjs';
|
|
10
|
+
import { safeLine } from './carriers.mjs';
|
|
11
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
12
|
+
import { tmpNote } from './ensure-ops.mjs';
|
|
13
|
+
import { CANON_README, applySetOps, refreshReadme } from './orchestration-config.mjs';
|
|
14
|
+
import { writeConfig } from './orchestration-write.mjs';
|
|
15
|
+
import { PROFILE_GAPS, readTierConfig } from './profile-gaps.mjs';
|
|
16
|
+
import { composeReadiness, planRecipe } from './recipes.mjs';
|
|
17
|
+
import { TIER_TARGETS, isDeclineCurrent, readDeclines, readShippedProfile } from './reference-profile.mjs';
|
|
18
|
+
|
|
19
|
+
const LF = String.fromCharCode(10);
|
|
20
|
+
const ARGV_OFFSET = 2;
|
|
21
|
+
const EXIT_SUCCESS = 0;
|
|
22
|
+
const EXIT_REFUSAL = 1;
|
|
23
|
+
const EXIT_USAGE = 2;
|
|
24
|
+
const CWD_FLAG = '--cwd';
|
|
25
|
+
const APPLY_FLAG = '--apply';
|
|
26
|
+
const DECLINE_FLAG = '--decline';
|
|
27
|
+
const HELP_FLAGS = new Set(['--help', '-h']);
|
|
28
|
+
const MODE_FLAGS = new Set([APPLY_FLAG, DECLINE_FLAG]);
|
|
29
|
+
const SLOTS_ID = 'epic-task-slots';
|
|
30
|
+
const STORE_ID = 'epic-store-seeded';
|
|
31
|
+
const STORE_DIR = 'docs/ai/epics';
|
|
32
|
+
const STORE_SEED = 'docs/ai/epics/.gitkeep';
|
|
33
|
+
const CONFIG_WRITTEN = 'docs/ai/orchestration.json';
|
|
34
|
+
const DECLINES_REL = 'docs/ai/profile-declines.json';
|
|
35
|
+
const SCHEMA = 1;
|
|
36
|
+
const INDENT = ' ';
|
|
37
|
+
const WORDS = Object.freeze({
|
|
38
|
+
present: 'present',
|
|
39
|
+
offered: 'offered',
|
|
40
|
+
declined: 'declined',
|
|
41
|
+
undecidable: 'undecidable',
|
|
42
|
+
applied: 'applied',
|
|
43
|
+
recorded: 'recorded',
|
|
44
|
+
});
|
|
45
|
+
const ABSENT_VERDICT = 'absent';
|
|
46
|
+
const PRESENT_VERDICT = 'present';
|
|
47
|
+
|
|
48
|
+
export const OUTCOME_LINES = Object.freeze({
|
|
49
|
+
entry: (id, word, detail) => `${id}: ${word}${detail ? ` — ${detail}` : ''}`,
|
|
50
|
+
declinedAt: (lineage) => `at lineage ${lineage}`,
|
|
51
|
+
applyLine: (command) => `${INDENT}apply: ${command}`,
|
|
52
|
+
noCommand: () => `${INDENT}apply: no runnable command — the root or the kit path carries a control byte or a backtick`,
|
|
53
|
+
slotValue: (activity, slot, value, skipped = []) => `${INDENT}${activity}.${slot} = ${value}`
|
|
54
|
+
+ skipped.map(({ candidate, reason }) => ` — ${candidate} skipped: ${reason}`).join(''),
|
|
55
|
+
slotPresent: (activity, slot) => `${INDENT}${activity}.${slot}: present`,
|
|
56
|
+
tmpLeft: (tmp) => tmpNote(STORE_SEED, tmp)[0],
|
|
57
|
+
nothingOffered: () => 'nothing offered — nothing recorded',
|
|
58
|
+
detectFailed: (cause) => `backend detection failed (${safeLine(cause)}) — every bridge is treated as not ready`,
|
|
59
|
+
refusal: (name, detail) => `tier-preview: refused — ${name}${detail ? `: ${safeLine(detail)}` : ''}`,
|
|
60
|
+
usage: (problem) => `usage: tier-preview.mjs --cwd DIR [--apply | --decline] — ${safeLine(problem)}`,
|
|
61
|
+
help: () => [
|
|
62
|
+
'tier-preview.mjs --cwd DIR [--apply | --decline]',
|
|
63
|
+
'Previews every profile gap of the project in DIR; writes nothing by default.',
|
|
64
|
+
'--apply writes the offered items: the absent epic and task settings and the epic store seed.',
|
|
65
|
+
'--decline records every offered item as declined at the shipped profile lineage.',
|
|
66
|
+
'Exit codes: 0 done, 1 one named refusal, 2 usage.',
|
|
67
|
+
].join(LF),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = (code, stdout = '', stderr = '') => ({ code, stdout, stderr });
|
|
71
|
+
const refuse = (name, detail) => result(EXIT_REFUSAL, '', OUTCOME_LINES.refusal(name, detail));
|
|
72
|
+
const causeOf = (error) => String(error?.message ?? error);
|
|
73
|
+
|
|
74
|
+
const parseArgs = (argv) => {
|
|
75
|
+
if (argv.length === 1 && HELP_FLAGS.has(argv[0])) return { help: true };
|
|
76
|
+
const options = {};
|
|
77
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
78
|
+
const flag = argv[index];
|
|
79
|
+
if (flag === CWD_FLAG) {
|
|
80
|
+
if (options.cwd !== undefined) return { problem: `${CWD_FLAG} given twice` };
|
|
81
|
+
const value = argv[index + 1];
|
|
82
|
+
if (value === undefined || value.startsWith('-')) return { problem: `${CWD_FLAG} needs a directory` };
|
|
83
|
+
options.cwd = value;
|
|
84
|
+
index += 1;
|
|
85
|
+
} else if (MODE_FLAGS.has(flag)) {
|
|
86
|
+
if (options[flag]) return { problem: `${flag} given twice` };
|
|
87
|
+
options[flag] = true;
|
|
88
|
+
} else {
|
|
89
|
+
return { problem: `unknown argument ${flag}` };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (options.cwd === undefined) return { problem: `${CWD_FLAG} is required` };
|
|
93
|
+
if (options[APPLY_FLAG] && options[DECLINE_FLAG]) return { problem: `${APPLY_FLAG} and ${DECLINE_FLAG} exclude each other` };
|
|
94
|
+
return { cwd: options.cwd, apply: Boolean(options[APPLY_FLAG]), decline: Boolean(options[DECLINE_FLAG]) };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const withFs = (deps) => ({
|
|
98
|
+
lstatSync, readFileSync, lstat: lstatSync, writeFile: writeFileSync, rename: renameSync, link: linkSync,
|
|
99
|
+
rm: (path) => rmSync(path, { force: true }), mkdir: (path) => mkdirSync(path, { recursive: true }), ...deps,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// The resolution walk: the first candidate planRecipe reports not degraded wins; every candidate
|
|
103
|
+
// before it carries that plan's own first degrade reason.
|
|
104
|
+
// solo, every target's last candidate, is always satisfiable, so the find always answers.
|
|
105
|
+
const resolveTarget = ({ candidates }, readiness) => {
|
|
106
|
+
const skipped = [];
|
|
107
|
+
const value = candidates.find((candidate) => {
|
|
108
|
+
const plan = planRecipe(candidate, readiness);
|
|
109
|
+
if (plan.degraded) skipped.push({ candidate, reason: plan.degradation[0].reason });
|
|
110
|
+
return !plan.degraded;
|
|
111
|
+
});
|
|
112
|
+
return { value, skipped };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const planSlots = (config, readiness) => TIER_TARGETS.map((target) => {
|
|
116
|
+
const declared = Object.hasOwn(config[target.activity] ?? {}, target.slot);
|
|
117
|
+
return declared ? { target, declared } : { target, declared, ...resolveTarget(target, readiness) };
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const slotLines = (slots) => slots.map(({ target, declared, value, skipped }) => (declared
|
|
121
|
+
? OUTCOME_LINES.slotPresent(target.activity, target.slot)
|
|
122
|
+
: OUTCOME_LINES.slotValue(target.activity, target.slot, value, skipped)));
|
|
123
|
+
|
|
124
|
+
const judgeEntries = (root, deps, profile, declined) => PROFILE_GAPS.map((entry) => {
|
|
125
|
+
const detected = entry.detect({ root, deps });
|
|
126
|
+
if (detected.verdict === PRESENT_VERDICT) return { entry, word: WORDS.present };
|
|
127
|
+
if (detected.verdict !== ABSENT_VERDICT) return { entry, word: WORDS.undecidable, detail: detected.reason };
|
|
128
|
+
if (isDeclineCurrent(declined, entry.id, profile.lineage)) {
|
|
129
|
+
return { entry, word: WORDS.declined, detail: OUTCOME_LINES.declinedAt(declined[entry.id]) };
|
|
130
|
+
}
|
|
131
|
+
return { entry, word: WORDS.offered };
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const renderEntry = ({ entry, word, detail }, root, slots) => {
|
|
135
|
+
const lines = [OUTCOME_LINES.entry(entry.id, word, detail)];
|
|
136
|
+
if (word === WORDS.offered) {
|
|
137
|
+
const command = entry.apply(root);
|
|
138
|
+
lines.push(command ? OUTCOME_LINES.applyLine(command) : OUTCOME_LINES.noCommand());
|
|
139
|
+
}
|
|
140
|
+
if (entry.id === SLOTS_ID && (word === WORDS.offered || word === WORDS.applied)) lines.push(...slotLines(slots));
|
|
141
|
+
return lines;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const writeSlots = (root, config, slots, deps) => {
|
|
145
|
+
const ops = slots.filter(({ declared }) => !declared).map(({ target, value }) => ({
|
|
146
|
+
kind: 'set', activity: target.activity, slot: target.slot, recipe: value,
|
|
147
|
+
}));
|
|
148
|
+
const merged = applySetOps(config, ops, { seedReadme: CANON_README });
|
|
149
|
+
writeConfig(root, refreshReadme(merged).config, deps);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const storeCreated = (root, deps) => {
|
|
153
|
+
try {
|
|
154
|
+
return deps.lstat(resolve(root, STORE_DIR)).isDirectory();
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const runApply = (root, deps, judged, slots, config) => {
|
|
161
|
+
const byId = new Map(judged.map((item) => [item.entry.id, item]));
|
|
162
|
+
const extra = [];
|
|
163
|
+
const made = [];
|
|
164
|
+
if (byId.get(SLOTS_ID)?.word === WORDS.offered) {
|
|
165
|
+
try {
|
|
166
|
+
writeSlots(root, config, slots, deps);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return { refusal: refuse('write-failed', causeOf(error)) };
|
|
169
|
+
}
|
|
170
|
+
made.push(`${CONFIG_WRITTEN} written`);
|
|
171
|
+
byId.set(SLOTS_ID, { ...byId.get(SLOTS_ID), word: WORDS.applied });
|
|
172
|
+
}
|
|
173
|
+
if (byId.get(STORE_ID)?.word === WORDS.offered) {
|
|
174
|
+
let seeded;
|
|
175
|
+
try {
|
|
176
|
+
seeded = writeProjectFileCreateOnly(root, STORE_SEED, '', deps, { noun: 'the epic store seed' });
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (storeCreated(root, deps)) made.push(`${STORE_DIR} created`);
|
|
179
|
+
return { refusal: refuse('write-failed', [...made, causeOf(error)].join('; ')) };
|
|
180
|
+
}
|
|
181
|
+
byId.set(STORE_ID, { ...byId.get(STORE_ID), word: seeded.created ? WORDS.applied : WORDS.present });
|
|
182
|
+
if (seeded.tmpLeftBehind) extra.push(OUTCOME_LINES.tmpLeft(seeded.tmpLeftBehind));
|
|
183
|
+
}
|
|
184
|
+
return { judged: judged.map((item) => byId.get(item.entry.id)), extra };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const serializeDeclines = (declined) => {
|
|
188
|
+
const sorted = Object.fromEntries(Object.keys(declined).sort().map((id) => [id, declined[id]]));
|
|
189
|
+
return `${JSON.stringify({ schema: SCHEMA, declined: sorted }, null, 2)}${LF}`;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const runDecline = (root, deps, judged, profile, declined) => {
|
|
193
|
+
const offered = judged.filter(({ word }) => word === WORDS.offered);
|
|
194
|
+
if (offered.length === 0) return { judged, extra: [OUTCOME_LINES.nothingOffered()] };
|
|
195
|
+
const merged = { ...(declined ?? {}) };
|
|
196
|
+
for (const { entry } of offered) merged[entry.id] = profile.lineage;
|
|
197
|
+
try {
|
|
198
|
+
writeDocsAiFileAtomic(root, DECLINES_REL, serializeDeclines(merged), deps, { noun: 'the decline record' });
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return { refusal: refuse('write-failed', causeOf(error)) };
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
judged: judged.map((item) => (item.word === WORDS.offered ? { ...item, word: WORDS.recorded } : item)),
|
|
204
|
+
extra: [],
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const runOffer = (options, given) => {
|
|
209
|
+
const deps = withFs(given);
|
|
210
|
+
const root = resolve(options.cwd);
|
|
211
|
+
try {
|
|
212
|
+
assertDocsAiDeployment(root, deps);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return refuse('no-deployment', causeOf(error));
|
|
215
|
+
}
|
|
216
|
+
const shipped = readShippedProfile(deps);
|
|
217
|
+
if (shipped.refusal) return refuse(shipped.refusal);
|
|
218
|
+
const record = readDeclines(root, deps);
|
|
219
|
+
if (record.refusal) return refuse(record.refusal);
|
|
220
|
+
const notes = [];
|
|
221
|
+
const readiness = composeReadiness(root, {
|
|
222
|
+
...deps, onDetectError: (error) => notes.push(OUTCOME_LINES.detectFailed(causeOf(error))),
|
|
223
|
+
});
|
|
224
|
+
const judged = judgeEntries(root, deps, shipped.profile, record.declined);
|
|
225
|
+
const offersSlots = judged.some(({ entry, word }) => entry.id === SLOTS_ID && word === WORDS.offered);
|
|
226
|
+
const config = offersSlots ? readTierConfig(root, deps).config : null;
|
|
227
|
+
const slots = offersSlots ? planSlots(config, readiness) : [];
|
|
228
|
+
let outcome = { judged, extra: [] };
|
|
229
|
+
if (options.apply) outcome = runApply(root, deps, judged, slots, config);
|
|
230
|
+
if (options.decline) outcome = runDecline(root, deps, judged, shipped.profile, record.declined);
|
|
231
|
+
if (outcome.refusal) return outcome.refusal;
|
|
232
|
+
const lines = [...notes, ...outcome.judged.flatMap((item) => renderEntry(item, root, slots)), ...outcome.extra];
|
|
233
|
+
return result(EXIT_SUCCESS, lines.join(LF));
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const main = (argv, deps = {}) => {
|
|
237
|
+
const parsed = parseArgs(argv);
|
|
238
|
+
if (parsed.help) return result(EXIT_SUCCESS, OUTCOME_LINES.help());
|
|
239
|
+
if (parsed.problem) return result(EXIT_USAGE, '', OUTCOME_LINES.usage(parsed.problem));
|
|
240
|
+
return runOffer(parsed, deps);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (isDirectRun(import.meta.url)) {
|
|
244
|
+
const outcome = main(process.argv.slice(ARGV_OFFSET));
|
|
245
|
+
if (outcome.stdout) console.log(outcome.stdout);
|
|
246
|
+
if (outcome.stderr) console.error(outcome.stderr);
|
|
247
|
+
process.exitCode = outcome.code;
|
|
248
|
+
}
|
|
@@ -87,6 +87,12 @@ export const UPGRADE_RUNLIST = Object.freeze([
|
|
|
87
87
|
'STOP',
|
|
88
88
|
],
|
|
89
89
|
),
|
|
90
|
+
entry(
|
|
91
|
+
'tier',
|
|
92
|
+
'node ${CLAUDE_SKILL_DIR}/tools/tier-preview.mjs --cwd <project>',
|
|
93
|
+
'preview first — on an explicit yes re-run with --apply and run every other offered line with --apply; on an explicit no re-run with --decline',
|
|
94
|
+
['present', 'offered', 'declined', 'undecidable', 'applied', 'recorded', 'refused'],
|
|
95
|
+
),
|
|
90
96
|
entry(
|
|
91
97
|
'bridge-settings',
|
|
92
98
|
'node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs --reconcile',
|
|
@@ -26,7 +26,7 @@ import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './decl
|
|
|
26
26
|
|
|
27
27
|
// Deployment-lineage head this velocity build targets; bump together with agent-workflow-memory
|
|
28
28
|
// LINEAGE_HEAD when the deployed docs/ai structure changes.
|
|
29
|
-
export const EXPECTED_WORKFLOW_VERSION = '
|
|
29
|
+
export const EXPECTED_WORKFLOW_VERSION = '4.0.0';
|
|
30
30
|
export const SETTINGS_FILE = '.claude/settings.json';
|
|
31
31
|
export const SETTINGS_LOCAL_FILE = '.claude/settings.local.json';
|
|
32
32
|
export const CLAUDE_DIR = '.claude';
|