@laitszkin/apollo-toolkit 3.11.5 → 3.11.7

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.
Files changed (26) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +30 -0
  3. package/README.md +1 -0
  4. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  5. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  6. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  7. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  8. package/generate-spec/SKILL.md +17 -13
  9. package/generate-spec/agents/openai.yaml +3 -3
  10. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  11. package/init-project-html/lib/atlas/cli.js +208 -91
  12. package/init-project-html/lib/atlas/render.js +29 -0
  13. package/init-project-html/lib/atlas/state.js +89 -7
  14. package/init-project-html/scripts/architecture.js +10 -17
  15. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  16. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  17. package/optimise-skill/SKILL.md +35 -0
  18. package/optimise-skill/agents/openai.yaml +4 -0
  19. package/optimise-skill/references/example_skill.md +35 -0
  20. package/package.json +1 -1
  21. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  22. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  23. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  24. package/spec-to-project-html/SKILL.md +4 -4
  25. package/spec-to-project-html/agents/openai.yaml +1 -1
  26. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
package/AGENTS.md CHANGED
@@ -38,6 +38,7 @@ This repository enables users to install and run a curated set of reusable agent
38
38
  - Users can run a shared submission-readiness pass that synchronizes changelog, project docs, `AGENTS.md`, and completed plan archives before commit, push, PR creation, or release.
39
39
  - Users can learn new or improved skills from recent Codex conversation history.
40
40
  - Users can audit and maintain the skill catalog itself, including dependency classification and shared-skill extraction decisions.
41
+ - Users can optimize existing agent skills by deriving the intended deliverable, tightening acceptance criteria, and rewriting `SKILL.md` into a leaner structure backed by extracted references.
41
42
  - Users can implement approved spec planning sets directly in the current checkout and commit them to the active branch.
42
43
  - Users can implement approved spec planning sets inside isolated git worktrees and keep the parent checkout clean.
43
44
  - Users can coordinate approved multi-spec implementation batches by assigning each spec directory to an independent worktree-backed subagent with bounded concurrency.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,36 @@ All notable changes to this repository are documented in this file.
10
10
 
11
11
  ### Fixed
12
12
 
13
+ ## [v3.11.7] - 2026-05-12
14
+
15
+ ### Added
16
+
17
+ - Add `optimise-skill`, a new catalog skill that reads a target skill and its supporting files, derives the intended deliverable and acceptance criteria, and rewrites the skill into a tighter `goal / acceptance criteria / workflow / examples / references` structure for higher-signal agent execution.
18
+
19
+ ### Changed
20
+
21
+ - Catalog docs now include `optimise-skill`, and its bundled example reference path is normalized to match the shipped file name.
22
+
23
+ ### Fixed
24
+
25
+ ## [v3.11.6] - 2026-05-12
26
+
27
+ ### Added
28
+
29
+ - Tests: spec-mode batch-root overlay writes, combined batch diff rendering, legacy HTML-only batch diff fallback, repeated spec render cleanup, and multi-step `undo` coverage for the declarative atlas CLI.
30
+
31
+ ### Changed
32
+
33
+ - `apltk architecture --spec <spec_dir>` now keeps the existing single-spec layout for standalone plans, but batch member paths resolve to one shared `architecture_diff/` beside `coordination.md` so the whole batch maintains a single overlay and rendered architecture diff.
34
+ - `generate-spec` and `spec-to-project-html` now document the shared batch-root overlay behavior and tighten architecture-diff completion criteria: all intended cross-feature edges, feature-to-feature relationships, and sub-module relationships must be declared explicitly through the CLI instead of being left implicit in prose.
35
+ - Spec-mode atlas persistence now derives overlay state from the merged proposed-after graph, which lets repeated edits collapse back to a minimal diff automatically and adds `apltk architecture undo --steps <n>` for multi-step rollback.
36
+
37
+ ### Fixed
38
+
39
+ - `apltk architecture diff` now renders batch specs as one combined macro/viewer result instead of showing separate incomplete macro pages per member spec.
40
+ - `apltk architecture diff` preserves compatibility with legacy batch artifacts that only contain rendered HTML plus `_removed.txt`, instead of silently dropping those member diffs when atlas overlay state is absent.
41
+ - Spec-mode scoped renders now clean stale HTML pages and removal state correctly, preventing repeated render/diff runs from crashing on already-removed pages or leaving ghost overlay output behind.
42
+
13
43
  ## [v3.11.5] - 2026-05-12
14
44
 
15
45
  ### Added
package/README.md CHANGED
@@ -41,6 +41,7 @@ A curated skill catalog for Codex, OpenClaw, Trae, Agents, and Claude Code with
41
41
  - open-source-pr-workflow
42
42
  - openai-text-to-image-storyboard
43
43
  - openclaw-configuration
44
+ - optimise-skill
44
45
  - recover-missing-plan
45
46
  - record-spending
46
47
  - resolve-review-comments
@@ -1,11 +1,14 @@
1
1
  ---
2
2
  name: generate-spec
3
3
  description: >-
4
- Author docs/plans trees: run `apltk create-specs`, hydrate `spec/tasks/checklist/contract/design` (+ `coordination.md`/`preparation.md` when parallel/prep dictates), cite official docs for external deps, plan tests via **`test-case-strategy`**, block code edits until explicit user approval.
5
- Architecture deltas use `apltk architecture --spec <spec_dir> …` (**flags: `apltk architecture --help`**) overlay under `<spec_dir>/architecture_diff/`; **`apltk architecture diff`** pages every `docs/plans/**/architecture_diff/` vs the base atlas — never hand-edit `architecture_diff/`.
6
- Use when drafting/refreshing specs or restructuring batches not when executing approved plans (use **`implement-specs*`** instead).
7
- Reject vague `tasks.md` missing file/mutation/verifier; SPLIT >3-module scope; never overwrite a neighbor `{change}`.
8
- Bad: `- [ ] Add tests`… OK: `- [ ] src/auth/scope.rs — deny unknown scopes — Verify: cargo test scope::defaults`…
4
+ Author docs/plans trees: run `apltk create-specs`, fill `spec/tasks/checklist/contract/design`
5
+ (+ `coordination.md`/`preparation.md` for batches), cite official docs, plan tests via
6
+ **`test-case-strategy`**, and block code edits until approval. Architecture deltas use
7
+ `apltk architecture --spec <spec_dir> …`: single specs write under `<spec_dir>/architecture_diff/`,
8
+ while batch member paths resolve to the shared batch-root `architecture_diff/` beside
9
+ `coordination.md`; `apltk architecture diff` renders the before/after viewer. Use for
10
+ drafting/refreshing specs or restructuring batches, not execution. Reject vague `tasks.md`
11
+ rows and split scope beyond 3 modules.
9
12
  ---
10
13
 
11
14
  # Generate Spec
@@ -27,7 +30,7 @@ description: >-
27
30
  - **`tasks.md` checklist items**: **every** `- [ ]` **MUST** specify (a) concrete file/function target, (b) specific modification and expected outcome, (c) a verification hook—**no** vague rows (`Implement integration`, `Add tests`). Forbidden vague items **MUST** be rewritten before approval.
28
31
  - **MUST** use `test-case-strategy` when planning non-trivial logic tests and checklist mapping (test IDs, drift checks). Every **non-trivial** `tasks.md` implementation item **MUST** name a focused unit drift check, another concrete verification hook, or **`N/A`** with a concrete reason.
29
32
  - **MUST NOT** modify implementation code before **explicit user approval** of the spec set. Clarifications **MUST** sync across affected files and **MUST** re-trigger approval. If scope becomes a **different issue**, **MUST** stop editing the old set and create a **new** `change_name`.
