axstack 0.22.0 → 0.24.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/README.md +4 -0
- package/bin/axstack.js +18 -3
- package/docs/installation.md +36 -5
- package/docs/workflows.md +7 -0
- package/package.json +1 -1
- package/skills/axstack/references/candidate-publication.md +11 -1
- package/skills/axstack/references/contracts.md +4 -2
- package/skills/axstack/references/diligence.md +10 -1
- package/skills/axstack/references/evidence-archive.md +1 -1
- package/skills/axstack/references/lifecycle.md +3 -1
- package/skills/axstack/references/performance-checklist.md +22 -0
- package/skills/axstack/references/routing.md +2 -0
- package/skills/axstack/references/run-record.md +15 -0
- package/skills/axstack/references/ste-writing.md +22 -0
- package/skills/axstack/references/ui-verification.md +6 -0
- package/skills/axstack/references/workspace-hygiene.md +1 -1
- package/skills/axstack-audit/SKILL.md +9 -2
- package/skills/axstack-audit/references/record.md +1 -1
- package/skills/axstack-correct/SKILL.md +41 -0
- package/skills/axstack-debug/SKILL.md +2 -0
- package/skills/axstack-diagram/SKILL.md +25 -0
- package/skills/axstack-diagram/references/archify.md +76 -0
- package/skills/axstack-diagram/references/fidelity.md +25 -0
- package/skills/axstack-explain/SKILL.md +15 -4
- package/skills/axstack-explain/references/visual-qa.md +3 -0
- package/skills/axstack-implement/SKILL.md +5 -0
- package/skills/axstack-improve/SKILL.md +2 -0
- package/skills/axstack-review/SKILL.md +2 -0
- package/skills/axstack-spec/SKILL.md +6 -0
- package/src/archify-pin.js +5 -0
- package/src/archify.js +149 -0
- package/src/capabilities.js +30 -0
- package/src/installer.js +82 -0
- package/src/manifest.js +9 -0
package/README.md
CHANGED
|
@@ -23,11 +23,15 @@ coordination. You can start at the phase you need.
|
|
|
23
23
|
| Verify | [axstack-review](skills/axstack-review/SKILL.md) | Review a PR or bounded codebase at an exact revision. |
|
|
24
24
|
| Verify | [axstack-improve](skills/axstack-improve/SKILL.md) | Find evidenced codebase improvements without editing code. |
|
|
25
25
|
| Verify | [axstack-audit](skills/axstack-audit/SKILL.md) | Measure a run's outcomes and evidence gaps. |
|
|
26
|
+
| Verify | [axstack-correct](skills/axstack-correct/SKILL.md) | Report repeated mistakes and propose stronger checks when invoked by the user. |
|
|
26
27
|
| Operate | [axstack-watch](skills/axstack-watch/SKILL.md) | Observe or maintain an existing PR within its authority. |
|
|
27
28
|
| Operate | [axstack-cleanup](skills/axstack-cleanup/SKILL.md) | Retire eligible completed agent resources. |
|
|
28
29
|
| Operate | [axstack-relay](skills/axstack-relay/SKILL.md) | Send an explicit message or authorized notification. |
|
|
29
30
|
| Understand | [axstack-research](skills/axstack-research/SKILL.md) | Answer one bounded question with sources. |
|
|
30
31
|
| Understand | [axstack-explain](skills/axstack-explain/SKILL.md) | Explain a system and separate known behavior from gaps. |
|
|
32
|
+
| Understand | [axstack-diagram](skills/axstack-diagram/SKILL.md) | Draw Mermaid diagrams or verified interactive archify viewers. |
|
|
33
|
+
|
|
34
|
+
Interactive viewers use [archify](https://github.com/tt-a1i/archify) (MIT).
|
|
31
35
|
|
|
32
36
|
Small, bounded changes can begin with your request or an existing issue;
|
|
33
37
|
substantial work needs an approved spec and matching tickets before
|
package/bin/axstack.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
uninstallBundle,
|
|
14
14
|
validateBundle,
|
|
15
15
|
} from '../src/installer.js';
|
|
16
|
-
import { BUN_FLOOR, checkCapabilities, meetsFloor, runRealCheck } from '../src/capabilities.js';
|
|
16
|
+
import { BUN_FLOOR, checkArchify, checkCapabilities, meetsFloor, runRealCheck } from '../src/capabilities.js';
|
|
17
17
|
import { findLegacyRoutingLines } from '../src/instructions.js';
|
|
18
18
|
import { harnessLocations } from '../src/locations.js';
|
|
19
19
|
|
|
@@ -43,7 +43,7 @@ function packageVersion() {
|
|
|
43
43
|
const HELP = `axstack — Axstack setup CLI (installation bookkeeping only)
|
|
44
44
|
|
|
45
45
|
Usage:
|
|
46
|
-
axstack install --preset <mixed|codex-only|claude-only> --bundle <dir> --skills-dir <dir> [--instructions <file>] [--claude-settings <file>|--no-claude-settings] [--harness <name>] [--force] [--yes]
|
|
46
|
+
axstack install --preset <mixed|codex-only|claude-only> --bundle <dir> --skills-dir <dir> [--tools-dir <dir>] [--instructions <file>] [--claude-settings <file>|--no-claude-settings] [--harness <name>] [--force] [--yes]
|
|
47
47
|
axstack check [--bundle <dir>] [--instructions <file>] [--skills-dir <dir>|--harness <name>]
|
|
48
48
|
axstack uninstall --skills-dir <dir> [--instructions <file>] [--claude-settings <file>|--no-claude-settings] [--harness <name>] [--force] [--yes]
|
|
49
49
|
axstack --help | --version
|
|
@@ -62,6 +62,8 @@ Flags:
|
|
|
62
62
|
Aliases: codex = codex-only; claude = claude-only.
|
|
63
63
|
--skills-dir <dir> Explicit install target. Overrides harness skill defaults
|
|
64
64
|
and automatic legacy Codex skill retirement.
|
|
65
|
+
--tools-dir <dir> Pinned archify copies. Defaults to ~/.axstack/tools.
|
|
66
|
+
Must be outside skills roots and contain no symlink path.
|
|
65
67
|
--instructions <file>
|
|
66
68
|
Instruction file to receive the owned routing block.
|
|
67
69
|
Harness defaults: ~/.claude/CLAUDE.md for Claude,
|
|
@@ -105,7 +107,7 @@ function parseArgs(argv) {
|
|
|
105
107
|
}
|
|
106
108
|
out.command = rest.shift();
|
|
107
109
|
const wantsValue = new Set([
|
|
108
|
-
'--bundle', '--skills-dir', '--instructions', '--harness', '--preset', '--claude-settings',
|
|
110
|
+
'--bundle', '--skills-dir', '--tools-dir', '--instructions', '--harness', '--preset', '--claude-settings',
|
|
109
111
|
]);
|
|
110
112
|
while (rest.length > 0) {
|
|
111
113
|
const tok = rest.shift();
|
|
@@ -300,6 +302,7 @@ async function main() {
|
|
|
300
302
|
bundleDir: flags.bundle ? resolve(flags.bundle) : PACKAGE_ROOT,
|
|
301
303
|
skillsDir,
|
|
302
304
|
preset: flags.preset,
|
|
305
|
+
toolsDir: flags['tools-dir'] ? expandHome(flags['tools-dir']) : undefined,
|
|
303
306
|
instructionsPath,
|
|
304
307
|
inheritedInstructions: legacyCodexManifest?.instructions,
|
|
305
308
|
force: !!flags.force,
|
|
@@ -399,6 +402,18 @@ async function main() {
|
|
|
399
402
|
}
|
|
400
403
|
if (command === 'check') {
|
|
401
404
|
const report = await checkCapabilities(runRealCheck);
|
|
405
|
+
const toolSkillsDir = flags['skills-dir'] ? expandHome(flags['skills-dir']) :
|
|
406
|
+
flags.harness ? resolveHarnessTarget(flags.harness) : null;
|
|
407
|
+
if (toolSkillsDir) {
|
|
408
|
+
const tool = await checkArchify(toolSkillsDir);
|
|
409
|
+
console.log(`archify record: ${tool.recordFile}`);
|
|
410
|
+
if (tool.record) console.log(`archify copy: ${tool.record.path}; recorded SHA: ${tool.record.sha}; checked-out SHA: ${tool.actual ?? 'unavailable'}`);
|
|
411
|
+
if (tool.reason) {
|
|
412
|
+
console.log(`archify: ${tool.reason}`);
|
|
413
|
+
report.gaps.push(`archify: ${tool.reason}`);
|
|
414
|
+
}
|
|
415
|
+
console.log(tool.chrome ? `Chrome: ${tool.chrome}` : 'WARN Chrome: unavailable (viewer verification needs Chrome)');
|
|
416
|
+
}
|
|
402
417
|
if (flags.bundle) {
|
|
403
418
|
const bundle = await validateBundle(resolve(flags.bundle));
|
|
404
419
|
const presetNames = Object.keys(bundle.presets);
|
package/docs/installation.md
CHANGED
|
@@ -8,8 +8,10 @@ workflow database.
|
|
|
8
8
|
Requirements: Bun >=1.3.14, Git, `gh`, the `gh stack` extension, and a running
|
|
9
9
|
T3 Code `0.0.46-nightly.20261003.2610` or newer. The driver is a T3 thread
|
|
10
10
|
with the `t3-code` MCP. See the [T3 runtime boundary](../skills/axstack/references/t3-runtime.md).
|
|
11
|
-
|
|
12
|
-
and `node:fs/promises
|
|
11
|
+
Axstack has no runtime package dependencies. Filesystem access uses Bun-backed
|
|
12
|
+
`node:fs` and `node:fs/promises`. The pinned archify tool is the sole external-tool
|
|
13
|
+
exception: installation uses Git and the network, and Bun runs its Node-oriented
|
|
14
|
+
code. This exception introduces no Node.js runtime requirement.
|
|
13
15
|
|
|
14
16
|
## T3 setup
|
|
15
17
|
|
|
@@ -45,7 +47,7 @@ and its configured home channel under recorded notification authority.
|
|
|
45
47
|
### Install
|
|
46
48
|
|
|
47
49
|
```text
|
|
48
|
-
axstack install --preset <mixed|codex-only|claude-only> --bundle <dir> --skills-dir <dir> [--instructions <file>] [--claude-settings <file>|--no-claude-settings] [--harness <name>] [--force] [--yes]
|
|
50
|
+
axstack install --preset <mixed|codex-only|claude-only> --bundle <dir> --skills-dir <dir> [--tools-dir <dir>] [--instructions <file>] [--claude-settings <file>|--no-claude-settings] [--harness <name>] [--force] [--yes]
|
|
49
51
|
```
|
|
50
52
|
|
|
51
53
|
- `--preset` is required. `codex` and `claude` are aliases for the canonical
|
|
@@ -56,6 +58,9 @@ axstack install --preset <mixed|codex-only|claude-only> --bundle <dir> --skills-
|
|
|
56
58
|
- `--skills-dir` is required unless a verified harness default resolves it.
|
|
57
59
|
Codex defaults to the shared `~/.agents/skills` root; an explicit override
|
|
58
60
|
remains authoritative and disables automatic legacy Codex-root retirement.
|
|
61
|
+
- `--tools-dir` defaults to `~/.axstack/tools`. It must not overlap a skills
|
|
62
|
+
root or pass through a symlink. Home writes require `--yes`, including the
|
|
63
|
+
default tools directory.
|
|
59
64
|
- `--instructions` selects the instruction file that receives Axstack's owned
|
|
60
65
|
marker block. `--harness claude` defaults to `~/.claude/CLAUDE.md`;
|
|
61
66
|
`--harness codex` defaults to `$CODEX_HOME/AGENTS.md` or `~/.codex/AGENTS.md`;
|
|
@@ -90,6 +95,16 @@ the settings sidecar preserves the value while any other install still owns it.
|
|
|
90
95
|
- `--yes` confirms writes under the user's home directory. Tests use temporary
|
|
91
96
|
homes and fixtures only.
|
|
92
97
|
|
|
98
|
+
Installation sparse-clones archify at the reviewed full SHA in `src/archify-pin.js`
|
|
99
|
+
into `<tools-dir>/archify-<sha>`. It verifies HEAD before use and keeps the
|
|
100
|
+
payload's licence and third-party notices. The manifest owns
|
|
101
|
+
`<skills-dir>/axstack-diagram/archify.json`, which records `{path, sha}`.
|
|
102
|
+
The adjacent `<tools-dir>/archify-<sha>.owners.json` lists its skills-root owners.
|
|
103
|
+
Multiple roots share one copy. Repeat installs leave unchanged files untouched.
|
|
104
|
+
An offline, Git-less, or failed clone still installs the skills, reports
|
|
105
|
+
`archify: unavailable (<reason>)`, and exits 0. An existing SHA mismatch fails.
|
|
106
|
+
Tests use a local repository through `AXSTACK_ARCHIFY_REPO` and never use the network.
|
|
107
|
+
|
|
93
108
|
## Role presets
|
|
94
109
|
|
|
95
110
|
The selected bundle input is one of:
|
|
@@ -121,6 +136,13 @@ owned, missing, unowned, edited, or bound to a different path. Hand-written
|
|
|
121
136
|
legacy routing outside the owned block is reported for manual migration and
|
|
122
137
|
preserved byte for byte.
|
|
123
138
|
|
|
139
|
+
With `--skills-dir` or `--harness`, check reports the archify record path, copy
|
|
140
|
+
path, recorded SHA, and checked-out SHA. A missing record, missing copy, or SHA
|
|
141
|
+
mismatch fails the check. Chrome absence prints a warning and does not fail it.
|
|
142
|
+
`ARCHIFY_CHROME` selects the Chrome executable. Without it, check looks for
|
|
143
|
+
common Chrome and Chromium executables on PATH. A check without a skills target
|
|
144
|
+
probes host capabilities only.
|
|
145
|
+
|
|
124
146
|
A successful check is not provider/model availability, effective permission,
|
|
125
147
|
skill reload, task execution, mobile delivery, or end-to-end compatibility
|
|
126
148
|
proof. Those require their own runtime receipts.
|
|
@@ -135,6 +157,15 @@ Uninstall removes only unchanged Axstack-owned files whose current bytes match
|
|
|
135
157
|
the manifest. Edited, custom, unknown, and unrelated files survive. Directories
|
|
136
158
|
are pruned only when empty, and the target root is never removed.
|
|
137
159
|
|
|
160
|
+
Uninstall drops that skills root from archify's owners file. It removes the copy
|
|
161
|
+
only when no owners remain, HEAD matches the recorded SHA, and
|
|
162
|
+
`git status --porcelain --ignored` is empty. Otherwise it keeps the copy and
|
|
163
|
+
reports why, even with `--force`. A pin bump or a changed `--tools-dir` releases
|
|
164
|
+
the old copy through the same guard. Installation clones into a temporary sibling
|
|
165
|
+
and renames it into place. A later owner or manifest write failure restores
|
|
166
|
+
ownership and removes only a copy created by that installation. Older manifests
|
|
167
|
+
without an archify record still load.
|
|
168
|
+
|
|
138
169
|
## Owned instruction block
|
|
139
170
|
|
|
140
171
|
The deterministic `<!-- axstack:begin v1 -->` / `<!-- axstack:end -->` block
|
|
@@ -323,8 +354,8 @@ need separate authority and verified backups.
|
|
|
323
354
|
## Examples
|
|
324
355
|
|
|
325
356
|
```sh
|
|
326
|
-
axstack install --preset mixed --bundle ./bundle --skills-dir /tmp/ax-skills --instructions /tmp/AGENTS.md
|
|
327
|
-
axstack install --preset mixed --bundle ./bundle --skills-dir /tmp/ax-skills --instructions /tmp/AGENTS.md
|
|
357
|
+
axstack install --preset mixed --bundle ./bundle --skills-dir /tmp/ax-skills --tools-dir /tmp/ax-tools --instructions /tmp/AGENTS.md
|
|
358
|
+
axstack install --preset mixed --bundle ./bundle --skills-dir /tmp/ax-skills --tools-dir /tmp/ax-tools --instructions /tmp/AGENTS.md
|
|
328
359
|
axstack check --bundle ./bundle --skills-dir /tmp/ax-skills --instructions /tmp/AGENTS.md
|
|
329
360
|
axstack uninstall --skills-dir /tmp/ax-skills --instructions /tmp/AGENTS.md
|
|
330
361
|
```
|
package/docs/workflows.md
CHANGED
|
@@ -18,8 +18,15 @@ and PR shape.
|
|
|
18
18
|
Direct routes need no spec ceremony:
|
|
19
19
|
|
|
20
20
|
- `axstack-research` answers one bounded source-backed question.
|
|
21
|
+
- `axstack-correct` reports repeated mistakes and proposes stronger checks.
|
|
22
|
+
Only the user invokes it.
|
|
21
23
|
- `axstack-explain` separates implemented, intended, tested, live, and unknown
|
|
22
24
|
behavior; complex visuals receive exact-artifact QA where applicable.
|
|
25
|
+
- [axstack-diagram](../skills/axstack-diagram/SKILL.md) selects Mermaid for chat,
|
|
26
|
+
GitHub, and docs, or an interactive viewer for required complex visuals.
|
|
27
|
+
Explain loads it for every diagram. Viewers use
|
|
28
|
+
[archify](https://github.com/tt-a1i/archify) (MIT) with a pinned tool,
|
|
29
|
+
a passing finalize receipt, rendered QA, and node-and-edge source review.
|
|
23
30
|
- `axstack-improve` returns a small ranked set of evidenced improvement
|
|
24
31
|
candidates without editing code. Its test-audit lens marks every declaration
|
|
25
32
|
in one owner boundary R/F/C/D, reports reviewed and eligible counts, and routes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "axstack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Axstack installer and setup CLI: installs owned chat skills and role data, configures supported harness settings, and checks T3 Code capabilities.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -17,7 +17,16 @@ protection; a mismatch holds publication. If the push outcome is ambiguous,
|
|
|
17
17
|
inspect remote state before retrying.
|
|
18
18
|
|
|
19
19
|
Before reviewer dispatch, read the remote ref back and confirm that it resolves
|
|
20
|
-
to the candidate SHA; also pin the current base.
|
|
20
|
+
to the candidate SHA; also pin the current base.
|
|
21
|
+
After every push, before post-push diligence or merge-ready, compare the PR body's
|
|
22
|
+
stated head SHA with `Confirmed remote SHA` and record `PR body head SHA: <sha or none>`.
|
|
23
|
+
Accept `none` for a PR body without a stated head SHA.
|
|
24
|
+
Accept a stated full head SHA only when it equals `Confirmed remote SHA`.
|
|
25
|
+
Accept a stated abbreviated head SHA only when it is a matching prefix of
|
|
26
|
+
`Confirmed remote SHA` with at least 7 hexadecimal characters.
|
|
27
|
+
Hold on a mismatched head SHA or an abbreviation shorter than 7 characters.
|
|
28
|
+
Leave other SHAs in the PR body unchanged.
|
|
29
|
+
Record:
|
|
21
30
|
|
|
22
31
|
```text
|
|
23
32
|
Candidate: <sha>
|
|
@@ -25,6 +34,7 @@ Base: <sha>
|
|
|
25
34
|
Remote ref: <branch>
|
|
26
35
|
Expected-old remote SHA: <sha | absent>
|
|
27
36
|
Confirmed remote SHA: <sha>
|
|
37
|
+
PR body head SHA: <sha or none>
|
|
28
38
|
PR: <url>
|
|
29
39
|
CI: <run ID or URL and triggered/pending/completed status>
|
|
30
40
|
```
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Apply these authority, scope, and model rules before consequential action.
|
|
4
4
|
|
|
5
|
+
For user-facing output, follow [STE-inspired writing](ste-writing.md).
|
|
6
|
+
|
|
5
7
|
## Required lifecycle load
|
|
6
8
|
|
|
7
|
-
Except for `axstack-audit` and `axstack-
|
|
8
|
-
must load and follow [Shared lifecycle](lifecycle.md) before acting. When a
|
|
9
|
+
Except for `axstack-audit`, `axstack-relay`, and `axstack-correct`, every
|
|
10
|
+
independently called phase must load and follow [Shared lifecycle](lifecycle.md) before acting. When a
|
|
9
11
|
substantive run ends or reaches a meaningful checkpoint, apply the lifecycle
|
|
10
12
|
audit hook. The audit phase loads these contracts, writes its assigned record,
|
|
11
13
|
and stops; it never audits itself.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Read the [T3 runtime boundary](t3-runtime.md) before dispatch.
|
|
4
4
|
Dispatch `axstack-diligence` with async `delegate_task` in the driver worktree,
|
|
5
5
|
using a pinned brief and private evidence paths.
|
|
6
|
-
It is read-only, never authors or edits, and returns `PASS` or `
|
|
6
|
+
It is read-only, never authors or edits, and returns `PASS`, `FINDINGS`, or `UNKNOWN`
|
|
7
7
|
with locations, observed evidence, and limits. A stale or missing receipt is
|
|
8
8
|
not a pass. Keep its first pass independent of other reviewers and workers.
|
|
9
9
|
|
|
@@ -21,5 +21,14 @@ publication, compare the author receipt with its evidence folder: red/green
|
|
|
21
21
|
logs exist, and counts, SHAs, and paths match. For release preparation, compare
|
|
22
22
|
the release PR body with the merged PRs.
|
|
23
23
|
|
|
24
|
+
Retain the full output of every full-suite run as a named log in the dispatch's
|
|
25
|
+
private evidence folder.
|
|
26
|
+
For an unattributed full-suite failure, report `UNKNOWN` and return the failure to the driver.
|
|
27
|
+
Never report `PASS` for an unattributed full-suite failure.
|
|
28
|
+
The driver records the disposition of each full-suite failure in the run record.
|
|
29
|
+
Report an attributed failure with its retained output normally.
|
|
30
|
+
An observed full-suite failure still fails the suite, including when attributed or reported `UNKNOWN`.
|
|
31
|
+
No reruns are required.
|
|
32
|
+
|
|
24
33
|
`FINDINGS` identifies a mismatch for the driver to resolve at the owning phase;
|
|
25
34
|
it does not edit the artifact or create another review round by itself.
|
|
@@ -118,6 +118,6 @@ After preservation and salvage checks, archive the exact eligible T3 thread
|
|
|
118
118
|
with `t3_thread_organize`, then remove its exact recorded checkout path using
|
|
119
119
|
`git worktree remove <path>` without force. Verify absence with
|
|
120
120
|
`git worktree list --porcelain`; retire only eligible local-only branches with
|
|
121
|
-
`git branch -d` under workspace hygiene. Never use shell recursive deletion or
|
|
121
|
+
`git branch -d` under workspace hygiene. Never use shell recursive deletion to remove a whole worktree or
|
|
122
122
|
treat archive success as ownership, settlement, liveness or cleanup proof.
|
|
123
123
|
Failure or uncertainty preserves the resource.
|
|
@@ -143,6 +143,8 @@ nothing without tested independent review.
|
|
|
143
143
|
|
|
144
144
|
## Close-out
|
|
145
145
|
|
|
146
|
+
Close-out requires the [close-out acceptance table](run-record.md#close-out-acceptance).
|
|
147
|
+
|
|
146
148
|
PRs merge by forge state; close out: (1) settle every T3 worker run and archive eligible threads; (2) compact record with counts and denominators—user
|
|
147
149
|
interventions/deviations from plan/repairs; (3) `axstack-auditor`: settle
|
|
148
150
|
non-zero/requested, else `counts zero`. A base auditor preflight rejection
|
|
@@ -153,7 +155,7 @@ use `axstack-cleanup`, remove the run's own scheduled tasks under
|
|
|
153
155
|
[Workspace hygiene](workspace-hygiene.md), and close selected external-tracker tickets;
|
|
154
156
|
(5) mark the
|
|
155
157
|
[Run record](run-record.md) `Archived`. `Archived`—one each:
|
|
156
|
-
settlement receipt; compact record path; auditor decision plus settlement
|
|
158
|
+
settlement receipt; compact record path; close-out acceptance table; auditor decision plus settlement
|
|
157
159
|
receipt, `counts zero`, or the unlaunchable UNKNOWN archive receipt; scheduled-task,
|
|
158
160
|
release, and ticket receipts; archive timestamp.
|
|
159
161
|
`active`/receipt-incomplete record: close-out pending, never done. One-step
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Performance checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist only for performance claims.
|
|
4
|
+
Keep workflow count accounting with axstack-audit.
|
|
5
|
+
A limiter is the resource or code path that bounds performance.
|
|
6
|
+
|
|
7
|
+
1. **Limiter: What bounds the result?**
|
|
8
|
+
Identify the resource or code path that limits the result from profiles or counters captured during a run.
|
|
9
|
+
2. **Tuning: Did each option use suitable settings?**
|
|
10
|
+
Check each option with comparable production settings, versions, and data.
|
|
11
|
+
3. **Limits: Is the result physically plausible?**
|
|
12
|
+
Compare the result with hardware limits and the changed code's share of total elapsed time.
|
|
13
|
+
4. **Errors: Did the run succeed?**
|
|
14
|
+
Verify successful work and correct outputs using error counts from the measurement run.
|
|
15
|
+
5. **Reproducibility: Does the difference repeat?**
|
|
16
|
+
Report the median and range from repeated, alternating runs of each option.
|
|
17
|
+
6. **Relevance: Does the result matter to users?**
|
|
18
|
+
Measure the end-to-end user path with realistic data and concurrency.
|
|
19
|
+
7. **Work happened: Did the timed work finish?**
|
|
20
|
+
Verify that the intended work completed inside the timed region.
|
|
21
|
+
|
|
22
|
+
Ideas paraphrased from [pstack benchmark-checklist](https://github.com/cursor/plugins/blob/e43c7ee26e0038c6c1fa8380dd34ce86ff94cb2a/pstack/skills/benchmark-checklist/SKILL.md) (MIT), using Brendan Gregg's seven benchmark questions.
|
|
@@ -65,6 +65,8 @@ step (3) for user routing: no substitution or same-provider review.
|
|
|
65
65
|
off a classified repair (explain: how; debug: what's wrong).
|
|
66
66
|
- Code quality/refactor discovery -> `axstack-improve`: rank bounded
|
|
67
67
|
candidates with evidence; report only, no source edits.
|
|
68
|
+
- Repeated mistakes need evidence and stronger checks -> `axstack-correct`:
|
|
69
|
+
user-invoked, report only.
|
|
68
70
|
- Accepted worker/task/run completion or bounded backlog request -> driver invokes
|
|
69
71
|
`axstack-cleanup` inline; never dispatch it.
|
|
70
72
|
- Preparation completion, watch expiry, resume, or reconciliation -> the
|
|
@@ -119,6 +119,21 @@ by the next owner:
|
|
|
119
119
|
evidence lives. Read it on resume before reconciling; append, never rewrite,
|
|
120
120
|
and keep entries as short as the evidence pointer allows.
|
|
121
121
|
|
|
122
|
+
## Close-out acceptance
|
|
123
|
+
|
|
124
|
+
Use one row per acceptance check, including each clause of a compound check.
|
|
125
|
+
Record in each row a passing evidence pointer or a user-accepted hold with its `Decisions` row.
|
|
126
|
+
If neither passing evidence nor a `Decisions` row with a user-accepted hold exists for an acceptance clause, hold close-out.
|
|
127
|
+
A recorded user-accepted hold in `Decisions` satisfies that clause for close-out; keep the unmet result explicit.
|
|
128
|
+
Record the reason for each driver-elected repair in `Decisions`.
|
|
129
|
+
|
|
130
|
+
```markdown
|
|
131
|
+
| Acceptance check | Result | Evidence or user-accepted hold | Repair election (Decisions row + reason, or none) |
|
|
132
|
+
| --- | --- | --- | --- |
|
|
133
|
+
| <check + clause> | passed | <SHA + check/log pointer> | <Decisions row + reason, or none> |
|
|
134
|
+
| <check + unmet clause> | held (user accepted) | <Decisions row + user acceptance receipt> | <Decisions row + reason, or none> |
|
|
135
|
+
```
|
|
136
|
+
|
|
122
137
|
## Privacy
|
|
123
138
|
|
|
124
139
|
Record concise IDs, SHAs, URLs, status, timestamps, next actions, and evidence
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# STE-inspired writing
|
|
2
|
+
|
|
3
|
+
STE means Simplified Technical English.
|
|
4
|
+
This reference borrows clarity rules from STE.
|
|
5
|
+
|
|
6
|
+
Follow these rules only for new or materially revised user-facing output.
|
|
7
|
+
User-facing output includes reports, PR bodies, briefs, read-backs, and packaged guidance.
|
|
8
|
+
Do not rewrite existing prose for style.
|
|
9
|
+
Keep exact identifiers unchanged.
|
|
10
|
+
Keep quotes unchanged.
|
|
11
|
+
Keep safety contracts unchanged.
|
|
12
|
+
Keep prompt-byte contracts unchanged.
|
|
13
|
+
|
|
14
|
+
Give each sentence one instruction.
|
|
15
|
+
Keep sentences short.
|
|
16
|
+
Write in active voice.
|
|
17
|
+
If a step has a condition, state the condition before the step.
|
|
18
|
+
Define technical terms before their first use.
|
|
19
|
+
Use one name consistently for each term.
|
|
20
|
+
Use words instead of slashes for "and" or "or".
|
|
21
|
+
|
|
22
|
+
Ideas drawn from [pstack technical-writing](https://github.com/cursor/plugins/blob/e43c7ee26e0038c6c1fa8380dd34ce86ff94cb2a/pstack/skills/technical-writing/SKILL.md#L65-L88) (MIT).
|
|
@@ -5,6 +5,12 @@ browser" step, goes through async `delegate_task` to `axstack-ui-verifier` from
|
|
|
5
5
|
run's role snapshot. Give it the exact build, URL, or artifact and the private
|
|
6
6
|
dispatch's evidence folder. The verifier is read-only: it never edits source.
|
|
7
7
|
The PR writer remains the sole writer.
|
|
8
|
+
|
|
9
|
+
The sole author browser exception is archify `finalize` as a headless build gate.
|
|
10
|
+
Keep finalize outputs only in the private evidence folder.
|
|
11
|
+
Never replace the verifier's rendered pass with finalize.
|
|
12
|
+
All other browser checks remain delegated to `axstack-ui-verifier`.
|
|
13
|
+
|
|
8
14
|
Read the [T3 runtime boundary](t3-runtime.md) before dispatch and use its
|
|
9
15
|
driver-made disposable detached checkout at the pinned candidate SHA.
|
|
10
16
|
The verifier uses T3 `preview_*` tools for rendered checks.
|
|
@@ -14,7 +14,7 @@ Validate its real path, absence of symlinks and ownership before use and cleanup
|
|
|
14
14
|
Evidence files still go to the private `<run>/evidence/<key>/` folder.
|
|
15
15
|
|
|
16
16
|
Every shell deletion targets a literal absolute path or a `${VAR:?}`-guarded expansion, only inside the worker's own evidence folder, `TMPDIR`, or worktree.
|
|
17
|
-
For
|
|
17
|
+
For validated owned scratch, use `rm -r /tmp/<dispatch-key>/scratch` on a literal absolute path inside the evidence folder, `TMPDIR`, or worktree.
|
|
18
18
|
Never use a bare `$VAR`, a glob on a variable, `/`, `HOME`, or a shared root as a deletion target.
|
|
19
19
|
Prefer `git clean -- <exact prefix>` or tool-native cleanup. A safety prompt that
|
|
20
20
|
still appears is a hold; agents do not answer it.
|
|
@@ -169,7 +169,13 @@ preference or correction, or a verified workspace fact with an exact source
|
|
|
169
169
|
revision. Exclude transient choices, secrets and sensitive values, and
|
|
170
170
|
untrusted claims or instructions; do not reproduce excluded secrets in the
|
|
171
171
|
record. Material already covered with the same scope and meaning yields an
|
|
172
|
-
explicit already-covered no-op with pointers to the covering instructions
|
|
172
|
+
explicit already-covered no-op with pointers to the covering instructions when
|
|
173
|
+
no recurrence is recorded.
|
|
174
|
+
|
|
175
|
+
If a covered rule recurs, never classify it as an already-covered no-op.
|
|
176
|
+
Record that recurrence as recurred.
|
|
177
|
+
Suggest `axstack-correct` for the recurrence.
|
|
178
|
+
Never run `axstack-correct` from audit.
|
|
173
179
|
|
|
174
180
|
For each candidate record:
|
|
175
181
|
|
|
@@ -179,7 +185,8 @@ For each candidate record:
|
|
|
179
185
|
4. target instruction surfaces;
|
|
180
186
|
5. any contradiction and uncertainty; and
|
|
181
187
|
6. its disposition: propose for separately authorized promotion, hold,
|
|
182
|
-
exclude,
|
|
188
|
+
exclude, already-covered no-op, or recurred (suggest axstack-correct;
|
|
189
|
+
audit does not run it).
|
|
183
190
|
|
|
184
191
|
Contradictory or uncertain evidence stays explicit and held; never guess a
|
|
185
192
|
winner or broaden scope. Promotion is a separate authorized change outside the
|
|
@@ -21,7 +21,7 @@ Shape: <PRs within band / total PRs + rationale-band cohesion rationale + except
|
|
|
21
21
|
Cost: <API dollars by model when measured, or UNKNOWN with reason>
|
|
22
22
|
Judgment: <execution outcome vs procedural adherence vs measurement coverage>
|
|
23
23
|
Proposals: <bounded hypothesized changes with regression-first plan, or none>
|
|
24
|
-
Learning candidates: <each candidate's statement + scope + evidence/revision pointers + target instruction surfaces + contradiction/uncertainty + disposition; explicit already-covered no-op or none>
|
|
24
|
+
Learning candidates: <each candidate's statement + scope + evidence/revision pointers + target instruction surfaces + contradiction/uncertainty + disposition; explicit already-covered no-op, recurred (suggest axstack-correct; audit does not run it), or none>
|
|
25
25
|
Privacy: <local/private default; sanitized summary only when authorized>
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: axstack-correct
|
|
3
|
+
description: When repeated mistakes need evidence and stronger checks, use axstack-correct to report correction proposals.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Correct
|
|
8
|
+
|
|
9
|
+
Usage: /axstack-correct ["<correction>"] [runs=<ids> | last=<N, default 10>]
|
|
10
|
+
|
|
11
|
+
Load [Standing contracts](../axstack/references/contracts.md) before acting.
|
|
12
|
+
|
|
13
|
+
Run only at the user's request.
|
|
14
|
+
Write a report only.
|
|
15
|
+
Never edit files.
|
|
16
|
+
Never dispatch workers.
|
|
17
|
+
Never merge.
|
|
18
|
+
Never read transcripts.
|
|
19
|
+
|
|
20
|
+
Read [run records](../axstack/references/run-record.md): Decisions, deviations, Learnings, FAILED receipts, audit proposals.
|
|
21
|
+
Read git history bounded by selected runs.
|
|
22
|
+
Read only PR reviews named in those records.
|
|
23
|
+
Record selected runs and git bounds.
|
|
24
|
+
Report inaccessible evidence.
|
|
25
|
+
|
|
26
|
+
A class needs at least two distinct incidents.
|
|
27
|
+
Each incident needs a distinct pointer: run id + attempt or file:line, SHA, or PR or review URL.
|
|
28
|
+
Count an incident echoed in several records once.
|
|
29
|
+
If a pointer is missing, report recurrence UNKNOWN and keep the class open.
|
|
30
|
+
|
|
31
|
+
Try fixes in this order: remove copied pattern > script or helper error naming the fix > brief or receipt template > bun test > prose.
|
|
32
|
+
Label each rule `shipped` or `runtime: advisory`.
|
|
33
|
+
Use `shipped` only when a check fails in CI on the mistake itself.
|
|
34
|
+
When only audit can observe a rule, label it `runtime: advisory`.
|
|
35
|
+
A prose-contract test alone leaves a rule `runtime: advisory`.
|
|
36
|
+
Propose fixes through a small-change intent and [axstack-implement](../axstack-implement/SKILL.md).
|
|
37
|
+
|
|
38
|
+
The `## Enforced rules` table in the target repo's AGENTS.md changes only in the PR that adds its check.
|
|
39
|
+
The first row's PR adds a test that fails when any row's enforcement path disappears.
|
|
40
|
+
If a `shipped` check missed a later recorded recurrence, report `enforcement failed`.
|
|
41
|
+
For that failure, propose relabelling the row `runtime: advisory` in a separately reviewed PR.
|
|
@@ -22,6 +22,8 @@ read it before phase 7 and before any fan-out.
|
|
|
22
22
|
Redact secrets before showing any command, output, or artifact; build loops
|
|
23
23
|
against environment variables so credentials never appear in what is shown.
|
|
24
24
|
|
|
25
|
+
For performance claims only, load [Performance checklist](../axstack/references/performance-checklist.md).
|
|
26
|
+
|
|
25
27
|
## Phases
|
|
26
28
|
|
|
27
29
|
Each phase has an observable completion criterion. Skip one only with a
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: axstack-diagram
|
|
3
|
+
description: When an explanation needs a diagram, use axstack-diagram to choose Mermaid or a pinned archify viewer with source fidelity and rendered QA.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Diagram
|
|
7
|
+
|
|
8
|
+
Usage: `/axstack-diagram <question + destination>` or load this skill from explain.
|
|
9
|
+
Load [Standing contracts](../axstack/references/contracts.md) before acting.
|
|
10
|
+
Follow [Fidelity](references/fidelity.md) for every format.
|
|
11
|
+
|
|
12
|
+
Use archify HTML for an explicit viewer request or explain's complex-visual path.
|
|
13
|
+
For the viewer, follow [Archify](references/archify.md).
|
|
14
|
+
Otherwise use Mermaid for chat, GitHub, or docs.
|
|
15
|
+
Include `accTitle` and `accDescr` in each Mermaid view.
|
|
16
|
+
Never use theme init in Mermaid.
|
|
17
|
+
Split large Mermaid diagrams into views.
|
|
18
|
+
T3 chat renders the Mermaid fence once the reply settles.
|
|
19
|
+
|
|
20
|
+
Explain owns evidence, claim labels, the word cap, dispatch, and delivery.
|
|
21
|
+
Diagram owns format and visual rules.
|
|
22
|
+
Never call explain.
|
|
23
|
+
For direct use, keep fidelity and rendered QA.
|
|
24
|
+
|
|
25
|
+
Ideas: [archify](https://github.com/tt-a1i/archify), [diagram-design](https://github.com/cathrynlavery/diagram-design), [visual-explainer](https://github.com/nicobailon/visual-explainer), [Matt Pocock](https://github.com/mattpocock/skills), and [poteto](https://github.com/poteto/how); guidance uses Axstack's own words.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Archify
|
|
2
|
+
|
|
3
|
+
Archify turns a typed JSON intermediate representation (IR) into an HTML viewer.
|
|
4
|
+
Follow [Fidelity](fidelity.md) before authoring.
|
|
5
|
+
|
|
6
|
+
## Bind the tool
|
|
7
|
+
|
|
8
|
+
Read `archify.json` beside the skill entrypoint for its `{path, sha}` record.
|
|
9
|
+
Verify that `git -C <path> rev-parse HEAD` equals the recorded SHA.
|
|
10
|
+
If the record or copy is missing, HEAD mismatches the recorded SHA, or Chrome is missing, hold and ask the user.
|
|
11
|
+
Honor `ARCHIFY_CHROME` when set.
|
|
12
|
+
Otherwise find Chrome or Chromium on `PATH`.
|
|
13
|
+
Set `ARCHIFY_UPDATE_CHECK_DISABLED=1` on every archify call.
|
|
14
|
+
Never substitute Mermaid or unverified HTML when the viewer is required.
|
|
15
|
+
|
|
16
|
+
## Author the IR
|
|
17
|
+
|
|
18
|
+
If the pinned archify procedure conflicts with this file, this file takes precedence.
|
|
19
|
+
Read `<path>/archify/SKILL.md` and follow its named references for IR authoring and repair.
|
|
20
|
+
Run the referenced archify commands with Bun.
|
|
21
|
+
|
|
22
|
+
Set `meta.title` and a portable relative HTML filename in `meta.output`.
|
|
23
|
+
Use `meta.quality_profile: "showcase"`.
|
|
24
|
+
Set `meta.animation: "none"`.
|
|
25
|
+
Keep trace motion, exports, share cards, and brand marks off unless the user asks.
|
|
26
|
+
Deliver the viewer with `?theme=dark` unless the user names a theme.
|
|
27
|
+
Theme is viewer state rather than an IR field; without the query, localStorage and `prefers-color-scheme` determine it.
|
|
28
|
+
Preserve explain's claim labels in `meta.subtitle` or the companion explanation.
|
|
29
|
+
Set `meta.repository` to the inspected repository URL and full `revision`.
|
|
30
|
+
Keep per-node source pins in each node's `sources` entries for inspected paths and line ranges.
|
|
31
|
+
Keep the private source map for relationships that have no source field.
|
|
32
|
+
Keep proposed sources distinct from factual pins.
|
|
33
|
+
Use [Fidelity](fidelity.md) for visible planned and unknown markers.
|
|
34
|
+
For a separate check, run `bun <path>/archify/bin/archify.mjs validate <type> <ir.json> --json --repo-root <repo>`.
|
|
35
|
+
When sources are declared, require `--repo-root <repo>` on both `validate` and `finalize`.
|
|
36
|
+
For sequence and dataflow, a schema-valid legend cue is `meta.legend.entries.dashed: {"label":"Planned or unknown"}`.
|
|
37
|
+
|
|
38
|
+
Use one top-level `cards[]` entry with `{dot, title, items[]}` per key node as its node detail card.
|
|
39
|
+
Each node detail card has at most 40 words and cites its source.
|
|
40
|
+
Count the title plus all items toward each card's 40-word cap.
|
|
41
|
+
Cite each card's source inline as `file:lines` in its `items[]`.
|
|
42
|
+
Node detail cards are exempt from explain's 700-word cap.
|
|
43
|
+
Count node labels, headings, captions, and all non-card text.
|
|
44
|
+
|
|
45
|
+
## Build and hold
|
|
46
|
+
|
|
47
|
+
Write the IR, HTML, and receipt only inside the private evidence folder.
|
|
48
|
+
For direct invocation, create a private run evidence folder first.
|
|
49
|
+
Override archify's default `.archify/` folder with an evidence output path and `--out-dir <evidence dir>`.
|
|
50
|
+
Run `bun <path>/archify/bin/archify.mjs finalize <type> <ir.json> <out.html> --quality showcase --json --out-dir <evidence dir> --repo-root <repo>`.
|
|
51
|
+
This is the author's headless build gate.
|
|
52
|
+
Read the saved receipt named by the JSON result's `evidence.receipt`.
|
|
53
|
+
Accept only receipt `status: "pass"` as success.
|
|
54
|
+
Never trust exit codes as proof of a pass.
|
|
55
|
+
When a receipt is missing or remains non-pass after two repair rounds, hold delivery and ask the user.
|
|
56
|
+
If Chrome is missing, report "viewer not verified: Chrome missing".
|
|
57
|
+
Chrome absence yields `status: "skipped"` and exit 2.
|
|
58
|
+
Follow archify's receipt fixes and references for layout and composition repairs.
|
|
59
|
+
After every repair edit, rerun the complete `finalize` command.
|
|
60
|
+
Count one repair round as one complete `finalize` rerun after an edit.
|
|
61
|
+
Validate runs never count as repair rounds.
|
|
62
|
+
Do not require validate before finalize.
|
|
63
|
+
Run at most two repair rounds, then hold with remaining defects for the user's decision.
|
|
64
|
+
Never use archify's extra evidence-based retry.
|
|
65
|
+
|
|
66
|
+
## Rendered and source checks
|
|
67
|
+
|
|
68
|
+
Follow [UI verification](../../axstack/references/ui-verification.md) for dispatch.
|
|
69
|
+
Keep `finalize` as the only author browser check.
|
|
70
|
+
Delegate `visual-check`, browser opening, preview, and first-screen inspection to `axstack-ui-verifier`.
|
|
71
|
+
Require `axstack-ui-verifier` checks of the receipt's `artifact.sha256` bytes on desktop, 390px mobile, keyboard, reduced motion, and the text alternative.
|
|
72
|
+
Include theme, search or focus, and directional reach interactions.
|
|
73
|
+
Require `axstack-ui-verifier` checks with the theme set explicitly.
|
|
74
|
+
Require `explainer-review` of every node and edge against source for archify output.
|
|
75
|
+
Every byte change requires fresh checks.
|
|
76
|
+
Keep these independent verdicts with the build receipt before delivery.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Fidelity
|
|
2
|
+
|
|
3
|
+
Map factual nodes and edges to an inspected source path at a recorded full revision.
|
|
4
|
+
Drop unsupported factual elements.
|
|
5
|
+
For uncommitted sources, use the content hash and mark their pins unknown.
|
|
6
|
+
Keep the source map outside the reader view.
|
|
7
|
+
Display the short SHA.
|
|
8
|
+
|
|
9
|
+
Give planned and unknown nodes a visible marker in the node label and their card, if any.
|
|
10
|
+
Use the literal marker `planned` or `unknown` for all five types.
|
|
11
|
+
Give planned and unknown edges a visible `planned` or `unknown` marker in the edge label.
|
|
12
|
+
For dashed styling, use only properties the pinned schema accepts and verify with `validate`.
|
|
13
|
+
This is an extra cue, such as a dashed relationship where supported.
|
|
14
|
+
The pinned node collections reject `variant`.
|
|
15
|
+
Add a planned or unknown legend entry only for sequence and dataflow.
|
|
16
|
+
Architecture, workflow, and lifecycle reject a `dashed` legend entry.
|
|
17
|
+
In Mermaid, use the same visible markers and a dashed cue where its syntax permits.
|
|
18
|
+
|
|
19
|
+
Treat about 9 core nodes as a signal to split.
|
|
20
|
+
This is a design signal rather than a hard gate.
|
|
21
|
+
Use 1–2 accent nodes.
|
|
22
|
+
Keep labels short and name each relationship's direction and meaning.
|
|
23
|
+
Provide a text alternative that describes nodes and edges.
|
|
24
|
+
Never load network assets in the HTML.
|
|
25
|
+
Write diagrams into repo docs only when the user asks.
|
|
@@ -51,6 +51,10 @@ immediately before an actual profile dispatch.
|
|
|
51
51
|
|
|
52
52
|
## 2. Choose proportional output
|
|
53
53
|
|
|
54
|
+
Load [Diagram](../axstack-diagram/SKILL.md) for every diagram.
|
|
55
|
+
Explain owns evidence gathering, claim labels, the 700-word cap, dispatch, and delivery.
|
|
56
|
+
Diagram never calls explain.
|
|
57
|
+
|
|
54
58
|
1. Keep the primary reader-facing explanation to a maximum of 700 words in
|
|
55
59
|
chat, HTML, and every other requested format. Preserve in that primary view
|
|
56
60
|
the answer or purpose, key rationale, meaningful alternatives, main data or
|
|
@@ -63,11 +67,17 @@ immediately before an actual profile dispatch.
|
|
|
63
67
|
maximum. If the draft is longer, compress repetition first and move only
|
|
64
68
|
supporting detail to a separate linked ticket or appendix. Essential answers
|
|
65
69
|
must not be hidden behind links, and evidence must not be silently discarded.
|
|
70
|
+
Only archify node detail cards are exempt from the 700-word cap.
|
|
71
|
+
Each card has at most 40 words and cites its source, as defined in
|
|
72
|
+
[Archify](../axstack-diagram/references/archify.md).
|
|
73
|
+
Count node labels, headings, captions, and all non-card text.
|
|
66
74
|
2. For a simple request, answer concisely in the current chat. Use a compact
|
|
67
75
|
diagram when useful. This needs no mandatory agent or intermediate artifact.
|
|
76
|
+
For simple chat answers, never dispatch an agent when applying its Mermaid rules inline.
|
|
68
77
|
3. For a complex visual, use the configured `axstack-explainer` role to create
|
|
69
|
-
self-contained HTML
|
|
70
|
-
|
|
78
|
+
self-contained HTML through the archify path by default.
|
|
79
|
+
For a complex visual, honor an explicitly requested artifact format.
|
|
80
|
+
An explicit user theme wins; otherwise use the dark default.
|
|
71
81
|
4. Profile IDs are presets, not availability proof. Before dispatch, follow the
|
|
72
82
|
launch sequence and preserve the configured model, mode, and effort. Report
|
|
73
83
|
an unavailable route; never substitute a model.
|
|
@@ -77,8 +87,9 @@ immediately before an actual profile dispatch.
|
|
|
77
87
|
1. Any HTML explanation requires the full [visual QA
|
|
78
88
|
checklist](references/visual-qa.md): actual desktop and mobile rendering,
|
|
79
89
|
interaction, accessibility, and reduced-motion checks where relevant.
|
|
80
|
-
2.
|
|
81
|
-
|
|
90
|
+
2. For archify output, require the configured independent `axstack-explainer-review`;
|
|
91
|
+
for other artifacts, use it when warranted. Bind it to the exact artifact identity.
|
|
92
|
+
Any byte change invalidates
|
|
82
93
|
that review and requires a fresh check. In `claude-only`, separate Sonnet
|
|
83
94
|
author xhigh and reviewer high sessions are allowed for explanations as
|
|
84
95
|
session independence only. This exception never permits same-model code
|
|
@@ -7,6 +7,8 @@ Browser and visual checks must run in the delegated `axstack-ui-verifier` in its
|
|
|
7
7
|
|
|
8
8
|
1. Identify the final artifact bytes and theme. The explicit user theme wins;
|
|
9
9
|
otherwise use the dark default.
|
|
10
|
+
For archify output, bind the `axstack-ui-verifier` pass to the finalize receipt's `artifact.sha256`.
|
|
11
|
+
For archify output, bind `axstack-explainer-review` to the same finalize receipt's `artifact.sha256`.
|
|
10
12
|
2. Delegate the rendered pass through [UI verification](../../axstack/references/ui-verification.md).
|
|
11
13
|
Record actual desktop and mobile observations, or name the missing layout
|
|
12
14
|
check.
|
|
@@ -16,5 +18,6 @@ Browser and visual checks must run in the delegated `axstack-ui-verifier` in its
|
|
|
16
18
|
4. The explainer reviewer checks text and source fidelity. Keep source
|
|
17
19
|
correctness, tests, rendered behavior, independent review, and publication
|
|
18
20
|
evidence separate.
|
|
21
|
+
For archify output, require `axstack-explainer-review` of every node and edge against source.
|
|
19
22
|
5. Bind review to the exact artifact identity. Any byte change invalidates the
|
|
20
23
|
affected approval and requires fresh QA and review.
|
|
@@ -230,6 +230,11 @@ For each PR:
|
|
|
230
230
|
to step 1.
|
|
231
231
|
Merge-ready also requires a current diligence `PASS` at that head; diligence
|
|
232
232
|
`FINDINGS` return to the same author within the review round.
|
|
233
|
+
Diligence `UNKNOWN` records the PR as `held` with the reason in the run record
|
|
234
|
+
and blocks merge-ready pending the driver's recorded disposition.
|
|
235
|
+
Before merge-ready, treat a run-record `Learning` that contradicts a shipped
|
|
236
|
+
rule as a finding on the owning PR and hold merge-ready until the contradiction is resolved.
|
|
237
|
+
An unrelated run-record `Learning` leaves merge-ready eligibility unchanged.
|
|
233
238
|
A round with reviewer `REQUEST_CHANGES` and/or diligence `FINDINGS` increments
|
|
234
239
|
`repairs` once and counts once toward the third-round hold.
|
|
235
240
|
|
|
@@ -27,6 +27,8 @@ Sol pair that fails to launch is fenced, recorded `absent (<reason>)`, and
|
|
|
27
27
|
named once in the next read-back, then skipped without relay or substitution.
|
|
28
28
|
In mixed fan-out retain a Codex and a Claude seat or hold the affected work.
|
|
29
29
|
|
|
30
|
+
For performance claims only, load [Performance checklist](../axstack/references/performance-checklist.md).
|
|
31
|
+
|
|
30
32
|
## 1. Bound discovery
|
|
31
33
|
|
|
32
34
|
1. Start with the user's named subsystem or pain. Otherwise inspect recent
|
|
@@ -33,6 +33,8 @@ When the caller is a bounded review-manager PR job, load
|
|
|
33
33
|
carry the required escalation field and every eligible peer PR takes a binding
|
|
34
34
|
`APPROVE` or `REQUEST_CHANGES` verdict under the automation exception below.
|
|
35
35
|
|
|
36
|
+
For performance claims only, load [Performance checklist](../axstack/references/performance-checklist.md).
|
|
37
|
+
|
|
36
38
|
## Codebase findings mode
|
|
37
39
|
|
|
38
40
|
Use this manual mode for existing code at a pinned exact source revision and a
|
|
@@ -62,6 +62,12 @@ and the lifecycle's [audit skill](../axstack-audit/SKILL.md) hook.
|
|
|
62
62
|
session to return plain AGREE. Present one
|
|
63
63
|
reviewable, identified revision for this checkpoint. Its user approval
|
|
64
64
|
creates the execution baseline.
|
|
65
|
+
Name the draft revision covered by each adviser receipt at the checkpoint.
|
|
66
|
+
If draft text changed and an adviser receipt covers an older revision, hold
|
|
67
|
+
approval until fresh receipts cover the presented revision.
|
|
68
|
+
Except for high-stakes decisions, a change confined to a `Decisions` row
|
|
69
|
+
reuses adviser receipts only while draft text, evidence, scope, and question
|
|
70
|
+
remain unchanged.
|
|
65
71
|
Before user approval, dispatch `axstack-diligence` under
|
|
66
72
|
[Diligence](../axstack/references/diligence.md) to check the draft against
|
|
67
73
|
the Align decisions for anything dropped, added, or softened.
|
package/src/archify.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { lstat, mkdir, mkdtemp, readFile, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from './posixpath.js';
|
|
3
|
+
import { ARCHIFY_PIN } from './archify-pin.js';
|
|
4
|
+
|
|
5
|
+
export const ARCHIFY_RECORD = 'axstack-diagram/archify.json';
|
|
6
|
+
|
|
7
|
+
export function archifyGit(path, ...args) {
|
|
8
|
+
try {
|
|
9
|
+
const result = Bun.spawnSync(['git', ...(path ? ['-C', path] : []), ...args], {
|
|
10
|
+
stdout: 'pipe', stderr: 'pipe', timeout: 60000,
|
|
11
|
+
env: { ...Bun.env, GIT_TERMINAL_PROMPT: '0' },
|
|
12
|
+
});
|
|
13
|
+
if (result.exitCode !== 0) throw new Error(result.stderr.toString().trim() || `git exit ${result.exitCode}`);
|
|
14
|
+
return result.stdout.toString().trim();
|
|
15
|
+
} catch (err) {
|
|
16
|
+
throw new Error(`git: ${err.message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function verifySha(record, actual) {
|
|
21
|
+
if (actual === record.sha) return;
|
|
22
|
+
const error = new Error(`archify SHA mismatch: expected ${record.sha}, got ${actual}`);
|
|
23
|
+
error.code = 'ARCHIFY_SHA_MISMATCH';
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function ownersSnapshot(path) {
|
|
28
|
+
const st = await lstat(path).catch((err) => {
|
|
29
|
+
if (err.code === 'ENOENT') return null;
|
|
30
|
+
throw err;
|
|
31
|
+
});
|
|
32
|
+
if (st && (!st.isFile() || st.isSymbolicLink())) throw new Error(`unsafe archify owners file: ${path}`);
|
|
33
|
+
const bytes = st ? await readFile(path) : null;
|
|
34
|
+
const owners = bytes ? JSON.parse(bytes) : [];
|
|
35
|
+
if (!Array.isArray(owners) || owners.some((owner) => typeof owner !== 'string' || !owner.startsWith('/'))) {
|
|
36
|
+
throw new Error(`invalid archify owners file: ${path}`);
|
|
37
|
+
}
|
|
38
|
+
return { bytes, owners, mode: st ? st.mode & 0o777 : null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Drop ownership transactionally; defer destructive cleanup until the skills
|
|
42
|
+
// manifest commits. A failed manifest write can then restore the owner list.
|
|
43
|
+
export async function planArchifyRelease({ record, skillsRoot, writeAtomic, log }) {
|
|
44
|
+
const ownersFile = `${record.path}.owners.json`;
|
|
45
|
+
const before = await ownersSnapshot(ownersFile);
|
|
46
|
+
const remaining = before.owners.filter((owner) => owner !== skillsRoot);
|
|
47
|
+
if (!before.owners.includes(skillsRoot)) {
|
|
48
|
+
return { rollback: async () => {}, finish: async () => log(`archify: kept ${record.path} (owner is absent)`) };
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await writeAtomic(ownersFile, JSON.stringify(remaining) + '\n', { mode: 0o600 });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
const current = await readFile(ownersFile);
|
|
54
|
+
if (!current.equals(before.bytes)) {
|
|
55
|
+
try { await writeAtomic(ownersFile, before.bytes, { mode: before.mode }); }
|
|
56
|
+
catch (restoreErr) { err.message += ` (archify owner restore failed: ${restoreErr.message})`; }
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
rollback: () => writeAtomic(ownersFile, before.bytes, { mode: before.mode }),
|
|
62
|
+
finish: async () => {
|
|
63
|
+
try {
|
|
64
|
+
if (remaining.length) throw new Error('other skills roots still own this copy');
|
|
65
|
+
const st = await lstat(record.path);
|
|
66
|
+
if (!st.isDirectory() || st.isSymbolicLink()) throw new Error('copy is not a real directory');
|
|
67
|
+
if (archifyGit(record.path, 'rev-parse', 'HEAD') !== record.sha) throw new Error('HEAD differs from the recorded SHA');
|
|
68
|
+
if (archifyGit(record.path, 'status', '--porcelain', '--ignored')) throw new Error('copy has tracked, untracked, or ignored changes');
|
|
69
|
+
await rm(record.path, { recursive: true });
|
|
70
|
+
await rm(ownersFile);
|
|
71
|
+
log(`archify: removed ${record.path}`);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
log(`archify: kept ${record.path} (${err.message})`);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The caller includes this transaction in the skills/manifest rollback.
|
|
80
|
+
export async function provisionArchify({ toolsRoot, skillsRoot, writeAtomic, log }) {
|
|
81
|
+
if (!Bun.which('git')) {
|
|
82
|
+
log('archify: unavailable (git is not on PATH)');
|
|
83
|
+
return { record: null, rollback: async () => {} };
|
|
84
|
+
}
|
|
85
|
+
const record = { path: join(toolsRoot, `archify-${ARCHIFY_PIN.sha}`), sha: ARCHIFY_PIN.sha };
|
|
86
|
+
const ownersFile = `${record.path}.owners.json`;
|
|
87
|
+
const before = await ownersSnapshot(ownersFile);
|
|
88
|
+
let created = false;
|
|
89
|
+
let ownersWritten = false;
|
|
90
|
+
const rollback = async () => {
|
|
91
|
+
const errors = [];
|
|
92
|
+
if (ownersWritten) {
|
|
93
|
+
try {
|
|
94
|
+
if (before.bytes === null) await rm(ownersFile);
|
|
95
|
+
else await writeAtomic(ownersFile, before.bytes, { mode: before.mode });
|
|
96
|
+
} catch (err) { errors.push(`${ownersFile}: ${err.message}`); }
|
|
97
|
+
}
|
|
98
|
+
if (created) {
|
|
99
|
+
try { await rm(record.path, { recursive: true }); }
|
|
100
|
+
catch (err) { errors.push(`${record.path}: ${err.message}`); }
|
|
101
|
+
}
|
|
102
|
+
if (errors.length) throw new Error(`incomplete archify rollback: ${errors.join('; ')}`);
|
|
103
|
+
};
|
|
104
|
+
const existing = await lstat(record.path).catch((err) => {
|
|
105
|
+
if (err.code === 'ENOENT') return null;
|
|
106
|
+
throw err;
|
|
107
|
+
});
|
|
108
|
+
if (existing && (!existing.isDirectory() || existing.isSymbolicLink())) {
|
|
109
|
+
throw new Error(`unsafe archify copy: ${record.path}`);
|
|
110
|
+
}
|
|
111
|
+
if (existing) {
|
|
112
|
+
const actual = archifyGit(record.path, 'rev-parse', 'HEAD');
|
|
113
|
+
verifySha(record, actual);
|
|
114
|
+
} else {
|
|
115
|
+
let temp = null;
|
|
116
|
+
try {
|
|
117
|
+
await mkdir(toolsRoot, { recursive: true });
|
|
118
|
+
temp = await mkdtemp(join(toolsRoot, '.archify-'));
|
|
119
|
+
archifyGit(null, 'clone', '--filter=blob:none', '--no-checkout', '--', Bun.env.AXSTACK_ARCHIFY_REPO || ARCHIFY_PIN.repo, temp);
|
|
120
|
+
archifyGit(temp, 'sparse-checkout', 'set', '--no-cone', '/archify/');
|
|
121
|
+
archifyGit(temp, 'checkout', '--detach', record.sha);
|
|
122
|
+
const actual = archifyGit(temp, 'rev-parse', 'HEAD');
|
|
123
|
+
verifySha(record, actual);
|
|
124
|
+
await rename(temp, record.path);
|
|
125
|
+
temp = null;
|
|
126
|
+
created = true;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (err.code === 'ARCHIFY_SHA_MISMATCH') throw err;
|
|
129
|
+
log(`archify: unavailable (${err.message.replace(/\s+/g, ' ').slice(0, 180)})`);
|
|
130
|
+
return { record: null, rollback: async () => {} };
|
|
131
|
+
} finally {
|
|
132
|
+
if (temp) await rm(temp, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
if (!before.owners.includes(skillsRoot)) {
|
|
137
|
+
ownersWritten = true;
|
|
138
|
+
await writeAtomic(ownersFile, JSON.stringify([...before.owners, skillsRoot].sort()) + '\n', { mode: 0o600 });
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const current = await readFile(ownersFile).catch(() => null);
|
|
142
|
+
ownersWritten = before.bytes === null ? current !== null : current?.equals(before.bytes) !== true;
|
|
143
|
+
try { await rollback(); }
|
|
144
|
+
catch (restoreErr) { err.message += ` (${restoreErr.message})`; }
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
log(`archify: ${record.path} (${record.sha})`);
|
|
148
|
+
return { record, rollback };
|
|
149
|
+
}
|
package/src/capabilities.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// Host capability checks. Injected execution keeps tests off live runtimes.
|
|
2
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from './posixpath.js';
|
|
4
|
+
import { ARCHIFY_RECORD, archifyGit } from './archify.js';
|
|
5
|
+
import { ARCHIFY_PIN } from './archify-pin.js';
|
|
2
6
|
export const BUN_FLOOR = '1.3.14';
|
|
3
7
|
export const T3_FLOOR = '0.0.46-nightly.20261003.2610';
|
|
4
8
|
|
|
@@ -106,3 +110,29 @@ export async function checkCapabilities(exec) {
|
|
|
106
110
|
const gaps = checks.filter((c) => !c.ok).map((c) => `missing ${c.label}`);
|
|
107
111
|
return { checks, gaps, limitations: [...PROBE_LIMITATIONS] };
|
|
108
112
|
}
|
|
113
|
+
|
|
114
|
+
export async function checkArchify(skillsDir) {
|
|
115
|
+
const recordFile = join(skillsDir, ARCHIFY_RECORD);
|
|
116
|
+
const override = Bun.env.ARCHIFY_CHROME;
|
|
117
|
+
const chrome = override ? Bun.which(override) :
|
|
118
|
+
['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome']
|
|
119
|
+
.map((name) => Bun.which(name)).find(Boolean);
|
|
120
|
+
let record = null;
|
|
121
|
+
let actual = null;
|
|
122
|
+
let reason = null;
|
|
123
|
+
try {
|
|
124
|
+
if ((await lstat(recordFile)).isSymbolicLink()) throw new Error('record is a symlink');
|
|
125
|
+
record = JSON.parse(await readFile(recordFile, 'utf8'));
|
|
126
|
+
if (!record || typeof record.path !== 'string' || !record.path.startsWith('/') ||
|
|
127
|
+
!/^[a-f0-9]{40}$/.test(record.sha)) throw new Error('invalid record');
|
|
128
|
+
const copy = await lstat(record.path);
|
|
129
|
+
if (!copy.isDirectory() || copy.isSymbolicLink()) throw new Error('copy is not a real directory');
|
|
130
|
+
actual = archifyGit(record.path, 'rev-parse', 'HEAD');
|
|
131
|
+
if (actual !== record.sha || record.sha !== ARCHIFY_PIN.sha) {
|
|
132
|
+
throw new Error(`SHA mismatch: recorded ${record.sha}, checked-out ${actual}, expected ${ARCHIFY_PIN.sha}`);
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
reason = err.code === 'ENOENT' ? 'missing record or copy' : err.message;
|
|
136
|
+
}
|
|
137
|
+
return { recordFile, record, actual, chrome: chrome ?? null, reason };
|
|
138
|
+
}
|
package/src/installer.js
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
writeManifest,
|
|
39
39
|
} from './manifest.js';
|
|
40
40
|
import { planInstallClaudeSettings, planUninstallClaudeSettings } from './claude-settings.js';
|
|
41
|
+
import { ARCHIFY_RECORD, planArchifyRelease, provisionArchify } from './archify.js';
|
|
41
42
|
import {
|
|
42
43
|
applyInstructionPlan,
|
|
43
44
|
findLegacyRoutingLines,
|
|
@@ -205,6 +206,15 @@ export function assertOutsideHome(target, { yes = false, kind = 'target' } = {})
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
async function guardArchifyRecord(record, skillsRoot, yes) {
|
|
210
|
+
if (!record) return;
|
|
211
|
+
await assertNoSymlinksBelow('/', record.path);
|
|
212
|
+
if (withinRoot(record.path, skillsRoot) || withinRoot(skillsRoot, dirname(record.path))) {
|
|
213
|
+
throw new Error('recorded archify tools directory overlaps the skills root; refusing');
|
|
214
|
+
}
|
|
215
|
+
assertOutsideHome(record.path, { yes, kind: 'archify copy' });
|
|
216
|
+
}
|
|
217
|
+
|
|
208
218
|
// Validate the bundle directory and return its payload. Throws before any
|
|
209
219
|
// target mutation on any problem.
|
|
210
220
|
export async function validateBundle(bundleDir, selectedPreset = null) {
|
|
@@ -461,6 +471,7 @@ export async function installBundle({
|
|
|
461
471
|
bundleDir,
|
|
462
472
|
skillsDir,
|
|
463
473
|
preset,
|
|
474
|
+
toolsDir = null,
|
|
464
475
|
instructionsPath = null,
|
|
465
476
|
inheritedInstructions = null,
|
|
466
477
|
force = false,
|
|
@@ -479,6 +490,14 @@ export async function installBundle({
|
|
|
479
490
|
? await canonicalInstructionFile(instructionsPath)
|
|
480
491
|
: null;
|
|
481
492
|
assertOutsideHome(skillsRoot, { yes, kind: 'skills directory' });
|
|
493
|
+
const toolsPath = toolsDir ?? (homeDir() ? join(homeDir(), '.axstack', 'tools') : null);
|
|
494
|
+
if (!toolsPath) throw new Error('HOME is unavailable; pass --tools-dir explicitly');
|
|
495
|
+
await assertNoSymlinksBelow('/', resolve(toolsPath));
|
|
496
|
+
const toolsRoot = await canonicalTargetDir(toolsPath);
|
|
497
|
+
if (withinRoot(toolsRoot, skillsRoot) || withinRoot(skillsRoot, toolsRoot)) {
|
|
498
|
+
throw new Error('tools directory overlaps the skills root; refusing');
|
|
499
|
+
}
|
|
500
|
+
assertOutsideHome(toolsRoot, { yes, kind: 'tools directory' });
|
|
482
501
|
if (instructionsFile) assertOutsideHome(instructionsFile, { yes, kind: 'instruction file' });
|
|
483
502
|
const claudePlan = await planInstallClaudeSettings({ claude, skillsRoot });
|
|
484
503
|
if (claudePlan.report.path) {
|
|
@@ -503,6 +522,7 @@ export async function installBundle({
|
|
|
503
522
|
);
|
|
504
523
|
}
|
|
505
524
|
const ownedFiles = prevManifest.files ?? {};
|
|
525
|
+
await guardArchifyRecord(prevManifest.archify, skillsRoot, yes);
|
|
506
526
|
const legacyProfiles = prevManifest.profiles;
|
|
507
527
|
const legacyProfileNote = Object.keys(legacyProfiles?.entries ?? {}).length > 0
|
|
508
528
|
? 'legacy Paseo profile provenance retained inert; see legacy cleanup guidance in docs/installation.md'
|
|
@@ -545,6 +565,11 @@ export async function installBundle({
|
|
|
545
565
|
// manifest entry resolves its destination, current bytes, and pristine
|
|
546
566
|
// flag through the same guard before Phase 2 mutates anything.
|
|
547
567
|
const desired = [];
|
|
568
|
+
const archifyTarget = await readOwnedTarget(skillsRoot, ARCHIFY_RECORD);
|
|
569
|
+
if (archifyTarget.current !== null &&
|
|
570
|
+
hashContent(archifyTarget.current) !== ownedFiles[ARCHIFY_RECORD]) {
|
|
571
|
+
throw new Error('archify record is edited or unowned; refusing to replace it');
|
|
572
|
+
}
|
|
548
573
|
for (const { rel, abs, content: inlineContent } of bundle.files) {
|
|
549
574
|
const { dest, current, mode } = await readOwnedTarget(skillsRoot, rel);
|
|
550
575
|
const content = inlineContent ?? await readFile(abs);
|
|
@@ -562,6 +587,7 @@ export async function installBundle({
|
|
|
562
587
|
}
|
|
563
588
|
|
|
564
589
|
const bundleRels = new Set(bundle.files.map((file) => file.rel));
|
|
590
|
+
bundleRels.add(ARCHIFY_RECORD);
|
|
565
591
|
const stalePlan = [];
|
|
566
592
|
for (const rel of Object.keys(ownedFiles)) {
|
|
567
593
|
if (bundleRels.has(rel)) continue;
|
|
@@ -591,8 +617,23 @@ export async function installBundle({
|
|
|
591
617
|
const installedHashes = {};
|
|
592
618
|
const claudeWritten = [];
|
|
593
619
|
let instructionWritten = false;
|
|
620
|
+
let archify = null;
|
|
621
|
+
let archifyRelease = null;
|
|
622
|
+
const manifestFile = join(skillsRoot, '.axstack-manifest.json');
|
|
623
|
+
const manifestBefore = await readFile(manifestFile).catch((err) => {
|
|
624
|
+
if (err.code === 'ENOENT') return null;
|
|
625
|
+
throw err;
|
|
626
|
+
});
|
|
627
|
+
const manifestMode = manifestBefore === null ? null : (await stat(manifestFile)).mode & 0o777;
|
|
628
|
+
let manifestAttempted = false;
|
|
594
629
|
log('plan complete');
|
|
595
630
|
try {
|
|
631
|
+
archify = await provisionArchify({ toolsRoot, skillsRoot, writeAtomic, log });
|
|
632
|
+
const archifyRecord = archify.record ?? prevManifest.archify ?? null;
|
|
633
|
+
if (archifyRecord) {
|
|
634
|
+
desired.push({ rel: ARCHIFY_RECORD, ...archifyTarget,
|
|
635
|
+
content: Buffer.from(JSON.stringify(archifyRecord, null, 2) + '\n') });
|
|
636
|
+
}
|
|
596
637
|
if (instructionsFile) {
|
|
597
638
|
await assertFileSnapshot(instructionsFile, existingInstructionsRaw);
|
|
598
639
|
}
|
|
@@ -709,6 +750,12 @@ export async function installBundle({
|
|
|
709
750
|
)
|
|
710
751
|
: null;
|
|
711
752
|
|
|
753
|
+
if (prevManifest.archify && archifyRecord?.path !== prevManifest.archify.path) {
|
|
754
|
+
archifyRelease = await planArchifyRelease({
|
|
755
|
+
record: prevManifest.archify, skillsRoot, writeAtomic, log,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
manifestAttempted = true;
|
|
712
759
|
await writeManifest(skillsRoot, {
|
|
713
760
|
version: MANIFEST_VERSION,
|
|
714
761
|
files: installedHashes,
|
|
@@ -717,7 +764,9 @@ export async function installBundle({
|
|
|
717
764
|
path: claudePlan.settingsPath ?? boundClaudeSettingsPath,
|
|
718
765
|
},
|
|
719
766
|
instructions: nextInstructions,
|
|
767
|
+
archify: archifyRecord,
|
|
720
768
|
});
|
|
769
|
+
if (archifyRelease) await archifyRelease.finish();
|
|
721
770
|
if (legacyProfileNote || legacyInstructionNote) {
|
|
722
771
|
summary.notes = [
|
|
723
772
|
...(summary.notes ?? []),
|
|
@@ -751,6 +800,26 @@ export async function installBundle({
|
|
|
751
800
|
// Restore failures are collected and reported: a swallowed restore would
|
|
752
801
|
// leave ownership claims describing bytes that are not on disk.
|
|
753
802
|
const rollbackErrors = [];
|
|
803
|
+
if (manifestAttempted) {
|
|
804
|
+
try {
|
|
805
|
+
const current = await readFile(manifestFile).catch((readErr) => {
|
|
806
|
+
if (readErr.code === 'ENOENT') return null;
|
|
807
|
+
throw readErr;
|
|
808
|
+
});
|
|
809
|
+
if (manifestBefore === null && current !== null) await rm(manifestFile);
|
|
810
|
+
else if (manifestBefore !== null && current?.equals(manifestBefore) !== true) {
|
|
811
|
+
await writeAtomic(manifestFile, manifestBefore, { mode: manifestMode });
|
|
812
|
+
}
|
|
813
|
+
} catch (restoreErr) { rollbackErrors.push(`manifest: ${restoreErr.message}`); }
|
|
814
|
+
}
|
|
815
|
+
if (archifyRelease) {
|
|
816
|
+
try { await archifyRelease.rollback(); }
|
|
817
|
+
catch (restoreErr) { rollbackErrors.push(`old archify owner: ${restoreErr.message}`); }
|
|
818
|
+
}
|
|
819
|
+
if (archify) {
|
|
820
|
+
try { await archify.rollback(); }
|
|
821
|
+
catch (restoreErr) { rollbackErrors.push(`archify: ${restoreErr.message}`); }
|
|
822
|
+
}
|
|
754
823
|
for (const write of claudeWritten.reverse()) {
|
|
755
824
|
try {
|
|
756
825
|
if (write.before === null) await rm(write.path);
|
|
@@ -812,6 +881,7 @@ export async function uninstallBundle({
|
|
|
812
881
|
instructions: { path: null, hash: null },
|
|
813
882
|
};
|
|
814
883
|
const boundClaudeSettingsPath = manifest.claudeSettings?.path ?? null;
|
|
884
|
+
await guardArchifyRecord(manifest.archify, skillsRoot, yes);
|
|
815
885
|
const claudePlan = await planUninstallClaudeSettings({
|
|
816
886
|
claude,
|
|
817
887
|
skillsRoot,
|
|
@@ -920,6 +990,7 @@ export async function uninstallBundle({
|
|
|
920
990
|
const deletedFiles = [];
|
|
921
991
|
const claudeWritten = [];
|
|
922
992
|
let instructionWritten = false;
|
|
993
|
+
let archifyRelease = null;
|
|
923
994
|
try {
|
|
924
995
|
for (const rel of uninstallPlan) {
|
|
925
996
|
const { dest, current, mode } = await readOwnedTarget(skillsRoot, rel);
|
|
@@ -958,6 +1029,11 @@ export async function uninstallBundle({
|
|
|
958
1029
|
}
|
|
959
1030
|
summary.claudeSettings = claudePlan.report;
|
|
960
1031
|
|
|
1032
|
+
const remainingArchify = ARCHIFY_RECORD in remainingFiles ? manifest.archify : null;
|
|
1033
|
+
if (manifest.archify && !remainingArchify) {
|
|
1034
|
+
archifyRelease = await planArchifyRelease({ record: manifest.archify, skillsRoot, writeAtomic, log });
|
|
1035
|
+
}
|
|
1036
|
+
|
|
961
1037
|
if (
|
|
962
1038
|
Object.keys(remainingFiles).length === 0 &&
|
|
963
1039
|
!hasLegacyProfiles &&
|
|
@@ -975,11 +1051,17 @@ export async function uninstallBundle({
|
|
|
975
1051
|
profiles: legacyProfiles,
|
|
976
1052
|
claudeSettings: { path: remainingClaudeSettingsPath },
|
|
977
1053
|
instructions: remainingInstructions,
|
|
1054
|
+
archify: remainingArchify,
|
|
978
1055
|
});
|
|
979
1056
|
}
|
|
1057
|
+
if (archifyRelease) await archifyRelease.finish();
|
|
980
1058
|
return summary;
|
|
981
1059
|
} catch (err) {
|
|
982
1060
|
const rollbackErrors = [];
|
|
1061
|
+
if (archifyRelease) {
|
|
1062
|
+
try { await archifyRelease.rollback(); }
|
|
1063
|
+
catch (restoreErr) { rollbackErrors.push(`archify owner: ${restoreErr.message}`); }
|
|
1064
|
+
}
|
|
983
1065
|
for (const write of claudeWritten.reverse()) {
|
|
984
1066
|
try {
|
|
985
1067
|
if (write.before === null) await rm(write.path);
|
package/src/manifest.js
CHANGED
|
@@ -80,6 +80,13 @@ function normalizeManifest(parsed) {
|
|
|
80
80
|
throw new Error('invalid ownership manifest: files must be an object of path hashes');
|
|
81
81
|
}
|
|
82
82
|
const rawClaudeSettings = parsed.claudeSettings ?? { path: null };
|
|
83
|
+
const archify = parsed.archify ?? null;
|
|
84
|
+
if (archify !== null && (typeof archify !== 'object' ||
|
|
85
|
+
typeof archify.path !== 'string' || !isAbsolute(archify.path) ||
|
|
86
|
+
!/^[a-f0-9]{40}$/.test(archify.sha) ||
|
|
87
|
+
!archify.path.endsWith(`/archify-${archify.sha}`))) {
|
|
88
|
+
throw new Error('invalid ownership manifest: archify must bind an absolute pinned copy');
|
|
89
|
+
}
|
|
83
90
|
if (
|
|
84
91
|
typeof rawClaudeSettings !== 'object' || rawClaudeSettings === null ||
|
|
85
92
|
Array.isArray(rawClaudeSettings) ||
|
|
@@ -118,6 +125,7 @@ function normalizeManifest(parsed) {
|
|
|
118
125
|
profiles: { path: null, preset: null, entries: { ...rawProfiles } },
|
|
119
126
|
claudeSettings: { path: rawClaudeSettings.path },
|
|
120
127
|
instructions,
|
|
128
|
+
...(archify ? { archify } : {}),
|
|
121
129
|
};
|
|
122
130
|
}
|
|
123
131
|
if (
|
|
@@ -137,6 +145,7 @@ function normalizeManifest(parsed) {
|
|
|
137
145
|
},
|
|
138
146
|
claudeSettings: { path: rawClaudeSettings.path },
|
|
139
147
|
instructions,
|
|
148
|
+
...(archify ? { archify } : {}),
|
|
140
149
|
};
|
|
141
150
|
}
|
|
142
151
|
throw new Error('invalid ownership manifest: profiles must bind a config path to id hashes');
|