@yargote/artifacts-cli 0.1.0-beta.5 → 0.1.0-beta.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.
package/README.md CHANGED
@@ -11,9 +11,9 @@ Use Node 24 or later. The beta targets macOS and Linux. Windows has not been ver
11
11
  Choose one persistent installation:
12
12
 
13
13
  ```sh
14
- npm install --global @yargote/artifacts-cli@0.1.0-beta.5
14
+ npm install --global @yargote/artifacts-cli@0.1.0-beta.7
15
15
  # Or, with pnpm and an existing global bin directory on PATH:
16
- pnpm add --global @yargote/artifacts-cli@0.1.0-beta.5
16
+ pnpm add --global @yargote/artifacts-cli@0.1.0-beta.7
17
17
  artifacts --version
18
18
  ```
19
19
 
@@ -22,18 +22,18 @@ If pnpm reports that no global bin directory exists, run `pnpm setup` and reopen
22
22
  For one-off execution, pin the same version:
23
23
 
24
24
  ```sh
25
- npx --yes @yargote/artifacts-cli@0.1.0-beta.5 --help
26
- pnpm dlx @yargote/artifacts-cli@0.1.0-beta.5 --help
25
+ npx --yes @yargote/artifacts-cli@0.1.0-beta.7 --help
26
+ pnpm dlx @yargote/artifacts-cli@0.1.0-beta.7 --help
27
27
  ```
28
28
 
29
29
  `@beta` follows future prereleases. Exact versions remain reproducible. No stable `latest` release is part of this beta. An organization's pnpm minimum release age or trust policy can delay a new release. Follow that policy rather than disabling it globally.
30
30
 
31
31
  ## Complete first use
32
32
 