30
- - **MUST** when the proposed change touches the architecture surface (feature / sub-module add / rename / remove, edge add / remove, variable rows, function I/O rows, internal dataflow / error deltas), declare the proposed-after state **only** through `apltk architecture --spec <spec_dir> …` using the exact verbs, subverbs, and flags from **`apltk architecture --help`** (this file must not be treated as the command list). The CLI writes overlay YAML to `<spec_dir>/architecture_diff/atlas/` and renders only the affected proposed-after HTML pages under `<spec_dir>/architecture_diff/`. **`apltk architecture diff`** then builds a paginated **before/after viewer**: it walks every `docs/plans/**/architecture_diff/` tree, pairs each overlay HTML path with the matching file under `resources/project-architecture/` when it exists, and labels pages **modified**, **added**, or **removed** (via `_removed.txt` / removal manifests) so reviewers can scroll the whole architecture delta without opening files by hand. **MUST NOT** hand-author anything under `architecture_diff/**` — the renderer owns layout, DOM, CSS, ARIA, and pan/zoom. For batch specs the overlay lives in **each member spec’s own directory** only — **MUST NOT** duplicate overlay at the batch root. Use this skill’s `references/TEMPLATE_SPEC.md` for field/enum schema; use `init-project-html/SKILL.md` for semantic rules (**subagent gate** + edge kinds + dataflow integrity). When using subagents to draft atlas overlay, the authoring agent **MUST** wait until **all** feature subagents finish before declaring cross-feature **`edge`** (or overlay **`meta`** / **`actor`** that only exists to stitch features), matching `init-project-html` / `spec-to-project-html`.
33
+ - **MUST** when the proposed change touches the architecture surface (feature / sub-module add / rename / remove, edge add / remove, variable rows, function I/O rows, internal dataflow / error deltas), declare the proposed-after state **only** through `apltk architecture --spec <spec_dir> …` using the exact verbs, subverbs, and flags from **`apltk architecture --help`** (this file must not be treated as the command list). The CLI writes overlay YAML to `<spec_dir>/architecture_diff/atlas/` and renders only the affected proposed-after HTML pages under `<spec_dir>/architecture_diff/` for single-spec plans. For batch plans, passing any member spec path to `--spec` resolves to the shared batch-root overlay beside `coordination.md`, so the whole batch maintains **one** `architecture_diff/atlas/` and **one** rendered architecture diff. **`apltk architecture diff`** then builds a paginated **before/after viewer**: it walks every `docs/plans/**/architecture_diff/` tree, pairs each overlay HTML path with the matching file under `resources/project-architecture/` when it exists, and labels pages **modified**, **added**, or **removed** (via `_removed.txt` / removal manifests) so reviewers can scroll the whole architecture delta without opening files by hand. **MUST NOT** hand-author anything under `architecture_diff/**` — the renderer owns layout, DOM, CSS, ARIA, and pan/zoom. Use this skill’s `references/TEMPLATE_SPEC.md` for field/enum schema; use `init-project-html/SKILL.md` for semantic rules (**subagent gate** + edge kinds + dataflow integrity). When using subagents to draft atlas overlay, the authoring agent **MUST** wait until **all** feature subagents finish before declaring cross-feature **`edge`** (or overlay **`meta`** / **`actor`** that only exists to stitch features), matching `init-project-html` / `spec-to-project-html`.
31
34
  - Write prose in the **user’s language** by default; keep requirement/task/test IDs traceable across `spec.md`, `tasks.md`, and `checklist.md`.
32
35
  - **MUST** use **kebab-case** `change_name`; **MUST NOT** use spaces or arbitrary special characters in names.
33
36
 
