@vegastack/vegafactory 0.19.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/LICENSE +21 -0
- package/README.md +417 -0
- package/dist/dashboard-artifact.json +21240 -0
- package/dist/index.js +22752 -0
- package/dist/run-wrapper.js +377 -0
- package/package.json +46 -0
- package/skill/dev-architect/SKILL.md +106 -0
- package/skill/dev-architect/agents/openai.yaml +4 -0
- package/skill/dev-architect/references/ai-agents.md +96 -0
- package/skill/dev-architect/references/conventions.md +113 -0
- package/skill/dev-architect/references/data.md +89 -0
- package/skill/dev-architect/references/infra.md +99 -0
- package/skill/dev-architect/references/mobile.md +75 -0
- package/skill/dev-architect/references/pinned-facts.md +133 -0
- package/skill/dev-architect/references/principles.md +117 -0
- package/skill/dev-architect/references/security.md +90 -0
- package/skill/dev-architect/references/stack.md +38 -0
- package/skill/dev-architect/references/web.md +102 -0
- package/skill/dev-architect/refresh/REFRESH.md +31 -0
- package/skill/dev-architect/refresh/sources.json +239 -0
- package/skill/dev-chronicle/SKILL.md +52 -0
- package/skill/dev-chronicle/agents/openai.yaml +4 -0
- package/skill/dev-chronicle/references/conventions.md +113 -0
- package/skill/dev-chronicle/references/styles.md +75 -0
- package/skill/dev-chronicle/refresh/REFRESH.md +3 -0
- package/skill/dev-chronicle/refresh/sources.json +6 -0
- package/skill/dev-debug/SKILL.md +43 -0
- package/skill/dev-debug/agents/openai.yaml +4 -0
- package/skill/dev-debug/references/conventions.md +113 -0
- package/skill/dev-debug/references/loop-ladder.md +20 -0
- package/skill/dev-debug/refresh/REFRESH.md +3 -0
- package/skill/dev-debug/refresh/sources.json +6 -0
- package/skill/dev-implement/SKILL.md +81 -0
- package/skill/dev-implement/agents/openai.yaml +4 -0
- package/skill/dev-implement/assets/workflows/implement-children.js +12 -0
- package/skill/dev-implement/references/ask-route.md +80 -0
- package/skill/dev-implement/references/changelog-and-chronicle.md +21 -0
- package/skill/dev-implement/references/conventions.md +113 -0
- package/skill/dev-implement/references/ledger-and-resume.md +52 -0
- package/skill/dev-implement/references/parallel-children.md +35 -0
- package/skill/dev-implement/references/worktrees.md +63 -0
- package/skill/dev-implement/refresh/REFRESH.md +3 -0
- package/skill/dev-implement/refresh/sources.json +6 -0
- package/skill/dev-implement/scripts/children.mjs +479 -0
- package/skill/dev-implement/scripts/effective-policy.mjs +694 -0
- package/skill/dev-implement/scripts/evidence-check.mjs +107 -0
- package/skill/dev-implement/scripts/evidence-upload.mjs +181 -0
- package/skill/dev-implement/scripts/learning.mjs +41 -0
- package/skill/dev-implement/scripts/lib/approval.mjs +1182 -0
- package/skill/dev-implement/scripts/lib/gh.mjs +101 -0
- package/skill/dev-implement/scripts/preflight.mjs +153 -0
- package/skill/dev-implement/scripts/questions.mjs +381 -0
- package/skill/dev-implement/scripts/reclaim.mjs +116 -0
- package/skill/dev-implement/scripts/recovery.mjs +213 -0
- package/skill/dev-implement/scripts/worktree.mjs +991 -0
- package/skill/dev-intake/SKILL.md +80 -0
- package/skill/dev-intake/agents/openai.yaml +4 -0
- package/skill/dev-intake/references/ask-route.md +80 -0
- package/skill/dev-intake/references/brief-template.md +89 -0
- package/skill/dev-intake/references/conventions.md +113 -0
- package/skill/dev-intake/refresh/REFRESH.md +3 -0
- package/skill/dev-intake/refresh/sources.json +6 -0
- package/skill/dev-intake/scripts/brief-lint.mjs +87 -0
- package/skill/dev-intake/scripts/lib/approval.mjs +1182 -0
- package/skill/dev-intake/scripts/questions.mjs +381 -0
- package/skill/dev-plan/SKILL.md +54 -0
- package/skill/dev-plan/agents/openai.yaml +4 -0
- package/skill/dev-plan/references/ask-route.md +80 -0
- package/skill/dev-plan/references/conventions.md +113 -0
- package/skill/dev-plan/references/plan-format.md +82 -0
- package/skill/dev-plan/refresh/REFRESH.md +3 -0
- package/skill/dev-plan/refresh/sources.json +6 -0
- package/skill/dev-plan/scripts/lib/approval.mjs +1182 -0
- package/skill/dev-plan/scripts/plan-lint.mjs +319 -0
- package/skill/dev-plan/scripts/questions.mjs +381 -0
- package/skill/dev-review/SKILL.md +76 -0
- package/skill/dev-review/agents/openai.yaml +4 -0
- package/skill/dev-review/assets/review-known-patterns.md.template +32 -0
- package/skill/dev-review/references/conventions.md +113 -0
- package/skill/dev-review/references/cross-agent.md +39 -0
- package/skill/dev-review/references/dispatch-prompts.md +121 -0
- package/skill/dev-review/references/security-axis.md +45 -0
- package/skill/dev-review/refresh/REFRESH.md +5 -0
- package/skill/dev-review/refresh/sources.json +6 -0
- package/skill/dev-setup/SKILL.md +113 -0
- package/skill/dev-setup/agents/openai.yaml +4 -0
- package/skill/dev-setup/assets/agents-section.md.template +24 -0
- package/skill/dev-setup/assets/dev-profile.md.template +102 -0
- package/skill/dev-setup/assets/factory-board.yml.template +162 -0
- package/skill/dev-setup/assets/hooks/decision-nudge.mjs +30 -0
- package/skill/dev-setup/assets/hooks/prompt-skill-mention.mjs +29 -0
- package/skill/dev-setup/assets/hooks/session-end.mjs +14 -0
- package/skill/dev-setup/assets/hooks/session-start.mjs +178 -0
- package/skill/dev-setup/assets/hooks/ship-guard.mjs +738 -0
- package/skill/dev-setup/assets/hooks/skill-activated.mjs +47 -0
- package/skill/dev-setup/assets/hooks/stop-heartbeat.mjs +14 -0
- package/skill/dev-setup/assets/workflows/implement-children.js +12 -0
- package/skill/dev-setup/references/ask-route.md +80 -0
- package/skill/dev-setup/references/conventions.md +113 -0
- package/skill/dev-setup/references/github-app.md +286 -0
- package/skill/dev-setup/references/harness-facts.md +169 -0
- package/skill/dev-setup/references/stack-playbooks.md +124 -0
- package/skill/dev-setup/refresh/REFRESH.md +13 -0
- package/skill/dev-setup/refresh/sources.json +602 -0
- package/skill/dev-setup/scripts/effective-policy.mjs +694 -0
- package/skill/dev-setup/scripts/questions.mjs +381 -0
- package/skill/dev-setup/scripts/ship-policy.mjs +195 -0
- package/skill/dev-ship/SKILL.md +56 -0
- package/skill/dev-ship/agents/openai.yaml +4 -0
- package/skill/dev-ship/references/conventions.md +113 -0
- package/skill/dev-ship/references/runbook.md +72 -0
- package/skill/dev-ship/refresh/REFRESH.md +3 -0
- package/skill/dev-ship/refresh/sources.json +6 -0
- package/skill/dev-ship/scripts/lib/approval.mjs +1182 -0
- package/skill/dev-ship/scripts/ship-gate.mjs +485 -0
- package/skill/dev-status/SKILL.md +56 -0
- package/skill/dev-status/agents/openai.yaml +4 -0
- package/skill/dev-status/references/conventions.md +113 -0
- package/skill/dev-status/refresh/REFRESH.md +3 -0
- package/skill/dev-status/refresh/sources.json +6 -0
- package/skill/dev-status/scripts/effective-policy.mjs +694 -0
- package/skill/dev-status/scripts/status.mjs +311 -0
- package/skill/skill-maintainer/SKILL.md +82 -0
- package/skill/skill-maintainer/agents/openai.yaml +4 -0
- package/skill/skill-maintainer/references/release-ops.md +61 -0
- package/skill/skill-maintainer/references/standards.md +137 -0
- package/skill/skill-maintainer/refresh/REFRESH.md +40 -0
- package/skill/skill-maintainer/refresh/sources.json +103 -0
- package/skill/skill-scan/SKILL.md +59 -0
- package/skill/skill-scan/agents/openai.yaml +4 -0
- package/skill/skill-scan/refresh/REFRESH.md +18 -0
- package/skill/skill-scan/refresh/sources.json +133 -0
- package/skill/skill-scan/scripts/lib/skillspector.mjs +226 -0
- package/skill/skill-scan/scripts/skill-scan.mjs +1002 -0
- package/skill/skillify/SKILL.md +108 -0
- package/skill/skillify/agents/openai.yaml +4 -0
- package/skill/skillify/assets/templates/README.md.template +26 -0
- package/skill/skillify/assets/templates/REFRESH.md.template +16 -0
- package/skill/skillify/assets/templates/SKILL.md.template +27 -0
- package/skill/skillify/assets/templates/evals.json.template +12 -0
- package/skill/skillify/assets/templates/openai.yaml.template +4 -0
- package/skill/skillify/assets/templates/skill.test.ts.template +27 -0
- package/skill/skillify/assets/templates/sources.json.template +6 -0
- package/skill/skillify/assets/templates/trigger-queries.json.template +1 -0
- package/skill/skillify/references/authoring.md +103 -0
- package/skill/skillify/references/eval-playbook.md +112 -0
- package/skill/skillify/refresh/REFRESH.md +23 -0
- package/skill/skillify/refresh/sources.json +6 -0
- package/skill/skillify/scripts/scaffold-skill.mjs +334 -0
- package/skill/skillify/scripts/trigger-check.mjs +209 -0
- package/skill/vegafactory-setup/SKILL.md +77 -0
- package/skill/vegafactory-setup/agents/openai.yaml +4 -0
- package/skill/vegafactory-setup/assets/control-room/boards.md.template +17 -0
- package/skill/vegafactory-setup/assets/control-room/decisions.md.template +5 -0
- package/skill/vegafactory-setup/assets/control-room/group.md.template +44 -0
- package/skill/vegafactory-setup/assets/control-room/onboarding/dispatcher-box.md.template +86 -0
- package/skill/vegafactory-setup/assets/control-room/onboarding/new-repo.md.template +13 -0
- package/skill/vegafactory-setup/assets/control-room/onboarding/new-teammate.md.template +12 -0
- package/skill/vegafactory-setup/assets/control-room/org.md.template +60 -0
- package/skill/vegafactory-setup/assets/control-room/people.csv.template +2 -0
- package/skill/vegafactory-setup/assets/control-room/repos.md.template +7 -0
- package/skill/vegafactory-setup/assets/control-room/rules/CODEOWNERS.template +6 -0
- package/skill/vegafactory-setup/assets/control-room/rules/README.md.template +9 -0
- package/skill/vegafactory-setup/assets/control-room/rules/stats-privacy.md.template +44 -0
- package/skill/vegafactory-setup/assets/control-room/templates/README.md.template +9 -0
- package/skill/vegafactory-setup/references/control-room.md +149 -0
- package/skill/vegafactory-setup/references/conventions.md +113 -0
- package/skill/vegafactory-setup/refresh/REFRESH.md +5 -0
- package/skill/vegafactory-setup/refresh/sources.json +6 -0
- package/skill-integrity.json +252 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dev-status data gatherer: everything the board report needs, deterministically,
|
|
3
|
+
// read-only, markers-only. The skill renders; this script never invents state.
|
|
4
|
+
//
|
|
5
|
+
// Usage: node status.mjs [--repo o/r] [--orphan-hours 6] [--dev-md <path>] [--viewer <login>] [--me | --all] --json
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
const policyUrl = new URL('./effective-policy.mjs', import.meta.url);
|
|
12
|
+
const { resolveState, readWorkflowLabels } = await import(existsSync(policyUrl) ? policyUrl.href : new URL('../../dev-setup/scripts/effective-policy.mjs', import.meta.url).href);
|
|
13
|
+
|
|
14
|
+
export function readKnobs(devMdText) {
|
|
15
|
+
const labelMap = readWorkflowLabels(devMdText ?? '');
|
|
16
|
+
return {
|
|
17
|
+
labelMap,
|
|
18
|
+
states: Object.values(labelMap),
|
|
19
|
+
risky: 'risky',
|
|
20
|
+
scopes: ['research', 'quick-build', 'full-plan'],
|
|
21
|
+
register: /^decisions:\s*(\S+)/m.exec(devMdText ?? '')?.[1] ?? '.vegastack/decisions.md',
|
|
22
|
+
operators: (/^operators:\s*([^\n#]+)/m.exec(devMdText ?? '')?.[1] ?? '')
|
|
23
|
+
.split(',').map((t) => t.trim()).filter(Boolean),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// stdio mode for a discarded fd, hoisted out of quote-adjacency: SkillSpector reads the
|
|
28
|
+
// bare word beside its own closing quote as a removal cue and fails closed on the whole
|
|
29
|
+
// file (skill-maintainer's standards.md, known behaviours). Same value, same behaviour.
|
|
30
|
+
const DISCARD = 'ignore';
|
|
31
|
+
|
|
32
|
+
function gh(args) {
|
|
33
|
+
// env spread at call time: some runtimes pass a startup env snapshot to
|
|
34
|
+
// children, which would hide the VSK_GH/GH_STUB_DIR test seam.
|
|
35
|
+
const out = execFileSync(process.env.VSK_GH || 'gh', args, { encoding: 'utf8', stdio: [DISCARD, 'pipe', 'pipe'], env: { ...process.env } });
|
|
36
|
+
return JSON.parse(out);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Link markup carries no meaning in a terminal, so the board renders link-free
|
|
40
|
+
// variants alongside the raw fields — and register comparison uses them, so a
|
|
41
|
+
// linked gist still matches its plain register line. URLs containing `)` are not
|
|
42
|
+
// a shape this workflow produces (issue and commit URLs never do).
|
|
43
|
+
const LINK = /\[([^\]]*)\]\((?:[^)]*)\)/g;
|
|
44
|
+
|
|
45
|
+
export function stripLinks(text) {
|
|
46
|
+
if (typeof text !== 'string') return '';
|
|
47
|
+
return text.replace(LINK, '$1');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function ageDays(iso, now = Date.now()) {
|
|
51
|
+
return Math.floor((now - Date.parse(iso)) / 86_400_000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Ledger liveness is measured in hours, not days: a session that hands back in
|
|
55
|
+
// hours can go dark for a fraction of a day, which whole-day granularity cannot
|
|
56
|
+
// even represent. The ledger's updated_at is the only liveness proxy an agent
|
|
57
|
+
// session exposes — a live session (even a multi-day one) checkpoints and keeps
|
|
58
|
+
// this small; a dead one freezes it.
|
|
59
|
+
export function ageHours(iso, now = Date.now()) {
|
|
60
|
+
return Math.floor((now - Date.parse(iso)) / 3_600_000);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseMarker(body) {
|
|
64
|
+
const match = /<!--\s*vsk:v1\s+([^>]*?)\s*-->/.exec(body ?? '');
|
|
65
|
+
if (!match) return null;
|
|
66
|
+
const keys = {};
|
|
67
|
+
for (const pair of match[1].split(/\s+/)) {
|
|
68
|
+
const eq = pair.indexOf('=');
|
|
69
|
+
if (eq > 0) keys[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
70
|
+
}
|
|
71
|
+
return { keys };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The approval-marker comment's author — the first tier of the operator rule.
|
|
75
|
+
// Last approval wins: a plan approval supersedes the brief's.
|
|
76
|
+
export function approvalAuthor(comments) {
|
|
77
|
+
let author = null;
|
|
78
|
+
for (const c of comments ?? []) {
|
|
79
|
+
if (parseMarker(c.body)?.keys?.type === 'approval') author = c.user?.login ?? null;
|
|
80
|
+
}
|
|
81
|
+
return author;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// conventions' operator rule, deterministic: approval-marker author if listed,
|
|
85
|
+
// else the issue author if listed, else the first listed. An empty operators
|
|
86
|
+
// list has no operator — the project never filled the knob, and inventing one
|
|
87
|
+
// would assign work to a human who never agreed to it.
|
|
88
|
+
export function resolveOperator({ approvalAuthor: approver = null, issueAuthor = null, operators = [] } = {}) {
|
|
89
|
+
if (operators.length === 0) return null;
|
|
90
|
+
if (approver && operators.includes(approver)) return approver;
|
|
91
|
+
if (issueAuthor && operators.includes(issueAuthor)) return issueAuthor;
|
|
92
|
+
return operators[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Count plan-comment checkboxes: [done, total]. No plan comment → null.
|
|
96
|
+
export function taskProgress(comments) {
|
|
97
|
+
for (const c of comments ?? []) {
|
|
98
|
+
if (parseMarker(c.body)?.keys?.type === 'plan') {
|
|
99
|
+
const done = (c.body.match(/^- \[x\]/gim) ?? []).length;
|
|
100
|
+
const total = done + (c.body.match(/^- \[ \]/gm) ?? []).length;
|
|
101
|
+
return total > 0 ? [done, total] : null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Latest ledger comment's updated_at → staleness signal for working issues.
|
|
108
|
+
export function ledgerMovedAt(comments) {
|
|
109
|
+
let at = null;
|
|
110
|
+
for (const c of comments ?? []) {
|
|
111
|
+
if (parseMarker(c.body)?.keys?.type === 'ledger') at = c.updated_at;
|
|
112
|
+
}
|
|
113
|
+
return at;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Register lines are written plain, proposals may carry links — compare link-free
|
|
117
|
+
// so a recorded decision does not stay "pending" forever on its markup alone.
|
|
118
|
+
function recorded(gist, registerText) {
|
|
119
|
+
return stripLinks(registerText).includes(stripLinks(gist).slice(0, 60));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Decision proposals not yet in the register: marker type=decision comments
|
|
123
|
+
// (and evidence **Decision:** lines) whose text isn't in the register file.
|
|
124
|
+
export function pendingDecisions(comments, registerText) {
|
|
125
|
+
const pending = [];
|
|
126
|
+
for (const c of comments ?? []) {
|
|
127
|
+
const type = parseMarker(c.body)?.keys?.type;
|
|
128
|
+
if (type === 'decision') {
|
|
129
|
+
const gist = (c.body.split('\n').find((l) => l.trim() && !l.startsWith('<!--') && !l.startsWith('#')) ?? '').trim();
|
|
130
|
+
if (gist && !recorded(gist, registerText)) pending.push(gist);
|
|
131
|
+
}
|
|
132
|
+
if (type === 'evidence') {
|
|
133
|
+
const m = /\*\*Decision:\*\*\s*([^\n]+)/.exec(c.body);
|
|
134
|
+
if (m && !/^none\b/i.test(m[1].trim()) && !recorded(m[1].trim(), registerText)) pending.push(m[1].trim());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return pending;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// CheckRuns carry `conclusion`; StatusContexts carry `state`. An empty rollup
|
|
141
|
+
// is "no-checks", never green.
|
|
142
|
+
export function checksState(rollup) {
|
|
143
|
+
if (!rollup || rollup.length === 0) return 'no-checks';
|
|
144
|
+
const ok = (c) => ['SUCCESS', 'NEUTRAL', 'SKIPPED'].includes(c.conclusion ?? c.state ?? '');
|
|
145
|
+
return rollup.every(ok) ? 'green' : 'pending-or-red';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
// --- control-room drift ---------------------------------------------------------------
|
|
150
|
+
// This script ships standalone into consumer projects, so it carries its own copies of the
|
|
151
|
+
// knob grammar rather than importing packages/cli. Every function here is total: a control
|
|
152
|
+
// room nobody has synced is a reported fact, never an error, and never an edit to dev.md.
|
|
153
|
+
|
|
154
|
+
export function knobMap(text) {
|
|
155
|
+
const map = {};
|
|
156
|
+
for (const line of String(text ?? '').split('\n')) {
|
|
157
|
+
const m = /^([a-z][a-z0-9-]*):\s*(.+)$/.exec(line);
|
|
158
|
+
if (!m) continue;
|
|
159
|
+
map[m[1]] = m[2].split(/\s+#/)[0].trim();
|
|
160
|
+
}
|
|
161
|
+
return map;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function controlRoomKnob(devMdText) {
|
|
165
|
+
const value = knobMap(devMdText)['control-room'];
|
|
166
|
+
if (!value || value === 'none') return null;
|
|
167
|
+
const [repoPart, tail = ''] = value.split('#');
|
|
168
|
+
if (!repoPart || !repoPart.includes('/')) return null;
|
|
169
|
+
const [groupPart, shaPart] = tail.split('@');
|
|
170
|
+
return { org: repoPart.split('/')[0], repo: repoPart, group: groupPart || null, sha: shaPart || null };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Drift is only ever a proposal: a hand edit in dev.md outranks the org and group defaults, so
|
|
174
|
+
// a knob both sides name with different values is shown with both values and the operator decides.
|
|
175
|
+
export function controlRoomDrift({ devMdText, orgText, groupText, cloneSha }) {
|
|
176
|
+
const knob = controlRoomKnob(devMdText);
|
|
177
|
+
if (!knob) return null;
|
|
178
|
+
const repoKnobs = knobMap(devMdText);
|
|
179
|
+
const orgKnobs = knobMap(orgText);
|
|
180
|
+
const groupKnobs = knobMap(groupText);
|
|
181
|
+
const merged = { ...orgKnobs, ...groupKnobs };
|
|
182
|
+
const knobs = Object.keys(merged)
|
|
183
|
+
.filter((name) => name in repoKnobs && repoKnobs[name] !== merged[name])
|
|
184
|
+
.sort()
|
|
185
|
+
.map((name) => ({ knob: name, repo: repoKnobs[name], controlRoom: merged[name], source: name in groupKnobs ? 'group' : 'org' }));
|
|
186
|
+
return {
|
|
187
|
+
recordedSha: knob.sha,
|
|
188
|
+
cloneSha: cloneSha ?? null,
|
|
189
|
+
behind: Boolean(knob.sha && cloneSha && knob.sha !== cloneSha),
|
|
190
|
+
knobs,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function controlRoomState(devMdText, home) {
|
|
195
|
+
const knob = controlRoomKnob(devMdText);
|
|
196
|
+
if (!knob) return null;
|
|
197
|
+
let path = join(home, '.vegastack/control-room', knob.org);
|
|
198
|
+
let lastSyncedAt = null;
|
|
199
|
+
try {
|
|
200
|
+
const state = JSON.parse(readFileSync(join(home, '.vegastack/factory.json'), 'utf8'));
|
|
201
|
+
const entry = state?.controlRooms?.[knob.org];
|
|
202
|
+
if (entry && typeof entry === 'object') {
|
|
203
|
+
if (typeof entry.path === 'string') path = entry.path;
|
|
204
|
+
if (typeof entry.lastSyncedAt === 'string') lastSyncedAt = entry.lastSyncedAt;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// no state file, or one this machine cannot read: the default path still answers
|
|
208
|
+
}
|
|
209
|
+
if (!existsSync(path)) {
|
|
210
|
+
return { available: false, reason: `no local control-room clone at ${path} — run \`vegafactory sync\``, recordedSha: knob.sha, lastSyncedAt };
|
|
211
|
+
}
|
|
212
|
+
const read = (relative) => { try { return readFileSync(join(path, relative), 'utf8'); } catch { return ''; } };
|
|
213
|
+
let cloneSha = null;
|
|
214
|
+
try {
|
|
215
|
+
cloneSha = execFileSync('git', ['-C', path, 'rev-parse', '--short=7', 'HEAD'], { encoding: 'utf8', stdio: [DISCARD, 'pipe', DISCARD] }).trim();
|
|
216
|
+
} catch {
|
|
217
|
+
// a clone without a readable head still answers on its files
|
|
218
|
+
}
|
|
219
|
+
const drift = controlRoomDrift({
|
|
220
|
+
devMdText,
|
|
221
|
+
orgText: read('org.md'),
|
|
222
|
+
groupText: knob.group ? read(join('groups', knob.group, 'group.md')) : '',
|
|
223
|
+
cloneSha,
|
|
224
|
+
});
|
|
225
|
+
return { available: true, path, lastSyncedAt, recordedSha: drift.recordedSha, cloneSha: drift.cloneSha, behind: drift.behind, knobs: drift.knobs };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function gatherStatus({ repo, orphanHours = 6, devMdPath = '.vegastack/dev.md', chroniclePath = '.vegastack/chronicle.md', me, view = 'me', now = Date.now(), home = homedir() } = {}) {
|
|
229
|
+
const resolvedRepo = repo || gh(['repo', 'view', '--json', 'nameWithOwner']).nameWithOwner;
|
|
230
|
+
// One caller lookup for the whole run; a gh failure propagates to the CLI's exit 2.
|
|
231
|
+
const viewer = me || gh(['api', 'user']).login;
|
|
232
|
+
const devMdText = existsSync(devMdPath) ? readFileSync(devMdPath, 'utf8') : '';
|
|
233
|
+
const knobs = readKnobs(devMdText);
|
|
234
|
+
const board = { unresolved: [] };
|
|
235
|
+
const unresolvedSeen = new Set();
|
|
236
|
+
for (const label of knobs.states) {
|
|
237
|
+
board[label] = gh(['issue', 'list', '-R', resolvedRepo, '--label', label, '--state', 'open',
|
|
238
|
+
'--json', 'number,title,url,updatedAt,labels,assignees,author']).filter((i) => {
|
|
239
|
+
const state = resolveState((i.labels ?? []).map(l => l.name), knobs.labelMap);
|
|
240
|
+
if (state.blocks.length) {
|
|
241
|
+
if (!unresolvedSeen.has(i.number)) board.unresolved.push({ ...i, state: null, blocks: state.blocks });
|
|
242
|
+
unresolvedSeen.add(i.number);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
return knobs.labelMap[state.state] === label;
|
|
246
|
+
}).map((i) => ({
|
|
247
|
+
number: i.number, title: i.title, url: i.url,
|
|
248
|
+
ageDays: ageDays(i.updatedAt, now),
|
|
249
|
+
scope: (i.labels ?? []).map((l) => l.name).find((n) => knobs.scopes.includes(n)) ?? null,
|
|
250
|
+
risky: (i.labels ?? []).some((l) => l.name === knobs.risky),
|
|
251
|
+
assignees: (i.assignees ?? []).map((a) => a.login),
|
|
252
|
+
author: i.author?.login ?? null,
|
|
253
|
+
operator: resolveOperator({ issueAuthor: i.author?.login ?? null, operators: knobs.operators }),
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Enrich working + for-operator issues with comment-derived signals.
|
|
258
|
+
const registerText = existsSync(knobs.register) ? readFileSync(knobs.register, 'utf8') : '';
|
|
259
|
+
const decisions = [];
|
|
260
|
+
for (const bucket of [knobs.states[3], knobs.states[4]]) {
|
|
261
|
+
for (const issue of board[bucket]) {
|
|
262
|
+
const comments = gh(['api', 'repos/' + resolvedRepo + '/issues/' + issue.number + '/comments', '--paginate']);
|
|
263
|
+
issue.tasks = taskProgress(comments);
|
|
264
|
+
issue.operator = resolveOperator({ approvalAuthor: approvalAuthor(comments), issueAuthor: issue.author, operators: knobs.operators });
|
|
265
|
+
const moved = ledgerMovedAt(comments);
|
|
266
|
+
issue.ledgerAgeHours = moved ? ageHours(moved, now) : null;
|
|
267
|
+
// possiblyOrphaned: a working issue whose ledger has been silent past the
|
|
268
|
+
// orphan threshold — or which never got a ledger comment at all (claimed,
|
|
269
|
+
// then died before its first write). A fact for the operator to act on,
|
|
270
|
+
// never an automatic reclaim: the reset is theirs to run.
|
|
271
|
+
issue.possiblyOrphaned = bucket === knobs.states[3] && (issue.ledgerAgeHours === null || issue.ledgerAgeHours >= orphanHours);
|
|
272
|
+
decisions.push(...pendingDecisions(comments, registerText).map((d) => ({ issue: issue.number, gist: d, gistPlain: stripLinks(d) })));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const prs = gh(['pr', 'list', '-R', resolvedRepo, '--json', 'number,title,url,statusCheckRollup'])
|
|
277
|
+
.map((p) => ({
|
|
278
|
+
number: p.number, title: p.title, url: p.url,
|
|
279
|
+
checks: checksState(p.statusCheckRollup),
|
|
280
|
+
}));
|
|
281
|
+
|
|
282
|
+
let lastChronicle = null;
|
|
283
|
+
if (existsSync(chroniclePath)) {
|
|
284
|
+
const m = /^## (\d{2}-\d{2}-\d{4}) — (.+)$/m.exec(readFileSync(chroniclePath, 'utf8'));
|
|
285
|
+
if (m) lastChronicle = { date: m[1], title: m[2], titlePlain: stripLinks(m[2]) };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// "Whose move is it" is the question this script exists to answer, so the
|
|
289
|
+
// filter is data, not the skill's judgment. Human states only: `ready` and
|
|
290
|
+
// `working` belong to agents, and an unassigned `ready` issue is correct.
|
|
291
|
+
const humanStates = [knobs.states[0], knobs.states[4]];
|
|
292
|
+
const human = humanStates.flatMap((s) => board[s].map((i) => ({ ...i, state: s })));
|
|
293
|
+
const needsYou = human.filter((i) => view === 'all' || i.assignees.includes(viewer));
|
|
294
|
+
const unowned = human.filter((i) => i.assignees.length === 0);
|
|
295
|
+
|
|
296
|
+
return { repo: resolvedRepo, orphanHours, viewer, view, operators: knobs.operators, board, needsYou, unowned, prs, pendingDecisions: decisions, lastChronicle, controlRoom: controlRoomState(devMdText, home) };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
300
|
+
if (invokedDirectly) {
|
|
301
|
+
const argv = process.argv.slice(2);
|
|
302
|
+
const get = (f) => { const i = argv.indexOf(f); return i === -1 ? undefined : argv[i + 1]; };
|
|
303
|
+
try {
|
|
304
|
+
const orphanRaw = Number(get('--orphan-hours'));
|
|
305
|
+
const data = gatherStatus({ repo: get('--repo'), orphanHours: Number.isFinite(orphanRaw) && orphanRaw >= 1 ? orphanRaw : 6, devMdPath: get('--dev-md'), me: get('--viewer'), view: argv.includes('--all') ? 'all' : 'me' });
|
|
306
|
+
console.log(JSON.stringify(data, null, argv.includes('--json') ? 2 : 0));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error(`status: cannot verify — ${error.message}`);
|
|
309
|
+
process.exit(2);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-maintainer
|
|
3
|
+
description: Standards and release operations for the vegafactory repository itself. Use when working on this repository - editing an existing skill (SKILL.md, references, refresh registry, tests), renaming, deprecating, or removing a skill, creating or checking a skill group, cutting a release or rolling one back, adjudicating a skill-scan finding, or checking cross-agent portability across Claude Code, Codex, Hermes, and the agentskills.io standard. Not for scaffolding a new skill or scoring one against the contract (skillify), and not for skills that live in other projects.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# VegaStack Skill Maintainer
|
|
7
|
+
|
|
8
|
+
This skill states what must be true; skillify states how to get there. When a rule here disagrees with `CONTRIBUTING.md` or `.vegastack/dev.md`, those win — then fix this skill.
|
|
9
|
+
|
|
10
|
+
## Operating rules
|
|
11
|
+
|
|
12
|
+
1. Skill content lives only in the skill's own directory, at exactly two depths: `skills/<name>/` or `skills/<group>/<name>/`; deeper is a build error. A group is a directory of skills plus a `GROUP.md` (title, then a blurb line); group and skill names share one namespace and grammar. Wiring lives outside the skill — the `packages/cli/packaging.json` allowlist, keyed by **bare** skill name because the bundle is flat and an install carries no group; the root README row; the changeset (a `.changeset/<slug>.md` file whose frontmatter names the package and bump, shaped per dev-implement's changelog rule; changesets are compiled into `packages/cli/CHANGELOG.md` at release) — and the skillify scaffolder writes all three. "The operator" throughout is the human running the workflow, named by their GitHub username.
|
|
13
|
+
2. Frontmatter is exactly `name` and `description`; the spec's `license`, `compatibility`, and `metadata` are a policy exception needing maintainer sign-off.
|
|
14
|
+
3. `name` equals the directory name: a lowercase letter first, then `[a-z0-9-]`, no leading, trailing, or consecutive hyphens, at most 64 chars.
|
|
15
|
+
4. `description`: at most 1024 chars, third person, no angle brackets, no space-hash. It states what the skill does and when to load it as a calm "Use when …" conditional carrying the concrete phrasings users type, plus one "Not for …" clause naming the nearest neighbour, because harnesses quote descriptions verbatim in the skill list and emphatic wording (all-caps "must", "critical", "load this before") over-triggers there. Triggers go in the first sentence because listings truncate. The description never summarises the workflow: agents follow the summary and skip the body.
|
|
16
|
+
5. `SKILL.md` under 500 lines (target 150), roughly 5k tokens; detail in `references/`, executables in `scripts/`, templates in `assets/`; relative links stay one level deep inside the skill.
|
|
17
|
+
6. No Claude-only body syntax (token list in [standards](references/standards.md)); reference scripts as plain relative paths from the skill directory.
|
|
18
|
+
7. Checksums, versions, and timestamps in any `refresh/sources.json` come only from a refresh-runner run, because a hand-written value records a verification that never happened.
|
|
19
|
+
8. Before finishing any change, from the repo root: `node packages/cli/scripts/validate-skill.mjs <skill-dir>`, `bun test <skill-dir>`, and `node packages/cli/scripts/structure.mjs check` must pass, and `bun run readme:sync --write` follows any packaging change so the skill README's file table matches packaging.json. `bun run check` includes the structure check.
|
|
20
|
+
|
|
21
|
+
## Route progressively
|
|
22
|
+
|
|
23
|
+
| Need | Read |
|
|
24
|
+
|---|---|
|
|
25
|
+
| tri-harness standards: discovery paths, frontmatter, budgets, install surfaces, portability | [standards](references/standards.md) |
|
|
26
|
+
| skill-scan triage and the suppression baseline | [standards](references/standards.md) |
|
|
27
|
+
| a new skill: the should-it-exist gate, scaffolding, audit, evals | the `skillify` skill |
|
|
28
|
+
| repo shape, groups, the structure check | the group workflow below |
|
|
29
|
+
| release, rename, deprecate, rollback | [release ops](references/release-ops.md) |
|
|
30
|
+
| this skill's freshness contract | [REFRESH](refresh/REFRESH.md) |
|
|
31
|
+
| authoritative repo policy | `CONTRIBUTING.md` and `.vegastack/dev.md` |
|
|
32
|
+
|
|
33
|
+
## Workflow: create or maintain a group
|
|
34
|
+
|
|
35
|
+
Groups are this skill's; the tool is `packages/cli/scripts/structure.mjs` at the repo root.
|
|
36
|
+
|
|
37
|
+
1. **Create a group** — `node packages/cli/scripts/structure.mjs create-group <name> --title "<Display Title>" --blurb "<one line>"` prints the plan and each refusal before anything is written; `--write` applies it.
|
|
38
|
+
2. **Put skills in it** — skillify's scaffolder places a new skill with `--group <name>`, refusing an unknown or malformed group before writing. Moving a skill is a `git mv` plus its README row and its test's validator-import depth.
|
|
39
|
+
3. **Check the shape** — `node packages/cli/scripts/structure.mjs check` blocks on depth, name collisions, `GROUP.md`, README-section, packaging, and README-row faults; it warns on an empty group, a group of one, and placeholder text (`--strict`: warnings exit 1). Dot-prefixed files are ignored everywhere.
|
|
40
|
+
|
|
41
|
+
An installed skill is always `<surface>/<bare-name>/`; a group is a selection: `add`, `verify`, and `remove` take a name, `--group <name>`, or `--all`, one transaction per group. `packages/cli/repo-only.json` marks `skill-maintainer` and `skillify` repo-only, so `--all` skips them; the list is build-validated data, independent of grouping.
|
|
42
|
+
|
|
43
|
+
## Workflow: scaffold a new skill
|
|
44
|
+
|
|
45
|
+
Run skillify's `scripts/scaffold-skill.mjs` (`--group <name>` for a grouped skill), fill in the README row and changeset placeholders it leaves, and add later packaged files to the packaging entry by hand because the build fails on unlisted files. Then rules 2–6 and 8; the authoring and eval discipline is skillify's.
|
|
46
|
+
|
|
47
|
+
## Workflow: update or maintain
|
|
48
|
+
|
|
49
|
+
- **Content versioning.** Per the content-semver bullet in `.vegastack/dev.md`: new rules, references, recorded decisions, and skill renames are MINOR; weakening a normative rule, removing a skill, or breaking a per-project profile format is MAJOR (recorded pre-1.0 exception aside); factual refreshes are PATCH.
|
|
50
|
+
- **Tag volatile claims.** A sentence carrying a vendor version, mechanism name, or numeric budget gets a source marker naming an ID in that skill's `refresh/sources.json` (its `affected` list names the reference); untagged volatile facts rot silently.
|
|
51
|
+
- **Description budgets.** Keep triggers in the first sentence: both harness listings truncate, and the limits table below carries the numbers.
|
|
52
|
+
- **Packaged-file changes.** Adding, removing, or renaming a packaged file updates its packaging entry in the same PR.
|
|
53
|
+
|
|
54
|
+
## Workflow: rename, deprecate, or remove
|
|
55
|
+
|
|
56
|
+
Playbook in [release ops](references/release-ops.md): a rename changes the directory and the frontmatter `name` in the same commit, updates the packaging entry, root README row, changeset, and every cross-reference to the old name (sibling descriptions, fixtures, `ambiguous_with` entries, docs — `grep -rn <old-name>` finds them), and is MINOR unless the operator declares MAJOR; deprecation is announced in README and CHANGELOG first; removal deletes the tree, unwires it, and is MAJOR.
|
|
57
|
+
|
|
58
|
+
## Workflow: release
|
|
59
|
+
|
|
60
|
+
The `## Ship` runbook in `.vegastack/dev.md` is the release sequence and says which steps take the operator's word. One version identity covers the installer and every bundled skill, so a bump leaves every deployed per-project profile valid. Rollback is roll-forward plus `npm deprecate`. Details: [release ops](references/release-ops.md).
|
|
61
|
+
|
|
62
|
+
## Workflow: portability check
|
|
63
|
+
|
|
64
|
+
- [ ] Frontmatter keys and limits per rules 2–4; `name` equals the directory name.
|
|
65
|
+
- [ ] `SKILL.md` under 500 lines; relative links resolve inside the skill; no Claude-only body syntax.
|
|
66
|
+
- [ ] `agents/openai.yaml` present for Codex (other harnesses ignore extra files).
|
|
67
|
+
- [ ] Install surfaces: Claude Code `.claude/skills` or `~/.claude/skills`; Codex `.agents/skills`; Hermes `~/.hermes/skills`, global only.
|
|
68
|
+
- [ ] Rule 8's checks are clean.
|
|
69
|
+
|
|
70
|
+
## Hard limits
|
|
71
|
+
|
|
72
|
+
| Limit | Value |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `name` | ≤ 64 chars, lowercase letter first, then `[a-z0-9-]`, no consecutive hyphens, equals directory name |
|
|
75
|
+
| `description` | 1–1024 chars, no angle brackets |
|
|
76
|
+
| `SKILL.md` | under 500 lines / ~5k tokens; target under 150 lines |
|
|
77
|
+
| Claude Code listing | name + description truncated at 1,536 chars per skill |
|
|
78
|
+
| Claude Code body | first 5,000 tokens persist across compaction; 25,000-token shared skills budget |
|
|
79
|
+
| Codex skill list | 2% of context window / 8,000 chars — descriptions shortened first |
|
|
80
|
+
| Hermes skills_list | ~3k tokens |
|
|
81
|
+
|
|
82
|
+
These mirror marked sentences in [standards](references/standards.md); update both in one PR when a source changes.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Release, rename, and rollback operations
|
|
2
|
+
|
|
3
|
+
The expanded release/rename/rollback detail behind the `## Ship` runbook and content-semver bullet in `.vegastack/dev.md` at the repo root. dev.md is the canonical process doc and wins on any disagreement; this file only elaborates it and must never contradict it.
|
|
4
|
+
|
|
5
|
+
## Semver for skill content
|
|
6
|
+
|
|
7
|
+
Content is advisory prose and decision tables — no rule IDs, no machine-extracted rule format.
|
|
8
|
+
|
|
9
|
+
| Bump | Content change |
|
|
10
|
+
|---|---|
|
|
11
|
+
| MAJOR | Removing a skill. A breaking change to the per-project profile format (`.vegastack/dev.md`) that invalidates existing committed profiles — the operator may also declare any other change major. |
|
|
12
|
+
| MINOR | Renaming a skill (default — the operator declares major when the break warrants it). New reference file or reference section. New or changed recorded decision (e.g. a new "use/not/why" row, a new red line). New skill. |
|
|
13
|
+
| PATCH | Factual refreshes: pinned-fact updates, version pins, vendor mechanism names, URLs, registry checksums. Wording clarifications that don't change the recorded decision. Test/fixture-only changes. |
|
|
14
|
+
|
|
15
|
+
Installer/CLI changes follow ordinary semver on the same package version; a release takes the highest bump either side requires.
|
|
16
|
+
|
|
17
|
+
## One version identity
|
|
18
|
+
|
|
19
|
+
There is a single source of truth: the **package version** (`packages/cli/package.json`,
|
|
20
|
+
changesets-managed) — the npm release identity for the installer and every bundled skill's
|
|
21
|
+
content snapshot. No skill tracks a separate content-contract version, and no per-project
|
|
22
|
+
profile carries a schema version to validate against.
|
|
23
|
+
|
|
24
|
+
## Release flow (tag-driven)
|
|
25
|
+
|
|
26
|
+
1. Every PR that changes released behavior lands with a changeset — a `.changeset/<slug>.md` written directly, since the `changeset` add prompt is interactive (`bunx changeset version` at release time is the only CLI use) — whose bump follows the table above and whose shape follows dev-implement's changelog rule.
|
|
27
|
+
2. Maintainer, at release time: `bunx changeset version` (applies changesets to `packages/cli/package.json` and the changelog), then `bun install` so any dependency changes riding along reach the lockfile, commit on a `chore/release-<version>` branch and open its PR — main is branch-protected with no admin exemption, so the bump reaches main by merge and never by direct push. That install does **not** update the workspace's own version inside `bun.lock`: measured on the bun the root `packageManager` line pins, the recorded version is left unchanged by a plain install, by `--force`, and even by `--lockfile-only`, so it sits at an older number indefinitely. `bun install --frozen-lockfile`, which CI and the release workflow both run, passes with it, and nothing in this repo reads it: `packages/cli/package.json` is the single release identity. Never hand-edit the lockfile to "correct" it. The release record is `packages/cli/CHANGELOG.md`, changesets-written — never by hand; the root `CHANGELOG.md` is the frozen pre-0.3.0 record pointing there.
|
|
28
|
+
3. Merge the release PR on the operator's word, pull main, then tag that merged commit `v<version>` and push the tag. Tags are not branch-protected, so the tag push is unaffected.
|
|
29
|
+
4. The release workflow prepares one immutable CLI/dashboard pair before either package publishes. `scripts/release-artifacts.mjs prepare <evidence-dir> v<version>` requires clean source and records full source/tree identity, toolchains, check and built-skill scan evidence, build-graph and shipped-runtime SBOMs. It builds/assembles dashboard and CLI/skills, scans, packs dashboard with scripts disabled, derives its regular-file descriptor into generated CLI dist, then packs CLI with scripts disabled. Rebuilding CLI invalidates its descriptor. The final external manifest binds both exact tarballs and each required build/runtime SBOM by filename, scope and SHA-256; publication and release asset verification refuse missing or changed evidence. Only completed preparation writes the finalized manifest; generated descriptors are never committed.
|
|
30
|
+
5. `scripts/release-publish.mjs <manifest> --publish --promote` runs only inside the repository-wide serialized Release workflow on the operator's word. All versions and recovery reruns share that workflow concurrency group with cancellation disabled; direct local live publishing refuses. A successful immutable pair upload precedes this command. It uses retained tarballs with scripts disabled and the existing authentication arrangement; this work does not restore provenance or change runners. Stage dashboard before CLI under a non-latest candidate tag. Compare registry integrity and downloaded payload bytes separately from provenance; #156 owns actual registry/attestation evidence. Only a definite public/authenticated 404 means absent. Network, rate-limit and auth failures stop recovery; a timed-out publish is read back before any later attempt. Matching existing versions resume; conflicting bytes require an operator-selected new version.
|
|
31
|
+
6. The states are prepared → dashboard-present → pair-present → smoked → promoted. Smoke the downloaded pair in clean homes before changing either latest tag or claiming release success. Promotion is not atomic: each pending mutation and confirmed per-package tag readback is recorded, so a failed second-tag promotion leaves a recorded partial state. Rerun the same workflow: it restores the finalized pair and available prior observations, verifies both registry identities, repeats smoke, then reconciles tags and lost responses before retrying. If no finalized pair exists, only affirmative skipped-publication records for every prior attempt permit retrying failed preparation; missing/expired pair evidence after possible publication is a refusal, never permission to rebuild. Never retag historical releases or move latest backward. Retain the finalized pair before the first registry mutation, and upload attempt outcomes separately. After workflow completion, download/verify those same artifacts instead of preparing or publishing a second local pair. Retain tarballs, manifests, SBOMs and failure logs as CI artifacts and release assets; copy needed evidence before artifact retention expires. A platform is qualified only by results for this same descriptor-backed pair on that actual OS/architecture. Missing Linux guest execution remains unperformed; macOS or source engines cannot prove Ubuntu x64 compatibility.
|
|
32
|
+
|
|
33
|
+
Contributors do not bump versions in PRs; releases are maintainer-driven.
|
|
34
|
+
|
|
35
|
+
## Rollback
|
|
36
|
+
|
|
37
|
+
`npm unpublish` is limited to 72 hours and breaks pinned consumers — it is not the rollback mechanism.
|
|
38
|
+
|
|
39
|
+
1. **Roll forward:** revert the offending commits through a PR onto `main` (protection applies to reverts too), changeset, tag, release the previous known-good content as a **new patch version**.
|
|
40
|
+
2. **Deprecate the bad version:** `npm deprecate @vegastack/vegafactory@<bad> "Broken — use <new>"`.
|
|
41
|
+
3. Unpublish only if the bad version leaked secrets or is actively harmful, still within 72 hours, and always *in addition to* steps 1–2, never instead.
|
|
42
|
+
|
|
43
|
+
## Rename a skill
|
|
44
|
+
|
|
45
|
+
Skill names are consumer-facing identifiers — treat a rename as a stable-ID break:
|
|
46
|
+
|
|
47
|
+
1. Rename the directory and the frontmatter `name` in the same commit — they must always stay equal.
|
|
48
|
+
2. Update every wiring point in the same PR: the skill's entry in `packages/cli/packaging.json`, the root README skills table row, and any cross-skill or docs links.
|
|
49
|
+
3. Changeset: MINOR by default — major only when the operator declares it. Either way, note the migration in `CHANGELOG.md`: copies installed under the old name are orphaned, and installer operations addressed to the old name stop resolving once the shipped manifest no longer knows it.
|
|
50
|
+
4. Registry source IDs inside `refresh/sources.json` are skill-internal and unaffected, but every `affected` ref must still resolve to a real reference in the renamed tree.
|
|
51
|
+
5. Re-run `node packages/cli/scripts/validate-skill.mjs <new-skill-dir>`, the skill's tests, and `node packages/cli/scripts/structure.mjs check` — name/directory equality is validated, and the structure check catches a README row left pointing at the old path.
|
|
52
|
+
|
|
53
|
+
## Deprecate / remove a skill
|
|
54
|
+
|
|
55
|
+
1. Announce deprecation in the root README table and `CHANGELOG.md` at least one release before removal when practical.
|
|
56
|
+
2. Removal: delete the skill's directory, remove its allowlist entries and README row, MAJOR changeset with migration notes. Removing the last skill from a group also removes the group's `GROUP.md` and its README section.
|
|
57
|
+
3. Removing a skill in a new MAJOR does **not** deprecate previously published package versions — `npm deprecate` only versions that are themselves broken.
|
|
58
|
+
|
|
59
|
+
## Refresh branches
|
|
60
|
+
|
|
61
|
+
Branches named `refresh/**` are reserved for the automated freshness loop and are CI-restricted to refresh metadata at either legal depth (`skills/<name>/refresh/` and `skills/<group>/<name>/refresh/`). Human content changes go on normal branches. Never hand-edit checksums/versions/timestamps anywhere — CI re-fetches claimed baselines, so hand-edited values cannot merge.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Tri-harness skill standards
|
|
2
|
+
|
|
3
|
+
The complete standards reference for skills in this repository, covering the three target harnesses — Claude Code, Codex, Hermes — and the agentskills.io open standard they converge on. Verified 2026-08-08.
|
|
4
|
+
|
|
5
|
+
Sentences carrying volatile vendor facts end with an HTML `source:` comment naming a registry ID; each ID maps to an entry in [sources.json](../refresh/sources.json). When a source changes, the marked sentences are the edit surface — see [REFRESH.md](../refresh/REFRESH.md). Items flagged **UNVERIFIED** must never be asserted as fact in skill content or reviews.
|
|
6
|
+
|
|
7
|
+
## agentskills.io open standard
|
|
8
|
+
|
|
9
|
+
Source: https://agentskills.io/specification. Reference validator: `skills-ref validate` from github.com/agentskills/agentskills. <!-- source: AGENTSKILLS-SPEC -->
|
|
10
|
+
|
|
11
|
+
- Directory layout: `skill-name/SKILL.md` required; optional `scripts/`, `references/`, `assets/`. <!-- source: AGENTSKILLS-SPEC -->
|
|
12
|
+
- Frontmatter — the spec defines exactly six fields: <!-- source: AGENTSKILLS-SPEC -->
|
|
13
|
+
- `name` (required): 1–64 chars, `[a-z0-9-]` only, no leading/trailing hyphen, no consecutive hyphens, must match the parent directory name. <!-- source: AGENTSKILLS-SPEC -->
|
|
14
|
+
- `description` (required): 1–1024 chars, what + when, keyword-rich. <!-- source: AGENTSKILLS-SPEC -->
|
|
15
|
+
- `license` (optional): short string or bundled-file reference. <!-- source: AGENTSKILLS-SPEC -->
|
|
16
|
+
- `compatibility` (optional): 1–500 chars, environment requirements only. <!-- source: AGENTSKILLS-SPEC -->
|
|
17
|
+
- `metadata` (optional): string→string map. <!-- source: AGENTSKILLS-SPEC -->
|
|
18
|
+
- `allowed-tools` (optional): space-separated string; experimental, support varies across harnesses. <!-- source: AGENTSKILLS-SPEC -->
|
|
19
|
+
- Progressive disclosure: metadata costs ~100 tokens at startup; SKILL.md body should stay under 5,000 tokens (under 500 lines); bundled resources load on demand; relative file references one level deep. <!-- source: AGENTSKILLS-SPEC -->
|
|
20
|
+
- The spec's own version identifier: **UNVERIFIED** — do not cite a spec version number.
|
|
21
|
+
|
|
22
|
+
## Claude Code
|
|
23
|
+
|
|
24
|
+
Source: https://code.claude.com/docs/en/skills. <!-- source: CLAUDE-CODE-SKILLS -->
|
|
25
|
+
|
|
26
|
+
- Discovery: project `.claude/skills/<name>/SKILL.md`, loaded from the start directory and every parent up to the repo root; personal `~/.claude/skills/`; enterprise via managed settings; nested `<subdir>/.claude/skills/` lazy-loaded; plugin `<plugin>/skills/`. Precedence: enterprise > personal > project. Symlinks are followed; skill changes are detected live mid-session. <!-- source: CLAUDE-CODE-SKILLS -->
|
|
27
|
+
- Frontmatter: all fields optional in Claude Code itself (directory name is the command name; description recommended). It accepts an extended set beyond the spec six — `when_to_use`, `argument-hint`, `arguments`, `disable-model-invocation`, `user-invocable`, `allowed-tools`, `disallowed-tools`, `model`, `effort`, `context` (fork), `agent`, `background`, `hooks`, `paths`, `shell`, plus `license`/`compatibility`/`metadata` (accepted but not acted on). <!-- source: CLAUDE-CODE-SKILLS -->
|
|
28
|
+
- **Packaging trap:** claude.ai uploads, the Skills API, and `package_skill.py` hard-error on any key outside the spec six (`name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`) — a skill that works in Claude Code can still be unpackageable. <!-- source: CLAUDE-CODE-SKILLS -->
|
|
29
|
+
- Context budgets: each skill's `description` (+`when_to_use`) is always in context, truncated at 1,536 chars combined per skill; the body loads on invocation and persists — the first 5,000 tokens are re-attached after compaction, within a 25,000-token budget shared across skills. <!-- source: CLAUDE-CODE-SKILLS -->
|
|
30
|
+
- Invocation: implicit matching on description, or direct `/skill-name`. `allowed-tools` is a per-turn permission pre-grant only, not a sandbox. <!-- source: CLAUDE-CODE-SKILLS -->
|
|
31
|
+
- Claude-only body features — **never use in this repo's skills** (broken or dead weight elsewhere): <!-- source: CLAUDE-CODE-SKILLS -->
|
|
32
|
+
- `` !`cmd` `` dynamic command-output injection
|
|
33
|
+
- the `ARGUMENTS`, `0` and `name` argument placeholders, each written after a dollar sign
|
|
34
|
+
- the `CLAUDE_SKILL_DIR` and `CLAUDE_PROJECT_DIR` environment paths in their dollar-brace form
|
|
35
|
+
- 2026 changes: commands and skills merged; `context: fork` subagents; skill-level hooks; `paths` glob gating; `skillOverrides`; skills-dir plugins; bundled skills. <!-- source: CLAUDE-CODE-SKILLS -->
|
|
36
|
+
|
|
37
|
+
## Codex (OpenAI)
|
|
38
|
+
|
|
39
|
+
Source: https://developers.openai.com/codex/skills (canonical content at learn.chatgpt.com/docs/build-skills.md). <!-- source: CODEX-SKILLS -->
|
|
40
|
+
|
|
41
|
+
- Discovery order: `<cwd>/.agents/skills` → parent directories' `.agents/skills` within a git repo → `<repo root>/.agents/skills` → `~/.agents/skills` → `/etc/codex/skills` → bundled. Symlinks followed. <!-- source: CODEX-SKILLS -->
|
|
42
|
+
- Legacy `~/.codex/skills` discovery: **UNVERIFIED** — no longer documented; do not rely on it.
|
|
43
|
+
- Frontmatter: `name` + `description` required; same SKILL.md format as the spec. <!-- source: CODEX-SKILLS -->
|
|
44
|
+
- Unknown frontmatter keys: officially undocumented; community evidence says ignored — **UNVERIFIED** officially.
|
|
45
|
+
- Optional per-skill `agents/openai.yaml`: <!-- source: CODEX-SKILLS -->
|
|
46
|
+
- `interface`: `display_name`, `short_description`, `icon_small`, `icon_large`, `brand_color`, `default_prompt`
|
|
47
|
+
- `policy`: `allow_implicit_invocation: false` (default true)
|
|
48
|
+
- `dependencies`: `tools: [{type: "mcp", value: "..."}]`
|
|
49
|
+
- Invocation: `$` mention, `/skills` list, implicit matching on description. The skill list is capped at 2% of the context window / 8,000 chars — descriptions are shortened first, so front-load trigger words. <!-- source: CODEX-SKILLS -->
|
|
50
|
+
|
|
51
|
+
## Hermes (Nous Research)
|
|
52
|
+
|
|
53
|
+
Source: https://hermes-agent.nousresearch.com/docs/user-guide/features/skills. <!-- source: HERMES-SKILLS -->
|
|
54
|
+
|
|
55
|
+
- Hermes Agent is Nous Research's open agent harness (github.com/NousResearch/hermes-agent; CLI + desktop + messengers; v0.9.0 Apr 2026), explicitly compatible with the agentskills.io standard. <!-- source: HERMES-SKILLS -->
|
|
56
|
+
- Discovery: a single global directory `~/.hermes/skills/` — **no project-level discovery at all**. Extra directories only via `~/.hermes/config.yaml` under `skills.external_dirs` (e.g. `[~/.agents/skills]`). `hermes skills install <source>` installs from hubs/URLs into the global directory after a security scan. <!-- source: HERMES-SKILLS -->
|
|
57
|
+
- Frontmatter: `name` + `description` required; optional Hermes fields: `version`, `platforms` (macos, linux), `required_environment_variables`, `requires_toolsets`, `fallback_for_toolsets`, `metadata.hermes.{tags, category, config}`. Name pattern `^[a-z][a-z0-9_-]*$` — must start with a letter (underscores allowed by Hermes but not by the spec; use hyphens). <!-- source: HERMES-SKILLS -->
|
|
58
|
+
- Unknown-key handling, and treatment of `allowed-tools`/`license`/`compatibility`: **UNVERIFIED** (presumed ignored; no error reports).
|
|
59
|
+
- Triggering: every skill becomes a slash command (`/name args`, chainable); model-side progressive disclosure via `skills_list()` (~3k tokens) then `skill_view(name)`. `/learn` auto-authors SKILL.md. <!-- source: HERMES-SKILLS -->
|
|
60
|
+
|
|
61
|
+
## Install surfaces
|
|
62
|
+
|
|
63
|
+
| Harness | Project install | Global install | Notes |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| Claude Code | `.claude/skills/` | `~/.claude/skills/` | parents scanned to repo root; nested dirs lazy-loaded <!-- source: CLAUDE-CODE-SKILLS --> |
|
|
66
|
+
| Codex | `.agents/skills/` | `~/.agents/skills/` | also `/etc/codex/skills` and bundled <!-- source: CODEX-SKILLS --> |
|
|
67
|
+
| Hermes | — none | `~/.hermes/skills/` | **global only**; extension only via `skills.external_dirs` config <!-- source: HERMES-SKILLS --> |
|
|
68
|
+
|
|
69
|
+
Installer implication: the `@vegastack/vegafactory` installer must treat Hermes as global-only — a "project install" for Hermes does not exist.
|
|
70
|
+
|
|
71
|
+
These paths are the harnesses' own discovery rules and are unaffected by how skills are selected. The installer's `--group` and `--all` flags choose *which* skills to act on; an installed skill is always `<surface>/<bare-name>/`, never `<surface>/<group>/<name>/`.
|
|
72
|
+
|
|
73
|
+
## Portability rules (this repo's policy)
|
|
74
|
+
|
|
75
|
+
One authored tree, three harnesses. Every skill in `skills/` follows all seven:
|
|
76
|
+
|
|
77
|
+
1. **Frontmatter:** only `name` + `description` (spec also allows `license`, `compatibility`, `metadata` — off by default here). Never depend on `allowed-tools`. No Claude-only keys: they break claude.ai packaging and are dead weight elsewhere.
|
|
78
|
+
2. **Name:** equals the directory name; grammar intersection across harnesses: starts with a lowercase letter, then `[a-z0-9-]`, no consecutive hyphens, no underscores, ≤ 64 chars.
|
|
79
|
+
3. **Description:** ≤ 1024 chars, trigger words front-loaded (Codex 2%/8,000-char list budget; Claude Code 1,536-char per-skill listing truncation).
|
|
80
|
+
4. **Body syntax:** no Claude-only tokens (list above); scripts referenced as plain relative paths runnable from the skill directory; relative links one level deep.
|
|
81
|
+
5. **Size:** SKILL.md under 500 lines / under 5k tokens; detail in `references/`, executables in `scripts/`, templates in `assets/`.
|
|
82
|
+
6. **Extra files:** `agents/openai.yaml` is safe to ship — Claude Code and Hermes ignore unknown files.
|
|
83
|
+
7. **Per-harness metadata** that must survive claude.ai packaging goes under `metadata:` with namespaced keys (e.g. `metadata.hermes.*`).
|
|
84
|
+
|
|
85
|
+
Repo enforcement: `packages/cli/scripts/validate-skill.mjs` (run by `bun run check`) accepts exactly the spec six (`name`, `description`, `license`, `compatibility`, `allowed-tools`, `metadata`) and rejects everything else, enforces the full name grammar (lowercase-letter start, no consecutive hyphens, ≤64 chars, name equals the skill directory name), and rejects empty or over-length descriptions and angle brackets. Policy (rule 1) is stricter than the validator; the minimal two keys are the default.
|
|
86
|
+
|
|
87
|
+
## Skill scanning and the suppression baseline
|
|
88
|
+
|
|
89
|
+
Every skill this repo ships is scanned by [NVIDIA SkillSpector](https://github.com/NVIDIA/skillspector) through the `skill-scan` skill's `scripts/skill-scan.mjs`, at the Verify gate before a push and again as a blocking `guard:` before publish. The guard blocks on any unsuppressed **HIGH or CRITICAL** finding and ignores the aggregate risk score: a skills repo documents the very mechanics the scanner matches on, so the score reflects our subject matter more than our risk.
|
|
90
|
+
|
|
91
|
+
Suppressions live in `.vegastack/skillspector-baseline.json` — a real SkillSpector baseline, passed with `--baseline`, so nothing here forks the scanner's own matching and upstream changes to it arrive for free. The rules this repo adds on top:
|
|
92
|
+
|
|
93
|
+
- **A suppression needs the operator's word.** It is a security decision on the record, the same as an operator dismissal appended to `.vegastack/review-known-patterns.md` — never a step taken to get a guard green.
|
|
94
|
+
- **Matchers must be LITERAL — the guard rejects `*`, `?`, `[` and `]` outright.** This is not a style preference: a rule of `{"id": "*"}` silenced all 39 findings while the guard reported "pass with warnings", and a first fix that rejected `*` and `**` was bypassed by `?*` on the next attempt. Matching wildcard *spellings* is an arms race; "name the thing" is the only mechanically checkable form of "scope a rule as narrowly as its cause". Two files means two rules, which reads better in a diff anyway. (Consequence worth knowing: a filename that literally contains `[` or `]` cannot be suppressed by a rule — the scanner would glob-interpret it too. Use a fingerprint.)
|
|
95
|
+
- **Scope a rule as narrowly as its cause.** `{"id": "P2"}` alone silences prompt injection across every skill forever; `{"id": "P2", "path": "references/conventions.md"}` silences one documented protocol in one file. A rule with no `path` needs a reason that explains why the whole repo is the cause.
|
|
96
|
+
- **`path` and `file` are the same matcher, `id` and `rule_id` likewise** — SkillSpector resolves `path = raw.get("path") or raw.get("file")`. Both spellings are accepted and both go through the literal check.
|
|
97
|
+
- **A baseline carrying `fingerprints:` must also set `scanner_version`** — the scanner refuses it otherwise, once per skill, and the guard catches that up front so one misconfiguration does not surface as a dozen unreadable-report failures.
|
|
98
|
+
- **Every `reason` carries a "Still flag if:" clause**, naming the condition that makes the pattern a real finding again. The guard blocks on a missing, empty, scanner-placeholder, or clause-less reason — this is enforced, not trusted.
|
|
99
|
+
- **`fingerprints:` are for one-off accepted findings only.** They are content-hashed, so any edit to the surrounding file re-triggers them — which is their re-trigger condition, and why they need a real reason but not the clause. A structural pattern suppressed by fingerprint will reappear at the worst possible moment; use a rule.
|
|
100
|
+
- **Never `--use-shipped-baseline`.** A baseline discovered inside a scanned skill was written by whoever wrote that skill.
|
|
101
|
+
- **The scan reads the built bundle** (`packages/cli/skill/`), not `skills/`: the authored tree carries unpackaged `tests/` fixtures that are deliberately adversarial and score higher than anything that ships. Build before scanning.
|
|
102
|
+
- **The semantic pass (`--llm`) is advisory and never a gate.** It is non-deterministic, and a run whose LLM calls partially fail reports a higher score than a clean one.
|
|
103
|
+
### Triaging a scan finding — the decision order
|
|
104
|
+
|
|
105
|
+
Work down this list; stop at the first that fits. Every acceptance needs the operator's word and a `reason` carrying its "Still flag if:" clause.
|
|
106
|
+
|
|
107
|
+
1. **Is it real?** Trace it before anything else. A real finding gets fixed, not accepted. Two on this repo turned out real and were fixed at source — a detection row whose wording introduced an `AE1`, and a template placeholder that shipped a literal `<group>`.
|
|
108
|
+
2. **Is the cause one file, shared by many skills?** Use a **rule** with a literal `id` + `path`. One cause, one entry, however many skills report it — the `references/conventions.md` marker protocol produces ten findings from one rule.
|
|
109
|
+
3. **Is it a one-off in specific content?** Use a **fingerprint**. Content-hashed, so it re-triggers the moment that content changes. Beware: the scanner's own `skillspector baseline` **deduplicates** what it emits, so a group of occurrences can come back as fewer hashes than the matcher needs — check the count actually drops before trusting it.
|
|
110
|
+
4. **Is it a completeness signal rather than a behaviour?** Use a **`coverage:`** entry, naming the skill, the file, and the file's `sha256`. This is ours, not the scanner's — SkillSpector's baseline suppresses findings only, and has no way to accept "I could not finish reading this". `AE1` belongs here despite arriving as a HIGH finding: its own text is *"Referenced artifact was not completely inspected."* Two things to know when writing one:
|
|
111
|
+
- **It is content-bound and expires.** Edit the accepted file and the acceptance stops applying, and the skill blocks until you re-adjudicate. That is the point: an acceptance that outlives the file it describes is a reason with nothing behind it. Refresh the digest **after** the final `bun run build`, since the bundle is what gets scanned — and note that editing the guard's own script invalidates its own entry.
|
|
112
|
+
- **Say what it hides.** A coverage entry accepts every `AE1` on that file and the degradation of every analyzer for that skill. Name the reason code and the number of degraded analyzers in the reason, so the next reader can tell whether the cause is still the one that was accepted.
|
|
113
|
+
|
|
114
|
+
5. **Write the "Still flag if:" clause so it can actually fire.** A clause naming something already true is decoration. Check it against the file before committing: "still flag if it gains a shell invocation" is worthless on a file that already shells out. Good clauses name a *change* — a different reason code, more degraded analyzers, a rule id the entry does not cover.
|
|
115
|
+
6. **None of the above?** Park it with a written ruling. Do not widen an entry to make a guard green — that is the failure this whole system exists to prevent.
|
|
116
|
+
|
|
117
|
+
### Known SkillSpector behaviours on this repo
|
|
118
|
+
|
|
119
|
+
Recorded so nobody re-derives them. All traced in issue 62.
|
|
120
|
+
|
|
121
|
+
- **`static_parse_limit`: a backtick at a shell-word start whose first inner token carries an expansion degrades `static_patterns_tool_misuse` for the whole skill.** The bounded shell parser reads a backtick preceded by whitespace or one of `;|&()<>/` as a command substitution, and gives up when the first whitespace-delimited token inside it holds a dollar-sign expansion — a JavaScript template literal opening on its interpolation (an API path built from a variable, a message that leads with a value), or a Markdown code span opening on a shell variable. Statement position is irrelevant: call arguments and assignments trip it alike, and a literal that opens with a plain word scans complete wherever it sits. Bisected against the scanner's own predicate in issue 104 (skillify's `trigger-check.mjs` carries the rule); it replaces an earlier note that blamed "assignment position", which was neither necessary nor sufficient. Fix: concatenate, or lead with a literal word; it clears only when every site in the file is fixed. Same code, different construct: a shell command substitution whose first word is printf and whose body the scanner cannot evaluate statically (a pipe inside it) is refused by design, so lead the substitution with another command or accept it.
|
|
122
|
+
- **`obfuscated_instruction_text` degrades every analyzer without a bounded-parse hook (twelve).** The cause is a removal-cue verb — `ignore`, `strip`, `drop`, `remove`, `omit` … — directly beside its own closing quote, which the reconstruction pass reads as an unsupported "ignore the literal …" directive and fails closed on once any later quoted string in the file activates it. In this repo that is Node's `stdio` mode word for a discarded fd; hoisting the word into a named constant clears it while the same bytes reach the child. Which files trip it depends on their other string literals, so the fix is applied at every site, not only the reporting one.
|
|
123
|
+
- **`AE1` on a `SKILL.md` usually means its references, not its behaviour** — either repo-root paths that cannot resolve relative to a skill directory (correct for a repo-scoped meta-skill), or a file it links to that the parser could not finish. Check which before accepting.
|
|
124
|
+
- **`P2` fires on every HTML comment**, because a hidden instruction is a genuine injection vector. This repo uses HTML comments as machine-readable markers — `vsk:v1`, `vsk-dev:start`, `<!-- source: … -->`, `<!-- mirrored -->` — so the finding is the documentation of a mechanism, not an instance of one. Scope the rule to the file, never to the id alone.
|
|
125
|
+
- **`RA1` on a `refresh/REFRESH.md` is correct about the pattern.** The refresh contract genuinely instructs an agent to rewrite the skill's own files. It is acceptable only because the runner is the only writer, it edits marked sections, checksums are runner-only, and every change lands as a reviewed PR — write those bounds into the reason.
|
|
126
|
+
- **The aggregate risk score is not a gate.** It is inflated by unresolvable-path artifacts in meta-content and deflated by unrelated suppressions.
|
|
127
|
+
|
|
128
|
+
- **A skill authored elsewhere is scanned the same way, before it reaches an agent** — `--root <path to the skill>`. This repo does not yet redistribute anyone else's skill; when it does, the curation, audit, upstream-drift, release, and retirement rules are this skill's to own, and a curated skill is never hand-edited locally (a local fix is overwritten by the next upstream sync and forks us from its author). Scanning a third-party skill you are evaluating works today and needs none of that.
|
|
129
|
+
|
|
130
|
+
## UNVERIFIED register
|
|
131
|
+
|
|
132
|
+
Do not assert any of these; if one becomes load-bearing, verify against the live source first and move it into a marked sentence:
|
|
133
|
+
|
|
134
|
+
- agentskills.io spec version identifier.
|
|
135
|
+
- Codex official unknown-frontmatter-key behavior (community: ignored).
|
|
136
|
+
- Codex legacy `~/.codex/skills` discovery.
|
|
137
|
+
- Hermes unknown-key handling and its treatment of `allowed-tools`/`license`/`compatibility`.
|