33
- An existing Artifacts account is required. Select the complete OAuth resource, including `/mcp`:
33
+ An existing Artifacts account is required. Select the REST OAuth resource, which is the API origin without `/mcp`. When upgrading an older deployment, complete the [API migration](../../docs/deployment.md#migrate-the-api-hostname) before using the address below.
34
34
 
35
35
  ```sh
36
- export ARTIFACTS_SERVER='https://artifacts-mcp.yersonargotev.workers.dev/mcp'
36
+ export ARTIFACTS_SERVER='https://artifacts-api.yersonargotev.workers.dev'
37
37
  artifacts login
38
38
  artifacts whoami
39
39
  ```
@@ -64,7 +64,7 @@ artifacts export ARTIFACT_ID --revision REVISION_ID --out artifact.zip
64
64
  artifacts logout
65
65
  ```
66
66
 
67
- Publication URLs require authenticated owner access. A retained request ID appears on stderr before each mutation. If the response is lost, run `artifacts retry REQUEST_ID` with the same server and state directory. Keep that state directory when upgrading, reinstalling, or switching between global and one-off execution.
67
+ A publication URL requires sign-in. It opens for the owner and for a user whose verified email equals an email share of the artifact. That user sees only the published revision and its attachments. A retained request ID appears on stderr before each mutation. If the response is lost, run `artifacts retry REQUEST_ID` with the same server and state directory. Keep that state directory when upgrading, reinstalling, or switching between global and one-off execution.
68
68
 
69
69
  ## Upgrade or uninstall
70
70
 
@@ -82,7 +82,9 @@ Run `artifacts logout` before uninstalling if you want to clear credentials. Uni
82
82
 
83
83
  Skills, help, and version commands work without a server or login. Skills commands do not read credentials or create private state. They accept and ignore global `--server` and `--state-dir` values, including their environment defaults.
84
84
 
85
- `--server RESOURCE` selects the exact canonical OAuth resource. `ARTIFACTS_SERVER` supplies the same value. A resource ending in `/mcp` uses REST under `/mcp/v1`. The CLI has no production default.
85
+ `--server RESOURCE` selects the exact canonical REST OAuth resource. `ARTIFACTS_SERVER` supplies the same value. Use the configured `API_URL` origin to call REST under `/v1`. The CLI has no production default and does not rewrite the supplied URL. Older deployments without `API_URL` still use their complete MCP resource and routes under `/mcp/v1`.
86
+
87
+ A hostname change requires a new `artifacts login --server RESOURCE`. Credentials and retained operations remain bound to their exact original resource. Do not move an old operation file to the new resource or redirect its bearer requests.
86
88
 
87
89
  `--state-dir DIR` overrides `ARTIFACTS_STATE_DIR`. The default is `~/.local/state/artifacts`. The state directory must be private and owned by the current user. Credentials are separate from operation files. Profiles bind credentials to the exact resource. Retained operation paths hash the resource and request ID, so request IDs cannot select filesystem paths.
88
90
 
@@ -124,6 +126,9 @@ artifacts publish [ID] --command FILE
124
126
  artifacts export ID --revision REV --out FILE
125
127
  artifacts move ID --project NAME [--request-id ID]
126
128
  artifacts move ID --no-project [--request-id ID]
129
+ artifacts share ID --email EMAIL [--notify] [--request-id ID]
130
+ artifacts shares ID
131
+ artifacts unshare ID SHARE_ID [--request-id ID]
127
132
  artifacts retry REQUEST_ID
128
133
  artifacts skills [list]
129
134
  artifacts skills get NAME [--full]
@@ -197,6 +202,27 @@ Every artifact read and list item includes `project`, a name or null. `projects`
197
202
 
198
203
  Version `0.1.0-beta.4` and earlier parse artifact responses strictly and fail with `invalid_response` against a server that returns `project`. Upgrade the CLI together with the server.
199
204
 
205
+ ## Share publication access
206
+
207
+ These commands are available from `0.1.0-beta.6`. Check installed help for support.
208
+
209
+ ```sh
210
+ artifacts share ARTIFACT_ID --email ana@example.com --request-id share-ana-001
211
+ artifacts shares ARTIFACT_ID
212
+ artifacts unshare ARTIFACT_ID SHARE_ID --request-id unshare-ana-001
213
+ artifacts retry share-ana-001
214
+ ```
215
+
216
+ `share` trims and lowercases the email and prints one share as JSON with `shareId`, `artifactId`, `kind: "email"`, `value`, and `createdAt`. `shares` prints `{ "items": [...] }` with current grants and retains no operation. Take `SHARE_ID` from that output. `unshare` and a successful retry of an unshare exit zero with empty stdout. All three commands require the owner's `artifacts:publish` scope.
217
+
218
+ A signed-in user whose verified email matches a share can view the published revision and its attachments. History, export, edits, and project moves remain owner-only. Shares created before first publication remain stored, but the URL opens only after publication. Add `--notify` to request one invitation email. Invited people can sign in with an email code when the server enables email.
219
+
220
+ Share and unshare retain the complete normalized command before submission. Recover an uncertain result with `retry REQUEST_ID`. A replay confirms the original operation, so a historical share replay does not restore a revoked grant. An old unshare replay leaves a deliberate new share intact. Run `shares ARTIFACT_ID` to verify current grants.
221
+
222
+ Share with `--notify` to request one email attempt. Access succeeds when mail is disabled or the owner has used the daily allowance. The JSON result adds `notification.status` as `requested` or `skipped`, with `disabled` or `rate_limited` as the skip reason. `requested` does not confirm delivery. A crash after commit can lose the message. An exact retry preserves the captured flag and never sends a second email. Use a new deliberate command to request another attempt for existing access.
223
+
224
+ Owners can reserve 100 invitations per UTC day. Revoke, deletion, and failed delivery do not refund reservations. New access is limited to 100 people per artifact and 1000 grants per owner. Existing access remains listable and revocable.
225
+
200
226
  ## Complete commands and edits
201
227
 
202
228
  `--command FILE` accepts a complete create, update, or publish command. It cannot be combined with other mutation options. For update and publish, an optional positional artifact ID must equal the command's `artifactId`. Every field, including the omission of `response`, is retained.
@@ -228,7 +254,7 @@ Publish always selects an immutable revision. `--expected-version 0` expects no
228
254
 
229
255
  Each invocation resolves one access token for its reads, owner check, and submission. A concurrent login cannot change the command owner between these requests. Every mutation verifies the owner and retains an exclusive private operation file before submission. Missing request IDs default to random UUIDs. The operation stores its exact command, owner, and resource. It contains no credentials. Existing operation files require explicit `retry` and are never replaced by another preparation.
230
256
 
231
- `retry REQUEST_ID` verifies the current resource and owner, then sends the unchanged command once. Retry does not read a new head, change expected versions, add a response projection, or rebase a conflict. Its diagnostic identifies the result as a retained receipt. A fresh `read` returns current state.
257
+ `retry REQUEST_ID` verifies the current resource and owner, then sends the unchanged command once. Retry does not read a new head, change expected versions, add a response projection, or rebase a conflict. Its diagnostic identifies replay of the original operation and warns that replay does not establish current state. A fresh `read` or `shares` returns current state.
232
258
 
233
259
  If a response is lost after commit, retry recovers the original receipt. If a conflict occurs, the retained operation remains available for inspection. A new read and a deliberate new command with a new request ID are required to incorporate another writer's changes.
234
260
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yargote/artifacts-cli",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Command-line client for the Artifacts REST API.",
@@ -27,8 +27,8 @@
27
27
  "skills"
28
28
  ],
29
29
  "dependencies": {
30
- "@yargote/artifacts-contracts": "0.1.0-beta.5",
31
- "@yargote/artifacts-client": "0.1.0-beta.5",
30
+ "@yargote/artifacts-contracts": "0.1.0-beta.7",
31
+ "@yargote/artifacts-client": "0.1.0-beta.7",
32
32
  "openid-client": "6.8.8",
33
33
  "zod": "4.6.5"
34
34
  },
@@ -17,11 +17,11 @@ Run `artifacts whoami` before the first mutation when the current identity is no
17
17
 
18
18
  A revision is a complete snapshot with `metadata`, `content`, and `files`. The command reference contains its JSON shape. Use the requested title, language, content, and attachments. Do not include unrelated files or credentials.
19
19
 
20
- Use self-contained HTML for interactive content. Inline styles and scripts and embed required assets as data URLs. External requests, CDN dependencies, relative executable assets, forms, browser storage, and multi-file websites are unsupported. Attachments are downloads, not importable website resources. Markdown and text display inertly.
20
+ Use self-contained HTML for interactive content. For several HTML boards in one canvas, use `design` and follow the design skill's `references/canvas.md`. Inline styles and scripts and embed required assets as data URLs. External requests, CDN dependencies, relative executable assets, forms, browser storage, and multi-file websites are unsupported. Attachments are downloads, not importable website resources. Markdown and text display inertly.
21
21
 
22
22
  Keep the whole revision within 1 MiB of decoded content, metadata, attachment paths, media types, and attachment bytes. At most 32 attachments are accepted. Base64 changes wire size, not the decoded limit. If required content exceeds a limit, explain the constraint without silently removing it.
23
23
 
24
- Before writing or restyling HTML, run `artifacts skills get artifacts-design --full` and follow it. It owns typography, embedded fonts, color, layout, and the rendered checks. Finish those checks before saving when browser tools are available. A successful CLI write proves persistence, not visual quality or browser compatibility. Interactive HTML follows the server's Chrome policy; do not promise it in every browser.
24
+ Before writing or restyling HTML or design boards, run `artifacts skills get artifacts-design --full` and follow it. It owns typography, embedded fonts, color, layout, and the rendered checks. Finish those checks before saving when browser tools are available. A successful CLI write proves persistence, not visual quality or browser compatibility. Interactive HTML follows the server's Chrome policy; do not promise it in every browser.
25
25
 
26
26
  ## Create or revise
27
27
 
@@ -41,7 +41,17 @@ Use `move ID --project NAME` to file an existing artifact under a project and `m
41
41
 
42
42
  Publish when the user requests publication, a shareable artifact URL, or a change to the live artifact. Use the exact saved revision selected by the user or returned by the edit. Read the current publication version before preparing publication. Use zero only when the artifact is unpublished.
43
43
 
44
- A published URL still requires its owner's Artifacts login. Publishing a historical revision leaves the draft head unchanged. Report the returned protected URL only after the publication succeeds and its current state is verified.
44
+ A published URL requires sign-in by the owner or a user whose verified email matches a current share. Publishing a historical revision leaves the draft head unchanged. Report the returned protected URL only after the publication succeeds and its current state is verified.
45
+
46
+ ## Share publication access
47
+
48
+ Check installed help for `share`, `shares`, and `unshare` support. These commands are available from `0.1.0-beta.6`. Share only when the user requests access for an exact email. Use `share ARTIFACT_ID --email EMAIL`, then verify the normalized email with `shares ARTIFACT_ID`. Use the observed `shareId` with `unshare ARTIFACT_ID SHARE_ID` to revoke it. Unshare and its successful retry exit zero with empty stdout.
49
+
50
+ A matching signed-in user must have a verified email. Access covers the published revision and its attachments. History, export, editing, and project moves stay owner-only. A share before first publication remains stored and waits for publication. Add `--notify` when the user asks for an invitation email. Invited people can sign in with an email code when the server enables email.
51
+
52
+ Recover uncertain share or unshare results with `retry REQUEST_ID`. Replay confirms the original operation and does not establish current grants. An old share replay does not restore revoked access. An old unshare replay does not remove a deliberate new share. Verify current grants with `shares ARTIFACT_ID`.
53
+
54
+ When the user asks to notify an invited person, add `--notify` to `artifacts share`. Preserve that flag in the retained command and use `artifacts retry` after an uncertain response. Report `requested` as an invitation request, never as confirmed delivery. A skipped notification still grants access. A new deliberate share command can request another attempt for existing access, subject to the owner's daily allowance.
45
55
 
46
56
  ## Recover an uncertain write
47
57
 
@@ -53,10 +63,12 @@ A `request_conflict` means the ID was used with different arguments. Do not desc
53
63
 
54
64
  ## Verify and deliver
55
65
 
56
- After create or update, read the explicit returned revision. Use `read ARTIFACT_ID --revision REVISION_ID --out FILE --quiet` to retain the full response without printing its snapshot. Check that the installed `--help` supports `--quiet` first. For older versions, use the command workflow's stdout redirection fallback. Compare the saved complete metadata, content, and decoded attachment bytes with the intended snapshot. For an update, check that its parent is the head used for preparation. Compact responses cannot verify omitted content.
66
+ Retain successful create, update, or edit stdout in a private receipt file. After create or update, read the explicit returned revision. Use `read ARTIFACT_ID --revision REVISION_ID --out FILE --quiet` to retain the full response without printing its snapshot. Check that the installed `--help` supports `--quiet` first. For older versions, use the command workflow's stdout redirection fallback. Compare the saved complete metadata, content, and decoded attachment bytes with the intended snapshot. For an update, check that its parent is the head used for preparation. Compact responses cannot verify omitted content.
57
67
 
58
68
  After publication, read current publication and compare revision, version, and URL with the committed result. A replayed receipt describes the original commit; another writer may have changed the current publication afterward. Report that difference.
59
69
 
70
+ For real panel and requested protected-publication checks, follow [the exact saved revision recipe](references/verify-retained.md). It prepares the existing verifier inputs from the write receipt and independent full read. It also gives an installed-browser route when the complete verifier runtime is unavailable. Report persistence, local rendering, panel rendering, and protected publication separately. Input preparation is not a browser pass.
71
+
60
72
  Follow list and history cursors when the request needs more than the first page. Export an explicit retained revision to the requested destination. The CLI checks the archive digest. Confirm that the file exists and inspect its manifest and relevant content before linking it.
61
73
 
62
74
  Report the title, saved revision, and draft or published state in the user's language. Link the actual export or returned protected publication URL. Distinguish persistence from any visual or browser checks that did not run. Keep local credential and session identifiers private.
@@ -61,7 +61,7 @@ Save this JSON as `snapshot.json`, replacing its content with the requested arti
61
61
  }