@@ -85,15 +88,15 @@ Always materialize: `spec.md`, `tasks.md`, `checklist.md`, `contract.md`, `desig
85
88
 
86
89
  When the spec changes a feature module, sub-module, edge, variable row, function I/O row, internal dataflow, or error row:
87
90
 
88
- 1. **Discover commands:** run **`apltk architecture --help`** in the target workspace; use that output for every `add` / `set` / `remove` / `reorder` spelling and required flag. Do not copy long command tables from skills they go stale.
89
- 2. **Declare proposed-after state** with `apltk architecture --spec <spec_dir> …` for each mutation (pass `--no-render` while batching if you prefer a single render at the end).
90
- 3. **`apltk architecture render --spec <spec_dir>`** — emits/updates only the HTML files touched by this overlay plus assets.
91
- 4. **`apltk architecture validate --spec <spec_dir>`** — **MUST** return OK before the spec is approval-ready (resolve dangling edges, unknown enums, bad dataflow references).
92
- 5. **`apltk architecture diff`** — **MUST** run before hand-off when atlas changed; confirm the paginated viewer shows sensible **modified** / **added** / **removed** counts and that each interesting path pairs correctly (base `resources/project-architecture/…` vs `<spec_dir>/architecture_diff/…`). A page appearing as **remove + add** instead of **modified** usually means a **slug rename** was split wrong — fix with intentional remove+add or a single coherent mutation sequence.
91
+ **Completion standard:** the overlay is not complete until every intended cross-feature **edge**, every feature-to-feature dependency or call/return relationship, and every sub-module-to-sub-module relationship inside the affected scope has been declared precisely through the CLI. It is **not** acceptable to leave relationship structure implied only by prose in `spec.md` / `design.md`; the architecture diff must explicitly express the real proposed-after topology.
93
92
 
94
- **Where files land:** overlay YAML under `<spec_dir>/architecture_diff/atlas/`; rendered proposed-after HTML under `<spec_dir>/architecture_diff/`. Cross-feature edges whose far endpoint exists only in the **base** atlas still resolve when merged but you **must** `validate` to catch mistakes.
93
+ 1. **Discover commands:** run **`apltk architecture --help`** in the target workspace; use that output for every `add` / `set` / `remove` / `reorder` spelling and required flag. Do not copy long command tables from skills — they go stale.
94
+ 2. **Declare proposed-after state** with `apltk architecture --spec <spec_dir> …` for each mutation (pass `--no-render` while batching if you prefer a single render at the end). In a batch, keep using the member spec path; the CLI resolves writes to the batch-root `architecture_diff/` beside `coordination.md`.
95
+ 3. **`apltk architecture render --spec <spec_dir>`** — emits/updates only the HTML files touched by this overlay plus assets. In a batch, this updates the shared batch-root render.
96
+ 4. **`apltk architecture validate --spec <spec_dir>`** — **MUST** return OK before the spec is approval-ready (resolve dangling edges, unknown enums, bad dataflow references). In a batch, this validates the shared batch-root overlay.
97
+ 5. **`apltk architecture diff`** — **MUST** run before hand-off when atlas changed; confirm the paginated viewer shows sensible **modified** / **added** / **removed** counts and that each interesting path pairs correctly (base `resources/project-architecture/…` vs the spec or batch-root `architecture_diff/…`). Use this pass to verify that the rendered graph actually contains the full intended relationship set: all relevant feature-level seams, all required sub-module seams, and no missing edge that the design prose depends on. A page appearing as **remove + add** instead of **modified** usually means a **slug rename** was split wrong — fix with intentional remove+add or a single coherent mutation sequence.
95
98
 
96
- **Batch specs:** each `<spec_dir>` is one member directory under `docs/plans/...`; **never** duplicate overlay at the batch root.
99
+ **Where files land:** single-spec plans write overlay YAML under `<spec_dir>/architecture_diff/atlas/` and rendered proposed-after HTML under `<spec_dir>/architecture_diff/`. Batch plans write both under the batch root beside `coordination.md`, even when the CLI call uses a member spec path. Cross-feature edges whose far endpoint exists only in the **base** atlas still resolve when merged — but you **must** `validate` to catch mistakes.
97
100
 
98
101
  **Subagent coordination:** if multiple features are drafted in parallel, **do not** declare cross-feature **`edge`** (or overlay **`meta` / `actor`** used only to stitch features) until **all** feature workers report done — see `init-project-html/SKILL.md` Rule 3 and `spec-to-project-html`.
99
102
 
@@ -102,6 +105,7 @@ When the spec changes a feature module, sub-module, edge, variable row, function
102
105
  - **Pause →** Did I touch any file under `architecture_diff/` by hand? Revert and re-run the CLI verb instead.
103
106
  - **Pause →** Does `apltk architecture validate --spec <spec_dir>` return OK?
104
107
  - **Pause →** Does `apltk architecture diff` pair the spec’s pages correctly?
108
+ - **Pause →** Have I explicitly declared every intended edge, feature-to-feature relationship, and sub-module relationship in the CLI output, or am I still relying on prose to imply missing structure?
105
109
 
106
110
  ### 4) Clarifications and approval
107
111
 
@@ -4,7 +4,7 @@ interface:
4
4
  default_prompt: >-
5
5
  Use $generate-spec to create or update single-spec plans under docs/plans/<date>/<change_name>/ or parallel batches under docs/plans/<date>/<batch_name>/<change_name>/ with shared coordination.md and, only when specs cannot be parallel-safe without prior shared work, minimal non-business preparation.md; treat references/templates/*.md as binding format; member specs assume preparation finished—do not duplicate preparation tasks; surface collisions early and resolve ownership via coordination.md; fill BDD in spec.md; integrate $test-case-strategy into tasks/checklists.
6
6
  **Critical layering:** design.md + contract.md are higher-level guiding context only (architecture + cite-backed external truth; coarse INT-### / EXT-### anchors). tasks.md MUST be the ONLY enumerated runnable queue with path-level edits and verification hooks—derive tasks FROM spec + design + contract WITHOUT mirroring checklist rows into design/contract; optionally cite INT/EXT on task lines for traceability—never duplicate task choreography inside design.md or contract.md.
7
- **Architecture overlay + diff:** When the spec touches atlas surface (feature/sub-module, edges, function/variable rows, dataflow, errors), declare proposed-after state ONLY via `apltk architecture --spec <spec_dir> …`. **Exact verbs/flags: ALWAYS `apltk architecture --help`.** CLI writes `<spec_dir>/architecture_diff/atlas/` + renders affected HTML under `<spec_dir>/architecture_diff/`. NEVER hand-author `architecture_diff/**`.
8
- After mutations: `render --spec`, `validate --spec` (must be OK pre-approval). **`apltk architecture diff`** opens the paginated before/after viewer over all `docs/plans/**/architecture_diff/` vs base `resources/project-architecture/` — run it when atlas changed; verify modified/added/removed pairing.
9
- Batch specs: one overlay per member directory—never at batch root. If subagents draft multiple features: **wait until ALL finish** before cross-feature `edge` / stitching `meta` / `actor` (same gate as $init-project-html / $spec-to-project-html).
7
+ **Architecture overlay + diff:** When the spec touches atlas surface (feature/sub-module, edges, function/variable rows, dataflow, errors), declare proposed-after state ONLY via `apltk architecture --spec <spec_dir> …`. **Exact verbs/flags: ALWAYS `apltk architecture --help`.** Single-spec plans write `<spec_dir>/architecture_diff/atlas/` + rendered HTML under `<spec_dir>/architecture_diff/`. Batch plans resolve any member `--spec` path to the shared batch-root `architecture_diff/` beside `coordination.md`, so the whole batch keeps one architecture diff. NEVER hand-author `architecture_diff/**`.
8
+ Completion standard for atlas work: every intended cross-feature edge, every feature-to-feature relationship, and every sub-module relationship in scope must be expressed precisely through the CLI output — not left implicit in prose. After mutations: `render --spec`, `validate --spec` (must be OK pre-approval). **`apltk architecture diff`** opens the paginated before/after viewer over all `docs/plans/**/architecture_diff/` vs base `resources/project-architecture/` — run it when atlas changed; verify modified/added/removed pairing and confirm the rendered graph contains the full intended relationship set.
9
+ If subagents draft multiple features: **wait until ALL finish** before cross-feature `edge` / stitching `meta` / `actor` (same gate as $init-project-html / $spec-to-project-html).
10
10
  Field/enums: this skill's references/TEMPLATE_SPEC.md. Semantics: $init-project-html SKILL.md.
@@ -23,7 +23,7 @@
23
23
  //
24
24
  // Global flags:
25
25
  // --project <root> project root; creates resources/project-architecture/ if missing
26
- // --spec <spec_dir> spec directory; mutations go to <spec_dir>/architecture_diff/atlas/
26
+ // --spec <spec_dir> single specs write to <spec_dir>/architecture_diff/atlas/; batch member paths resolve to the coordination.md root
27
27
  // --no-render skip auto-render after a mutation
28
28
  // --no-open for open/diff: skip launching the browser
29
29
  // --out <dir> for diff: override viewer output directory
@@ -41,6 +41,7 @@ const ATLAS_INDEX_REL = path.join(ATLAS_REL, 'index.html');
41
41
  const ATLAS_DIRNAME = stateLib.ATLAS_DIRNAME;
42
42
  const DIFF_DIRNAME = 'architecture_diff';
43
43
  const PLANS_REL = path.join('docs', 'plans');
44
+ const COORDINATION_FILE = 'coordination.md';
44
45
  const REMOVED_TXT = '_removed.txt';
45
46
  const DEFAULT_DIFF_OUT_REL = path.join('.apollo-toolkit', 'architecture-diff');
46
47
 
@@ -63,12 +64,12 @@ Verbs:
63
64
  meta set edit meta.title / meta.summary
64
65
  actor add|remove manage top-level actors
65
66
  validate run schema + referential checks
66
- undo revert the most recent mutation
67
+ undo revert the most recent mutation (use --steps <n> for multi-step rollback)
67
68
  help show this help
68
69
 
69
70
  Global flags:
70
71
  --project <root> explicit project root (default: nearest ancestor with atlas markers, else cwd); missing directories under resources/project-architecture/ are created automatically
71
- --spec <spec_dir> mutations write to <spec_dir>/architecture_diff/atlas/
72
+ --spec <spec_dir> single specs write to <spec_dir>/architecture_diff/atlas/; batch member paths write to the coordination.md root
72
73
  --no-render skip auto-render after a mutation
73
74
  --no-open for open/diff: skip launching the browser
74
75
  --out <dir> for diff: override viewer output directory
@@ -81,6 +82,7 @@ Examples:
81
82
  apltk architecture dataflow add --feature register --submodule api --step "Validate body" --fn handlePost --reads "body" --writes "token"
82
83
  apltk architecture --spec docs/plans/2026-05-11/add-2fa submodule set --feature register --slug api --role "..."
83
84
  apltk architecture validate
85
+ apltk architecture undo --steps 3 --spec docs/plans/2026-05-11/add-2fa
84
86
  apltk architecture diff
85
87
  `;
86
88
 
