@sabaiway/agent-workflow-kit 14.19.1 → 14.21.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 +71 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +45 -50
- package/bridges/antigravity-cli-bridge/bin/agy-model-posture.test.mjs +257 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-harness.test.mjs +10 -3
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -21
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +4 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +120 -48
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +22 -62
- package/bridges/antigravity-cli-bridge/bin/agy.sh +109 -16
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +5 -5
- package/bridges/antigravity-cli-bridge/capability.json +14 -7
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +16 -20
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +10 -27
- package/bridges/codex-cli-bridge/SKILL.md +57 -49
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +145 -46
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +28 -55
- package/bridges/codex-cli-bridge/bin/codex-model-posture.test.mjs +371 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +134 -36
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +33 -45
- package/bridges/codex-cli-bridge/capability.json +22 -8
- package/bridges/codex-cli-bridge/references/driving-codex.md +18 -15
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
- package/bridges/codex-cli-bridge/setup/README.md +3 -3
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bridge-settings.md +10 -7
- package/references/modes/jev.md +3 -2
- package/references/modes/recipes.md +1 -1
- package/references/modes/recommendations.md +2 -1
- package/references/modes/review-state.md +1 -1
- package/references/vendor/typesafe-ai/LICENSE +21 -0
- package/references/vendor/typesafe-ai/SKILL.md.pinned +149 -0
- package/tools/ack-store.mjs +2 -0
- package/tools/bridge-catalog.mjs +114 -0
- package/tools/bridge-posture.mjs +11 -2
- package/tools/bridge-settings-read.mjs +41 -5
- package/tools/bridge-settings.mjs +111 -23
- package/tools/detect-backends.mjs +6 -6
- package/tools/family-registry.mjs +3 -3
- package/tools/jev-facts.mjs +101 -4
- package/tools/jev-guide.mjs +31 -12
- package/tools/jev-skill.mjs +231 -0
- package/tools/manifest/schema.md +1 -1
- package/tools/manifest/validate.mjs +3 -2
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +2 -2
- package/tools/recommendations.mjs +49 -4
- package/tools/review-state.mjs +1 -1
package/tools/jev-facts.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// spec:jev-guide — docs/ai/specs/kit/jev-guide/index.md
|
|
2
|
-
// The one leaf the Jev surfaces share: the key rule, the connect line,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
import
|
|
2
|
+
// The one leaf the Jev surfaces share: the key rule, the connect line, the vendor facts the connect
|
|
3
|
+
// command's one request uses, and the vendor skill's pin, targets, state rule and apply line. No side
|
|
4
|
+
// effect, no import beyond node:path and node:crypto — the read-only guide imports it under its closed
|
|
5
|
+
// import graph, the advisor and the two commands beside it; file primitives arrive through `io`.
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { join, posix, win32 } from 'node:path';
|
|
6
8
|
|
|
7
9
|
export const KEY_VARIABLE = 'TYPESAFE_API_KEY';
|
|
8
10
|
|
|
@@ -56,3 +58,98 @@ export const JEV_ERROR_MEANINGS = Object.freeze({
|
|
|
56
58
|
429: 'You have exceeded your rate limit. Back off and retry after a short delay.',
|
|
57
59
|
529: 'TypeSafe is temporarily overloaded. Retry after a short delay.',
|
|
58
60
|
});
|
|
61
|
+
|
|
62
|
+
// ── the vendor skill (part jev-skill) ─────────────────────────────────────────────────────────────
|
|
63
|
+
// Each target file name -> the name the kit ships it under. The shipped SKILL.md is renamed so no agent
|
|
64
|
+
// walking the kit's own tree (a skill folder itself) loads the vendor's text before the user's --apply.
|
|
65
|
+
export const SKILL_FILES = Object.freeze({ 'SKILL.md': 'SKILL.md.pinned', LICENSE: 'LICENSE' });
|
|
66
|
+
// Newest first; a release that moves the pin puts the new entry first and removes none.
|
|
67
|
+
export const SKILL_PINS = Object.freeze([Object.freeze({
|
|
68
|
+
tag: 'v0.5.7',
|
|
69
|
+
commit: '65a39f393687675ce170e6094757de20370365b9',
|
|
70
|
+
digests: Object.freeze({
|
|
71
|
+
'SKILL.md': '71ea90d7906c6554c4f4c460ef7361b2d26f59116ccdae986dc6d997b9389f52',
|
|
72
|
+
LICENSE: '835f233f1d6ed84a9b9a351aba0689b47644a4137d6316911fc7957bde523b02',
|
|
73
|
+
}),
|
|
74
|
+
})]);
|
|
75
|
+
export const AFTER_INSTALL = 'After an install, quit the agent and start it again from a new terminal.';
|
|
76
|
+
export const NO_APPLY_LINE = 'the apply line cannot be printed here: a path holds a control character';
|
|
77
|
+
|
|
78
|
+
const isControl = (char) => char.charCodeAt(0) < 0x20 || char.charCodeAt(0) === 0x7f;
|
|
79
|
+
// Display only: every path, entry name and reason a surface prints; never a command line.
|
|
80
|
+
export const printable = (text) => [...String(text)].map((char) => (isControl(char) ? '?' : char)).join('');
|
|
81
|
+
|
|
82
|
+
// The Claude Code dir the agent's environment names: an absolute CLAUDE_CONFIG_DIR for the platform, else <home>/.claude.
|
|
83
|
+
export const claudeDirOf = (env = {}, home = '', platform) => {
|
|
84
|
+
const value = env.CLAUDE_CONFIG_DIR;
|
|
85
|
+
const set = typeof value === 'string' && value !== '';
|
|
86
|
+
if (set && (platform === 'win32' ? win32 : posix).isAbsolute(value)) return { dir: value, fromEnv: true, note: '' };
|
|
87
|
+
const dir = home === '' ? '' : join(home, '.claude');
|
|
88
|
+
return { dir, fromEnv: false,
|
|
89
|
+
note: set ? printable(`CLAUDE_CONFIG_DIR is set to ${value}, not an absolute path: the Claude Code target is ${dir}`) : '' };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// The host separator, read off the host's own join.
|
|
93
|
+
const SEP = join('a', 'b').slice(1, -1);
|
|
94
|
+
const within = (path, dir) => path.startsWith(dir.endsWith(SEP) ? dir : `${dir}${SEP}`);
|
|
95
|
+
// The three targets in their order; a later record on an earlier one's path merges into it, and a later
|
|
96
|
+
// record inside an earlier one's path, or holding it, carries `overlaps`.
|
|
97
|
+
export const skillTargets = ({ home = '', claudeDir = '' } = {}) => {
|
|
98
|
+
if (home === '') return [];
|
|
99
|
+
const records = [
|
|
100
|
+
{ agent: 'Codex', base: home, root: join(home, '.agents', 'skills') },
|
|
101
|
+
{ agent: 'Claude Code', base: within(claudeDir, home) ? home : claudeDir, root: join(claudeDir, 'skills') },
|
|
102
|
+
{ agent: 'Antigravity CLI', base: home, root: join(home, '.gemini', 'config', 'skills') },
|
|
103
|
+
].map((record) => ({ ...record, path: join(record.root, 'typesafe-ai') }));
|
|
104
|
+
return records.reduce((kept, record) => {
|
|
105
|
+
const same = kept.find(({ path }) => path === record.path);
|
|
106
|
+
if (same) return kept.map((item) => (item === same ? { ...item, agent: `${item.agent} and ${record.agent}` } : item));
|
|
107
|
+
const other = kept.find(({ path }) => within(record.path, path) || within(path, record.path));
|
|
108
|
+
return [...kept, other ? { ...record, overlaps: other.path } : record];
|
|
109
|
+
}, []);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const digestOf = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
113
|
+
const codeOf = (error) => error?.code ?? error?.message ?? String(error);
|
|
114
|
+
// The state of one target, by content and never by name; pure over io.lstat, io.readdir, io.readFile and io.pins.
|
|
115
|
+
export const skillState = (target, io) => {
|
|
116
|
+
const foreign = (reason) => ({ state: 'foreign', tag: null, reason });
|
|
117
|
+
if (target.overlaps) return foreign(`overlaps ${target.overlaps}`);
|
|
118
|
+
const pins = io.pins ?? SKILL_PINS;
|
|
119
|
+
try {
|
|
120
|
+
const names = target.path.slice(target.base.length).split(SEP).filter(Boolean);
|
|
121
|
+
let at = target.base;
|
|
122
|
+
for (const [index, name] of names.entries()) {
|
|
123
|
+
at = join(at, name);
|
|
124
|
+
let stat;
|
|
125
|
+
try { stat = io.lstat(at); } catch (error) {
|
|
126
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return { state: 'absent', tag: null, reason: null };
|
|
127
|
+
return foreign(codeOf(error));
|
|
128
|
+
}
|
|
129
|
+
if (stat.isSymbolicLink()) return foreign(`a link at ${at}`);
|
|
130
|
+
if (!stat.isDirectory()) return foreign(index === names.length - 1 ? 'not a directory' : `${at} is not a directory`);
|
|
131
|
+
}
|
|
132
|
+
const entries = [...io.readdir(target.path)].map(String).sort();
|
|
133
|
+
if (entries.join('/') !== 'LICENSE/SKILL.md') return foreign(`holds ${entries.join(', ') || 'no entry'}`);
|
|
134
|
+
for (const name of entries) {
|
|
135
|
+
if (!io.lstat(join(target.path, name)).isFile()) return foreign(`${name} is not a regular file`);
|
|
136
|
+
}
|
|
137
|
+
const found = Object.fromEntries(entries.map((name) => [name, digestOf(io.readFile(join(target.path, name)))]));
|
|
138
|
+
const pinAt = pins.findIndex(({ digests }) => Object.keys(SKILL_FILES).every((name) => digests[name] === found[name]));
|
|
139
|
+
if (pinAt === 0) return { state: 'current', tag: pins[0].tag, reason: null };
|
|
140
|
+
if (pinAt > 0) return { state: 'earlier', tag: pins[pinAt].tag, reason: null };
|
|
141
|
+
return foreign('matches no pin');
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return foreign(codeOf(error));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// The skill command's apply line, always with the Claude Code dir its caller resolved; '' when a path holds a control byte.
|
|
148
|
+
export const skillLine = (toolsDir, platform, claudeDir) => {
|
|
149
|
+
if ([...`${toolsDir}${claudeDir}`].some(isControl)) return '';
|
|
150
|
+
if (platform === 'win32') {
|
|
151
|
+
const quoted = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
152
|
+
return `node ${quoted(`${toolsDir}\\jev-skill.mjs`)} --apply --claude-dir ${quoted(claudeDir)}`;
|
|
153
|
+
}
|
|
154
|
+
return `node ${quoteArg(join(toolsDir, 'jev-skill.mjs'))} --apply --claude-dir ${quoteArg(claudeDir)}`;
|
|
155
|
+
};
|
package/tools/jev-guide.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// spec:jev-guide — docs/ai/specs/kit/jev-guide/index.md
|
|
3
|
-
import { statSync } from 'node:fs';
|
|
3
|
+
import { lstatSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
4
4
|
import { homedir, hostname } from 'node:os';
|
|
5
5
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { fail } from '../references/scripts/markdown-blocks.mjs';
|
|
8
8
|
import { isDirectRun } from './direct-run.mjs';
|
|
9
|
-
import { KEY_VARIABLE, RESTART_STEP, connectLine, keySet, placeOf } from './jev-facts.mjs';
|
|
9
|
+
import { AFTER_INSTALL, KEY_VARIABLE, NO_APPLY_LINE, RESTART_STEP, SKILL_PINS, claudeDirOf, connectLine, keySet, placeOf, printable, skillLine, skillState, skillTargets } from './jev-facts.mjs';
|
|
10
10
|
|
|
11
11
|
const SCHEMA = 1;
|
|
12
12
|
const TOOLS = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const SKILL_FILE = join('typesafe-ai', 'SKILL.md');
|
|
14
|
-
const HELP = `jev — read-only: what Jev (TypeSafe) is, why it pays here, and the two steps
|
|
14
|
+
const HELP = `jev — read-only: what Jev (TypeSafe) is, why it pays here, and the two steps: the key and the vendor skill.
|
|
15
15
|
|
|
16
16
|
Usage:
|
|
17
17
|
node jev-guide.mjs [--dir PROJECT] [--json]
|
|
@@ -22,7 +22,7 @@ Usage:
|
|
|
22
22
|
|
|
23
23
|
Exit codes: 0 rendered; 1 nothing rendered (a --dir that is not a directory); 2 usage.
|
|
24
24
|
|
|
25
|
-
Reads only: the --dir, the home directory, the key variable's presence, five skill paths, and for the place WSL_DISTRO_NAME, SSH_CONNECTION, REMOTE_CONTAINERS, CODESPACES, the platform, the hostname and /.dockerenv; no network.`;
|
|
25
|
+
Reads only: the --dir, the home directory, the key variable's presence, CLAUDE_CONFIG_DIR, five skill paths, the three skill targets, and for the place WSL_DISTRO_NAME, SSH_CONNECTION, REMOTE_CONTAINERS, CODESPACES, the platform, the hostname and /.dockerenv; no network.`;
|
|
26
26
|
|
|
27
27
|
export const JEV_TEXT = Object.freeze([
|
|
28
28
|
'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.',
|
|
@@ -49,8 +49,10 @@ const KEY_CONNECT = 'Connect it from a terminal of your own, never through the a
|
|
|
49
49
|
const KEY_RESTART = `Then ${RESTART_STEP}. Run /agent-workflow-kit jev again: the key mark should read set.`;
|
|
50
50
|
const KEY_NEVER = 'Never paste the key into the chat or into a project file.';
|
|
51
51
|
const KEY_NOT_SET = `key: ${KEY_VARIABLE} not set. A host setting that filters the environment of the agent's commands also hides it. If it still reads not set after that restart and no such setting applies, run the connect line again and follow its last line.`;
|
|
52
|
+
const SKILL_OPTIONAL = 'Optional, for your own code and prompts — the vendor skill. The kit installs it pinned, one verified copy per agent skill root (Codex, Claude Code, Antigravity CLI); after your yes in the chat, run this in a terminal of your own:';
|
|
53
|
+
const NO_HOME = 'no home directory found: the kit\'s install needs one';
|
|
52
54
|
const INSTALL = [
|
|
53
|
-
'
|
|
55
|
+
'Or the vendor\'s own routes:',
|
|
54
56
|
'Claude Code:',
|
|
55
57
|
'claude plugin marketplace add typesafe-ai/skills',
|
|
56
58
|
'claude plugin install typesafe@typesafe-ai',
|
|
@@ -63,8 +65,8 @@ const NOT_SEEN = [
|
|
|
63
65
|
'in a Claude Code session (the vendor); ask any other agent to use the TypeSafe skill (the vendor); or run Claude Code\'s own',
|
|
64
66
|
'claude plugin list',
|
|
65
67
|
];
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
+
const FIRST_USE = [
|
|
69
|
+
AFTER_INSTALL,
|
|
68
70
|
'Then 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?".',
|
|
69
71
|
'Success: three choices, each with a confidence.',
|
|
70
72
|
];
|
|
@@ -109,11 +111,26 @@ const skillPaths = (dir, home) => [...new Set([join(dir, '.claude', 'skills'), j
|
|
|
109
111
|
...(home === '' ? [] : [join(home, '.claude', 'skills'), join(home, '.codex', 'skills'), join(home, '.cursor', 'skills')])]
|
|
110
112
|
.map((root) => join(root, SKILL_FILE)))];
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
// The skill targets of the part jev-skill, each judged by the facts leaf's one state rule over the guide's own reads.
|
|
115
|
+
const skillOf = (io, env, home, platform) => {
|
|
116
|
+
const { dir, note } = claudeDirOf(env, home, platform);
|
|
117
|
+
const fs = { lstat: lstatSync, readdir: readdirSync, readFile: readFileSync, pins: io.pins ?? SKILL_PINS };
|
|
118
|
+
const targets = skillTargets({ home, claudeDir: dir }).map((target) => {
|
|
119
|
+
const { state, tag, reason } = skillState(target, fs);
|
|
120
|
+
return { agent: target.agent, path: printable(target.path), state, tag, reason: reason === null ? null : printable(reason) };
|
|
121
|
+
});
|
|
122
|
+
return { targets, install: home === '' ? '' : skillLine(io.toolsDir ?? TOOLS, platform, dir), note: home === '' ? '' : note };
|
|
123
|
+
};
|
|
124
|
+
const markOf = ({ agent, path, state, tag, reason }) => `${agent} skill root: ${state} at ${path}${tag ? ` (${tag})` : ''}${
|
|
125
|
+
reason ? ` — ${reason}` : ''}`;
|
|
126
|
+
const renderSteps = (found, set, connect, where, home, skill) => [
|
|
113
127
|
['STEP 1 — the key', KEY_GET, KEY_CONNECT, ...(where ? [`Run it in ${where}:`] : []), connect, KEY_RESTART, KEY_NEVER,
|
|
114
128
|
set ? `key: ${KEY_VARIABLE} set.` : KEY_NOT_SET],
|
|
115
|
-
['STEP 2 — the vendor skill',
|
|
116
|
-
...(
|
|
129
|
+
['STEP 2 — the vendor skill', SKILL_OPTIONAL,
|
|
130
|
+
...(home === '' ? [NO_HOME] : [...(where ? [`Run it in ${where}:`] : []), skill.install || NO_APPLY_LINE,
|
|
131
|
+
...(skill.note ? [skill.note] : [])]),
|
|
132
|
+
...INSTALL, ...skill.targets.map(markOf),
|
|
133
|
+
...(found.length ? found.map((path) => `skill: found at ${path}`) : NOT_SEEN), ...FIRST_USE],
|
|
117
134
|
];
|
|
118
135
|
|
|
119
136
|
export const main = (argv, io = {}) => {
|
|
@@ -132,9 +149,11 @@ export const main = (argv, io = {}) => {
|
|
|
132
149
|
const platform = io.platform ?? process.platform;
|
|
133
150
|
const connect = connectLine(io.toolsDir ?? TOOLS, platform);
|
|
134
151
|
const where = placeFor(io, env, platform);
|
|
135
|
-
const
|
|
152
|
+
const home = homeOf(io);
|
|
153
|
+
const skill = skillOf(io, env, home, platform);
|
|
154
|
+
const steps = renderSteps(found, set, connect, where, home, skill);
|
|
136
155
|
log(parsed.json
|
|
137
|
-
? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found }, key: { set }, connect, where, steps, prompts: PROMPTS }, null, 2)
|
|
156
|
+
? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found, targets: skill.targets, install: skill.install }, key: { set }, connect, where, steps, prompts: PROMPTS }, null, 2)
|
|
138
157
|
: [...JEV_TEXT, ...steps.flat(), 'WHERE IT PAYS IN THIS WORKFLOW', ...PROMPTS].join('\n'));
|
|
139
158
|
return 0;
|
|
140
159
|
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// spec:jev-guide — docs/ai/specs/kit/jev-guide/jev-skill.md
|
|
3
|
+
// The one command that installs the vendor's Jev skill, pinned and digest-checked, as a verified COPY into
|
|
4
|
+
// the three agent skill roots. The USER runs it with --apply in a terminal of their own after their yes in
|
|
5
|
+
// the chat; without --apply it is a read-only dry run. It reads no secret, needs no terminal, opens no
|
|
6
|
+
// connection, never writes through a link below a base, and never removes what it did not mint this run.
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { dirname, join, posix, win32 } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { fail } from '../references/scripts/markdown-blocks.mjs';
|
|
13
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
14
|
+
import { AFTER_INSTALL, NO_APPLY_LINE, SKILL_FILES, SKILL_PINS, claudeDirOf, printable, skillLine, skillState,
|
|
15
|
+
skillTargets } from './jev-facts.mjs';
|
|
16
|
+
|
|
17
|
+
export const EXIT = Object.freeze({ done: 0, nothingWritten: 1, usage: 2, inPart: 4 });
|
|
18
|
+
const TOOLS = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const VENDOR_DIR = join(TOOLS, '..', 'references', 'vendor', 'typesafe-ai');
|
|
20
|
+
const TMP_PREFIX = '.typesafe-ai.kit-tmp-';
|
|
21
|
+
const OLD_PREFIX = '.typesafe-ai.kit-old-';
|
|
22
|
+
const HELP = `jev-skill — installs the vendor's Jev (TypeSafe) skill, pinned, as one verified copy per agent skill root.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
node jev-skill.mjs [--apply] [--claude-dir DIR]
|
|
26
|
+
|
|
27
|
+
Without --apply: a dry run that writes nothing and prints the plan.
|
|
28
|
+
--apply install or replace the planned copies (Codex ~/.agents/skills, Claude Code <claude dir>/skills,
|
|
29
|
+
Antigravity CLI ~/.gemini/config/skills); a foreign copy is left untouched.
|
|
30
|
+
--claude-dir the Claude Code dir (absolute); default CLAUDE_CONFIG_DIR when absolute, else ~/.claude.
|
|
31
|
+
--help answered only when it is the whole invocation.
|
|
32
|
+
|
|
33
|
+
Exit codes: 0 the dry run rendered or every planned write done; 1 nothing written (the kit's own copy fails
|
|
34
|
+
its check, or no home directory); 2 usage; 4 applied in part (a target refused).`;
|
|
35
|
+
|
|
36
|
+
const parseArgv = (argv, platform) => {
|
|
37
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
38
|
+
if (argv.length !== 1) throw fail(EXIT.usage, '--help is answered only when it is the whole invocation');
|
|
39
|
+
return { help: true };
|
|
40
|
+
}
|
|
41
|
+
const parsed = { help: false, apply: false, claudeDir: null };
|
|
42
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
43
|
+
const arg = argv[index];
|
|
44
|
+
if (arg === '--apply') parsed.apply = true;
|
|
45
|
+
else if (arg === '--claude-dir') {
|
|
46
|
+
const value = argv[index + 1];
|
|
47
|
+
if (value === undefined || value === '' || value.startsWith('-')) throw fail(EXIT.usage, '--claude-dir takes a value');
|
|
48
|
+
if (!(platform === 'win32' ? win32 : posix).isAbsolute(value)) throw fail(EXIT.usage, `--claude-dir takes an absolute path, not ${printable(value)}`);
|
|
49
|
+
parsed.claudeDir = value;
|
|
50
|
+
index += 1;
|
|
51
|
+
} else throw fail(EXIT.usage, `unknown argument "${printable(arg)}" — run with --help`);
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const digestOf = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
57
|
+
const codeOf = (error) => error?.code ?? error?.message ?? String(error);
|
|
58
|
+
const DEFAULT_IO = Object.freeze({
|
|
59
|
+
lstat: lstatSync, readdir: readdirSync, readFile: readFileSync, mkdir: mkdirSync, mkdtemp: mkdtempSync,
|
|
60
|
+
write: (path, bytes) => writeFileSync(path, bytes, { flag: 'wx' }), rename: renameSync, rm: rmSync,
|
|
61
|
+
});
|
|
62
|
+
const homeOf = (io) => {
|
|
63
|
+
if (io.home !== undefined) return io.home;
|
|
64
|
+
try { return homedir(); } catch { return ''; }
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// The kit's own pair, digest-checked against the first pin: the bytes every install writes, or the refused file.
|
|
68
|
+
const vendorBytes = (io) => {
|
|
69
|
+
const bytes = {};
|
|
70
|
+
for (const [name, shipped] of Object.entries(SKILL_FILES)) {
|
|
71
|
+
try { bytes[name] = io.readFile(join(io.vendorDir, shipped)); } catch { return { refused: shipped }; }
|
|
72
|
+
if (digestOf(bytes[name]) !== io.pins[0].digests[name]) return { refused: shipped };
|
|
73
|
+
}
|
|
74
|
+
return { bytes };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const ACTIONS = Object.freeze({ current: 'keep', earlier: 'replace', absent: 'install', foreign: 'untouched' });
|
|
78
|
+
// Every target with its state and its action, the effective Claude Code dir, the note and the leftovers.
|
|
79
|
+
export const planFor = (io) => {
|
|
80
|
+
const resolved = claudeDirOf(io.env, io.home, io.platform);
|
|
81
|
+
const claudeDir = io.claudeDir ?? resolved.dir;
|
|
82
|
+
const note = io.claudeDir ? '' : resolved.note;
|
|
83
|
+
const targets = skillTargets({ home: io.home, claudeDir }).map((target) => {
|
|
84
|
+
const state = skillState(target, io);
|
|
85
|
+
return { ...target, ...state, action: ACTIONS[state.state] };
|
|
86
|
+
});
|
|
87
|
+
const parents = [...new Set(targets.map(({ root }) => dirname(root)))];
|
|
88
|
+
const leftovers = parents.flatMap((parent) => {
|
|
89
|
+
try {
|
|
90
|
+
return [...io.readdir(parent)].map(String).filter((name) => name.startsWith(TMP_PREFIX) || name.startsWith(OLD_PREFIX))
|
|
91
|
+
.sort().map((name) => join(parent, name));
|
|
92
|
+
} catch { return []; }
|
|
93
|
+
});
|
|
94
|
+
return { claudeDir, note, targets, leftovers };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const countsOf = (targets) => Object.fromEntries(Object.values(ACTIONS)
|
|
98
|
+
.map((action) => [action, targets.filter((target) => target.action === action).length]));
|
|
99
|
+
const detailOf = (target, newest) => ({
|
|
100
|
+
keep: `${target.tag}, keep`, replace: `${target.tag}, replace with ${newest}`, install: 'install',
|
|
101
|
+
untouched: `${printable(target.reason)}, left untouched`,
|
|
102
|
+
})[target.action];
|
|
103
|
+
|
|
104
|
+
// One target's write: install or replace, re-checking the target before each rename. Returns its lines and outcome.
|
|
105
|
+
export const applyTarget = (entry, io) => {
|
|
106
|
+
const path = printable(entry.path);
|
|
107
|
+
const parent = dirname(entry.root);
|
|
108
|
+
const lines = [];
|
|
109
|
+
const minted = [];
|
|
110
|
+
const stateNow = () => skillState(entry, io).state;
|
|
111
|
+
const changed = (planned) => {
|
|
112
|
+
const now = stateNow();
|
|
113
|
+
return now === planned ? null : `changed since the plan: now ${now}`;
|
|
114
|
+
};
|
|
115
|
+
let aside = null;
|
|
116
|
+
const refuse = (cause) => {
|
|
117
|
+
lines.push(`refused: ${path} — ${cause}`);
|
|
118
|
+
for (const dir of minted) io.rm(dir, { recursive: true, force: true });
|
|
119
|
+
if (aside) {
|
|
120
|
+
const now = stateNow();
|
|
121
|
+
let kept = now === 'absent' ? null : `the target is now ${now}`;
|
|
122
|
+
if (kept === null) {
|
|
123
|
+
try {
|
|
124
|
+
io.rename(join(aside, 'typesafe-ai'), entry.path);
|
|
125
|
+
io.rm(aside, { recursive: true, force: true });
|
|
126
|
+
} catch (error) { kept = codeOf(error); }
|
|
127
|
+
}
|
|
128
|
+
if (kept !== null) lines.push(`kept aside: ${printable(join(aside, 'typesafe-ai'))} — ${kept}`);
|
|
129
|
+
}
|
|
130
|
+
return { lines, outcome: 'refused' };
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
const below = entry.root.slice(entry.base.length).split(/[\\/]/).filter(Boolean);
|
|
134
|
+
below.reduce((at, name) => {
|
|
135
|
+
const next = join(at, name);
|
|
136
|
+
try { io.lstat(next); } catch (error) {
|
|
137
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
138
|
+
io.mkdir(next, { mode: 0o700 });
|
|
139
|
+
}
|
|
140
|
+
return next;
|
|
141
|
+
}, entry.base);
|
|
142
|
+
const temp = io.mkdtemp(join(parent, TMP_PREFIX));
|
|
143
|
+
minted.push(temp);
|
|
144
|
+
const child = join(temp, 'typesafe-ai');
|
|
145
|
+
io.mkdir(child, { mode: 0o700 });
|
|
146
|
+
for (const name of Object.keys(SKILL_FILES)) io.write(join(child, name), io.bytes[name]);
|
|
147
|
+
for (const name of Object.keys(SKILL_FILES)) {
|
|
148
|
+
if (digestOf(io.readFile(join(child, name))) !== io.pins[0].digests[name]) return refuse('digest mismatch');
|
|
149
|
+
}
|
|
150
|
+
if (entry.state === 'earlier') {
|
|
151
|
+
const old = io.mkdtemp(join(parent, OLD_PREFIX));
|
|
152
|
+
minted.push(old);
|
|
153
|
+
const moved = changed('earlier');
|
|
154
|
+
if (moved) return refuse(moved);
|
|
155
|
+
io.rename(entry.path, join(old, 'typesafe-ai'));
|
|
156
|
+
minted.pop();
|
|
157
|
+
aside = old;
|
|
158
|
+
}
|
|
159
|
+
const placed = changed('absent');
|
|
160
|
+
if (placed) return refuse(placed);
|
|
161
|
+
io.rename(child, entry.path);
|
|
162
|
+
io.rm(temp, { recursive: true, force: true });
|
|
163
|
+
minted.length = 0;
|
|
164
|
+
if (!aside) {
|
|
165
|
+
lines.push(`installed: ${path} (${io.pins[0].tag})`);
|
|
166
|
+
return { lines, outcome: 'installed' };
|
|
167
|
+
}
|
|
168
|
+
const old = { base: aside, path: join(aside, 'typesafe-ai') };
|
|
169
|
+
const verified = skillState(old, io);
|
|
170
|
+
if (verified.state === 'earlier') io.rm(aside, { recursive: true, force: true });
|
|
171
|
+
else lines.push(`kept aside: ${printable(old.path)} — it reads ${verified.state}`);
|
|
172
|
+
lines.unshift(`replaced: ${path} (${entry.tag} → ${io.pins[0].tag})`);
|
|
173
|
+
return { lines, outcome: 'replaced' };
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return refuse(codeOf(error));
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const run = (parsed, io) => {
|
|
180
|
+
const pin = io.pins[0];
|
|
181
|
+
const { bytes, refused } = vendorBytes(io);
|
|
182
|
+
if (refused) {
|
|
183
|
+
io.error(`refused: the kit's own copy of ${refused} is missing, unreadable or does not match the pin — nothing written`);
|
|
184
|
+
return EXIT.nothingWritten;
|
|
185
|
+
}
|
|
186
|
+
if (io.home === '') { io.error('refused: no home directory found — nothing written'); return EXIT.nothingWritten; }
|
|
187
|
+
const plan = planFor({ ...io, claudeDir: parsed.claudeDir ?? undefined });
|
|
188
|
+
io.log(`pin: ${pin.tag}, commit ${pin.commit}`);
|
|
189
|
+
if (plan.note) io.log(plan.note);
|
|
190
|
+
const leftovers = plan.leftovers.map((path) => `leftover: ${printable(path)} — not removed; delete it by hand`);
|
|
191
|
+
const counts = countsOf(plan.targets);
|
|
192
|
+
if (!parsed.apply) {
|
|
193
|
+
for (const target of plan.targets) io.log(`${target.agent}: ${target.state} at ${printable(target.path)} — ${detailOf(target, pin.tag)}`);
|
|
194
|
+
for (const line of leftovers) io.log(line);
|
|
195
|
+
io.log(`dry run — nothing written. Planned: ${counts.install} to install, ${counts.replace} to replace, ${counts.keep} kept, ${counts.untouched} left untouched.`);
|
|
196
|
+
if (counts.install + counts.replace > 0) {
|
|
197
|
+
io.log('To install, run in a terminal of your own:');
|
|
198
|
+
io.log(skillLine(io.toolsDir, io.platform, plan.claudeDir) || NO_APPLY_LINE);
|
|
199
|
+
}
|
|
200
|
+
return EXIT.done;
|
|
201
|
+
}
|
|
202
|
+
const done = { installed: 0, replaced: 0, kept: 0, untouched: 0, refused: 0 };
|
|
203
|
+
for (const target of plan.targets) {
|
|
204
|
+
if (target.action === 'keep' || target.action === 'untouched') {
|
|
205
|
+
io.log(target.action === 'keep' ? `kept: ${printable(target.path)} (${target.tag})`
|
|
206
|
+
: `left untouched: ${printable(target.path)} — ${printable(target.reason)}`);
|
|
207
|
+
done[target.action === 'keep' ? 'kept' : 'untouched'] += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const result = applyTarget(target, { ...io, bytes });
|
|
211
|
+
for (const line of result.lines) io.log(line);
|
|
212
|
+
done[result.outcome] += 1;
|
|
213
|
+
}
|
|
214
|
+
for (const line of leftovers) io.log(line);
|
|
215
|
+
io.log(`done — ${done.installed} installed, ${done.replaced} replaced, ${done.kept} kept, ${done.untouched} left untouched, ${done.refused} refused.`);
|
|
216
|
+
if (done.installed + done.replaced > 0) io.log(AFTER_INSTALL);
|
|
217
|
+
return done.refused === 0 ? EXIT.done : EXIT.inPart;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const main = (argv, io = {}) => {
|
|
221
|
+
const resolved = {
|
|
222
|
+
...DEFAULT_IO, log: console.log, error: console.error, env: process.env, platform: process.platform,
|
|
223
|
+
pins: SKILL_PINS, vendorDir: VENDOR_DIR, toolsDir: TOOLS, ...io,
|
|
224
|
+
};
|
|
225
|
+
let parsed;
|
|
226
|
+
try { parsed = parseArgv(argv, resolved.platform); } catch (err) { resolved.error(err.message); return EXIT.usage; }
|
|
227
|
+
if (parsed.help) { resolved.log(HELP); return EXIT.done; }
|
|
228
|
+
return run(parsed, { ...resolved, home: homeOf(io) });
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (isDirectRun(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|
package/tools/manifest/schema.md
CHANGED
|
@@ -68,7 +68,7 @@ unlike `contract`, it is validated by `validate.mjs` itself: a malformed entry *
|
|
|
68
68
|
this block). Each entry:
|
|
69
69
|
|
|
70
70
|
- `key` (string, required) — UPPER_SNAKE_CASE env-var name; unique across the array.
|
|
71
|
-
- `kind` (string, required) — `enum | integer | duration | boolean
|
|
71
|
+
- `kind` (string, required) — `enum | integer | duration | boolean | posture` (`posture`: a non-empty string with no control byte and no leading or trailing whitespace — a bridge model or effort; the catalog, not the kind, decides which values a host offers).
|
|
72
72
|
- `enum` → `values` (string[], non-empty, unique).
|
|
73
73
|
- `integer` → `min` / `max` (safe integers, `min <= max`); values are decimal strings.
|
|
74
74
|
- `duration` → the wrappers' shell duration grammar (`5m`, `30m`, `90s`): a unit suffix
|
|
@@ -40,7 +40,7 @@ export const INVALID = 'invalid';
|
|
|
40
40
|
// declared `default`), the kit-side bridge-settings writer (for a user-supplied value), and — mirrored
|
|
41
41
|
// in shell as aw_settings_valid — the wrappers all use, so a value the writer accepts is exactly a
|
|
42
42
|
// value a wrapper honors. A value is always compared as a STRING (the settings-file wire format).
|
|
43
|
-
export const SETTING_KINDS = new Set(['enum', 'integer', 'duration', 'boolean']);
|
|
43
|
+
export const SETTING_KINDS = new Set(['enum', 'integer', 'duration', 'boolean', 'posture']);
|
|
44
44
|
// The wrappers' shell duration grammar: a unit suffix is REQUIRED (a bare integer is invalid), and
|
|
45
45
|
// zero durations are rejected — `timeout 0` DISABLES a hard cap, so a persistent settings line could
|
|
46
46
|
// otherwise silently remove the stall guard.
|
|
@@ -56,6 +56,7 @@ export const settingValueValid = (entry, value) => {
|
|
|
56
56
|
}
|
|
57
57
|
case 'duration': return DURATION_RE.test(value) && !ZERO_DURATION_RE.test(value);
|
|
58
58
|
case 'boolean': return value === '0' || value === '1';
|
|
59
|
+
case 'posture': return value !== '' && !/[\u0000-\u001f\u007f]/u.test(value) && !/^\s|\s$/u.test(value);
|
|
59
60
|
default: return false;
|
|
60
61
|
}
|
|
61
62
|
};
|
|
@@ -613,7 +614,7 @@ export const validateManifest = (skillDir) => {
|
|
|
613
614
|
}
|
|
614
615
|
}
|
|
615
616
|
if (!SETTING_KINDS.has(entry.kind)) {
|
|
616
|
-
errors.push(`${at}.kind must be one of enum|integer|duration|boolean`);
|
|
617
|
+
errors.push(`${at}.kind must be one of enum|integer|duration|boolean|posture`);
|
|
617
618
|
return; // the typed checks below are meaningless without a kind
|
|
618
619
|
}
|
|
619
620
|
if (entry.kind === 'enum'
|
package/tools/procedures.mjs
CHANGED
|
@@ -154,8 +154,8 @@ export const extractSection = (text, activity) => {
|
|
|
154
154
|
|
|
155
155
|
const resolveAllSlots = ({ activity, config, detection, overrides, surveyLens }) => {
|
|
156
156
|
// The host-level settings knobs (manifest-as-source), best-effort: a corrupt bundle degrades to none
|
|
157
|
-
// and the advisor never crashes. Attached per wrapper cmd via each knob's `appliesTo
|
|
158
|
-
//
|
|
157
|
+
// and the advisor never crashes. Attached per wrapper cmd via each knob's `appliesTo` — the model
|
|
158
|
+
// keys included, as knobs with their allowed-value label, never an effective-model claim.
|
|
159
159
|
const registry = (() => {
|
|
160
160
|
try {
|
|
161
161
|
return loadRegistry({});
|
|
@@ -494,7 +494,7 @@ const costLanesAdvice = () => [
|
|
|
494
494
|
'Cost lanes (orchestration.md §5) — route every step to the cheapest adequate executor:',
|
|
495
495
|
' • L0 deterministic script — run the gate matrix as ONE batch: /agent-workflow-kit gates (the project-declared docs/ai/gates.json); rotation headroom: archive-changelog / archive-issues / archive-decisions --check. An exit code beats a model re-read.',
|
|
496
496
|
' • L1 cheap subagent (small model, low effort, read-only tools — /agent-workflow-kit agents places the vehicles) — mechanical sweeps, changelog fact-skeletons, gate-failure triage. Extraction/drafting ONLY; the orchestrator verifies the output and owns every conclusion.',
|
|
497
|
-
' • L2 subscription bridge (codex / agy) — reviews per the resolved recipe above, on
|
|
497
|
+
' • L2 subscription bridge (codex / agy) — reviews per the resolved recipe above, on each bridge\'s host posture (the model the user set, else the bridge default); a weaker model is the user\'s setting, never a per-call choice — a probe is the one route to a one-off and its review receipt never attests.',
|
|
498
498
|
' • L3 frontier — judgment: plan/fold/synthesis, ADR/handover/changelog-entry wording, persuasive copy, go/no-go, real code.',
|
|
499
499
|
' • A step with no named guardrail does not move down a lane; red lines never move down (council review models · real code · memory/copy wording · the maintainer approval asks).',
|
|
500
500
|
' • Sandbox lanes (under an OS sandbox): the L0 surfaces are sandbox-safe — gates/ledger/state/fold checks, git reads, plain no-network tests; the bridge wrappers are genuinely unsandboxed (network); npm-cache-touching commands are COMMAND-SHAPE dependent — first try the sandbox-safe shape (cache under $TMPDIR, offline/notifier off). Move ONLY the failing command out of the sandbox, never its class; BATCH consecutive unsandboxed calls. Pre-dispatch host-diff: before the FIRST dispatch of each bridge, diff its manifest networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt. Nested-sandbox honesty: a backend CLI shipping its OWN OS sandbox cannot run nested inside a harness sandbox — route it outside (excludedCommands / a per-run consented bypass) on the OBSERVED failure, never a preemptive blanket.',
|
package/tools/recipes.mjs
CHANGED
|
@@ -171,11 +171,11 @@ const dispatchFor = (recipe, detection) => {
|
|
|
171
171
|
});
|
|
172
172
|
};
|
|
173
173
|
|
|
174
|
-
const QUOTA_NOTE = "
|
|
174
|
+
const QUOTA_NOTE = "Each bridge runs the host posture the user set; a weaker model is the user's bridge setting, never a per-call choice.";
|
|
175
175
|
const COUNCIL_QUOTA_NOTE = "Council spends two backends' quota for one decision — reserve it for changes that justify the cost.";
|
|
176
176
|
|
|
177
177
|
// The quota reminder rides a subscription-BACKEND dispatch only: the executor vehicle spends no
|
|
178
|
-
// bridge quota, and its recipe is a frontier one, so
|
|
178
|
+
// bridge quota, and its recipe is a frontier one, so a bridge-posture reminder would not apply to it.
|
|
179
179
|
const notesFor = (recipe, dispatch) => {
|
|
180
180
|
const notes = [];
|
|
181
181
|
if (dispatch.some((d) => d.role !== CARRY_ROLE)) notes.push(QUOTA_NOTE);
|