62
62
  ```
63
63
 
64
- `metadata.description` and `files` are required even when empty. Content kind is `html`, `markdown`, or `text`. Binary files use `encoding: "base64"` and canonical padded base64. Paths are unique normalized relative paths. Do not use empty, dot, parent, absolute, backslash, percent, or colon path segments. A file cannot also be another file's directory.
64
+ `metadata.description` and `files` are required even when empty. Content kind is `html`, `markdown`, `text`, or `design`. Binary files use `encoding: "base64"` and canonical padded base64. Paths are unique normalized relative paths. Do not use empty, dot, parent, absolute, backslash, percent, or colon path segments. A file cannot also be another file's directory.
65
65
 
66
66
  ```sh
67
67
  artifacts create --snapshot snapshot.json
@@ -75,6 +75,20 @@ For `read`, `--out FILE --quiet` saves the returned response without printing su
75
75
 
76
76
  Check installed help for `--quiet` support before running these examples. Older versions, including `0.1.0-beta.1`, can suppress stdout with `artifacts read ARTIFACT_ID --out saved.json > /dev/null`. This redirection preserves stderr and the command's exit status. Use the same fallback for the revision reads below.
77
77
 
78
+ ## Prepare a design canvas
79
+
80
+ Read the design skill's `references/canvas.md` through `artifacts skills get artifacts-design --full`. Keep the index in `my-canvas/canvas.json` and complete UTF-8 board documents in `my-canvas/boards/*.html`.
81
+
82
+ ```sh
83
+ artifacts skills path artifacts-cli
84
+ node <returned-skill-directory>/scripts/prepare-retained.mjs ./my-canvas > snapshot.json
85
+ artifacts create --snapshot snapshot.json
86
+ ```
87
+
88
+ The positional form accepts exactly one directory. It preserves the index text and board HTML, sorts snapshot files by path, validates the complete `design` snapshot with the installed contracts, and rejects missing or orphan boards. It uses the directory basename as the title and an empty description. Edit the emitted metadata before saving when needed. Keep the script under the path returned by `skills path` so its package dependencies resolve. All board data, embedded fonts, index text, and metadata share the 1 MiB budget.
89
+
90
+ The existing named-option form of `prepare-retained.mjs` builds a private retained-verification config from saved receipts and an independent read. It does not prepare a content snapshot. The two input forms cannot be combined.
91
+
78
92
  ## Organize projects