@@ -171,7 +173,15 @@ function resolveProjectRoot(flags) {
171
173
 
172
174
  function specOverlayDir(projectRoot, specFlag) {
173
175
  const specDir = path.isAbsolute(String(specFlag)) ? String(specFlag) : path.resolve(projectRoot, String(specFlag));
174
- return { specDir, overlayDir: path.join(specDir, DIFF_DIRNAME, ATLAS_DIRNAME), htmlOutDir: path.join(specDir, DIFF_DIRNAME) };
176
+ const plansRoot = path.join(projectRoot, PLANS_REL);
177
+ const batchRoot = fs.existsSync(path.join(specDir, COORDINATION_FILE)) ? specDir : findBatchRoot(specDir, plansRoot);
178
+ const rootDir = batchRoot || specDir;
179
+ return {
180
+ specDir,
181
+ rootDir,
182
+ overlayDir: path.join(rootDir, DIFF_DIRNAME, ATLAS_DIRNAME),
183
+ htmlOutDir: path.join(rootDir, DIFF_DIRNAME),
184
+ };
175
185
  }
176
186
 
177
187
  function baseAtlasDir(projectRoot) {
@@ -207,26 +217,21 @@ function ensureBaseAtlasDir(projectRoot) {
207
217
  async function performMutation(projectRoot, flags, action, args, mutate) {
208
218
  const isSpec = Boolean(flags.spec);
209
219
  const base = stateLib.load(baseAtlasDir(projectRoot));
210
- let overlay = null;
211
220
  let merged = base;
212
- let touchedFeatureSlugs = new Set();
213
221
 
214
222
  if (isSpec) {
215
223
  const { overlayDir } = specOverlayDir(projectRoot, flags.spec);
216
- overlay = stateLib.loadOverlay(overlayDir);
224
+ const overlay = stateLib.loadOverlay(overlayDir);
217
225
  merged = stateLib.mergeOverlay(base, overlay);
218
226
  const before = JSON.parse(JSON.stringify({ base, overlay }));
219
- const result = mutate(merged, base, overlay) || {};
220
- if (result.touchedFeatures) for (const slug of result.touchedFeatures) touchedFeatureSlugs.add(slug);
227
+ mutate(merged, base, overlay);
221
228
  stateLib.writeUndoSnapshot(overlayDir, before);
222
- syncOverlayFromMerged({ base, overlay, merged, touchedFeatureSlugs, removalsHint: result.removalsHint });
223
- stateLib.saveOverlay(overlayDir, overlay);
229
+ stateLib.saveOverlay(overlayDir, stateLib.deriveOverlay(base, merged));
224
230
  stateLib.appendHistory(overlayDir, { action, args, mode: 'spec' });
225
231
  } else {
226
232
  ensureBaseAtlasDir(projectRoot);
227
233
  const before = JSON.parse(JSON.stringify({ base }));
228
- const result = mutate(base, base, null) || {};
229
- if (result.touchedFeatures) for (const slug of result.touchedFeatures) touchedFeatureSlugs.add(slug);
234
+ mutate(base, base, null);
230
235
  stateLib.writeUndoSnapshot(baseAtlasDir(projectRoot), before);
231
236
  stateLib.save(baseAtlasDir(projectRoot), base);
232
237
  stateLib.appendHistory(baseAtlasDir(projectRoot), { action, args, mode: 'base' });
@@ -237,47 +242,6 @@ async function performMutation(projectRoot, flags, action, args, mutate) {
237
242
  }
238
243
  }
239
244
 
240
- function syncOverlayFromMerged({ base, overlay, merged, touchedFeatureSlugs, removalsHint }) {
241
- // Compare merged top-level fields against base; if they differ, sync into overlay.
242
- if (JSON.stringify(merged.meta || {}) !== JSON.stringify(base.meta || {})) overlay.meta = merged.meta;
243
- if (JSON.stringify(merged.actors || []) !== JSON.stringify(base.actors || [])) overlay.actors = merged.actors || [];
244
- if (JSON.stringify(merged.edges || []) !== JSON.stringify(base.edges || [])) overlay.edges = merged.edges || [];
245
-
246
- const baseOrder = (base.features || []).map((f) => f.slug);
247
- const mergedOrder = (merged.features || []).map((f) => f.slug);
248
- if (JSON.stringify(baseOrder) !== JSON.stringify(mergedOrder)) {
249
- overlay.featureOrder = mergedOrder;
250
- }
251
-
252
- // Persist touched (or simply differing) features into overlay.features
253
- const baseFeatureMap = new Map((base.features || []).map((f) => [f.slug, f]));
254
- const mergedFeatureMap = new Map((merged.features || []).map((f) => [f.slug, f]));
255
- for (const slug of touchedFeatureSlugs) {
256
- if (!mergedFeatureMap.has(slug)) continue;
257
- const baseFeat = baseFeatureMap.get(slug);
258
- const mergedFeat = mergedFeatureMap.get(slug);
259
- if (!baseFeat || JSON.stringify(baseFeat) !== JSON.stringify(mergedFeat)) {
260
- overlay.features[slug] = mergedFeat;
261
- }
262
- }
263
- // Apply hinted removals
264
- if (removalsHint) {
265
- if (Array.isArray(removalsHint.features)) {
266
- const seen = new Set(overlay.removed.features || []);
267
- for (const slug of removalsHint.features) seen.add(slug);
268
- overlay.removed.features = [...seen];
269
- // also drop from overlay.features if present
270
- for (const slug of removalsHint.features) delete overlay.features[slug];
271
- }
272
- if (Array.isArray(removalsHint.submodules)) {
273
- const seen = new Map();
274
- for (const item of overlay.removed.submodules || []) seen.set(`${item.feature}::${item.submodule}`, item);
275
- for (const item of removalsHint.submodules) seen.set(`${item.feature}::${item.submodule}`, item);
276
- overlay.removed.submodules = [...seen.values()];
277
- }
278
- }
279
- }
280
-
281
245
  async function runRender({ projectRoot, flags }) {
282
246
  if (flags.spec) {
283
247
  const { overlayDir, htmlOutDir } = specOverlayDir(projectRoot, flags.spec);
@@ -642,9 +606,14 @@ async function verbValidate(flags, projectRoot, io) {
642
606
 
643
607
  async function verbUndo(flags, projectRoot, io) {
644
608
  const dir = flags.spec ? specOverlayDir(projectRoot, flags.spec).overlayDir : baseAtlasDir(projectRoot);
645
- const snapshot = stateLib.readUndoSnapshot(dir);
609
+ const stepsRaw = flags.steps === undefined ? 1 : Number(flags.steps);
610
+ if (!Number.isInteger(stepsRaw) || stepsRaw < 1) {
611
+ io.stderr.write('--steps must be a positive integer.\n');
612
+ return 1;
613
+ }
614
+ const snapshot = stateLib.consumeUndoSnapshot(dir, stepsRaw);
646
615
  if (!snapshot) {
647
- io.stderr.write('No undo snapshot found.\n');
616
+ io.stderr.write(stepsRaw === 1 ? 'No undo snapshot found.\n' : `Unable to undo ${stepsRaw} steps; history is shorter.\n`);
648
617
  return 1;
649
618
  }
650
619
  if (flags.spec) {
@@ -655,9 +624,8 @@ async function verbUndo(flags, projectRoot, io) {
655
624
  stateLib.save(baseAtlasDir(projectRoot), snapshot.base);
656
625
  stateLib.appendHistory(baseAtlasDir(projectRoot), { action: 'undo', mode: 'base' });
657
626
  }
658
- stateLib.clearUndoSnapshot(dir);
659
627
  if (!flags['no-render']) await runRender({ projectRoot, flags });
660
- io.stdout.write('atlas: undo applied\n');
628
+ io.stdout.write(`atlas: undo applied (${stepsRaw} step${stepsRaw === 1 ? '' : 's'})\n`);
661
629
  return 0;
662
630
  }
663
631
 
@@ -678,36 +646,27 @@ async function verbOpen(flags, projectRoot, io) {
678
646
  async function verbDiff(flags, projectRoot, io) {
679
647
  const outDir = flags.out ? path.resolve(String(flags.out)) : path.join(projectRoot, DEFAULT_DIFF_OUT_REL);
680
648
  fs.mkdirSync(outDir, { recursive: true });
649
+ const changes = await collectDiffChanges({ projectRoot, outDir });
681
650
 
651
+ const html = renderDiffViewer({ changes, projectRoot, outDir });
652
+ const indexPath = path.join(outDir, 'index.html');
653
+ fs.writeFileSync(indexPath, html, 'utf8');
654
+ io.stdout.write(`${indexPath}\n`);
655
+ io.stdout.write(`Diff pages: ${changes.length} (modified=${changes.filter((c) => c.kind === 'modified').length}, added=${changes.filter((c) => c.kind === 'added').length}, removed=${changes.filter((c) => c.kind === 'removed').length})\n`);
656
+ if (!flags['no-open']) openInBrowser(indexPath);
657
+ return 0;
658
+ }
659
+
660
+ async function collectDiffChanges({ projectRoot, outDir }) {
682
661
  const plansRoot = path.join(projectRoot, PLANS_REL);
683
- const diffDirs = walkArchitectureDiffDirs(plansRoot);
684
- const resourcesRoot = path.join(projectRoot, ATLAS_REL);
662
+ const groups = groupDiffDirsByBatch({ projectRoot, plansRoot });
685
663
  const changes = [];
686
664
 
687
- for (const diffDir of diffDirs) {
688
- const specDir = path.dirname(diffDir);
689
- const specLabel = path.relative(projectRoot, specDir);
690
- for (const after of walkAfterStateHtml(diffDir)) {
691
- const beforeAbs = path.join(resourcesRoot, after.rel);
692
- const beforeExists = fs.existsSync(beforeAbs);
693
- changes.push({
694
- kind: beforeExists ? 'modified' : 'added',
695
- rel: after.rel,
696
- spec: specLabel,
697
- beforePath: beforeExists ? path.relative(projectRoot, beforeAbs) : null,
698
- afterPath: path.relative(projectRoot, after.abs),
699
- });
700
- }
701
- for (const removedRel of readRemovedManifest(diffDir)) {
702
- const beforeAbs = path.join(resourcesRoot, removedRel);
703
- if (!fs.existsSync(beforeAbs)) continue;
704
- changes.push({
705
- kind: 'removed',
706
- rel: removedRel,
707
- spec: specLabel,
708
- beforePath: path.relative(projectRoot, beforeAbs),
709
- afterPath: null,
710
- });
665
+ for (const group of groups) {
666
+ if (group.kind === 'batch') {
667
+ changes.push(...await collectBatchGroupChanges({ projectRoot, outDir, group }));
668
+ } else {
669
+ changes.push(...collectSingleSpecChanges({ projectRoot, specDir: group.specDir, specLabel: group.label }));
711
670
  }
712
671
  }
713
672
 
@@ -716,14 +675,171 @@ async function verbDiff(flags, projectRoot, io) {
716
675
  if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
717
676
  return a.rel.localeCompare(b.rel);
718
677
  });
678
+ return changes;
679
+ }
719
680
 
720
- const html = renderDiffViewer({ changes, projectRoot, outDir });
721
- const indexPath = path.join(outDir, 'index.html');
722
- fs.writeFileSync(indexPath, html, 'utf8');
723
- io.stdout.write(`${indexPath}\n`);
724
- io.stdout.write(`Diff pages: ${changes.length} (modified=${changes.filter((c) => c.kind === 'modified').length}, added=${changes.filter((c) => c.kind === 'added').length}, removed=${changes.filter((c) => c.kind === 'removed').length})\n`);
725
- if (!flags['no-open']) openInBrowser(indexPath);
726
- return 0;
681
+ function groupDiffDirsByBatch({ projectRoot, plansRoot }) {
682
+ const groups = new Map();
683
+ for (const diffDir of walkArchitectureDiffDirs(plansRoot)) {
684
+ const specDir = path.dirname(diffDir);
685
+ const batchRoot = findBatchRoot(specDir, plansRoot);
686
+ const isBatchMember = Boolean(batchRoot && batchRoot !== specDir);
687
+ const key = isBatchMember ? batchRoot : specDir;
688
+ if (!groups.has(key)) {
689
+ groups.set(key, {
690
+ kind: isBatchMember ? 'batch' : 'single',
691
+ key,
692
+ label: path.relative(projectRoot, key),
693
+ specDir: isBatchMember ? null : specDir,
694
+ members: [],
695
+ });
696
+ }
697
+ groups.get(key).members.push({ specDir, diffDir, label: path.relative(projectRoot, specDir) });
698
+ }
699
+ return [...groups.values()]
700
+ .map((group) => ({ ...group, members: group.members.sort((a, b) => a.specDir.localeCompare(b.specDir)) }))
701
+ .sort((a, b) => a.key.localeCompare(b.key));
702
+ }
703
+
704
+ function findBatchRoot(specDir, plansRoot) {
705
+ const absolutePlansRoot = path.resolve(plansRoot);
706
+ let current = path.resolve(path.dirname(specDir));
707
+ while (current.startsWith(`${absolutePlansRoot}${path.sep}`) || current === absolutePlansRoot) {
708
+ if (fs.existsSync(path.join(current, COORDINATION_FILE))) return current;
709
+ if (current === absolutePlansRoot) break;
710
+ current = path.dirname(current);
711
+ }
712
+ return null;
713
+ }
714
+
715
+ function collectSingleSpecChanges({ projectRoot, specDir, specLabel }) {
716
+ const overlayDir = path.join(specDir, DIFF_DIRNAME, ATLAS_DIRNAME);
717
+ if (!hasOverlayState(overlayDir)) {
718
+ return collectHtmlManifestChanges({ projectRoot, diffDir: path.join(specDir, DIFF_DIRNAME), specLabel });
719
+ }
720
+ const base = stateLib.load(baseAtlasDir(projectRoot));
721
+ const overlay = stateLib.loadOverlay(overlayDir);
722
+ const merged = stateLib.mergeOverlay(base, overlay);
723
+ const diff = stateLib.diffPages(base, merged);
724
+ return diffToChanges({
725
+ projectRoot,
726
+ specLabel,
727
+ htmlRoot: path.join(specDir, DIFF_DIRNAME),
728
+ diff,
729
+ });
730
+ }
731
+
732
+ function hasOverlayState(overlayDir) {
733
+ return fs.existsSync(path.join(overlayDir, stateLib.INDEX_FILE))
734
+ || fs.existsSync(path.join(overlayDir, stateLib.FEATURES_DIR))
735
+ || fs.existsSync(path.join(overlayDir, stateLib.REMOVED_FILE));
736
+ }
737
+
738
+ async function collectBatchGroupChanges({ projectRoot, outDir, group }) {
739
+ const batchRootOverlayDir = path.join(group.key, DIFF_DIRNAME, ATLAS_DIRNAME);
740
+ if (hasOverlayState(batchRootOverlayDir)) {
741
+ return collectSingleSpecChanges({ projectRoot, specDir: group.key, specLabel: group.label });
742
+ }
743
+
744
+ const memberOverlayDirs = group.members.map((member) => ({
745
+ ...member,
746
+ overlayDir: path.join(member.specDir, DIFF_DIRNAME, ATLAS_DIRNAME),
747
+ }));
748
+ if (memberOverlayDirs.some((member) => !hasOverlayState(member.overlayDir))) {
749
+ return group.members.flatMap((member) => (
750
+ collectSingleSpecChanges({ projectRoot, specDir: member.specDir, specLabel: member.label })
751
+ ));
752
+ }
753
+
754
+ const base = stateLib.load(baseAtlasDir(projectRoot));
755
+ let merged = JSON.parse(JSON.stringify(base));
756
+ for (const member of memberOverlayDirs) {
757
+ const overlay = stateLib.loadOverlay(member.overlayDir);
758
+ merged = stateLib.mergeOverlay(merged, overlay);
759
+ }
760
+ const diff = stateLib.diffPages(base, merged);
761
+ const htmlRoot = path.join(outDir, '_batch', group.label);
762
+ await renderLib.renderAll({
763
+ outDir: htmlRoot,
764
+ state: merged,
765
+ scope: renderLib.scopeFromDiff(diff),
766
+ removedPaths: renderLib.removedPagePathsFromDiff(diff),
767
+ });
768
+ return diffToChanges({
769
+ projectRoot,
770
+ specLabel: group.label,
771
+ htmlRoot,
772
+ diff,
773
+ });
774
+ }
775
+
776
+ function diffToChanges({ projectRoot, specLabel, htmlRoot, diff }) {
777
+ const resourcesRoot = path.join(projectRoot, ATLAS_REL);
778
+ const changes = [];
779
+ const add = (kind, rel) => {
780
+ const beforeAbs = path.join(resourcesRoot, rel);
781
+ const afterAbs = kind === 'removed' ? null : path.join(htmlRoot, rel);
782
+ if (kind === 'removed' && !fs.existsSync(beforeAbs)) return;
783
+ changes.push({
784
+ kind,
785
+ rel,
786
+ spec: specLabel,
787
+ beforePath: kind === 'added' ? null : path.relative(projectRoot, beforeAbs),
788
+ afterPath: afterAbs ? path.relative(projectRoot, afterAbs) : null,
789
+ });
790
+ };
791
+
792
+ if (diff.macroChanged) {
793
+ add('modified', renderLib.pagePathFor('macro'));
794
+ }
795
+ for (const slug of diff.modifiedFeatures || []) {
796
+ add('modified', renderLib.pagePathFor('feature', { featureSlug: slug }));
797
+ }
798
+ for (const slug of diff.addedFeatures || []) {
799
+ add('added', renderLib.pagePathFor('feature', { featureSlug: slug }));
800
+ }
801
+ for (const item of diff.modifiedSubmodules || []) {
802
+ add('modified', renderLib.pagePathFor('submodule', { featureSlug: item.feature, submoduleSlug: item.submodule }));
803
+ }
804
+ for (const item of diff.addedSubmodules || []) {
805
+ add('added', renderLib.pagePathFor('submodule', { featureSlug: item.feature, submoduleSlug: item.submodule }));
806
+ }
807
+ for (const slug of diff.removedFeatures || []) {
808
+ add('removed', renderLib.pagePathFor('feature', { featureSlug: slug }));
809
+ }
810
+ for (const item of diff.removedSubmodules || []) {
811
+ add('removed', renderLib.pagePathFor('submodule', { featureSlug: item.feature, submoduleSlug: item.submodule }));
812
+ }
813
+
814
+ return changes;
815
+ }
816
+
817
+ function collectHtmlManifestChanges({ projectRoot, diffDir, specLabel }) {
818
+ const resourcesRoot = path.join(projectRoot, ATLAS_REL);
819
+ const changes = [];
820
+ for (const after of walkAfterStateHtml(diffDir)) {
821
+ const beforeAbs = path.join(resourcesRoot, after.rel);
822
+ const beforeExists = fs.existsSync(beforeAbs);
823
+ changes.push({
824
+ kind: beforeExists ? 'modified' : 'added',
825
+ rel: after.rel,
826
+ spec: specLabel,
827
+ beforePath: beforeExists ? path.relative(projectRoot, beforeAbs) : null,
828
+ afterPath: path.relative(projectRoot, after.abs),
829
+ });
830
+ }
831
+ for (const removedRel of readRemovedManifest(diffDir)) {
832
+ const beforeAbs = path.join(resourcesRoot, removedRel);
833
+ if (!fs.existsSync(beforeAbs)) continue;
834
+ changes.push({
835
+ kind: 'removed',
836
+ rel: removedRel,
837
+ spec: specLabel,
838
+ beforePath: path.relative(projectRoot, beforeAbs),
839
+ afterPath: null,
840
+ });
841
+ }
842
+ return changes;
727
843
  }
728
844
 
729
845
  function walkArchitectureDiffDirs(plansRoot) {
@@ -1021,6 +1137,7 @@ module.exports = {
1021
1137
  specOverlayDir,
1022
1138
  runRender,
1023
1139
  walkArchitectureDiffDirs,
1140
+ collectDiffChanges,
1024
1141
  walkAfterStateHtml,
1025
1142
  readRemovedManifest,
1026
1143
  renderDiffViewer,
@@ -560,11 +560,40 @@ async function renderAll({ outDir, state, scope = null, removedPaths = [] }) {
560
560
  // do not linger with the previous (broken) markup or styling.
561
561
  if (!scope) {
562
562
  sweepOrphanFeaturePages(outDir, state);
563
+ } else {
564
+ sweepScopedHtml(outDir, new Set(written.map((file) => file.split(path.sep).join('/'))));
563
565
  }
564
566
 
565
567
  return { written, layout };
566
568
  }
567
569
 
570
+ function sweepScopedHtml(outDir, keepPaths) {
571
+ function recurse(dir) {
572
+ let entries;
573
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_e) { return; }
574
+ for (const entry of entries) {
575
+ if (entry.name === 'assets' || entry.name === 'atlas' || entry.name.startsWith('.')) continue;
576
+ const full = path.join(dir, entry.name);
577
+ if (entry.isDirectory()) {
578
+ recurse(full);
579
+ let remaining;
580
+ try { remaining = fs.readdirSync(full); } catch (_e) { remaining = null; }
581
+ if (remaining && remaining.length === 0) {
582
+ fs.rmSync(full, { recursive: true, force: true });
583
+ }
584
+ continue;
585
+ }
586
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.html')) continue;
587
+ const rel = path.relative(outDir, full).split(path.sep).join('/');
588
+ if (!keepPaths.has(rel)) {
589
+ fs.rmSync(full, { force: true });
590
+ }
591
+ }
592
+ }
593
+
594
+ recurse(outDir);
595
+ }
596
+
568
597
  function sweepOrphanFeaturePages(outDir, state) {
569
598
  const featuresRoot = path.join(outDir, 'features');
570
599
  if (!fs.existsSync(featuresRoot)) return;
@@ -26,6 +26,7 @@ const REMOVED_FILE = '_removed.yaml';
26
26
  const FEATURES_DIR = 'features';
27
27
  const HISTORY_FILE = 'atlas.history.log';
28
28
  const UNDO_FILE = 'atlas.history.undo.json';
29
+ const UNDO_STACK_FILE = 'atlas.history.undo.stack.json';
29
30
  const ATLAS_DIRNAME = 'atlas';
30
31
 
31
32
  function readYaml(file) {
@@ -257,6 +258,51 @@ function mergeOverlay(base, overlay) {
257
258
  return merged;
258
259
  }
259
260
 
261
+ function deriveOverlay(base, merged) {
262
+ const overlay = {
263
+ meta: null,
264
+ actors: null,
265
+ edges: null,
266
+ featureOrder: null,
267
+ features: {},
268
+ removed: { features: [], submodules: [] },
269
+ };
270
+
271
+ if (JSON.stringify(merged.meta || {}) !== JSON.stringify(base.meta || {})) {
272
+ overlay.meta = merged.meta || {};
273
+ }
274
+ if (JSON.stringify(merged.actors || []) !== JSON.stringify(base.actors || [])) {
275
+ overlay.actors = merged.actors || [];
276
+ }
277
+ if (JSON.stringify(merged.edges || []) !== JSON.stringify(base.edges || [])) {
278
+ overlay.edges = merged.edges || [];
279
+ }
280
+
281
+ const baseOrder = (base.features || []).map((feature) => feature.slug);
282
+ const mergedOrder = (merged.features || []).map((feature) => feature.slug);
283
+ if (JSON.stringify(mergedOrder) !== JSON.stringify(baseOrder)) {
284
+ overlay.featureOrder = mergedOrder;
285
+ }
286
+
287
+ const baseFeatures = new Map((base.features || []).map((feature) => [feature.slug, feature]));
288
+ const mergedFeatures = new Map((merged.features || []).map((feature) => [feature.slug, feature]));
289
+
290
+ for (const [slug, feature] of mergedFeatures) {
291
+ const baseFeature = baseFeatures.get(slug);
292
+ if (!baseFeature || JSON.stringify(feature) !== JSON.stringify(baseFeature)) {
293
+ overlay.features[slug] = feature;
294
+ }
295
+ }
296
+
297
+ for (const slug of baseFeatures.keys()) {
298
+ if (!mergedFeatures.has(slug)) {
299
+ overlay.removed.features.push(slug);
300
+ }
301
+ }
302
+
303
+ return overlay;
304
+ }
305
+
260
306
  // diffPages compares the merged after-state against the base and
261
307
  // classifies which HTML pages must be regenerated (modified) versus
262
308
  // emitted fresh (added) versus listed in _removed.txt (removed).
@@ -361,19 +407,52 @@ function appendHistory(atlasDir, entry) {
361
407
  }
362
408
 
363
409
  function writeUndoSnapshot(atlasDir, state) {
364
- fs.mkdirSync(atlasDir, { recursive: true });
365
- fs.writeFileSync(path.join(atlasDir, UNDO_FILE), JSON.stringify(state, null, 2), 'utf8');
410
+ const stack = readUndoStack(atlasDir);
411
+ stack.push(state);
412
+ writeUndoStack(atlasDir, stack);
366
413
  }
367
414
 
368
415
  function readUndoSnapshot(atlasDir) {
369
- const file = path.join(atlasDir, UNDO_FILE);
370
- if (!fs.existsSync(file)) return null;
371
- return JSON.parse(fs.readFileSync(file, 'utf8'));
416
+ const stack = readUndoStack(atlasDir);
417
+ return stack.length > 0 ? stack[stack.length - 1] : null;
372
418
  }
373
419
 
374
420
  function clearUndoSnapshot(atlasDir) {
375
- const file = path.join(atlasDir, UNDO_FILE);
376
- if (fs.existsSync(file)) fs.rmSync(file);
421
+ writeUndoStack(atlasDir, []);
422
+ }
423
+
424
+ function consumeUndoSnapshot(atlasDir, steps = 1) {
425
+ if (!Number.isInteger(steps) || steps < 1) return null;
426
+ const stack = readUndoStack(atlasDir);
427
+ if (stack.length < steps) return null;
428
+ const snapshot = stack[stack.length - steps];
429
+ writeUndoStack(atlasDir, stack.slice(0, stack.length - steps));
430
+ return snapshot;
431
+ }
432
+
433
+ function readUndoStack(atlasDir) {
434
+ const stackFile = path.join(atlasDir, UNDO_STACK_FILE);
435
+ if (fs.existsSync(stackFile)) {
436
+ return JSON.parse(fs.readFileSync(stackFile, 'utf8'));
437
+ }
438
+ const latestFile = path.join(atlasDir, UNDO_FILE);
439
+ if (fs.existsSync(latestFile)) {
440
+ return [JSON.parse(fs.readFileSync(latestFile, 'utf8'))];
441
+ }
442
+ return [];
443
+ }
444
+
445
+ function writeUndoStack(atlasDir, stack) {
446
+ const stackFile = path.join(atlasDir, UNDO_STACK_FILE);
447
+ const latestFile = path.join(atlasDir, UNDO_FILE);
448
+ if (!stack || stack.length === 0) {
449
+ if (fs.existsSync(stackFile)) fs.rmSync(stackFile);
450
+ if (fs.existsSync(latestFile)) fs.rmSync(latestFile);
451
+ return;
452
+ }
453
+ fs.mkdirSync(atlasDir, { recursive: true });
454
+ fs.writeFileSync(stackFile, JSON.stringify(stack, null, 2), 'utf8');
455
+ fs.writeFileSync(latestFile, JSON.stringify(stack[stack.length - 1], null, 2), 'utf8');
377
456
  }
378
457
 
379
458
  module.exports = {
@@ -383,6 +462,7 @@ module.exports = {
383
462
  FEATURES_DIR,
384
463
  HISTORY_FILE,
385
464
  UNDO_FILE,
465
+ UNDO_STACK_FILE,
386
466
  readYaml,
387
467
  writeYaml,
388
468
  load,
@@ -390,6 +470,7 @@ module.exports = {
390
470
  loadOverlay,
391
471
  saveOverlay,
392
472
  mergeOverlay,
473
+ deriveOverlay,
393
474
  diffPages,
394
475
  normalizeFeature,
395
476
  normalizeSubmodule,
@@ -397,6 +478,7 @@ module.exports = {
397
478
  writeUndoSnapshot,
398
479
  readUndoSnapshot,
399
480
  clearUndoSnapshot,
481
+ consumeUndoSnapshot,
400
482
  macroVisualOf,
401
483
  featureVisualOf,
402
484
  };
@@ -43,12 +43,12 @@ Usage:
43
43
  Manage component rows and edges
44
44
  apltk architecture meta set Update meta.title / meta.summary
45
45
  apltk architecture actor add|remove Manage top-level actors
46
- apltk architecture undo Revert the most recent mutation
46
+ apltk architecture undo [--steps <n>] Revert the most recent mutation or roll back multiple steps
47
47
  apltk architecture --help Show this help
48
48
 
49
49
  Global flags:
50
50
  --project <root> Project root (default: nearest ancestor with resources/project-architecture/, else cwd); missing layout dirs are created when needed
51
- --spec <spec_dir> Mutations write to <spec_dir>/architecture_diff/atlas/
51
+ --spec <spec_dir> Single specs write locally; batch member paths write to the coordination.md root architecture_diff/atlas/
52
52
  --no-render Skip auto-render after a mutation
53
53
  --no-open For open/diff: skip launching the browser
54
54
  --out <dir> For diff: override viewer output directory
@@ -255,21 +255,14 @@ function runDiff(opts, io) {
255
255
  if (!projectRoot) {
256
256
  projectRoot = process.cwd();
257
257
  }
258
- fs.mkdirSync(path.join(projectRoot, RESOURCES_REL), { recursive: true });
259
- const outDir = opts.out || path.join(projectRoot, DEFAULT_OUT_REL);
260
- fs.mkdirSync(outDir, { recursive: true });
261
-
262
- const changes = collectChanges(projectRoot);
263
- const html = renderViewer({ changes, projectRoot, outDir });
264
- const indexPath = path.join(outDir, 'index.html');
265
- fs.writeFileSync(indexPath, html, 'utf8');
266
-
267
- io.stdout.write(`${indexPath}\n`);
268
- io.stdout.write(
269
- `Diff pages: ${changes.length} (modified=${changes.filter((c) => c.kind === 'modified').length}, added=${changes.filter((c) => c.kind === 'added').length}, removed=${changes.filter((c) => c.kind === 'removed').length})\n`,
270
- );
271
- if (opts.open) openInBrowser(indexPath);
272
- return 0;
258
+ const bootstrap = path.join(__dirname, 'architecture-bootstrap-render.js');
259
+ const args = [bootstrap, 'diff', '--project', projectRoot];
260
+ if (opts.out) args.push('--out', opts.out);
261
+ if (!opts.open) args.push('--no-open');
262
+ const result = spawnSync(process.execPath, args, { encoding: 'utf8' });
263
+ if (result.stdout) io.stdout.write(result.stdout);
264
+ if (result.stderr) io.stderr.write(result.stderr);
265
+ return result.status || 0;
273
266
  }
274
267
 
275
268
  // main(argv, io) is sync and supports the legacy verbs `open` and
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: optimise-skill
3
+ description: Use prompt engineering to optimise agent skill. Use it when you need to optimise a `SKILL.md`
4
+ ---
5
+
6
+ ## 目標
7
+ 在不影響被優化技能整體流程前提下,利用提示詞工程,對技能進行優化,減少冗余表達,增強agent對技能的理解能力以及實際工作中的使用效率。
8
+
9
+ ## 驗收條件
10
+ - 被優化技能的最終交付產物不受影響
11
+ - 被優化技能有顯著冗余削減,且具備精確提示能力,盡最大可能釋放LLM的性能
12
+
13
+ ## 工作流程
14
+
15
+ 1. 識別交付物
16
+ 完整閱讀整個技能的`SKILL.md`文檔,以及其引用的所有額檔案,包括但不限於`references/`, `scripts/`。基於獲取到的技能上下文資訊,總結出該技能的最終交付產物。
17
+
18
+ 2. 制定驗收條件
19
+ 制定驗收條件,從而確保LLM能夠穩定、可復現地按照驗收條件產出符合要求高質量最終交付物
20
+
21
+ 3. 重寫技能
22
+ 將整個技能的`SKILL.md`重寫。重寫後的技能應該只包含以下幾個關鍵組成部分:
23
+ - 技能目標
24
+ - 技能驗收條件
25
+ - 技能工作流程
26
+ - 技能使用範例
27
+ - 技能參考資料索引
28
+ 對於技能有幫助的內但不符合上述技能架構規範,則應該被分類放置在`references/`下的markdown檔案。
29
+
30
+ ## 範例
31
+ - "一個專注在提示LLM agent進行自我迭代,並為代碼庫帶來性能優化的技能需要被優化"-> "定義驗收條件為優化後性能相較優化前至少提升X個百分比,並且項目之中不存在任何O(n^2)級別時間複雜度的函式和邏輯,並按照標準架構重寫技能。"
32
+ - "一個有大量前端開發範例被包含在`SKILL.md`之中的前端開發技能需要被優化" -> "定義驗收條件為前端頁面`reference`之中提供的大量建議範例重寫且不包含任何建議示例之中明確拒絕的設計,然後按照技能優化流程對技能進行全面的重寫。"
33
+
34
+ ## 參考資料
35
+ - `references/example_skill.md` - 優化後的技能範例
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Optimise Skill"
3
+ short_description: "Rewrite a SKILL.md into a leaner, higher-signal prompt"
4
+ default_prompt: "Use $optimise-skill to read the target skill and its supporting files, derive the intended deliverable plus acceptance criteria, then rewrite the skill into a tighter goal / acceptance criteria / workflow / examples / references structure without changing the final outcome."
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: optimise-skill
3
+ description: Use prompt engineering to optimise agent skill. Use it when you need to optimise a `SKILL.md`
4
+ ---
5
+
6
+ ## 目標
7
+ 在不影響被優化技能整體流程前提下,利用提示詞工程,對技能進行優化,減少冗余表達,增強agent對技能的理解能力以及實際工作中的使用效率。
8
+
9
+ ## 驗收條件
10
+ - 被優化技能的最終交付產物不受影響
11
+ - 被優化技能有顯著冗余削減,且具備精確提示能力,盡最大可能釋放LLM的性能
12
+
13
+ ## 工作流程
14
+
15
+ 1. 識別交付物
16
+ 完整閱讀整個技能的`SKILL.md`文檔,以及其引用的所有額檔案,包括但不限於`references/`, `scripts/`。基於獲取到的技能上下文資訊,總結出該技能的最終交付產物。
17
+
18
+ 2. 制定驗收條件
19
+ 制定驗收條件,從而確保LLM能夠穩定、可復現地按照驗收條件產出符合要求高質量最終交付物
20
+
21
+ 3. 重寫技能
22
+ 將整個技能的`SKILL.md`重寫。重寫後的技能應該只包含以下幾個關鍵組成部分:
23
+ - 技能目標
24
+ - 技能驗收條件
25
+ - 技能工作流程
26
+ - 技能使用範例
27
+ - 技能參考資料索引
28
+ 對於技能有幫助的內但不符合上述技能架構規範,則應該被分類放置在`references/`下的markdown檔案。
29
+
30
+ ## 範例
31
+ - "一個專注在提示LLM agent進行自我迭代,並為代碼庫帶來性能優化的技能需要被優化"-> "定義驗收條件為優化後性能相較優化前至少提升X個百分比,並且項目之中不存在任何O(n^2)級別時間複雜度的函式和邏輯,並按照標準架構重寫技能。"
32
+ - "一個有大量前端開發範例被包含在`SKILL.md`之中的前端開發技能需要被優化" -> "定義驗收條件為前端頁面`reference`之中提供的大量建議範例重寫且不包含任何建議示例之中明確拒絕的設計,然後按照技能優化流程對技能進行全面的重寫。"
33
+
34
+ ## 參考資料
35
+ - `references/example_skill.md` - 優化後的技能範例
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laitszkin/apollo-toolkit",
3
- "version": "3.11.5",
3
+ "version": "3.11.7",
4
4
  "description": "Apollo Toolkit npm installer for managed skill copying across Codex, OpenClaw, and Trae.",
5
5
  "license": "MIT",
6
6
  "author": "LaiTszKin",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: spec-to-project-html
3
3
  description: >-
4
- Sync the project HTML architecture atlas to active planning specs by driving `apltk architecture --spec <spec_dir>`. The CLI writes overlay YAML under `<spec_dir>/architecture_diff/atlas/` and re-renders only the affected proposed-after HTML pages — macro SVG and per-sub-module internal-dataflow diagrams stay zoomable like the base atlas — so `apltk architecture diff` pairs before/after by path under `docs/plans/**/architecture_diff/`. Preserve the two-layer rule and the responsibility split: **subagents only** — each subagent reads ONE affected feature and declares EVERY intra-feature overlay change; the main agent **waits until all subagents finish**, then aggregates outbound summaries and declares **only** cross-feature edges. **Exact CLI spelling:** always `apltk architecture --help`. Ground every declaration in repo evidence; mark `TBD` when code is missing.
4
+ Sync the project HTML architecture atlas to active planning specs by driving `apltk architecture --spec <spec_dir>`. Single specs write overlay YAML under `<spec_dir>/architecture_diff/atlas/`; batch member paths resolve to the shared batch-root `architecture_diff/` beside `coordination.md`. The CLI re-renders only the affected proposed-after HTML pages — macro SVG and per-sub-module internal-dataflow diagrams stay zoomable like the base atlas — so `apltk architecture diff` pairs before/after by path under `docs/plans/**/architecture_diff/`. Preserve the two-layer rule and the responsibility split: **subagents only** — each subagent reads ONE affected feature and declares EVERY intra-feature overlay change; the main agent **waits until all subagents finish**, then aggregates outbound summaries and declares **only** cross-feature edges. **Exact CLI spelling:** always `apltk architecture --help`. Ground every declaration in repo evidence; mark `TBD` when code is missing.
5
5
  ---
6
6
 
7
7
  # Spec To Project HTML
@@ -17,7 +17,7 @@ description: >-
17
17
  ## Non-negotiables
18
18
 
19
19
  - **MUST** read specs in order unless the user directs otherwise: `spec.md` → `design.md` → `contract.md` → coordination notes.
20
- - **MUST** declare every change through the CLI with `--spec <spec_dir>` (exact verbs/flags: **`apltk architecture --help`**). The CLI writes overlay YAML to `<spec_dir>/architecture_diff/atlas/` and re-renders only the affected proposed-after HTML pages there. **MUST NOT** hand-edit `architecture_diff/**/*.html` — the renderer owns those files.
20
+ - **MUST** declare every change through the CLI with `--spec <spec_dir>` (exact verbs/flags: **`apltk architecture --help`**). Single specs write overlay YAML to `<spec_dir>/architecture_diff/atlas/` and re-render only the affected proposed-after HTML pages there. Batch member paths resolve to the shared batch-root `architecture_diff/` beside `coordination.md`, so the whole batch keeps one overlay and one rendered diff. **MUST NOT** hand-edit `architecture_diff/**/*.html` — the renderer owns those files.
21
21
  - **MUST** obey the semantic rules from `init-project-html/SKILL.md`:
22
22
  - Sub-module pages stay self-only — express cross-boundary interactions via **edges** (cross-feature or intra-feature), never as sub-module page prose.
23
23
  - Feature pages stay lightweight — cross-sub-module choreography belongs in **edges**, not in `dataflow` prose that pretends to cross features.
@@ -38,7 +38,7 @@ description: >-
38
38
  - **Evidence**: cite the spec passage (design subsystem entry) alongside the code path; record the citation in `meta.summary` or in sub-module purposes when relevant.
39
39
  - **Execution**: locate the plan set → list affected feature modules → subagent fan-out → **all complete** → cross-feature edges → `render --spec` if batched → `validate --spec` → `diff` check → handover.
40
40
  - **Quality**: macro overlay still shows every cross-feature data-row the spec requires; sub-module declarations stay self-only; `apltk architecture diff` opens cleanly with no orphan pages; no dangling edges.
41
- - **Output**: touches only `<spec_dir>/architecture_diff/atlas/**` (overlay state) and `<spec_dir>/architecture_diff/**/*.html` (renderer output). Base `resources/project-architecture/` is **NEVER** mutated.
41
+ - **Output**: single specs touch only `<spec_dir>/architecture_diff/atlas/**` (overlay state) and `<spec_dir>/architecture_diff/**/*.html` (renderer output); batch specs write the same artifacts under the batch root beside `coordination.md`. Base `resources/project-architecture/` is **NEVER** mutated.
42
42
 
43
43
  ## Acceptance criteria (mirrors `init-project-html`)
44
44
 
@@ -92,7 +92,7 @@ List overlay files touched (or verb families used), the diff counts (`modified`
92
92
 
93
93
  ## Sample hints
94
94
 
95
- - **Batch merge**: each member spec’s `--spec <member_dir>` writes its own overlay; `diff` lists each spec’s section in the paginated viewer.
95
+ - **Batch merge**: each member spec still calls `--spec <member_dir>`, but the CLI resolves writes to the shared batch-root overlay beside `coordination.md`; `diff` shows the batch as one combined architecture delta.
96
96
  - **Spec shrinks scope**: prefer `submodule set --role "deprecated: ..."` before `submodule remove` so reviewers see intent.
97
97
  - **Design-only change**: still review edges — order shifts surface as edge mutations; `validate --spec` catches dangling references.
98
98
 
@@ -3,7 +3,7 @@ interface:
3
3
  short_description: "Sync the project HTML architecture atlas with active planning specs via `apltk architecture --spec`"
4
4
  default_prompt: >-
5
5
  Use $spec-to-project-html. Read docs/plans (spec → design → contract). Every overlay mutation: `apltk architecture --spec <spec_dir> …`. **Exact commands: `apltk architecture --help`** — do not trust long command lists in docs.
6
- CLI writes `<spec_dir>/architecture_diff/atlas/*.yaml` + affected HTML under `<spec_dir>/architecture_diff/`. Base `resources/project-architecture/` is read-only here. NEVER hand-edit `architecture_diff/**`.
6
+ Single specs write `<spec_dir>/architecture_diff/atlas/*.yaml` + affected HTML under `<spec_dir>/architecture_diff/`. Batch member paths resolve to the shared batch-root `architecture_diff/` beside `coordination.md`, so one batch keeps one overlay + one rendered diff. Base `resources/project-architecture/` is read-only here. NEVER hand-edit `architecture_diff/**`.
7
7
  **`apltk architecture diff`** builds a paginated viewer: scans all `docs/plans/**/architecture_diff/`, pairs overlay paths with base atlas HTML by relative path, labels modified/added/removed — use it to verify the architecture delta before hand-off.
8
8
  **Subagents only:** dispatch ONE write-capable subagent per AFFECTED feature; each applies ALL intra-feature overlay mutations (`submodule`/`function`/`variable`/`dataflow`/`error`/`edge` within that feature). Each returns ONLY change summary + outbound boundary deltas for OTHER features.
9
9
  **HARD GATE:** main agent MUST wait until ALL subagents finish before ANY cross-feature `edge add|remove`, overlay `meta` that stitches multiple features, or shared `actor` for cross-feature context. Then `render --spec`, `validate --spec`.