@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,162 @@
|
|
|
1
|
+
# factory-board.yml.template — dev-setup writes this to .github/workflows/factory-board.yml
|
|
2
|
+
# on the operator's yes, with three placeholders bound and every line above the strip
|
|
3
|
+
# marker below dropped:
|
|
4
|
+
# {{runs-on}} the job's runner label or label array, e.g. ubuntu-latest
|
|
5
|
+
# {{profile}} path to the project's dev.md, e.g. .vegastack/dev.md
|
|
6
|
+
# {{product-revision}} immutable product commit supplying the packaged policy compiler
|
|
7
|
+
# Requirements the offer must state: the VegaFactory App installed for the org with
|
|
8
|
+
# VEGAFACTORY_APP_ID and VEGAFACTORY_APP_PRIVATE_KEY set (references/github-app.md), and
|
|
9
|
+
# gh 2.97.0 or newer on the runner for name-based project field edits.
|
|
10
|
+
# ---
|
|
11
|
+
# Board mirror — the five state labels drive the project board's Status field.
|
|
12
|
+
#
|
|
13
|
+
# One way only: this workflow reads labels and writes Status. Nothing reads the board.
|
|
14
|
+
# A card dragged on the board is cosmetic until the next label change overwrites it.
|
|
15
|
+
#
|
|
16
|
+
# Needs the VegaFactory App (org variable VEGAFACTORY_APP_ID, org secret
|
|
17
|
+
# VEGAFACTORY_APP_PRIVATE_KEY) because GITHUB_TOKEN cannot write Projects, and gh 2.97.0
|
|
18
|
+
# or newer on the runner for `--field Status --value <name>`.
|
|
19
|
+
name: Board mirror
|
|
20
|
+
|
|
21
|
+
on:
|
|
22
|
+
issues:
|
|
23
|
+
types: [labeled, unlabeled, opened, reopened]
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
|
|
28
|
+
concurrency:
|
|
29
|
+
group: board-mirror-${{ github.event.issue.number }}
|
|
30
|
+
cancel-in-progress: false
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
mirror:
|
|
34
|
+
runs-on: {{runs-on}}
|
|
35
|
+
steps:
|
|
36
|
+
- name: Read the profile
|
|
37
|
+
uses: actions/checkout@v7
|
|
38
|
+
with:
|
|
39
|
+
# Its own directory, never the runner's shared checkout: a sparse checkout
|
|
40
|
+
# leaves git in sparse mode, and on a self-hosted runner the next job that
|
|
41
|
+
# reuses the same path would start from an almost-empty tree.
|
|
42
|
+
path: profile
|
|
43
|
+
sparse-checkout: "{{profile}}"
|
|
44
|
+
sparse-checkout-cone-mode: false
|
|
45
|
+
|
|
46
|
+
- name: Read the pinned policy compiler
|
|
47
|
+
uses: actions/checkout@v7
|
|
48
|
+
with:
|
|
49
|
+
repository: vegastack/vegafactory
|
|
50
|
+
ref: "{{product-revision}}"
|
|
51
|
+
path: .vegafactory-policy
|
|
52
|
+
sparse-checkout: "skills/dev/dev-setup/scripts/effective-policy.mjs"
|
|
53
|
+
sparse-checkout-cone-mode: false
|
|
54
|
+
persist-credentials: false
|
|
55
|
+
|
|
56
|
+
- name: Resolve the board and the state label
|
|
57
|
+
id: resolve
|
|
58
|
+
env:
|
|
59
|
+
PROFILE: "profile/{{profile}}"
|
|
60
|
+
POLICY_MODULE: ".vegafactory-policy/skills/dev/dev-setup/scripts/effective-policy.mjs"
|
|
61
|
+
LABELS: '${{ toJSON(github.event.issue.labels.*.name) }}'
|
|
62
|
+
APP_ID: "${{ vars.VEGAFACTORY_APP_ID }}"
|
|
63
|
+
run: |
|
|
64
|
+
set -eu
|
|
65
|
+
|
|
66
|
+
if [ ! -f "$PROFILE" ]; then
|
|
67
|
+
echo "board mirror: skipped - no such file $PROFILE in this repo"
|
|
68
|
+
echo 'decision=skip' >> "$GITHUB_OUTPUT"
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
board=$(sed -n 's/^board:[[:space:]]*\([^[:space:]#]*\).*/\1/p' "$PROFILE" | head -1)
|
|
73
|
+
if [ -z "$board" ] || [ "$board" = "none" ]; then
|
|
74
|
+
echo "board mirror: skipped - $PROFILE carries no board number"
|
|
75
|
+
echo 'decision=skip' >> "$GITHUB_OUTPUT"
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
if [ -z "${APP_ID:-}" ]; then
|
|
80
|
+
echo "board mirror: skipped - no VEGAFACTORY_APP_ID variable; the factory App is not installed for this org"
|
|
81
|
+
echo 'decision=skip' >> "$GITHUB_OUTPUT"
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
have=$(gh --version | sed -n '1s/^gh version \([0-9][0-9.]*\).*/\1/p')
|
|
86
|
+
have_n=$(awk -F. '{ printf "%d", $1 * 1000000 + $2 * 1000 + $3 }' <<<"$have")
|
|
87
|
+
if [ "${have_n:-0}" -lt 2097000 ]; then
|
|
88
|
+
echo "::error::board mirror needs gh 2.97.0 or newer for name-based project fields; this runner has ${have:-unknown}"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
node --input-type=module <<'JS'
|
|
93
|
+
import { readFileSync, appendFileSync } from 'node:fs';
|
|
94
|
+
import { resolve } from 'node:path';
|
|
95
|
+
import { pathToFileURL } from 'node:url';
|
|
96
|
+
const { parsePolicy, readWorkflowLabels, resolveState } = await import(pathToFileURL(resolve(process.env.POLICY_MODULE)).href);
|
|
97
|
+
try {
|
|
98
|
+
const policy = parsePolicy(readFileSync(process.env.PROFILE, 'utf8'));
|
|
99
|
+
if (policy.blocks.length) throw new Error(policy.blocks.join('; '));
|
|
100
|
+
const map = readWorkflowLabels(readFileSync(process.env.PROFILE, 'utf8'));
|
|
101
|
+
const state = resolveState(JSON.parse(process.env.LABELS), map);
|
|
102
|
+
if (state.blocks.length) throw new Error(state.blocks.join('; '));
|
|
103
|
+
const board = policy.extensions.board?.split(/\s/)[0];
|
|
104
|
+
if (!/^[1-9][0-9]*$/.test(board ?? '')) throw new Error('invalid board number');
|
|
105
|
+
appendFileSync(process.env.GITHUB_OUTPUT, 'decision=sync\nboard=' + board + '\nstatus=' + map[state.state] + '\n');
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.log('board mirror: skipped - ' + error.message);
|
|
108
|
+
appendFileSync(process.env.GITHUB_OUTPUT, 'decision=skip\n');
|
|
109
|
+
}
|
|
110
|
+
JS
|
|
111
|
+
|
|
112
|
+
- name: Mint the App token
|
|
113
|
+
id: token
|
|
114
|
+
if: steps.resolve.outputs.decision == 'sync'
|
|
115
|
+
uses: actions/create-github-app-token@v3
|
|
116
|
+
with:
|
|
117
|
+
app-id: "${{ vars.VEGAFACTORY_APP_ID }}"
|
|
118
|
+
private-key: "${{ secrets.VEGAFACTORY_APP_PRIVATE_KEY }}"
|
|
119
|
+
# owner resolves the org installation; repositories narrows the token to this one
|
|
120
|
+
# repository — without it the token reaches every repository the installation covers.
|
|
121
|
+
owner: "${{ github.repository_owner }}"
|
|
122
|
+
repositories: "${{ github.event.repository.name }}"
|
|
123
|
+
|
|
124
|
+
- name: Write Status on the board
|
|
125
|
+
id: mirror
|
|
126
|
+
if: steps.resolve.outputs.decision == 'sync'
|
|
127
|
+
env:
|
|
128
|
+
GH_TOKEN: "${{ steps.token.outputs.token }}"
|
|
129
|
+
OWNER: "${{ github.repository_owner }}"
|
|
130
|
+
BOARD: "${{ steps.resolve.outputs.board }}"
|
|
131
|
+
STATUS: "${{ steps.resolve.outputs.status }}"
|
|
132
|
+
ISSUE_URL: "${{ github.event.issue.html_url }}"
|
|
133
|
+
run: |
|
|
134
|
+
set -eu
|
|
135
|
+
options=$(mktemp)
|
|
136
|
+
gh project field-list "$BOARD" --owner "$OWNER" --format json > "$options"
|
|
137
|
+
OPTIONS_FILE="$options" node --input-type=module <<'JS'
|
|
138
|
+
import { readFileSync } from 'node:fs';
|
|
139
|
+
const fields = JSON.parse(readFileSync(process.env.OPTIONS_FILE, 'utf8')).fields;
|
|
140
|
+
const status = Array.isArray(fields) ? fields.filter(field => field.name === 'Status') : [];
|
|
141
|
+
if (status.length !== 1 || !Array.isArray(status[0].options) || !status[0].options.some(option => option.name === process.env.STATUS)) {
|
|
142
|
+
console.error('board mirror: configured Status option unavailable; no board mutation');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
JS
|
|
146
|
+
err=$(mktemp)
|
|
147
|
+
if gh project item-edit "$BOARD" --owner "$OWNER" --url "$ISSUE_URL" --field Status --value "$STATUS" 2>"$err"; then
|
|
148
|
+
echo "board mirror: Status=$STATUS on project $BOARD"
|
|
149
|
+
exit 0
|
|
150
|
+
fi
|
|
151
|
+
cat "$err" >&2
|
|
152
|
+
# Only the one failure that means "not on the board yet" earns an item-add; gh names
|
|
153
|
+
# it ("<url> is not an item in project <n>; add it first with `gh project item-add`").
|
|
154
|
+
# A permission, option or transport error is reported as itself and stops here.
|
|
155
|
+
if ! grep -q 'is not an item in project' "$err"; then
|
|
156
|
+
echo "::error::board mirror: item-edit failed for a reason other than a missing item (the message above) - not adding, not retrying"
|
|
157
|
+
exit 1
|
|
158
|
+
fi
|
|
159
|
+
echo "board mirror: item not on project $BOARD yet - adding it, then retrying once"
|
|
160
|
+
gh project item-add "$BOARD" --owner "$OWNER" --url "$ISSUE_URL"
|
|
161
|
+
gh project item-edit "$BOARD" --owner "$OWNER" --url "$ISSUE_URL" --field Status --value "$STATUS"
|
|
162
|
+
echo "board mirror: Status=$STATUS on project $BOARD"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Decision reminders remain session prose. This compatibility Stop hook only
|
|
3
|
+
// requests the same bounded, deduplicated local flush as stop-heartbeat.
|
|
4
|
+
//
|
|
5
|
+
// Installed to .vegastack/hooks/decision-nudge.mjs and wired on the Stop event. It replaces
|
|
6
|
+
// the inline shell recipe this skill used to carry: Node is guaranteed by the installer while
|
|
7
|
+
// `jq` is not, and a hook that silently exits because `jq` is missing is a guard that is not
|
|
8
|
+
// there. The prose instruction in the AGENTS.md dev section remains the portable base; this
|
|
9
|
+
// is a deterministic nudge on top of it, not a replacement.
|
|
10
|
+
|
|
11
|
+
import { pathToFileURL } from 'node:url'
|
|
12
|
+
|
|
13
|
+
export const NUDGE_REASON =
|
|
14
|
+
'Before finishing: if this session settled a directional choice (the Decisions test in .vegastack/dev.md), propose one dated register line and ask the user to confirm; otherwise finish.'
|
|
15
|
+
|
|
16
|
+
const DIRECTIONAL = /decided|chose|instead of|convention|from now on|standardi[sz]|switch(ed|ing)? to/i
|
|
17
|
+
|
|
18
|
+
export function isDirectional(message) {
|
|
19
|
+
return typeof message === 'string' && DIRECTIONAL.test(message)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Directional changes remain proposals in the session, never a Stop continuation
|
|
23
|
+
// or an unchecked session ID used as a temporary-file destination.
|
|
24
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
25
|
+
try {
|
|
26
|
+
const { runAdvisoryHook } = await import('./session-start.mjs')
|
|
27
|
+
await runAdvisoryHook('Stop', process.argv.slice(2))
|
|
28
|
+
} catch { /* advisory only */ }
|
|
29
|
+
process.exit(0)
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Codex prompt → skill mentions, recorded as a proxy.
|
|
3
|
+
//
|
|
4
|
+
// Installed to .vegastack/hooks/prompt-skill-mention.mjs and wired on Codex's UserPromptSubmit
|
|
5
|
+
// event. Codex exposes no skill-activation event, so a skill's name mentioned in the prompt is the best available
|
|
6
|
+
// signal — and the record says so: these land with `trigger: "mention"`, never with the confidence
|
|
7
|
+
// of Claude's real activation events. Proactive skill capture on Codex stays a documented gap
|
|
8
|
+
// rather than a number that looks precise and is not.
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
import { pathToFileURL } from 'node:url'
|
|
13
|
+
|
|
14
|
+
function main() {
|
|
15
|
+
const raw = readFileSync(0, 'utf8')
|
|
16
|
+
spawnSync(process.env.VSK_VEGAFACTORY || 'vegafactory', ['stats', 'record', '--source', 'codex-prompt'], {
|
|
17
|
+
input: raw,
|
|
18
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
23
|
+
try {
|
|
24
|
+
main()
|
|
25
|
+
} catch {
|
|
26
|
+
// a prompt must never fail to submit because nobody could count it
|
|
27
|
+
}
|
|
28
|
+
process.exit(0)
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SessionEnd requests only a bounded local flush. Never block exit, continue a turn,
|
|
3
|
+
// forward transcript/tool payloads, or start detached delivery/model work.
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
7
|
+
try {
|
|
8
|
+
// The installer copies the shared SessionStart adapter with either consumer.
|
|
9
|
+
// A missing sibling is an advisory failure, not an exit-blocking module error.
|
|
10
|
+
const { runAdvisoryHook } = await import('./session-start.mjs')
|
|
11
|
+
await runAdvisoryHook('SessionEnd', process.argv.slice(2))
|
|
12
|
+
} catch { /* missing CLI/adapter, invalid input and timeout remain non-blocking */ }
|
|
13
|
+
process.exit(0)
|
|
14
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bounded local advisory adapter shared by SessionStart, Stop and SessionEnd.
|
|
3
|
+
// Guard enforcement is separate. This adapter never reads transcripts or native memory,
|
|
4
|
+
// invents a write destination, contacts a network or asks a model to continue.
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import { accessSync, constants, lstatSync, readFileSync, realpathSync } from 'node:fs'
|
|
8
|
+
import { delimiter, dirname, isAbsolute, join } from 'node:path'
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
10
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
11
|
+
|
|
12
|
+
export const MAX_HOOK_INPUT_BYTES = 64 * 1024
|
|
13
|
+
export const LOCAL_FLUSH_MS = 500
|
|
14
|
+
const INPUT_WAIT_MS = 350
|
|
15
|
+
const EVENTS = new Set(['SessionStart', 'Stop', 'SessionEnd'])
|
|
16
|
+
const identity = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value)
|
|
17
|
+
|
|
18
|
+
export function sanitizeHookInput(payload, harness, event) {
|
|
19
|
+
if (!['claude', 'codex'].includes(harness) || !EVENTS.has(event)) return null
|
|
20
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null
|
|
21
|
+
if (payload.hook_event_name !== undefined && payload.hook_event_name !== event) return null
|
|
22
|
+
if (!identity(payload.session_id) || (payload.turn_id !== undefined && !identity(payload.turn_id))) return null
|
|
23
|
+
if (typeof payload.cwd !== 'string' || payload.cwd.length > 4096 || !isAbsolute(payload.cwd) || /[\0\r\n]/.test(payload.cwd)) return null
|
|
24
|
+
if (payload.stop_hook_active !== undefined && typeof payload.stop_hook_active !== 'boolean') return null
|
|
25
|
+
// These values are identifiers for the private CLI resolver, never filesystem destinations.
|
|
26
|
+
return {
|
|
27
|
+
harness, event, sessionId: payload.session_id,
|
|
28
|
+
...(payload.turn_id === undefined ? {} : { turnId: payload.turn_id }),
|
|
29
|
+
cwd: payload.cwd, stopHookActive: payload.stop_hook_active ?? false,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readBoundedHookInput(stream = process.stdin) {
|
|
34
|
+
return new Promise(resolve => {
|
|
35
|
+
const chunks = []
|
|
36
|
+
let size = 0, finished = false
|
|
37
|
+
const done = value => {
|
|
38
|
+
if (finished) return
|
|
39
|
+
finished = true
|
|
40
|
+
clearTimeout(timer)
|
|
41
|
+
stream.removeListener('data', data)
|
|
42
|
+
stream.removeListener('end', end)
|
|
43
|
+
stream.removeListener('error', error)
|
|
44
|
+
stream.pause()
|
|
45
|
+
resolve(value)
|
|
46
|
+
}
|
|
47
|
+
const data = chunk => {
|
|
48
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
49
|
+
size += bytes.length
|
|
50
|
+
if (size > MAX_HOOK_INPUT_BYTES) return done(null)
|
|
51
|
+
chunks.push(bytes)
|
|
52
|
+
}
|
|
53
|
+
const end = () => {
|
|
54
|
+
try { done(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)))) }
|
|
55
|
+
catch { done(null) }
|
|
56
|
+
}
|
|
57
|
+
const error = () => done(null)
|
|
58
|
+
const timer = setTimeout(() => done(null), INPUT_WAIT_MS)
|
|
59
|
+
stream.on('data', data).once('end', end).once('error', error)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function localCli() {
|
|
64
|
+
const explicit = process.env.VSK_VEGAFACTORY
|
|
65
|
+
const candidates = explicit ? [explicit] : (process.env.PATH ?? '').split(delimiter).filter(isAbsolute).map(path => join(path, 'vegafactory'))
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
try {
|
|
68
|
+
if (!isAbsolute(candidate)) continue
|
|
69
|
+
const path = realpathSync(candidate)
|
|
70
|
+
if (!lstatSync(path).isFile()) continue
|
|
71
|
+
accessSync(path, constants.R_OK)
|
|
72
|
+
const root = dirname(dirname(path))
|
|
73
|
+
const read = file => {
|
|
74
|
+
const stat = lstatSync(file)
|
|
75
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) throw new Error('unsupported package metadata')
|
|
76
|
+
return readFileSync(file)
|
|
77
|
+
}
|
|
78
|
+
const pkg = JSON.parse(read(join(root, 'package.json')))
|
|
79
|
+
if (pkg.name !== '@vegastack/vegafactory' || pkg.bin?.vegafactory !== 'dist/index.js'
|
|
80
|
+
|| path !== realpathSync(join(root, 'dist/index.js'))) continue
|
|
81
|
+
const manifest = JSON.parse(read(join(root, 'skill-integrity.json')))
|
|
82
|
+
const expected = manifest.schemaVersion === 2 && manifest.skills?.['dev-setup']?.files?.['assets/hooks/session-start.mjs']
|
|
83
|
+
if (typeof expected !== 'string' || !/^[a-f0-9]{64}$/.test(expected)) continue
|
|
84
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex')
|
|
85
|
+
if (hash(read(fileURLToPath(import.meta.url))) !== expected
|
|
86
|
+
|| hash(read(join(root, 'skill/dev-setup/assets/hooks/session-start.mjs'))) !== expected) continue
|
|
87
|
+
return path
|
|
88
|
+
} catch { /* missing installed CLI is an advisory no-op */ }
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function runLocalHookPhase(cli, command, input, entryAt = performance.now()) {
|
|
94
|
+
return new Promise(resolvePhase => {
|
|
95
|
+
const remaining = 1000 - (performance.now() - entryAt)
|
|
96
|
+
if (remaining <= 0) return resolvePhase({ completed: false, output: '', phaseMs: null, totalMs: performance.now() - entryAt })
|
|
97
|
+
const nonce = randomUUID()
|
|
98
|
+
let phase = 'waiting-ready', output = '', phaseAt = null, phaseTimer = null, settled = false
|
|
99
|
+
// Detached creates one owned POSIX process group; this child remains fully
|
|
100
|
+
// supervised and is never unref'd or left doing background delivery.
|
|
101
|
+
const child = spawn(process.execPath, [cli, ...command], {
|
|
102
|
+
detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'ignore', 'ipc'],
|
|
103
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
|
|
104
|
+
})
|
|
105
|
+
const kill = () => {
|
|
106
|
+
phase = 'failed'
|
|
107
|
+
try { if (child.pid && process.platform !== 'win32') process.kill(-child.pid, 'SIGKILL'); else child.kill('SIGKILL') } catch { /* already absent */ }
|
|
108
|
+
}
|
|
109
|
+
const deadline = setTimeout(kill, remaining)
|
|
110
|
+
const cancelled = () => kill()
|
|
111
|
+
process.once('SIGTERM', cancelled); process.once('SIGINT', cancelled)
|
|
112
|
+
const done = completed => {
|
|
113
|
+
const now = performance.now()
|
|
114
|
+
completed = completed && (phaseAt === null ? output === '' : now - phaseAt <= LOCAL_FLUSH_MS) && now - entryAt <= 1000
|
|
115
|
+
if (settled) return
|
|
116
|
+
settled = true
|
|
117
|
+
clearTimeout(deadline); clearTimeout(phaseTimer)
|
|
118
|
+
process.removeListener('SIGTERM', cancelled); process.removeListener('SIGINT', cancelled)
|
|
119
|
+
resolvePhase({ completed, output: completed ? output : '', phaseMs: phaseAt === null ? null : performance.now() - phaseAt, totalMs: performance.now() - entryAt })
|
|
120
|
+
}
|
|
121
|
+
child.on('error', () => { kill(); done(false) })
|
|
122
|
+
child.stdin.on('error', kill)
|
|
123
|
+
child.stdout.setEncoding('utf8')
|
|
124
|
+
child.stdout.on('data', chunk => { output += chunk; if (phaseAt === null || Buffer.byteLength(output) > 4096) kill() })
|
|
125
|
+
child.on('message', message => {
|
|
126
|
+
const now = performance.now()
|
|
127
|
+
if (now - entryAt > 1000 || phaseAt !== null && now - phaseAt > LOCAL_FLUSH_MS) return kill()
|
|
128
|
+
if (!message || typeof message !== 'object' || Array.isArray(message) || message.vskManagedHook !== 1) return kill()
|
|
129
|
+
if (message.phase === 'ready' && Object.keys(message).sort().join(',') === 'phase,vskManagedHook' && phase === 'waiting-ready') {
|
|
130
|
+
phase = 'validating'
|
|
131
|
+
// Read-only registry, session and current-policy validation remains
|
|
132
|
+
// under the entry-relative deadline. It grants no local write.
|
|
133
|
+
child.send({ vskManagedHook: 1, phase: 'start', nonce }, error => { if (error) kill() })
|
|
134
|
+
child.stdin.end(JSON.stringify(input))
|
|
135
|
+
} else if (message.phase === 'validated' && Object.keys(message).sort().join(',') === 'nonce,phase,vskManagedHook' && message.nonce === nonce && phase === 'validating') {
|
|
136
|
+
phase = 'flushing'; phaseAt = performance.now()
|
|
137
|
+
// Exactly one grant covers every claim, write, release, ACK and lesson
|
|
138
|
+
// mutation. No message can renew either deadline.
|
|
139
|
+
phaseTimer = setTimeout(kill, Math.min(LOCAL_FLUSH_MS, 1000 - (phaseAt - entryAt)))
|
|
140
|
+
child.send({ vskManagedHook: 1, phase: 'flush', nonce }, error => { if (error) kill() })
|
|
141
|
+
} else if (message.phase === 'finish' && Object.keys(message).sort().join(',') === 'nonce,phase,vskManagedHook' && message.nonce === nonce && ['validating', 'flushing'].includes(phase)) {
|
|
142
|
+
phase = 'finished'
|
|
143
|
+
} else kill()
|
|
144
|
+
})
|
|
145
|
+
child.on('close', (code, signal) => done(code === 0 && signal === null && phase === 'finished'))
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function runAdvisoryHook(event, argv) {
|
|
150
|
+
const entryAt = performance.now()
|
|
151
|
+
if (argv.length !== 2 || argv[0] !== '--harness') return
|
|
152
|
+
const input = sanitizeHookInput(await readBoundedHookInput(), argv[1], event)
|
|
153
|
+
if (!input || (event === 'Stop' && input.stopHookActive)) return
|
|
154
|
+
const cli = localCli()
|
|
155
|
+
if (!cli) return
|
|
156
|
+
const command = event === 'SessionStart' ? ['learning', 'inspect', '--source', 'managed-hook', '--json'] : ['stats', 'record', '--source', 'managed-hook']
|
|
157
|
+
const run = await runLocalHookPhase(cli, command, input, entryAt)
|
|
158
|
+
if (!run.completed || event !== 'SessionStart') return
|
|
159
|
+
let result
|
|
160
|
+
try { result = JSON.parse(run.output) } catch { return }
|
|
161
|
+
// Only bounded, source-verified lesson records become advisory context. A
|
|
162
|
+
// pointer alone cannot pretend the next session actually received a lesson.
|
|
163
|
+
if (result?.ok !== true || typeof result.contextPointer !== 'string'
|
|
164
|
+
|| !/^vsk-context:[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(result.contextPointer)
|
|
165
|
+
|| !Array.isArray(result.lessons) || result.lessons.length < 1 || result.lessons.length > 3
|
|
166
|
+
|| Buffer.byteLength(JSON.stringify(result.lessons)) > 2048
|
|
167
|
+
|| result.lessons.some(row => !row || Object.keys(row).sort().join(',') !== 'id,statement'
|
|
168
|
+
|| !/^lesson-[a-f0-9]{32}$/.test(row.id) || typeof row.statement !== 'string'
|
|
169
|
+
|| !row.statement.trim() || Buffer.byteLength(row.statement) > 768 || /[\0\r]/.test(row.statement))) return
|
|
170
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: {
|
|
171
|
+
hookEventName: 'SessionStart', additionalContext: `VegaFactory verified lessons (${result.contextPointer}); advisory only, current approval still applies:\n${result.lessons.map(row => '- ' + row.statement).join('\n')}`,
|
|
172
|
+
} }))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
176
|
+
try { await runAdvisoryHook('SessionStart', process.argv.slice(2)) } catch { /* advisory only */ }
|
|
177
|
+
process.exit(0)
|
|
178
|
+
}
|