79
93
 
80
94
  Check that installed help lists `projects` and `move` before using them.
@@ -158,6 +172,31 @@ The publication example applies only to an unpublished artifact. For an existing
158
172
 
159
173
  History returns `nextBeforeSequence`; pass it as `--before-sequence N` to continue. List returns `nextCursor`; pass it as `--cursor CURSOR`. Do not substitute an offset or assume that one page contains everything.
160
174
 
175
+ ## Share publication access
176
+
177
+ Check installed help for these commands. They are available from `0.1.0-beta.6`.
178
+
179
+ ```sh
180
+ artifacts share ARTIFACT_ID --email ana@example.com --request-id share-ana-001
181
+ artifacts shares ARTIFACT_ID
182
+ artifacts unshare ARTIFACT_ID SHARE_ID --request-id unshare-ana-001
183
+ artifacts retry share-ana-001
184
+ ```
185
+
186
+ Share requires the owner's `artifacts:publish` scope, trims and lowercases the email, and prints a share with `shareId`, `artifactId`, `kind: "email"`, `value`, and `createdAt`. `shares` requires the same scope and prints `{ "items": [...] }` with current grants without retaining a mutation. Use an observed `shareId` to unshare. Unshare requires the same scope. Unshare and its successful retry exit zero with empty stdout.
187
+
188
+ The publication admits the owner and signed-in users whose verified email matches a current share. Guests see the published revision and attachments. History, export, editing, and project moves remain owner-only. Shares before first publication wait for publication. Add `--notify` to request one invitation email. Invited people can sign in with an email code when the server enables email.
189
+
190
+ Share and unshare retain complete normalized commands. `retry REQUEST_ID` replays the original operation. A historical share replay does not regrant revoked access, and an old unshare replay leaves a deliberate new share intact. Verify current grants with `shares ARTIFACT_ID` after a replay.
191
+
192
+ Share with `--notify` to request one email attempt. Access succeeds when mail is disabled or the owner has used the daily allowance. The JSON result adds `notification.status` as `requested` or `skipped`, with `disabled` or `rate_limited` as the skip reason. `requested` does not confirm delivery. A crash after commit can lose the message. An exact retry preserves the captured flag and never sends a second email. Use a new deliberate command to request another attempt for existing access.
193
+
194
+ Owners can reserve 100 invitations per UTC day. Revoke, deletion, and failed delivery do not refund reservations. New access is limited to 100 people per artifact and 1000 grants per owner. Existing access remains listable and revocable.
195
+
196
+ ## Verify the exact saved revision
197
+
198
+ Follow [the retained revision recipe](verify-retained.md) after successful authoring and requested publication. Retain each successful write stdout in a private receipt file. The recipe extracts its IDs, performs a full independent explicit-revision read, and prepares the existing verifier config without authoring or account mutations. It includes an installed-browser path and identifies the additional capabilities for complete verification.
199
+
161
200
  ## Retry and export
162
201
 
