@sabaiway/agent-workflow-kit 14.15.0 → 14.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +1 -0
- package/SKILL.md +6 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/jev.md +21 -0
- package/tools/commands.mjs +7 -0
- package/tools/jev-guide.mjs +142 -0
- package/tools/sandbox-masks.mjs +28 -29
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
|
|
|
4
4
|
is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
|
|
5
5
|
every `migrations/<version>-<slug>.md` newer than it, in semver order.
|
|
6
6
|
|
|
7
|
+
## 14.16.0 — `/agent-workflow-kit jev` walks you to a first Jev request: what it is, what it is not, three steps and three prompts where it pays (AD-164)
|
|
8
|
+
|
|
9
|
+
**A new read-only command, `/agent-workflow-kit jev`, recommends Jev (TypeSafe), a decision model that returns a typed
|
|
10
|
+
answer with probabilities and a confidence.** It says what Jev is, why it pays in this workflow, and what it is not,
|
|
11
|
+
quoting the vendor's own sentence that it is not a drop-in replacement for the LLM behind Claude Code, Cursor, opencode
|
|
12
|
+
or Copilot, then the jagged edges of jev-1.13, the confidence caveat and the retention terms as the vendor states them,
|
|
13
|
+
and its sources, each with the date it was read.
|
|
14
|
+
|
|
15
|
+
- **Three steps to a first request.** The vendor skill: both install forms, Claude Code's and the other agents', in a
|
|
16
|
+
fixed order, and a mark: `found` names each of five skill paths that holds a `typesafe-ai/SKILL.md`, `not seen`
|
|
17
|
+
says a plugin install is not visible to a file check and names the checks that do see it. The key: where to get it,
|
|
18
|
+
`TYPESAFE_API_KEY` exported before the agent starts, never pasted into the chat, and a `set` / `not set` mark. The
|
|
19
|
+
first use: a prompt routing three tickets, then the vendor's smallest documented request and response with the
|
|
20
|
+
field and error meanings, quoted.
|
|
21
|
+
- **Three prompts where it pays here:** the first real error in a red gate log, the decided-register entry a council
|
|
22
|
+
finding repeats, a paragraph that restates a named rule. Each tells the agent to keep every request within the vendor
|
|
23
|
+
limits (255 options, 64k tokens, 32k for the state plus the longest question), and each answer is a hint the agent
|
|
24
|
+
checks, never a gate; every candidate text goes to the vendor.
|
|
25
|
+
- **Safe to run anywhere.** The guide writes nothing, opens no connection and never prints the key's value; `--json`
|
|
26
|
+
gives the same content as an envelope. The command appears in the command list, the skill description and the
|
|
27
|
+
README's Use table.
|
|
28
|
+
|
|
29
|
+
## 14.15.1 — a fenced empty read-only file no rule calls a mask stops the probe, so a concurrent sandboxed command no longer turns its masks into real paths (AD-163)
|
|
30
|
+
|
|
31
|
+
**`/agent-workflow-kit sandbox-masks` now withholds its verdict on a possible leftover.** While one sandboxed
|
|
32
|
+
command runs, a second one sees that command's masks as empty read-only regular files; until 14.15.1 the probe called
|
|
33
|
+
each of those fenced masks a real path, and the Recommendations screen offered `--apply --clear`, which would
|
|
34
|
+
remove a working block.
|
|
35
|
+
|
|
36
|
+
- **The refusal.** A fenced entry that neither rule calls a mask, and whose lstat is an empty regular file with a
|
|
37
|
+
numeric mode and no write bit, stops the probe with exit 1; the Recommendations screen records a skip. The message
|
|
38
|
+
lists every fenced entry that became a real path first, then each such file with its three readings: a
|
|
39
|
+
running sandbox's mask (re-run once no other sandboxed command runs), litter a sandbox left (remove the file outside
|
|
40
|
+
the sandbox, keep its line), or a real file (delete its line). No `--clear` is offered and no plain `--apply` drops
|
|
41
|
+
its line.
|
|
42
|
+
- **What stays.** An unfenced file of that shape is not a mask, a fenced foreign mount target of that shape stays a
|
|
43
|
+
mask, a stat without a numeric mode proves nothing, and `--apply --clear` still removes the block without judging
|
|
44
|
+
it. The derivation and the check of fenced entries share one lstat answer per work-tree path within a probe.
|
|
45
|
+
|
|
7
46
|
## 14.15.0 — a sandbox mask made by a bind mount counts as a mask, so the advisor no longer offers to clear a working exclude block (AD-162)
|
|
8
47
|
|
|
9
48
|
**`/agent-workflow-kit sandbox-masks` now reads the Linux mount table.** Until 14.15.0 a mask was only a device, a
|
package/README.md
CHANGED
|
@@ -235,6 +235,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
235
235
|
| `/agent-workflow-kit status` | any time | **read-only** single view of **versions + deployment + settings + bridges**: which members (kit / memory / engine / the two bridges) are installed and at what version (with an honest "installed on this machine" note when one is behind) and — in a project — what's deployed (`docs/ai`, the version stamps, and the **visibility**: visible / hidden / unclear), and the **feature-spec adoption state** (`not adopted` / `adopting (N draft)` / `adopted (N live, M draft)` / `could not be read`, plus `declined` when recorded), plus your settings (orchestration recipes, attribution, velocity) and the bridges' readiness. The two version axes (package number vs deployment-structure head) stay decoupled. Never writes, never commits, never runs a subscription CLI. |
|
|
236
236
|
| `/agent-workflow-kit now` | any time | **read-only** answer to where work stands in four blocks: **NOW** names every plan in flight, its phase, current and next step, and the tree evidence; **STEPS** gives every ledger row its evidence-derived status; **QUEUE** folds named rows under their full heading paths and calls position priority only in the judged backlog; **CAMPAIGN** shows the source-size practice figures and config state. The plan's own text never sets a status; a contradicted claim prints beside it. A block that cannot be derived is WITHHELD with its cause; NOW has two halves, plans and tree, each independently a value or WITHHELD, and NOW itself is WITHHELD only when both are. Never writes, never commits, never runs a subscription CLI. |
|
|
237
237
|
| `/agent-workflow-kit tier` | any time | **read-only** guide to the epic, story and task tier: what each level is, and the next step of the walk, with the command that runs it wherever a tool does. Never writes, never commits, never runs a subscription CLI. |
|
|
238
|
+
| `/agent-workflow-kit jev` | any time | **read-only** guide to Jev (TypeSafe), a decision model: what it is, why it pays here, what it is not, the three steps to a first request and three prompts where it pays. Opens no connection and never prints the key. |
|
|
238
239
|
| `/agent-workflow-kit recipes` | any time | **read-only** orchestration advisor: presents five named recipes for composing the carriers of a step — the bridges and the full-tool executor subagent — into plan → execute → review — **Solo / Reviewed / Council / Delegated / Subagent** — plans + recommends one for your environment (degrading with a stated reason when a backend isn't ready, or when the executor vehicle is missing/unusable), and offers the choice. The activity/slot registry (six activities) is rendered by `procedures` and by `set-recipe --help`. The orchestrator runs it via the bridge skills or the executor vehicle and **always commits**; the kit never executes a recipe, never runs a subscription CLI, never commits. |
|
|
239
240
|
| `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution` / `routine` / `feedback-triage` / `epic` / `task`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + carrier readiness (`plan-authoring`: author, fold, review; default Reviewed when a backend is ready, Council on request; slot-aware incl. Delegated and Subagent) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same) — except `feedback-triage`, whose record-bound block carries the check with `--excerpts`, the resolved member lines (ready bridge commands, the ready lens brief, or explicit skipped members), and either the record round-table command for a bridge obligation or the no-receipt fact otherwise: the record is the artifact. `--override <slot>=<value>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
|
|
240
241
|
| `/agent-workflow-kit set-recipe` | any time | **config writer** for `docs/ai/orchestration.json`: tell the agent your preference in plain language and it maps it to explicit `--set <activity>.<slot>=<value>` / `--unset` ops; the kit validates, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe), resolving the effective recipe vs live readiness. Writes **only** that file — **never runs a backend or a subagent, never commits**; hand-editing stays fully supported. |
|
package/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-workflow-kit
|
|
3
|
-
description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow". New to the epic, story and task tier? Run `/agent-workflow-kit tier`.
|
|
3
|
+
description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow". New to the epic, story and task tier? Run `/agent-workflow-kit tier`. A typed decision with a confidence from Jev? Run `/agent-workflow-kit jev`.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
|
-
version: '14.
|
|
6
|
+
version: '14.16.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
|
@@ -119,6 +119,10 @@ read-only — read `${CLAUDE_SKILL_DIR}/references/modes/now.md` before acting.
|
|
|
119
119
|
|
|
120
120
|
read-only — read `${CLAUDE_SKILL_DIR}/references/modes/tier.md` before acting.
|
|
121
121
|
|
|
122
|
+
### Mode: jev
|
|
123
|
+
|
|
124
|
+
read-only — read `${CLAUDE_SKILL_DIR}/references/modes/jev.md` before acting.
|
|
125
|
+
|
|
122
126
|
### Mode: recommendations
|
|
123
127
|
|
|
124
128
|
read-only — read `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md` before acting.
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.16.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
### Mode: jev
|
|
2
|
+
|
|
3
|
+
<!-- opt-in-capability: none — a read-only guide that configures nothing and calls no service -->
|
|
4
|
+
|
|
5
|
+
Read-only. The Jev guide: what Jev (TypeSafe) is, why it pays in this workflow, what it is not, the three steps to a first request and three prompts where it pays. It writes no file, opens no connection, never prints the key's value and never commits.
|
|
6
|
+
|
|
7
|
+
Run `node ${CLAUDE_SKILL_DIR}/tools/jev-guide.mjs --dir <project> --json`.
|
|
8
|
+
|
|
9
|
+
Render the returned envelope compactly in the user's conversational language. Never paste the JSON. Keep every path and every command verbatim, each command on its own line so it runs as printed; translate only the prose around them. Keep every vendor-quoted text verbatim and in quotes, never translated or reflowed: the not-a-replacement sentence, the request, the response, the three field meanings and the four error meanings.
|
|
10
|
+
|
|
11
|
+
Keep this order: the fixed text (WHAT, WHY HERE, NOT, SOURCES); then the three steps — the vendor skill with its install lines and its mark, the key with its mark, the first use; then the three prompts where it pays.
|
|
12
|
+
|
|
13
|
+
Exit codes:
|
|
14
|
+
|
|
15
|
+
| Code | Meaning |
|
|
16
|
+
|------|---------|
|
|
17
|
+
| 0 | Rendered, or `--help` / `-h` as the whole invocation. |
|
|
18
|
+
| 1 | Nothing rendered: a stat of the `--dir` does not report a directory; the cause is printed on stderr. |
|
|
19
|
+
| 2 | Usage: an unknown argument, a `--dir` without a value, or `--help` / `-h` combined with anything. |
|
|
20
|
+
|
|
21
|
+
**Invariants:** read-only · no network · the key's value never printed · compact user-language render with paths, commands and vendor quotes verbatim · `process.exitCode`, never `process.exit()`.
|
package/tools/commands.mjs
CHANGED
|
@@ -98,6 +98,13 @@ const CATALOG = [
|
|
|
98
98
|
kind: READ_ONLY,
|
|
99
99
|
oneLine: 'Explain the epic, story and task tier in plain words and name the next step of the walk, with the command that runs it wherever a tool does: the tier guide reads the disk alone.',
|
|
100
100
|
},
|
|
101
|
+
{
|
|
102
|
+
key: 'jev',
|
|
103
|
+
invocation: invocationOf('jev'),
|
|
104
|
+
group: 'Inspect',
|
|
105
|
+
kind: READ_ONLY,
|
|
106
|
+
oneLine: 'Guide to Jev (TypeSafe), a decision model: what it is, why it pays here, the steps to a first request and three prompts where it pays; the guide opens no connection.',
|
|
107
|
+
},
|
|
101
108
|
{
|
|
102
109
|
key: 'backends',
|
|
103
110
|
invocation: invocationOf('backends'),
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// spec:jev-guide — docs/ai/specs/kit/jev-guide/index.md
|
|
3
|
+
import { statSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { fail } from '../references/scripts/markdown-blocks.mjs';
|
|
7
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
8
|
+
|
|
9
|
+
const SCHEMA = 1;
|
|
10
|
+
const KEY_VARIABLE = 'TYPESAFE_API_KEY';
|
|
11
|
+
const SKILL_FILE = join('typesafe-ai', 'SKILL.md');
|
|
12
|
+
const HELP = `jev — read-only: what Jev (TypeSafe) is, why it pays here, and the steps to a first request.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
node jev-guide.mjs [--dir PROJECT] [--json]
|
|
16
|
+
|
|
17
|
+
--dir the project to read (default: the current directory).
|
|
18
|
+
--json the JSON envelope instead of the plain text.
|
|
19
|
+
--help answered only when it is the whole invocation.
|
|
20
|
+
|
|
21
|
+
Exit codes: 0 rendered; 1 nothing rendered (a --dir that is not a directory); 2 usage.
|
|
22
|
+
|
|
23
|
+
Reads only: the --dir, the home directory, the key variable's presence and five skill paths; no network.`;
|
|
24
|
+
|
|
25
|
+
export const JEV_TEXT = Object.freeze([
|
|
26
|
+
'WHAT: Jev is a decision model: it takes a state and typed questions (choice, score, noul) and returns typed answers with probabilities and a confidence; it does not write text.',
|
|
27
|
+
'WHY HERE: a typed decision with a confidence instead of prompt-and-parse, in the user\'s own code and in the agent\'s scratch scripts.',
|
|
28
|
+
'Price and latency as the vendor states them: 0.042 dollars per million input tokens, output free; sub-second.',
|
|
29
|
+
'NOT: "Jev is not a drop-in replacement for the LLM behind Claude Code, Cursor, opencode, Copilot" (the vendor).',
|
|
30
|
+
'Jagged edge of jev-1.13: it reads literally.',
|
|
31
|
+
'Jagged edge of jev-1.13: no counting or arithmetic.',
|
|
32
|
+
'Jagged edge of jev-1.13: dates are read as text.',
|
|
33
|
+
'Jagged edge of jev-1.13: a large irrelevant state distracts it.',
|
|
34
|
+
'Jagged edge of jev-1.13: adversarial content can move the answer.',
|
|
35
|
+
'Jagged edge of jev-1.13: indirection lowers accuracy.',
|
|
36
|
+
'Jagged edge of jev-1.13: no structural invariants across questions.',
|
|
37
|
+
'Jagged edge of jev-1.13: no generation.',
|
|
38
|
+
'A confidence is not a correctness: start with conservative thresholds and test with your own data.',
|
|
39
|
+
'Retention as the vendor states it: zero data retention for enterprise customers only; a data processing agreement covers retention; a commitment not to train on user data.',
|
|
40
|
+
'SOURCES: docs.typesafe.ai for every Jev claim, read 2026-09-29.',
|
|
41
|
+
'The skills CLI README (github.com/vercel-labs/skills) for the skill paths, read 2026-09-29.',
|
|
42
|
+
'code.claude.com for claude plugin list, read 2026-09-29.',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const INSTALL = [
|
|
46
|
+
'Claude Code:',
|
|
47
|
+
'claude plugin marketplace add typesafe-ai/skills',
|
|
48
|
+
'claude plugin install typesafe@typesafe-ai',
|
|
49
|
+
'Other agents:',
|
|
50
|
+
'npx skills add typesafe-ai/skills --skill typesafe-ai',
|
|
51
|
+
];
|
|
52
|
+
const NOT_SEEN = [
|
|
53
|
+
'skill: not seen at the five skill paths. A plugin install is not visible to a file check; check it by its owners:',
|
|
54
|
+
'/typesafe:typesafe-ai',
|
|
55
|
+
'in a Claude Code session (the vendor); ask any other agent to use the TypeSafe skill (the vendor); or run Claude Code\'s own',
|
|
56
|
+
'claude plugin list',
|
|
57
|
+
];
|
|
58
|
+
const KEY_TEXT = [
|
|
59
|
+
'Get a key at console.typesafe.ai and set it on every host the agent runs on.',
|
|
60
|
+
`Export ${KEY_VARIABLE}, the variable the vendor's SDKs read, in the shell the agent is started from: an agent inherits the environment its session started with, so export before starting it or restart it.`,
|
|
61
|
+
'Never paste the key into the chat or into a project file.',
|
|
62
|
+
];
|
|
63
|
+
const KEY_NOT_SET = `key: ${KEY_VARIABLE} not set. A host setting that filters the environment of the agent's commands also hides it.`;
|
|
64
|
+
const FIRST_USE = [
|
|
65
|
+
'Give the agent this prompt: using the TypeSafe skill, route these three tickets to billing, technical or sales, one request each, and print each choice and confidence — "I was charged twice this month.", "The app crashes when I upload a file.", "Is there a discount for a yearly plan?".',
|
|
66
|
+
'The vendor\'s smallest documented request:',
|
|
67
|
+
'POST https://api.typesafe.ai/v1/systemone',
|
|
68
|
+
'Authorization: Bearer <API_KEY>',
|
|
69
|
+
'{"state":"Help! My payouts have been failing for 3 days.","model":"jev-latest","questions":{"department":{"type":"choice","instructions":"Which team should handle this?","criteria":{"billing":"Payments, invoicing, refunds","technical":"Bugs, outages, integrations","sales":"Pricing, upgrades, new accounts"}}}}',
|
|
70
|
+
'Its documented response:',
|
|
71
|
+
'{"model":"jev-1.13.0","answers":{"department":{"type":"choice","choice":"billing","probabilities":{"billing":0.88,"technical":0.12,"sales":0.0},"confidence":0.81}},"usage":{"input_tokens":318,"output_tokens":34}}',
|
|
72
|
+
'choice: "The highest-probability option."',
|
|
73
|
+
'probabilities: "Every option mapped to its probability (floats that sum to 1)."',
|
|
74
|
+
'confidence: "How certain the model is, derived from probabilities."',
|
|
75
|
+
'401: "Missing or invalid API key. Check the `Authorization` header."',
|
|
76
|
+
'422: "The request body failed validation — for example a missing required field or a malformed question. The body details the offending field."',
|
|
77
|
+
'429: "You have exceeded your rate limit. Back off and retry after a short delay."',
|
|
78
|
+
'529: "TypeSafe is temporarily overloaded. Retry after a short delay."',
|
|
79
|
+
'Success: three choices, each with a confidence.',
|
|
80
|
+
];
|
|
81
|
+
const LIMITS = 'Each request stays within the vendor limits: 255 options, 64k tokens, 32k for the state plus the longest question.';
|
|
82
|
+
const HINT = 'The answer is a hint the agent checks, never a gate; every candidate text is sent to the vendor.';
|
|
83
|
+
const PROMPTS = [
|
|
84
|
+
`(a) A red gate log: give each failing gate's own lines ids and ask one choice of the first real error per failing gate. ${LIMITS} ${HINT}`,
|
|
85
|
+
`(b) Council findings against the decided register: for each finding, ask a choice of the register id it repeats, or none. ${LIMITS} ${HINT}`,
|
|
86
|
+
`(c) Candidate paragraphs with ids: ask one noul each that the paragraph restates a named rule. ${LIMITS} ${HINT}`,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const valueAt = (argv, index, flag) => {
|
|
90
|
+
const value = argv[index + 1];
|
|
91
|
+
if (value === undefined || value === '' || value.startsWith('-')) throw fail(2, `${flag} takes a value`);
|
|
92
|
+
return value;
|
|
93
|
+
};
|
|
94
|
+
const parseArgv = (argv) => {
|
|
95
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
96
|
+
if (argv.length !== 1) throw fail(2, '--help is answered only when it is the whole invocation');
|
|
97
|
+
return { help: true };
|
|
98
|
+
}
|
|
99
|
+
return argv.reduce((parsed, arg, index) => {
|
|
100
|
+
if (parsed.skip) return { ...parsed, skip: false };
|
|
101
|
+
if (arg === '--dir') return { ...parsed, dir: valueAt(argv, index, arg), skip: true };
|
|
102
|
+
if (arg === '--json') return { ...parsed, json: true };
|
|
103
|
+
throw fail(2, `unknown argument "${arg}" — run with --help`);
|
|
104
|
+
}, { help: false, dir: '.', json: false, skip: false });
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const statIs = (path, kind) => {
|
|
108
|
+
try { return kind === 'file' ? statSync(path).isFile() : statSync(path).isDirectory(); } catch { return false; }
|
|
109
|
+
};
|
|
110
|
+
const skillPaths = (dir, home) => [...new Set([join(dir, '.claude', 'skills'), join(dir, '.agents', 'skills'),
|
|
111
|
+
join(home, '.claude', 'skills'), join(home, '.codex', 'skills'), join(home, '.cursor', 'skills')]
|
|
112
|
+
.map((root) => join(root, SKILL_FILE)))];
|
|
113
|
+
const keySet = (env) => typeof env[KEY_VARIABLE] === 'string' && env[KEY_VARIABLE].trim() !== '';
|
|
114
|
+
|
|
115
|
+
const renderSteps = (found, set) => [
|
|
116
|
+
['STEP 1 — the vendor skill', ...INSTALL,
|
|
117
|
+
...(found.length ? found.map((path) => `skill: found at ${path}`) : NOT_SEEN)],
|
|
118
|
+
['STEP 2 — the key', ...KEY_TEXT, set ? `key: ${KEY_VARIABLE} set.` : KEY_NOT_SET],
|
|
119
|
+
['STEP 3 — the first use', ...FIRST_USE],
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
export const main = (argv, io = {}) => {
|
|
123
|
+
const log = io.log ?? console.log;
|
|
124
|
+
const error = io.error ?? console.error;
|
|
125
|
+
const parsed = (() => {
|
|
126
|
+
try { return parseArgv(argv); } catch (err) { error(err.message); return null; }
|
|
127
|
+
})();
|
|
128
|
+
if (parsed === null) return 2;
|
|
129
|
+
if (parsed.help) { log(HELP); return 0; }
|
|
130
|
+
const dir = resolve(io.cwd ?? process.cwd(), parsed.dir);
|
|
131
|
+
if (!statIs(dir, 'directory')) { error(`${dir} is not a directory`); return 1; }
|
|
132
|
+
const found = skillPaths(dir, io.home ?? homedir()).filter((path) => statIs(path, 'file'));
|
|
133
|
+
const set = keySet(io.env ?? process.env);
|
|
134
|
+
const steps = renderSteps(found, set);
|
|
135
|
+
log(parsed.json
|
|
136
|
+
? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found }, key: { set },
|
|
137
|
+
steps, prompts: PROMPTS }, null, 2)
|
|
138
|
+
: [...JEV_TEXT, ...steps.flat(), 'WHERE IT PAYS IN THIS WORKFLOW', ...PROMPTS].join('\n'));
|
|
139
|
+
return 0;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (isDirectRun(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|
package/tools/sandbox-masks.mjs
CHANGED
|
@@ -86,6 +86,9 @@ const mountTargets = (root, readMountinfo) => {
|
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
// A LEFTOVER: an empty regular file with a numeric mode and no write bit — it only withholds a verdict.
|
|
90
|
+
const isLeftoverStat = (stat) => stat?.isFile?.() === true && stat.size === 0 && typeof stat.mode === 'number' && (stat.mode & 0o222) === 0;
|
|
91
|
+
|
|
89
92
|
// A root-anchored gitignore pattern for one relative path: leading `/` (root-anchored — a mask
|
|
90
93
|
// name can never shadow a deeper real path), glob metacharacters + leading-! / leading-# handled
|
|
91
94
|
// by the anchor, trailing whitespace escaped (gitignore trims it otherwise).
|
|
@@ -150,9 +153,7 @@ export const patternToRel = (line) => {
|
|
|
150
153
|
return t.slice(1).replace(/\\([\\*?[\] ])/g, '$1');
|
|
151
154
|
};
|
|
152
155
|
|
|
153
|
-
//
|
|
154
|
-
// that is not a mask (the derivation's predicate) is a STALE-REAL flag — the D5 watch (a real file
|
|
155
|
-
// at an excluded path is silently skipped by bulk staging; delete the line before trusting git).
|
|
156
|
+
// A fenced entry that exists and fails the derivation's predicate is STALE-REAL (the D5 watch).
|
|
156
157
|
export const revalidateFence = (bodyLines, { root, lstat = lstatSync, readMountinfo = readMountinfoDefault, foreign = mountTargets(root, readMountinfo).foreign } = {}) => {
|
|
157
158
|
const staleReal = [];
|
|
158
159
|
for (const line of bodyLines) {
|
|
@@ -172,8 +173,8 @@ export const revalidateFence = (bodyLines, { root, lstat = lstatSync, readMounti
|
|
|
172
173
|
// ── the probe (read-only; exported — the Recommendations advisor consumes it) ──────
|
|
173
174
|
|
|
174
175
|
// probeSandboxMasks({ cwd, ... }) → everything the render/apply/advisor needs, or null when not a
|
|
175
|
-
// git work tree. Read-only always.
|
|
176
|
-
export const probeSandboxMasks = ({ cwd = process.cwd(), lstat = lstatSync, listUntracked = listUntrackedUnfilteredZ, readFile = readFileSync, readMountinfo = readMountinfoDefault } = {}) => {
|
|
176
|
+
// git work tree; a fenced LEFTOVER refuses unless clearOnly. Read-only always.
|
|
177
|
+
export const probeSandboxMasks = ({ cwd = process.cwd(), lstat = lstatSync, listUntracked = listUntrackedUnfilteredZ, readFile = readFileSync, readMountinfo = readMountinfoDefault, clearOnly = false } = {}) => {
|
|
177
178
|
const root = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
178
179
|
if (root == null) return null;
|
|
179
180
|
const gitPathRaw = gitLine(['rev-parse', '--git-path', 'info/exclude'], cwd);
|
|
@@ -183,23 +184,23 @@ export const probeSandboxMasks = ({ cwd = process.cwd(), lstat = lstatSync, list
|
|
|
183
184
|
// The write-containment root: info/exclude lives in the COMMON git dir (worktree-safe),
|
|
184
185
|
// and the apply must never write through a symlinked component or a non-regular leaf.
|
|
185
186
|
const gitCommonDir = resolve(cwd, commonDirRaw);
|
|
186
|
-
let text; // read ONCE: derivation and revalidation judge one table
|
|
187
|
+
let text; // read ONCE: derivation and revalidation judge one table, and one lstat answer per path
|
|
187
188
|
const once = () => (text === undefined ? (text = readMountinfo()) : text);
|
|
188
|
-
const
|
|
189
|
+
const answers = new Map();
|
|
190
|
+
const lstatOnce = (p) => {
|
|
191
|
+
if (!answers.has(p)) answers.set(p, (() => { try { return { stat: lstat(p) }; } catch (error) { return { error }; } })());
|
|
192
|
+
if (answers.get(p).error) throw answers.get(p).error;
|
|
193
|
+
return answers.get(p).stat;
|
|
194
|
+
};
|
|
195
|
+
const derived = deriveMasks({ root, lstat: lstatOnce, listUntracked, readMountinfo: once });
|
|
189
196
|
if (derived == null) return null;
|
|
190
197
|
const { masks, unrenderable, mountOnly, mountSignal } = derived;
|
|
191
|
-
// The
|
|
192
|
-
// symlinked parent (.git/info) must never be read through, and a FIFO/socket leaf would HANG
|
|
193
|
-
// the read-only probe — parent-chain containment + leaf lstat first, fail closed on everything
|
|
194
|
-
// but a plain absent file, then read the existing REGULAR content. The apply re-checks
|
|
195
|
-
// (defense in depth) before writing.
|
|
198
|
+
// The whole chain is guarded BEFORE any read (a symlinked component; a FIFO/socket leaf would hang).
|
|
196
199
|
try {
|
|
197
200
|
assertContainedRealPath(gitCommonDir, excludeFile, { lstat });
|
|
198
201
|
} catch (err) {
|
|
199
202
|
throw fail(1, `refusing to touch ${excludeFile}: ${err.message}`);
|
|
200
203
|
}
|
|
201
|
-
// The chain guard above already refused symlinked components (the leaf included) and PROPAGATED
|
|
202
|
-
// any non-ENOENT lstat failure — after it, the leaf can only be absent or exist non-symlinked.
|
|
203
204
|
const leafStat = (() => {
|
|
204
205
|
try {
|
|
205
206
|
return lstat(excludeFile);
|
|
@@ -222,16 +223,15 @@ export const probeSandboxMasks = ({ cwd = process.cwd(), lstat = lstatSync, list
|
|
|
222
223
|
}
|
|
223
224
|
const lines = content.split('\n');
|
|
224
225
|
const fence = findMasksFence(lines);
|
|
225
|
-
const staleReal = fence.state === 'ok' ? revalidateFence(fence.body, { root, lstat, readMountinfo: once }) : [];
|
|
226
|
+
const staleReal = fence.state === 'ok' ? revalidateFence(fence.body, { root, lstat: lstatOnce, readMountinfo: once }) : [];
|
|
227
|
+
const leftovers = clearOnly ? [] : staleReal.filter((rel) => isLeftoverStat(lstatOnce(join(root, rel))));
|
|
228
|
+
if (leftovers.length > 0) throw fail(1, [...staleReal.filter((rel) => !leftovers.includes(rel)).map(staleRealLine), ...leftovers.map(leftoverLine)].join('\n'));
|
|
226
229
|
const applyCmd = `node ${shellQuoteArg(join(HERE, 'sandbox-masks.mjs'))} --cwd ${shellQuoteArg(root)} --apply`;
|
|
227
230
|
return { root, excludeFile, gitCommonDir, content, lines, fence, masks, unrenderable, mountOnly, mountSignal, staleReal, applyCmd };
|
|
228
231
|
};
|
|
229
232
|
|
|
230
|
-
// needsMasksApply(probe) →
|
|
231
|
-
//
|
|
232
|
-
// became a real path (stale-real), or when visible masks exist and the derived set ≠ the fenced
|
|
233
|
-
// set. A malformed fence is NOT an apply item (the apply would fail closed — it needs a hand fix,
|
|
234
|
-
// which the probe render itself flags loudly). Pure over the probe result.
|
|
233
|
+
// needsMasksApply(probe) → the advisor's fire condition: a stale-real entry, or visible masks whose set
|
|
234
|
+
// differs from the fenced one; a malformed fence is never an item (the render flags it). Pure.
|
|
235
235
|
export const needsMasksApply = (probe) => {
|
|
236
236
|
if (probe == null) return false;
|
|
237
237
|
if (probe.staleReal.length > 0) return true;
|
|
@@ -262,10 +262,8 @@ export const planApply = (probe, { clear = false } = {}) => {
|
|
|
262
262
|
}
|
|
263
263
|
const masks = clear ? [] : probe.masks;
|
|
264
264
|
const block = masks.length === 0 ? [] : [MASKS_FENCE_START, ...masks.map(toExcludePattern), MASKS_FENCE_END];
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
// string offsets around the fence lines — a split/join('\n') rebuild would silently normalize
|
|
268
|
-
// CRLF hand content outside the block. Only the fenced block itself is ours (always LF).
|
|
265
|
+
// Every hand byte outside the fence is preserved EXACTLY (raw offsets, never a split/join rebuild
|
|
266
|
+
// that would normalize CRLF); only the fenced block itself is ours (always LF).
|
|
269
267
|
const rendered = block.length ? `${block.join('\n')}\n` : '';
|
|
270
268
|
let content;
|
|
271
269
|
if (probe.fence.state === 'ok') {
|
|
@@ -283,6 +281,9 @@ export const planApply = (probe, { clear = false } = {}) => {
|
|
|
283
281
|
|
|
284
282
|
// ── rendering ───────────────────────────────────────────────────────────────────────
|
|
285
283
|
|
|
284
|
+
const staleRealLine = (rel) => ` ⚠ fenced entry became a REAL path: ${rel} — delete its line from the managed block BEFORE relying on git for this path: an excluded real file is silently skipped by bulk staging (git add -A / git add .), so it stays OUT of your commits, and an explicit git add refuses it without -f; .git/info/exclude never warns`;
|
|
285
|
+
const leftoverLine = (rel) => ` ⚠ refusing: fenced entry ${rel} is an empty read-only file no arm calls a mask, so neither verdict is given — a running sandbox's mask: re-run once no other sandboxed command runs; litter a sandbox left: remove the file outside the sandbox and keep its line; a real file: delete its line`;
|
|
286
|
+
|
|
286
287
|
const formatProbe = (probe) => {
|
|
287
288
|
const lines = [
|
|
288
289
|
`sandbox-masks — sandbox masks (device/FIFO/socket or foreign mount target) in ${probe.root}`,
|
|
@@ -293,9 +294,7 @@ const formatProbe = (probe) => {
|
|
|
293
294
|
lines.push(` masks visible now: ${probe.masks.length === 0 ? '0 (outside the sandbox or clean)' : ''}`);
|
|
294
295
|
for (const m of probe.masks) lines.push(` · ${m}${probe.mountOnly.includes(m) ? ' (mount-only: it rides git add -A and the review fingerprint while git does not ignore it)' : ''}`);
|
|
295
296
|
if (probe.mountSignal === 'absent') lines.push(' mount signal: not read on this platform — only device/FIFO/socket masks are judged');
|
|
296
|
-
|
|
297
|
-
lines.push(` ⚠ fenced entry became a REAL path: ${rel} — delete its line from the managed block BEFORE relying on git for this path: an excluded real file is silently skipped by bulk staging (git add -A / git add .), so it stays OUT of your commits, and an explicit git add refuses it without -f; .git/info/exclude never warns`);
|
|
298
|
-
}
|
|
297
|
+
lines.push(...probe.staleReal.map(staleRealLine));
|
|
299
298
|
for (const rel of probe.unrenderable) {
|
|
300
299
|
lines.push(` ⚠ mask name carries a newline or ends with a tab and cannot be expressed as ONE exclude rule — NOT written (the mask itself stays untracked): ${JSON.stringify(rel)}`);
|
|
301
300
|
}
|
|
@@ -343,9 +342,9 @@ export const main = (argv, ctx = {}) => {
|
|
|
343
342
|
const cwd = cwdIdx !== -1 ? argv[cwdIdx + 1] : (ctx.cwd ?? process.cwd());
|
|
344
343
|
if (cwdIdx !== -1 && (!cwd || cwd.startsWith('--'))) throw fail(2, '--cwd requires a directory argument');
|
|
345
344
|
if (argv.includes('--clear') && !argv.includes('--apply')) throw fail(2, '--clear is only valid together with --apply');
|
|
346
|
-
// --apply --clear needs no derivation
|
|
345
|
+
// --apply --clear needs no derivation: it never reads the mount table and never judges the fence.
|
|
347
346
|
const clearOnly = argv.includes('--apply') && argv.includes('--clear');
|
|
348
|
-
const probe = probeSandboxMasks({ cwd, ...(ctx.deps ?? {}), ...(clearOnly ? { readMountinfo: () => null } : {}) });
|
|
347
|
+
const probe = probeSandboxMasks({ cwd, ...(ctx.deps ?? {}), ...(clearOnly ? { readMountinfo: () => null, clearOnly } : {}) });
|
|
349
348
|
if (probe == null) throw fail(1, `not a git work tree: ${cwd}`);
|
|
350
349
|
if (!argv.includes('--apply')) {
|
|
351
350
|
if (argv.includes('--json')) {
|