163
202
  ```sh
@@ -0,0 +1,119 @@
1
+ # Verify the saved revision in the panel
2
+
3
+ Use the successful create, update, or edit result and an independent full read to select the exact saved revision. Use an authorized browser session to inspect its real panel and, when requested, its protected publication. Preparation alone does not prove persistence or browser behavior.
4
+
5
+ ## Retain the authoring results
6
+
7
+ Before the authorized write, use `umask 077` and save successful stdout to a new private receipt file. For example, add `> write-result.json` to the create or update command you already intend to run. Keep stderr and the retained request ID for recovery. Continue only after exit zero. Do not repeat a write to obtain a receipt after an uncertain response. Follow [retry recovery](commands.md#retry-and-export).
8
+
9
+ If publication was requested, also retain the successful publish stdout as `publish-result.json`. Finish that authorized publication before the independent read below. Do not publish, share, or revoke access merely to verify an artifact.
10
+
11
+ ## Prepare the exact selection
12
+
13
+ Use Node 24+ and a CLI whose help lists `skills path` and `read --quiet`. For older read output, use the [stdout fallback](commands.md#create-a-complete-snapshot). The installed helper requires only Node built-ins. It makes no service request and does not open a browser.
14
+
15
+ Set `observed_panel_origin` to the panel origin observed in the authorized browser or supplied service configuration. Do not derive it from `ARTIFACTS_SERVER` or the publication host. Replace the example origin before execution.
16
+
17
+ ```sh
18
+ umask 077
19
+ observed_panel_origin=https://panel.example.test
20
+ skill_directory=$(artifacts skills path artifacts-cli)
21
+ artifact_id=$(node -e '
22
+ try {
23
+ const id = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).artifactId;
24
+ if (typeof id !== "string" || id.length === 0) throw new Error();
25
+ process.stdout.write(id);
26
+ } catch {
27
+ console.error("Cannot extract the artifact ID from the write receipt");
28
+ process.exitCode = 1;
29
+ }
30
+ ' write-result.json)
31
+ revision_id=$(node -e '
32
+ try {
33
+ const id = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).revision.id;
34
+ if (typeof id !== "string" || id.length === 0) throw new Error();
35
+ process.stdout.write(id);
36
+ } catch {
37
+ console.error("Cannot extract the revision ID from the write receipt");
38
+ process.exitCode = 1;
39
+ }
40
+ ' write-result.json)
41
+ artifacts read "$artifact_id" --revision "$revision_id" --response full --out saved-read.json --quiet
42
+ ```
43
+
44
+ Stop if any extraction or read fails. A compact write receipt can select a revision. The independent read must contain the complete snapshot. Compare its complete metadata, content, and decoded attachment bytes with the intended snapshot. For an update, check the saved parent against the head used to prepare that update.
45
+
46
+ Save `checks.json` with the requested viewport sizes and meaningful expectations for this document. Replace the example selectors and values with intended content behavior. Expectations run in order inside the content iframe after a fresh load at each viewport.
47
+
48
+ ```json
49
+ {
50
+ "viewports": [{ "width": 1440, "height": 900 }, { "width": 390, "height": 844 }],
51
+ "expectations": [
52
+ { "kind": "text", "selector": "#total", "text": "5.00" },
53
+ { "kind": "click", "selector": "#change" },
54
+ { "kind": "text", "selector": "#total", "text": "28.40" }
55
+ ],
56
+ "timeoutMs": 10000
57
+ }
58
+ ```
59
+
60
+ Use `text` for exact rendered text, `visible` for visibility, `count` for an element count, or `attribute` for an exact attribute value. Use `name` and `value` with `attribute`. Interactions also support `fill` with `value`, `check` with `checked`, and `select` with a `values` array. Do not use arbitrary JavaScript or target panel controls in these expectations.
61
+
62
+ Prepare a new private config with a future evidence directory that does not yet exist.
63
+
64
+ ```sh
65
+ node "$skill_directory/scripts/prepare-retained.mjs" \
66
+ --receipt write-result.json --read saved-read.json \
67
+ --panel-origin "$observed_panel_origin" --checks checks.json \
68
+ --evidence "$PWD/new-retained-evidence" --out retained-config.json
69
+ ```
70
+
71
+ When publication was requested, add `--publication-receipt publish-result.json` to that invocation. The helper compares its artifact, revision, version, and URL with the current publication in the saved read. A different publication or concurrent change fails preparation. Report the difference. Do not replace expected versions or publish again to make verification pass.
72
+
73
+ The helper derives all selected IDs and URLs from these inputs. It accepts a newer draft head while keeping the saved revision pinned and reports `headChanged: true`. It emits `status: "prepared"`, never a verification pass. Without a publication receipt, it omits publication coverage even when another revision is published. Existing output files are preserved.
74
+
75
+ ## Inspect with an authorized browser
76
+
77
+ This route works with the installed CLI, its bundled files, and browser-control capabilities. It requires no repository checkout. Use a user-supplied session or complete the normal sign-in flow. CLI OAuth login does not supply panel cookies. Keep browser authentication inside the browser. Never extract credentials to construct a Playwright context or state file.
78
+
79
+ After sign-in setup, perform these read-only checks on the selected artifact. Do not create revisions, publish, share, log out, or revoke sessions. Normal protected-viewer admission can consume its one-use grant through the existing GET handoff.
80
+
81
+ 1. Read `retained-config.json` privately and open its exact `panelUrl`. Compare the artifact, revision, digest, and publication version with `expected`. Current panels expose `data-artifact-id`, `data-revision-id`, `data-revision-sha256`, and `data-publication-version`. Version zero means no publication. Report a newer head separately. Never switch the selected revision to the head.
82
+ 2. Establish the actual browser version and available frame, response, console, download, and viewport controls. Record missing capabilities. A browser name, load event, or empty console does not establish that content loaded.
83
+ 3. Inspect the actual preview iframe source, pinned revision, sandbox, referrer policy, and dimensions. Wait for a distinctive expected element within the configured deadline. Exercise each authorized content control and record the resulting visible state. Repeat at each available requested viewport and check keyboard interactions and overflow where relevant.
84
+ 4. When response inspection or native DevTools is available, inspect the iframe request. Record completion, status, content type, CSP, connection allowlist, cache policy, and DNS policy. Distinguish interactive content, inert fallback, and error responses. A pending request has no final response evidence. A top-level preview navigation deliberately returns 403 and cannot replace this iframe check.
85
+ 5. When the config includes `publicationUrl`, open that separate protected viewer through normal admission. Compare its selected revision, digest, and version with `expected`. Observe the real content and repeat the authorized interactions. Open `Archivos` and compare the complete attachment inventory and revision-bound links. Check protected download bytes when that capability is available. Missing attachment or response evidence remains unavailable.
86
+ 6. After publication interactions, reread the same explicit revision with the quoted IDs into a new private file. Compare its current `publication.revisionId`, `version`, and `url` with the saved read and publish receipt. Reload the selected panel and compare its publication markers again. Report a changed publication as a concurrent change. Do not report protected-publication success for a different selected revision.
87
+ 7. If the frame is blank, distinguish admission, pending transport, policy, browser compatibility, content execution, and browser-control failures from observed evidence. If debugger attachment stalls it, disconnect through normal browser controls, reload the same pinned page, and repeat the interaction with native controls. Preserve authentication and containment. Attribute a debugger failure only when that comparison demonstrates it.
88
+ 8. Save a sanitized report and private screenshots. Include selected IDs, digest, browser version, viewport sizes, actions, resulting visible states, observed response policies, and unavailable checks. Exclude cookies, tokens, viewer grants, callback queries, raw authentication requests, and private content from diagnostic logs.
89
+
90
+ This is partial browser verification. Do not return the complete verifier's pass verdict. Missing exact bytes, ZIP, policy, attachment, console, or viewport evidence must remain explicit.
91
+
92
+ ## Run the existing complete verifier when available
93
+
94
+ The CLI package does not install Playwright, `tsx`, Chrome, or the repository's verifier and rendering-policy modules. Complete automated verification needs an operator-provided installation of that existing verifier with its dependencies and actual supported Chrome. It also needs an explicitly supplied authenticated Playwright `BrowserContext` or owner-private storage-state file prepared through an approved sign-in flow.
95
+
96
+ For repository operators, run this command from the repository root. The paths in this command are repository capabilities, not paths inside an installed CLI package.
97
+
98
+ ```sh
99
+ node --import tsx .agents/skills/verify-artifacts/scripts/verify-retained.ts \
100
+ --config /private/path/retained-config.json \
101
+ --storage-state /private/path/browser-state.json
102
+ ```
103
+
104
+ Pass the prepared config unchanged. The existing verifier validates the full schema and canonical snapshot hash. It checks pinned responses, markers, policies, interactions, ZIP, attachment bytes, viewports, and publication state before and after viewer checks. It writes private evidence and fails on mismatches or concurrent publication changes. Use its callable `verifyRetainedArtifact(context, config)` when an authorized context is supplied. Do not build another viewer or weaken sandbox policies to establish deployed behavior.
105
+
106
+ If this runtime or authenticated input is unavailable, use the partial route above and name that limit. If browser access is unavailable too, report only the persistence checks actually completed.
107
+
108
+ ## Deliver the observed outcomes
109
+
110
+ Report each outcome separately, with its evidence and missing checks.
111
+
112
+ | Outcome | Evidence needed |
113
+ | --- | --- |
114
+ | Persistence | Full independent explicit-revision read and complete intended-snapshot comparison. Name any exact export check separately. |
115
+ | Local rendering | Observed local document layout and interactions. This does not prove hosted rendering. |
116
+ | Panel rendering | Exact real panel selection, pinned iframe, visible content, and observed interaction. Name missing response, policy, byte, console, or viewport checks. |
117
+ | Protected publication | Separate authorized viewer admission for the selected published revision, visible content, interactions, and attachment checks. Report concurrent changes and unavailable checks. |
118
+
119
+ Use `passed`, `failed`, `unavailable`, or `not requested` for each outcome as the evidence permits. Keep session identifiers and private evidence private. A successful helper invocation establishes only input preparation.
@@ -0,0 +1,149 @@
1
+ import { lstat, readFile, readdir, writeFile } from 'node:fs/promises';
2
+ import { basename, join, resolve } from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ function requireValue(condition, message) {
7
+ if (!condition) throw new Error(message);
8
+ }
9
+
10
+ function object(value) {
11
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
12
+ }
13
+
14
+ function prepareRetainedSelection({ receipt, expected, checks, panelOrigin, evidenceDirectory, publicationReceipt }) {
15
+ requireValue(object(receipt) && object(receipt.revision), 'Write receipt must identify a saved revision');
16
+ requireValue(
17
+ typeof receipt.artifactId === 'string' && receipt.artifactId.length > 0 &&
18
+ typeof receipt.revision.id === 'string' && receipt.revision.id.length > 0 &&
19
+ /^[a-f0-9]{64}$/.test(receipt.revision.sha256),
20
+ 'Write receipt must contain artifact, revision, and SHA-256',
21
+ );
22
+ const snapshot = expected?.revision?.snapshot;
23
+ requireValue(
24
+ object(expected) && object(snapshot) && object(snapshot.metadata) &&
25
+ typeof snapshot.metadata.title === 'string' && typeof snapshot.metadata.description === 'string' &&
26
+ object(snapshot.content) && ['html', 'markdown', 'text', 'design'].includes(snapshot.content.kind) &&
27
+ typeof snapshot.content.text === 'string' && Array.isArray(snapshot.files),
28
+ 'Independent read must contain the full snapshot, not a compact response',
29
+ );
30
+ requireValue(expected.artifactId === receipt.artifactId && expected.revision.id === receipt.revision.id && expected.revision.sha256 === receipt.revision.sha256, 'Independent read differs from the saved write receipt');
31
+ requireValue(typeof expected.headRevisionId === 'string' && expected.headRevisionId.length > 0, 'Independent read must identify its current draft head');
32
+ requireValue(object(checks) && Object.keys(checks).every((key) => ['viewports', 'expectations', 'timeoutMs'].includes(key)) && Array.isArray(checks.viewports) && checks.viewports.length > 0 && Array.isArray(checks.expectations), 'Checks must contain viewports and expectations, with optional timeoutMs');
33
+ let panel;
34
+ try { panel = new URL(panelOrigin); } catch { throw new Error('Panel origin must be an observed HTTP origin'); }
35
+ requireValue(['http:', 'https:'].includes(panel.protocol) && !panel.username && !panel.password && panel.pathname === '/' && !panel.search && !panel.hash, 'Panel origin must be an observed HTTP origin without a path, credentials, query, or fragment');
36
+ panel.pathname = `/artifacts/${encodeURIComponent(expected.artifactId)}`;
37
+ panel.searchParams.set('revisionId', expected.revision.id);
38
+ let publicationUrl;
39
+ if (publicationReceipt !== undefined) {
40
+ const current = expected.publication;
41
+ requireValue(
42
+ object(publicationReceipt) && object(current) &&
43
+ publicationReceipt.artifactId === expected.artifactId &&
44
+ publicationReceipt.revisionId === expected.revision.id && publicationReceipt.revisionId === current.revisionId &&
45
+ Number.isInteger(publicationReceipt.version) && publicationReceipt.version > 0 && publicationReceipt.version === current.version &&
46
+ typeof publicationReceipt.url === 'string' && publicationReceipt.url === current.url,
47
+ 'Publication differs from the requested receipt or selected revision; report the changed or different publication',
48
+ );
49
+ let publication;
50
+ try { publication = new URL(current.url); } catch { throw new Error('Publication receipt must contain a stable HTTP URL'); }
51
+ requireValue(['http:', 'https:'].includes(publication.protocol) && !publication.username && !publication.password && !publication.search && !publication.hash && publication.pathname === `/a/${encodeURIComponent(expected.artifactId)}`, 'Publication receipt must contain the selected artifact stable HTTP URL');
52
+ publicationUrl = publication.href;
53
+ }
54
+ return {
55
+ expected,
56
+ panelUrl: panel.href,
57
+ ...(publicationUrl === undefined ? {} : { publicationUrl }),
58
+ ...checks,
59
+ evidenceDirectory: resolve(evidenceDirectory),
60
+ };
61
+ }
62
+
63
+ async function canvasContracts() {
64
+ const name = '@yargote/artifacts-contracts';
65
+ let path;
66
+ try { path = createRequire(import.meta.url).resolve(name); }
67
+ catch (error) {
68
+ if (error.code !== 'MODULE_NOT_FOUND') throw error;
69
+ path = createRequire(new URL('../../../apps/cli/package.json', import.meta.url)).resolve(name);
70
+ }
71
+ return import(pathToFileURL(path).href);
72
+ }
73
+
74
+ async function prepareCanvasDirectory(input) {
75
+ const directory = resolve(input);
76
+ requireValue((await lstat(directory)).isDirectory(), 'Canvas input must be a directory');
77
+ const { canvasIndexSchema, snapshotSchema } = await canvasContracts();
78
+ const indexFile = join(directory, 'canvas.json');
79
+ const indexInfo = await lstat(indexFile);
80
+ requireValue(indexInfo.isFile(), 'canvas.json must be a regular file');
81
+ requireValue(indexInfo.size <= 1_048_576, 'canvas.json exceeds the shared 1 MiB revision budget');
82
+ const indexBytes = await readFile(indexFile);
83
+ const text = indexBytes.toString('utf8');
84
+ requireValue(Buffer.from(text, 'utf8').equals(indexBytes), 'canvas.json must contain valid UTF-8');
85
+ const index = canvasIndexSchema.parse(JSON.parse(text));
86
+ const boardDirectory = join(directory, 'boards');
87
+ requireValue((await lstat(boardDirectory)).isDirectory(), 'boards must be a directory, not a symbolic link');
88
+ const entries = await readdir(boardDirectory, { withFileTypes: true });
89
+ for (const entry of entries) {
90
+ const path = `boards/${entry.name}`;
91
+ requireValue(entry.isFile(), `Canvas board ${path} must be a regular file`);
92
+ requireValue(Object.hasOwn(index.boards, path), `Orphan canvas board ${path} is absent from canvas.json`);
93
+ }
94
+ const files = [];
95
+ for (const path of [...index.order].sort()) {
96
+ const filename = join(directory, path);
97
+ let info;
98
+ try { info = await lstat(filename); }
99
+ catch (error) {
100
+ if (error.code === 'ENOENT') throw new Error(`Canvas board ${path} has no matching file`);
101
+ throw error;
102
+ }
103
+ requireValue(info.isFile(), `Canvas board ${path} must be a regular file`);
104
+ requireValue(info.size <= 1_048_576, `Canvas board ${path} exceeds the shared 1 MiB revision budget`);
105
+ const bytes = await readFile(filename);
106
+ const data = bytes.toString('utf8');
107
+ requireValue(Buffer.from(data, 'utf8').equals(bytes), `Canvas board ${path} must contain valid UTF-8`);
108
+ files.push({ path, mediaType: 'text/html', encoding: 'utf8', data });
109
+ }
110
+ return snapshotSchema.parse({
111
+ metadata: { title: basename(directory), description: '' },
112
+ content: { kind: 'design', text },
113
+ files,
114
+ });
115
+ }
116
+
117
+ const required = ['--receipt', '--read', '--panel-origin', '--checks', '--evidence', '--out'];
118
+ try {
119
+ const args = process.argv.slice(2);
120
+ if (args.length === 1 && !args[0].startsWith('--')) {
121
+ console.log(JSON.stringify(await prepareCanvasDirectory(args[0]), null, 2));
122
+ } else {
123
+ const options = new Map();
124
+ requireValue(args.length % 2 === 0, 'Use named options with one value each');
125
+ for (let index = 0; index < args.length; index += 2) {
126
+ const name = args[index];
127
+ requireValue([...required, '--publication-receipt'].includes(name) && !options.has(name) && typeof args[index + 1] === 'string' && args[index + 1].length > 0, 'Use each supported option once with a nonempty value');
128
+ options.set(name, args[index + 1]);
129
+ }
130
+ requireValue(required.every((name) => options.has(name)), 'Required options are --receipt --read --panel-origin --checks --evidence --out');
131
+ const json = async (name) => {
132
+ try { return JSON.parse(await readFile(options.get(name), 'utf8')); }
133
+ catch { throw new Error(`Cannot read valid JSON for ${name}`); }
134
+ };
135
+ const receipt = await json('--receipt');
136
+ const expected = await json('--read');
137
+ const checks = await json('--checks');
138
+ const publication = options.has('--publication-receipt') ? await json('--publication-receipt') : undefined;
139
+ const config = prepareRetainedSelection({ receipt, expected, checks, panelOrigin: options.get('--panel-origin'), evidenceDirectory: options.get('--evidence'), publicationReceipt: publication });
140
+ try { await writeFile(options.get('--out'), `${JSON.stringify(config, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); }
141
+ catch (error) { throw new Error(error?.code === 'EEXIST' ? 'Config output already exists; choose a new output path' : 'Cannot create the private config output'); }
142
+ const headChanged = expected.headRevisionId !== expected.revision.id;
143
+ console.log(JSON.stringify({ status: 'prepared', headChanged, publication: publication === undefined ? 'not-requested' : 'requested' }));
144
+ if (headChanged) console.error('Current draft head differs from the selected saved revision. The selection remains pinned.');
145
+ }
146
+ } catch (error) {
147
+ console.error(error.message);
148
+ process.exitCode = 1;
149
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: artifacts-design
3
- description: Design the HTML document of a retained Artifact with embedded fonts, light and dark Catppuccin tokens, purposeful layout, and rendered checks. Use before writing or restyling HTML for the artifacts or artifacts-cli skills.
3
+ description: Design HTML documents and multi-board design canvases of a retained Artifact with embedded fonts, light and dark Catppuccin tokens, purposeful layout, and rendered checks. Use before writing or restyling HTML or design boards for the artifacts or artifacts-cli skills.
4
4
  ---
5
5
 
6
6
  # Design HTML artifacts
@@ -9,6 +9,8 @@ The calling skill owns the snapshot, size limit, and save workflow. This skill o
9
9
 
10
10
  The document renders in a sandbox that blocks every network request. Fonts, images, and icons load only as `data:` URIs. External stylesheets, Google Fonts links, and CDN scripts silently fail. Inline all CSS and JavaScript.
11
11
 
12
+ For multiple boards, read [canvas authoring](references/canvas.md) before writing the index or HTML. It defines the board files, pages, geometry, shared font budget, and complete snapshot workflow.
13
+
12
14
  ## Choose the composition
13
15
 
14
16
  Before writing HTML, identify the reader's task, the facts and assets available, and what the reader should understand or do first. Use supplied facts. Label illustrative inputs and assumptions, and leave unavailable evidence absent.
@@ -49,7 +51,7 @@ After the HTML file is complete, embed the fonts it names:
49
51
  node <skill-directory>/scripts/embed-fonts.mjs page.html
50
52
  ```
51
53
 
52
- For the CLI, `<skill-directory>` is the output of `artifacts skills path artifacts-design`. The script rewrites the file in place and is safe to rerun after edits. It embeds only the families the page names and adds italic faces only for `font-style: italic`, `<em>`, `<i>`, or `<cite>`. It refuses to write a page above 1 MiB. Three families add about 135 KB. Newsreader adds about 80 to 165 KB. Do not name another family unless you embed its `woff2` yourself, because an unembedded face silently falls back. Without Node, keep the fallback stacks and report that the fonts were not embedded.
54
+ For the CLI, `<skill-directory>` is the output of `artifacts skills path artifacts-design`. The script rewrites the file in place and is safe to rerun after edits. It selects families from CSS `font-family` and `font` declarations in style elements and inline styles, including referenced custom properties. Comments, content text, and JavaScript strings do not select fonts. It adds italic faces for CSS `font-style: italic`, italic `font` shorthand, or real `<em>`, `<i>`, and `<cite>` elements. It refuses to write a page above 1 MiB. Three families add about 135 KB. Newsreader adds about 80 to 165 KB. Do not name another family unless you embed its `woff2` yourself, because an unembedded face silently falls back. Without Node, keep the fallback stacks and report that the fonts were not embedded.
53
55
 
54
56
  ## Color
55
57
 
@@ -77,6 +79,14 @@ Give the `<title>` a specific name of two to four words, or the name the user al
77
79
 
78
80
  ## Check the rendered page
79
81
 
82
+ Run the local helper after embedding fonts and before saving:
83
+
84
+ ```sh
85
+ node <skill-directory>/scripts/verify-local.mjs page.html --evidence ./page-evidence
86
+ ```
87
+
88
+ For the CLI, resolve `<skill-directory>` with `artifacts skills path artifacts-design`. Read [local checks](references/local-checks.md) for existing runtime requirements, optional declarative control expectations, browser selection, and report fields. The helper never installs a dependency, changes the HTML, saves, publishes, or requires service credentials. Its report proves **renderizado local** and leaves visual review required. It does not verify the retained revision or authenticated publication.
89
+
80
90
  Run the [interface checks](references/interface-checks.md) before saving. They distinguish required checks from visual judgment. Render the finished page at 375 px and 1280 px in both light and dark when browser tools are available. Fix observed failures and repeat the affected checks. Report checks you could not run.
81
91
 
82
92
  The license notices for bundled fonts and adapted guidance are in [third-party notices](THIRD-PARTY-NOTICES.md).