@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,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Skill invocation → one line in the session's skill sidecar.
|
|
3
|
+
//
|
|
4
|
+
// Installed to .vegastack/hooks/skill-activated.mjs and wired on Claude Code's PostToolUse (matcher
|
|
5
|
+
// `Skill`) and UserPromptExpansion events. Both carry a skill name; they differ in who chose it,
|
|
6
|
+
// and that difference is the whole point of capturing them separately — "the model reached for
|
|
7
|
+
// dev-architect" and "a person typed /dev-architect" are different facts about a skill.
|
|
8
|
+
//
|
|
9
|
+
// The invocations accumulate per session because they happen long before the session's own record
|
|
10
|
+
// exists; the session-end capture folds them in and deletes the sidecar.
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from 'node:child_process'
|
|
13
|
+
import { readFileSync } from 'node:fs'
|
|
14
|
+
import { pathToFileURL } from 'node:url'
|
|
15
|
+
|
|
16
|
+
// One payload, two shapes: a tool call names the tool, a prompt expansion names the command. An
|
|
17
|
+
// unrecognised payload is forwarded as neither — the parser would only return an empty list.
|
|
18
|
+
export function sourceFor(payload) {
|
|
19
|
+
if (payload && payload.tool_name === 'Skill') return 'claude-post-tool'
|
|
20
|
+
if (payload && typeof payload.command_name === 'string' && payload.command_name !== '') return 'claude-prompt-expansion'
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function main() {
|
|
25
|
+
const raw = readFileSync(0, 'utf8')
|
|
26
|
+
let payload = null
|
|
27
|
+
try {
|
|
28
|
+
payload = JSON.parse(raw)
|
|
29
|
+
} catch {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
const source = sourceFor(payload)
|
|
33
|
+
if (!source) return
|
|
34
|
+
spawnSync(process.env.VSK_VEGAFACTORY || 'vegafactory', ['stats', 'record', '--source', source], {
|
|
35
|
+
input: raw,
|
|
36
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
41
|
+
try {
|
|
42
|
+
main()
|
|
43
|
+
} catch {
|
|
44
|
+
// a skill must never fail to run because nobody could count it
|
|
45
|
+
}
|
|
46
|
+
process.exit(0)
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stop 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('Stop', process.argv.slice(2))
|
|
12
|
+
} catch { /* missing CLI/adapter, invalid input and timeout remain non-blocking */ }
|
|
13
|
+
process.exit(0)
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'implement-children',
|
|
3
|
+
description: 'Compatibility entry that directs independent children to the verified CLI owner',
|
|
4
|
+
whenToUse: 'An older installation invokes this workflow; use vegafactory children run from the registered parent session.',
|
|
5
|
+
phases: [{ title: 'Build children', detail: 'the verified CLI owns child execution and integration' }],
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Retain the discoverable name for existing installations. A saved workflow
|
|
9
|
+
// cannot replace durable execution, subscription qualification or source checks.
|
|
10
|
+
export default async function () {
|
|
11
|
+
throw new Error('Use vegafactory children run from the registered parent session; this legacy workflow does not execute children.')
|
|
12
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# The ask route
|
|
2
|
+
|
|
3
|
+
Where a round of questions goes when a skill needs the user. Two surfaces: the harness's question tool, or the issue itself. This file is the one home for the route, the comment shape and the reply grammar; `scripts/questions.mjs` is the deterministic method behind them, and the marker row lives in [conventions](conventions.md).
|
|
4
|
+
|
|
5
|
+
## The route
|
|
6
|
+
|
|
7
|
+
Three steps, in this order, first match wins:
|
|
8
|
+
|
|
9
|
+
1. `VSK_ASK_ROUTE` is set to `issue` or `tool` — the dispatcher and CI set it; any other non-empty value is refused rather than guessed.
|
|
10
|
+
2. This harness and this run have no question tool → `issue`.
|
|
11
|
+
3. The asker is not the issue's operator → `issue`. An unresolved asker or operator is also `issue`.
|
|
12
|
+
|
|
13
|
+
Otherwise the tool. The issue route needs an issue: where none exists yet — dev-intake's interview runs before its issue is created — the skill creates the issue first, at `needs-operator` with the operator assigned and the request as its body, and posts the round to it; the brief is written into that issue once the round is answered. On the tool route the issue is created after approval, as before. Settle it deterministically rather than by judgement:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
node <path-to-this-skill>/scripts/questions.mjs route --tool <name|none> --asker <login> --operator <login> --json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Which tool each harness has — and the Codex Plan-mode gating on `request_user_input` — is recorded in dev-setup's `references/harness-facts.md` (a backticked path, not a link: this file ships into dev-intake, dev-plan and dev-implement, which do not carry that reference) and not restated here. The operator identity comes from the caller under dev.md's `operators:` knob; the script takes both logins as inputs and never resolves them itself.
|
|
20
|
+
|
|
21
|
+
The bias is deliberate: a round in a comment is always readable by whoever owns the issue, and a round put to the wrong person is not.
|
|
22
|
+
|
|
23
|
+
## The comment
|
|
24
|
+
|
|
25
|
+
One comment per round, marker first, the whole round wrapped in `<questions>` tags so the parser and the model both find it inside a comment that may carry prose around it:
|
|
26
|
+
|
|
27
|
+
```markdown
|
|
28
|
+
<!-- vsk:v1 type=questions rev=1 -->
|
|
29
|
+
## Questions (v1)
|
|
30
|
+
|
|
31
|
+
<questions>
|
|
32
|
+
**Q1.** Where does the reminder queue live?
|
|
33
|
+
- a) A Postgres table
|
|
34
|
+
- b) A Redis list (recommended — Redis is already a dependency)
|
|
35
|
+
|
|
36
|
+
**Q2.** How late may a reminder fire?
|
|
37
|
+
- a) Within the hour (recommended — matches the existing cron cadence)
|
|
38
|
+
- b) Same day
|
|
39
|
+
|
|
40
|
+
Reply with `1: a` per question, or `all recommended`.
|
|
41
|
+
</questions>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
No question, option or reason may carry an HTML comment delimiter or a `questions` block tag: a round can be read back out of a comment anyone may have written, so text that could close the block or forge a marker is refused in both directions. Every question carries at least two options and exactly one recommendation, and every recommendation carries its one-line reason — a round that cannot say which way it leans is a round that has not been thought through, and the renderer refuses it. Options run `a` through `h`. After posting, the label moves to `needs-operator` and the session ends.
|
|
45
|
+
|
|
46
|
+
## Replying
|
|
47
|
+
|
|
48
|
+
Anyone on the issue may answer, in an ordinary comment, one line per question:
|
|
49
|
+
|
|
50
|
+
- `1: b`
|
|
51
|
+
- `2: a — the cron already runs` (trailing prose after the dash is kept)
|
|
52
|
+
- `3: other — a third way` (`other` is accepted for any question)
|
|
53
|
+
- `all recommended` on its own line fills every question not answered explicitly
|
|
54
|
+
|
|
55
|
+
An explicit line always beats `all recommended`. Bulleted lines, a `.` instead of `:`, and upper-case letters all parse. Surrounding prose is ignored; the first answer to a question stands and a repeat is reported. A numbered line whose letter runs straight into prose with no dash (`1: a is wrong, go with b`) is malformed, not an answer of `a` — the parser reports it and the question is re-asked.
|
|
56
|
+
|
|
57
|
+
## Re-asks
|
|
58
|
+
|
|
59
|
+
Parse before asking anything. A later session has no `round.json` on disk, so it reads the round back out of the posted comment — that is what the `<questions>` wrapper is for, and `--round <comment.md>` takes it in place of `--spec`. What comes back is answered questions, still-open questions, and malformed lines. If nothing is open, continue — never re-ask an answered question. If something is open, post a second comment at `rev=<n+1>` carrying only the open questions **at their original numbers**, so `**Q3.**` stays Q3. Earlier `questions` comments are left in place as record. A reply with no answer line at all is malformed, not empty: it gets one re-ask naming the expected shape.
|
|
60
|
+
|
|
61
|
+
## What it is not
|
|
62
|
+
|
|
63
|
+
An answer is not an approval. Any teammate may answer a round; only the operator approves, and approval is still its own `approval` marker comment recording the operator's own words in the `(<username>)` format ([conventions](conventions.md)). A round that is fully answered unblocks the work; it does not move the issue to `ready`.
|
|
64
|
+
|
|
65
|
+
Assignment is not this route's job either — the label moving to `needs-operator` is what puts the issue on the operator.
|
|
66
|
+
|
|
67
|
+
## Commands
|
|
68
|
+
|
|
69
|
+
Save the reply comment to `.vegastack/.tmp/<issue>-<slug>/reply.md` first; the parser reads a file, never a network.
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
node <path-to-this-skill>/scripts/questions.mjs render --spec round.json --rev 1 --json
|
|
73
|
+
node <path-to-this-skill>/scripts/questions.mjs parse --comment reply.md --round asked.md --json
|
|
74
|
+
node <path-to-this-skill>/scripts/questions.mjs re-ask --round asked.md --comment reply.md --rev 2 --json
|
|
75
|
+
node <path-to-this-skill>/scripts/questions.mjs route --tool none --asker <login> --operator <login> --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`asked.md` is the posted `questions` comment; `--spec round.json` takes the same round as JSON instead, where the rendering session still has it. A spec is `{ "questions": [ { "text": "…", "options": [ { "letter": "a", "text": "…" }, { "letter": "b", "text": "…", "recommended": true, "reason": "…" } ] } ] }`.
|
|
79
|
+
|
|
80
|
+
Exit codes: 0 pass · 1 answers still open or malformed, and for `re-ask` nothing left to ask · 2 refusal or usage error.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Workflow conventions
|
|
2
|
+
|
|
3
|
+
Artifact authority.
|
|
4
|
+
|
|
5
|
+
Defaults resolve repo, group, then org; locks require explicit org delegation. Repository dispatch/commands never inherit; registers concatenate. Policy/migration uses dev-setup's `scripts/effective-policy.mjs` and vegafactory-setup's control-room reference.
|
|
6
|
+
|
|
7
|
+
## Comment metadata markers
|
|
8
|
+
|
|
9
|
+
Comments open:
|
|
10
|
+
|
|
11
|
+
```markdown
|
|
12
|
+
<!-- vsk:v1 type=<type> rev=<n> [key=value ...] -->
|
|
13
|
+
## <Human title> (v<n>)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| type | required keys | instances |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `approval` | `scope=<brief\|brief+plan\|plan\|consolidated\|none>` and schema-v2 JSON | one per approval event |
|
|
19
|
+
| `questions` | `rev` | one per ask round; earlier rounds stay as record (dev-setup's `references/ask-route.md`) |
|
|
20
|
+
| `plan` | `rev` | one, edited in place |
|
|
21
|
+
| `ledger` | `branch` | one, edited in place |
|
|
22
|
+
| `evidence` | `rev branch sha` | one, edited in place |
|
|
23
|
+
| `review` | `round sha agent=<claude\|codex> verdict=<clean\|needs-fixes>` | one per review cycle, rounds appended inside |
|
|
24
|
+
| `decision` | — | one per decision proposal |
|
|
25
|
+
| `handback` | — | one per stop event |
|
|
26
|
+
|
|
27
|
+
`rev=<n>` and `(v<n>)` start at 1 only on brief, plan, questions and evidence; other types have neither. Locate by marker, never heading/legacy fallback.
|
|
28
|
+
|
|
29
|
+
## Operator identity
|
|
30
|
+
|
|
31
|
+
Use parenthesized GitHub usernames:
|
|
32
|
+
|
|
33
|
+
- Approval: `Approved by (<username>) on DD-MM-YYYY: "<their words>"`
|
|
34
|
+
- Register line: `- DD-MM-YYYY (<username>) — <decision>`
|
|
35
|
+
|
|
36
|
+
Approval.mjs’s publisher/relay contract lets current-policy provider-envelope publishers attest listed operators’ session words. Other recorders may only relay independently read identical operator-published scoped grants within complete authority history, without lifecycle mutations. Relays inherit source authority/lifecycle; account attestation cannot authenticate off-platform speech.
|
|
37
|
+
|
|
38
|
+
## Scoped approval records
|
|
39
|
+
|
|
40
|
+
Use only dev-implement’s `scripts/lib/approval.mjs` and follow its contract. Refresh current policy and complete GitHub histories. `ArtifactRef={repo,issue,kind,artifactId,rev,digest}` binds brief issue-node or unique plan/protocol comment-node identity, revision and canonical SHA-256.
|
|
41
|
+
|
|
42
|
+
Post exactly one approval comment: matching scope marker, then one fenced JSON `ApprovalRecord={schemaVersion:2,id,operator,scope,source:{kind,ref,quote},artifacts,supersedes,revokes}`. Exclude outer Markdown fences, future alternatives and unresolved source locators; validate the whole body with approval.mjs's `parseApproval`. Source kind is `session` or `github-comment`, with inspectable words. Reuse valid current grants/relays; avoid counterfactual plan-only or redundant approvals. Scope is `brief`, `plan` or `brief+plan`; planning requires brief, implementation both, research execution also its protocol. Empty-artifact revocations remove exact earlier IDs. Conflicts explicitly supersede; newest never wins.
|
|
43
|
+
|
|
44
|
+
Preserve legacy comments. Without writes, inventory refusals/current digests and request reconfirmation. For duplicate canonical plans preserve both identities/bodies and request record-preserving reconciliation; never delete to clear ambiguity. Follow approval.mjs’s exact correction schema, operator-publisher and target checks. Only malformed or demonstrably invalid-source targets qualify, never valid authority or unavailable/inconsistent facts. Resolve source facts first; corrections grant no scope. Exact child checkpoint actions bind one selected code child's branch/ref/base/tasks/files; other actions never substitute.
|
|
45
|
+
|
|
46
|
+
Consolidated parent events bind frozen manifests, canonical artifacts and exact task/action subsets. Use inline UTF-8 or immutable repository/commit/path plus blob hash, never local paths. Canonical `approvalBindings` authorize; requested `recordBinding` only audits. Follow approval.mjs’s preparation/research/recovery provenance, receipts, adapters, counted attempts and fresh admission; retain immutable history and unverified legacy records. Keep checkpoint/private/live/shipping gates separate.
|
|
47
|
+
|
|
48
|
+
Canonicalization normalizes CRLF; its only exceptions are structural plan checkboxes and one validated JSON `{tasks:[{id,evidenceUrls}]}` block between `<!-- vsk:progress:start -->` / `<!-- vsk:progress:end -->`. IDs must exist; URLs are HTTP(S); unknown fields/duplicates refuse. Stable task IDs/order, interfaces, actions, revisions and all other bytes remain scope. Brief/protocol bodies have no mutable fields; fenced examples stay immutable and grant no authority.
|
|
49
|
+
|
|
50
|
+
## Revision markers
|
|
51
|
+
|
|
52
|
+
Scope edits increment marker/heading revisions and append `Revisions: v2 — DD-MM-YYYY: <change>, per (<username>) correction`; preserve earlier lines and obtain fresh approval. Validated progress changes need neither.
|
|
53
|
+
|
|
54
|
+
## Scope classes
|
|
55
|
+
|
|
56
|
+
Intake explains scope; operator overrides:
|
|
57
|
+
|
|
58
|
+
- **`research`** — inquiry; throwaway code allowed, never merged. No branch/PR/changelog; evidence comment contains findings and recommendation.
|
|
59
|
+
- **`quick-build`** — existing flow: draft brief+plan together, approve both, then `ready`.
|
|
60
|
+
- **`full-plan`** — new ground: approve brief, `needs-plan`, separate grounded planning session, `needs-operator`, approve plan, `ready`. Split multiple deliverables into independently classified epic children.
|
|
61
|
+
|
|
62
|
+
Scope ratchet: `dev-plan`.
|
|
63
|
+
|
|
64
|
+
## Labels
|
|
65
|
+
|
|
66
|
+
One state; flips set assignees (colors: dev-setup):
|
|
67
|
+
|
|
68
|
+
| label | meaning | assignee |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `needs-operator` | question, brief or plan approval, proposal | the operator |
|
|
71
|
+
| `needs-plan` | brief approved; awaiting planning (full-plan only) | the operator |
|
|
72
|
+
| `ready` | approved — an agent may start | nobody |
|
|
73
|
+
| `working` | claimed; ledger shows live progress | the runner |
|
|
74
|
+
| `for-operator` | done — evidence posted, awaiting operator review | the operator |
|
|
75
|
+
|
|
76
|
+
Modifiers coexist with state: `risky` · scope `research` / `quick-build` / `full-plan` · `epic` (map parents without a native Epic type). Boards mirror states one-way.
|
|
77
|
+
|
|
78
|
+
## Titles, types, hierarchy
|
|
79
|
+
|
|
80
|
+
- **Title prefixes:** issues, branches and PRs use dev.md's `branch:` types plus `research:`; PR title = issue title.
|
|
81
|
+
- **Native issue types/fields:** Feature (feat) · Bug (fix) · Task (docs/chore/refactor/research) · Epic for parents (else label); intake sets Priority/Effort. Scope classes stay labels.
|
|
82
|
+
- **Hierarchy:** epic parent = map only (Destination · Decisions so far · Not clear yet · Out of scope), with native child sub-issues. Issues are work units (brief, approvals, branch, PR, evidence); tasks are checkboxes **only in the plan comment**. Blockers use dependencies; phases use milestones. Only non-epic issues get `ready`.
|
|
83
|
+
|
|
84
|
+
## The ledger
|
|
85
|
+
|
|
86
|
+
One implementation ledger:
|
|
87
|
+
|
|
88
|
+
```markdown
|
|
89
|
+
<!-- vsk:v1 type=ledger branch=<branch> -->
|
|
90
|
+
## Ledger — <branch>
|
|
91
|
+
- <issue>-T<N>: complete (commits <base7>..<head7>[, review clean | K parked])
|
|
92
|
+
- <issue>-T<N>: fix round <R>/3 (<X> addressed, <Y> open — <one-liners>; commits <a>..<b>)
|
|
93
|
+
- Ruling: <what> — <why> — cost if wrong: <cost>
|
|
94
|
+
- <issue>-T<N>: parked — <finding> — Ruling: <why the code stands>
|
|
95
|
+
- Deferred minor: <one-liner>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
**Resume protocol:** brief → plan comment → ledger → `git log`; then reconcile task IDs, canonical approval history, edited authority, source/evidence, ownership and delivery effects. Preserve completed work/provenance; stale heartbeat is not stop proof. Preparation never implies issue completion. Dev-implement's ledger reference owns recovery detail.
|
|
100
|
+
|
|
101
|
+
## `.vegastack/` workspaces
|
|
102
|
+
|
|
103
|
+
Drafts/reports: `.vegastack/.tmp/<issue-number>-<title-slug>/` (pre-issue: `intake-<slug>`), self-ignored by a `.gitignore` containing `*`. Branch checkouts: root-ignored `.vegastack/.worktrees/<issue-number>-<title-slug>/`; main stays on its default branch. Keep both outside `.git/`. Subagents save full reports and return short status. `<path-to-this-skill>` means SKILL.md’s directory.
|
|
104
|
+
|
|
105
|
+
## Verification gate
|
|
106
|
+
|
|
107
|
+
Prove claims with fresh command output and exit codes; report failures and skips. Delegate only substantial independent parallel work, never your own verification; keep spawn counts low. Guards block machine-verifiable failures (exit 2); heuristics warn. Guards contain no AI inference; unverifiable state fails closed.
|
|
108
|
+
|
|
109
|
+
## Review bindings
|
|
110
|
+
|
|
111
|
+
One fenced JSON each: `{"reviewBinding":{sha,baseSha,scopeDigest,verdict,findings:[{id,status}]}}` in review; `{"adjudication":{sha,reviewCommentId,operator,source:{kind,ref,quote},findings:[{id,disposition,reason}]}}` in evidence. Use full commit IDs and canonical-plan scopeDigest; status=open/resolved; disposition=accept-risk. Every open finding requires same-review operator acceptance. dev-ship’s README defines source checks. No prose exceptions.
|
|
112
|
+
|
|
113
|
+
Communicate starts/findings/direction plainly; self-contained outcomes include paths and remaining checks. Avoid invented labels/arrows; visualize usefully. Challenge ambiguity with options; never guess silently.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# The VegaFactory GitHub App
|
|
2
|
+
|
|
3
|
+
The one identity every automated write uses. Facts verified 03-09-2026 against the live App and GitHub's docs; the three `GH-APP-*` entries in `refresh/sources.json` pin this whole file, which is why it carries no HTML comments.
|
|
4
|
+
|
|
5
|
+
## What the App is for
|
|
6
|
+
|
|
7
|
+
Humans own issues. A person approves a brief, a person says "ship it", and a person's name is on every state flip. The App is the identity for the writes no person is sitting behind: the board mirror that sets a project Status when a label changes, an Actions job that edits a label, and the hosted token broker that serves other organisations. It is **not** the dispatcher's identity — the dispatcher runs headless sessions as the operator's own `gh` login, and giving it the App's identity would hide which human a run belongs to.
|
|
8
|
+
|
|
9
|
+
The alternative worth naming is a credential belonging to a person: it stands for their whole account, outlives the job that used it, and dies when they leave the org. The App stands for a named permission set instead, its tokens live an hour, and uninstalling it revokes every one of them at once.
|
|
10
|
+
|
|
11
|
+
The App is public, so any account may install it. That is the point: one App, installed by any org that wants the factory, with a permission set each of them can read before consenting.
|
|
12
|
+
|
|
13
|
+
| Fact | Value |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Name | VegaFactory |
|
|
16
|
+
| Slug | `vegafactory` |
|
|
17
|
+
| App ID | `4812956` |
|
|
18
|
+
| Public page | https://github.com/apps/vegafactory |
|
|
19
|
+
| Install URL | https://github.com/apps/vegafactory/installations/new |
|
|
20
|
+
| Actor a bot write shows | `vegafactory[bot]` |
|
|
21
|
+
| Webhook | off |
|
|
22
|
+
|
|
23
|
+
The slug is what GitHub derives from the name, and both the actor string and the install URL follow it — confirm it on the App's settings page rather than assuming it, because renaming the App changes the slug and every reference to it.
|
|
24
|
+
|
|
25
|
+
## Permissions
|
|
26
|
+
|
|
27
|
+
Exactly this set, and no others.
|
|
28
|
+
|
|
29
|
+
| Permission | Level | Why |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Issues | Read and write | Edit labels and assignees on the issues the mirror reacts to |
|
|
32
|
+
| Metadata | Read-only | Mandatory for every App; repository name and visibility only |
|
|
33
|
+
| Projects (organization) | Read and write | The ProjectsV2 GraphQL surface reads and writes item fields |
|
|
34
|
+
| Pull requests | Read and write | Comment on and label the PR an issue's work lands through |
|
|
35
|
+
| Contents | Read-only | The token can read a workflow file but cannot push, tag, or publish — this is what the drill proves |
|
|
36
|
+
|
|
37
|
+
Workflows stays at No access and no webhook is configured, so nothing about this App can change a workflow file or receive an event. `Contents: read` is deliberately not `No access`: `actions/checkout` with an App token needs to read the repository, and read cannot write.
|
|
38
|
+
|
|
39
|
+
## Hosted token broker
|
|
40
|
+
|
|
41
|
+
VegaStack runs a hosted broker so an organisation can use the factory **without holding any private
|
|
42
|
+
key**: install the public App, and your Actions jobs exchange their own OIDC token for a
|
|
43
|
+
token for one repository’s issues/metadata and **organization-wide project writes**. Written here
|
|
44
|
+
for an org that is not `vegastack`. The broker contract below was checked against its source and
|
|
45
|
+
[GitHub installation APIs](https://docs.github.com/en/rest/apps/installations) on 07-09-2026; local
|
|
46
|
+
verification does not claim a deployed exchange is ready.
|
|
47
|
+
|
|
48
|
+
| Fact | Value |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Endpoint | `POST https://vegafactory-token.vegastack.com/token` |
|
|
51
|
+
| Preview endpoint | `POST https://vegafactory-token.vegastack.dev/token` |
|
|
52
|
+
| Audience | `vegastack-factory` |
|
|
53
|
+
| Auth | `Authorization: Bearer <the job's OIDC token>` |
|
|
54
|
+
| Health probe | `GET https://vegafactory-token.vegastack.com/health` → `{"status":"ok"}`, unauthenticated liveness only; never authenticated readiness |
|
|
55
|
+
| Token lifetime | GitHub's fixed 1 hour; the broker reports `expires_at`, it does not set it |
|
|
56
|
+
|
|
57
|
+
Request: no body. Identity comes from the verified bearer JWT, never unsigned repository parameters.
|
|
58
|
+
The action’s `audience` input must match the deployment’s `OIDC_AUDIENCE`; both default to
|
|
59
|
+
`vegastack-factory` in preview and production. A GitHub Environment claim does not change the
|
|
60
|
+
audience. An intentionally different deployment audience needs a matching caller configuration.
|
|
61
|
+
|
|
62
|
+
Both canonical domains retain App `4812956`. Preview has production App authority: both deployments
|
|
63
|
+
require an explicit reviewed dispatch, protected GitHub Environment and a merged source SHA with
|
|
64
|
+
the reviewed Worker digest. These are prepared source defaults, not a claim that either endpoint
|
|
65
|
+
has passed live acceptance. Confirm actual store bindings, account/zone access and an eligible
|
|
66
|
+
operator reviewer under the current self-review rules before enabling deployment; an unavailable
|
|
67
|
+
reviewer blocks rollout. Keep current account runners.
|
|
68
|
+
|
|
69
|
+
Inventory caller action revisions and explicit endpoint/audience overrides before migration.
|
|
70
|
+
The public action name remains `vegastack/factory-token`; this repository's source change does not
|
|
71
|
+
publish its mirror. Preserve the previous compatible Worker/action/audience pair and deployment ID.
|
|
72
|
+
An existing endpoint is retired only after callers are verified and retirement is explicitly
|
|
73
|
+
approved; no alias is promised. The operator rollout checklist and bounded preview/production
|
|
74
|
+
acceptance live in the broker's repository README. `/health` alone never closes rollout.
|
|
75
|
+
|
|
76
|
+
Response `200`:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"token": "ghs_…",
|
|
81
|
+
"expires_at": "2026-09-03T12:00:00Z",
|
|
82
|
+
"repository": "acme/widgets",
|
|
83
|
+
"permissions": { "issues": "write", "metadata": "read", "organization_projects": "write" }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Status codes
|
|
88
|
+
|
|
89
|
+
| Code | Meaning | What to do |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| 200 | Minted | Use the token; it expires in an hour |
|
|
92
|
+
| 401 | The OIDC token did not verify — the body's `reason` is one of `malformed` `alg` `kid` `signature` `issuer` `audience` `expired` `not_yet_valid` `claims` | Check the job has `permissions: id-token: write` and requests the audience `vegastack-factory` |
|
|
93
|
+
| 403 | The App is not installed on that repository | Install it, or accept the refusal — this is the kill switch working |
|
|
94
|
+
| 404 / 405 | No such route, or the wrong method | Only `POST /token` and `GET /health` answer |
|
|
95
|
+
| 429 | Rate limited for that repository | Retry after the `Retry-After` seconds |
|
|
96
|
+
| 502 | Upstream exchange timed out or returned unusable evidence | Retry; no token was returned. A minted but rejected token receives a best-effort revocation attempt |
|
|
97
|
+
| 503 | The rate limiter was unavailable | Retry; the broker fails closed rather than granting |
|
|
98
|
+
| 500 | The broker refused its own result — excess permissions/repository reach, or an unusable App key | Report it; no token is returned; rejected minted tokens receive a best-effort revocation attempt |
|
|
99
|
+
|
|
100
|
+
### The permission cap
|
|
101
|
+
|
|
102
|
+
Every returned token carries exactly `issues: write`, `metadata: read`, `organization_projects: write`.
|
|
103
|
+
The first two are scoped to one repository. **Organization project write authority remains
|
|
104
|
+
organization-wide**, retained for the existing board use case; repository selection cannot prove
|
|
105
|
+
one-project isolation. Installers consent to that reach when choosing this trust model.
|
|
106
|
+
|
|
107
|
+
The mint requests the signed numeric repository ID and exact permission cap. Before returning the
|
|
108
|
+
token, the broker independently calls `GET /installation/repositories` using that token and requires
|
|
109
|
+
exactly one matching repository ID/full name/owner ID, with no pagination. The permission echo
|
|
110
|
+
alone is insufficient. Expiry must be more than 30s and at most 1h+60s in the future. There is no
|
|
111
|
+
`contents: write` and no way for a caller to request it.
|
|
112
|
+
|
|
113
|
+
### Tenancy
|
|
114
|
+
|
|
115
|
+
The repository a caller receives a token for comes from the **verified** `repository` and
|
|
116
|
+
`repository_owner`, `repository_id` and `repository_owner_id` claims in its OIDC token. The
|
|
117
|
+
installation App ID must match configuration and its account ID must match the signed owner ID.
|
|
118
|
+
There is no unsigned repository parameter. One organisation cannot mint a token for another's repository, and the
|
|
119
|
+
installation lookup refuses any repository the App is not installed on.
|
|
120
|
+
|
|
121
|
+
Any valid workflow/ref in an installed repository is eligible, including a newly added workflow.
|
|
122
|
+
No protected-workflow/ref/environment allowlist is required or imposed. A fork's signed identity
|
|
123
|
+
needs an installation for that fork. A privileged PR workflow bearing the installed base repository's
|
|
124
|
+
identity remains eligible even when it processes fork input; operators govern untrusted workflow
|
|
125
|
+
execution on that repository.
|
|
126
|
+
|
|
127
|
+
### What is stored
|
|
128
|
+
|
|
129
|
+
Nothing of yours. The broker declares **no storage binding at all** — no KV, no D1, no R2, no
|
|
130
|
+
Durable Object. GitHub's public signing keys sit in an in-isolate memo and the Cloudflare edge
|
|
131
|
+
cache for an hour. An unknown kid triggers a single-flight origin refresh at most once/minute per
|
|
132
|
+
isolate, bypassing both caches with `cache: "no-store"` and no positive edge TTL override. Failure
|
|
133
|
+
retains the previous memo and its original expiry. Each request emits one audit record holding the repository, owner, installation
|
|
134
|
+
id, decision and status — never a token, never code, never repository content. `GET /health` writes
|
|
135
|
+
no record at all.
|
|
136
|
+
|
|
137
|
+
The broker caps JWTs at 16KiB and a 600s lifetime; expiry must remain after now, with up to 60s skew
|
|
138
|
+
only for issued/not-before times. JWKS is capped at 256KiB/32 keys and GitHub JSON at 64KiB, including
|
|
139
|
+
chunked bodies. Fetch plus body reads have 3s deadlines inside a 15s whole exchange deadline. The broker's
|
|
140
|
+
failure handler never returns or logs a rejected token. It makes a best-effort revocation of only
|
|
141
|
+
the just-minted disposable token within the remaining budget, at most 3s; the broker still refuses
|
|
142
|
+
the exchange if cleanup fails.
|
|
143
|
+
These are broker limits, not issuer guarantees. [Cloudflare Request cache behavior](https://developers.cloudflare.com/workers/runtime-apis/request/)
|
|
144
|
+
and [GitHub token creation](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app)
|
|
145
|
+
are the upstream contracts.
|
|
146
|
+
|
|
147
|
+
### The rate limit
|
|
148
|
+
|
|
149
|
+
An abuse brake, not an exact global quota. [Cloudflare documents eventual consistency](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/), so its counters are not exact accounting. The number, written down: **30 token requests
|
|
150
|
+
per minute per repository, per Cloudflare location** — Cloudflare's rate-limit binding counts per
|
|
151
|
+
location rather than globally, and its period accepts only 10 or 60 seconds. A burst can
|
|
152
|
+
degrade to a 429 and a retry; enforcement is permissive and eventually consistent. It is never an authorization
|
|
153
|
+
decision — the OIDC claims and the installation lookup are — and it is keyed by
|
|
154
|
+
`<owner>/<repository>` from the verified claims, so one organisation's traffic cannot spend
|
|
155
|
+
another's allowance.
|
|
156
|
+
|
|
157
|
+
### Rotating the private key
|
|
158
|
+
|
|
159
|
+
VegaStack operates this; the order matters, so no window exists with zero valid keys.
|
|
160
|
+
|
|
161
|
+
1. Generate a new private key in the App's settings.
|
|
162
|
+
2. `openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in app.pem -out app.pkcs8.pem` — the
|
|
163
|
+
Worker accepts PKCS#8 only and refuses a PKCS#1 PEM with this same command in the error.
|
|
164
|
+
3. `wrangler secrets-store secret update <STORE-ID> --secret-id <SECRET-ID> --remote` with the new value.
|
|
165
|
+
4. Redeploy the Worker.
|
|
166
|
+
5. Only then delete the old key in the App's settings.
|
|
167
|
+
|
|
168
|
+
### Kill switch
|
|
169
|
+
|
|
170
|
+
Uninstall the App. The next request for any of your repositories fails at the installation lookup
|
|
171
|
+
with 403, and every token already minted for you is invalidated by GitHub. No broker change, no
|
|
172
|
+
deploy, no ticket.
|
|
173
|
+
|
|
174
|
+
### Support boundary
|
|
175
|
+
|
|
176
|
+
VegaStack operates the Worker, the App, and the key. You operate your installation and your
|
|
177
|
+
workflows. The broker is offered **as is, with no uptime commitment**; its dependency chain is
|
|
178
|
+
GitHub's OIDC JWKS endpoint and `api.github.com`, and it fails closed with a plain reason when
|
|
179
|
+
either is unavailable. An organisation that wants its own availability guarantee registers its own
|
|
180
|
+
App and uses `actions/create-github-app-token` directly, as the sections above describe. Issues go
|
|
181
|
+
to `vegastack/vegafactory`.
|
|
182
|
+
|
|
183
|
+
### Abuse surface
|
|
184
|
+
|
|
185
|
+
Anyone who can run Actions in a repository where the App is installed can obtain a
|
|
186
|
+
token with issue/metadata access to that repository and **organization-wide project write access**.
|
|
187
|
+
The cap excludes `contents: write`, but a compromised installed-repository workflow can affect
|
|
188
|
+
organization projects beyond a single repository. Repository enumeration does not reduce that
|
|
189
|
+
project authority. Do not describe the whole token as repository-only.
|
|
190
|
+
|
|
191
|
+
## Creating the App
|
|
192
|
+
|
|
193
|
+
The operator's own browser flow. `gh` has no create-app command and the manifest flow needs a browser redirect, so no agent does this step.
|
|
194
|
+
|
|
195
|
+
1. Organization settings → Developer settings → GitHub Apps → **New GitHub App**.
|
|
196
|
+
2. Name, homepage, and description in the operator's words.
|
|
197
|
+
3. **Where can this GitHub App be installed** → *Any account*.
|
|
198
|
+
4. **Webhook → Active** → unchecked.
|
|
199
|
+
5. Repository permissions: Issues = Read and write, Metadata = Read-only (preselected), Pull requests = Read and write, Contents = Read-only, Workflows = No access. Organization permissions: Projects = Read and write.
|
|
200
|
+
6. **Create GitHub App**, then **Generate a private key** on the App's settings page.
|
|
201
|
+
|
|
202
|
+
**Generating the private key is not automatable.** GitHub delivers the `.pem` once, as a browser download, to whoever pressed the button, and never shows it again. An automated browser session downloads it into its own profile directory where the operator never sees it — two such attempts on 03-09-2026 produced no file and registered no key. A setup skill's job is to open the page, name the button, and say where the file goes; never to press it.
|
|
203
|
+
|
|
204
|
+
## Where the secrets live
|
|
205
|
+
|
|
206
|
+
| Name | Kind | Value |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| `VEGAFACTORY_APP_ID` | organization variable | the numeric App ID |
|
|
209
|
+
| `VEGAFACTORY_APP_PRIVATE_KEY` | organization secret | the PEM, pasted whole |
|
|
210
|
+
|
|
211
|
+
The private key exists in exactly two places for its whole life: this organization secret, and the Cloudflare Secrets Store secret the hosted broker reads. Never on a workstation, never on the dispatcher box, never in a control-room file, never in an issue. Only the key's holder can mint installation tokens, which is the whole reason the broker has to exist for other organisations rather than handing each of them a copy.
|
|
212
|
+
|
|
213
|
+
Control-room files record these **names**. The values live in GitHub organization settings and nowhere a repository can read them.
|
|
214
|
+
|
|
215
|
+
Setting an organization secret needs `admin:org`. A `gh` token without it can write a repository secret but not an organization one, so this step reaches the operator even when everything around it is automated.
|
|
216
|
+
|
|
217
|
+
## Minting a token in a workflow
|
|
218
|
+
|
|
219
|
+
```yaml
|
|
220
|
+
permissions:
|
|
221
|
+
contents: read
|
|
222
|
+
steps:
|
|
223
|
+
- uses: actions/create-github-app-token@v3
|
|
224
|
+
id: app-token
|
|
225
|
+
with:
|
|
226
|
+
app-id: ${{ vars.VEGAFACTORY_APP_ID }}
|
|
227
|
+
private-key: ${{ secrets.VEGAFACTORY_APP_PRIVATE_KEY }}
|
|
228
|
+
owner: ${{ github.repository_owner }}
|
|
229
|
+
repositories: ${{ github.event.repository.name }}
|
|
230
|
+
- run: gh issue edit "$NUMBER" --add-label ready
|
|
231
|
+
env:
|
|
232
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
233
|
+
NUMBER: ${{ github.event.issue.number }}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- The action is at major `v3` (v3.2.0, released 12-05-2026). `app-id` is v3's retained legacy alias for `client-id`; either works.
|
|
237
|
+
- Its outputs are `token`, `installation-id`, and `app-slug`.
|
|
238
|
+
- The minted installation token **expires after one hour**, and the action revokes it in its post step unless `skip-token-revoke` is set.
|
|
239
|
+
- `permission-<name>` inputs narrow a token further — never wider than the installation already grants.
|
|
240
|
+
- `repositories:` narrows the token to the named repositories. With `owner:` alone the action mints for **every** repository the installation covers — on an org installed "all repositories, current and future", that is the whole org — so a job that touches one repository always names it; `owner:` stays, because it is what resolves the organization installation behind the Projects surface.
|
|
241
|
+
- The job's own `permissions:` block stays `contents: read`, so a push is refused twice over: once by the job's `GITHUB_TOKEN` scope and once by the App's own Contents level.
|
|
242
|
+
|
|
243
|
+
Rate limits are not a design constraint here. An installation token starts at 5,000 requests per hour, gains 50 per hour for each repository beyond 20 and 50 per hour for each user beyond 20, caps at 12,500, and gets 15,000 on a GitHub Enterprise Cloud organization. A workflow's built-in `GITHUB_TOKEN` gets 1,000 per hour per repository and cannot touch Projects at all, which is why the board mirror needs the App rather than the built-in token.
|
|
244
|
+
|
|
245
|
+
## Recording the installation
|
|
246
|
+
|
|
247
|
+
```sh
|
|
248
|
+
gh api orgs/<org>/installations --jq '.installations[] | select(.app_slug == "vegafactory") | .id'
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`GET /orgs/{org}/installations` answers organization owners only. A 403 is a fact to report — "this account is not an owner, so the installation could not be read" — not a failure and not evidence the App is missing.
|
|
252
|
+
|
|
253
|
+
The id goes in the control room's `org.md`, on its `app-install:` line, and nowhere else. On the `vegastack` organization it is `158664419`, installed on all repositories, current and future.
|
|
254
|
+
|
|
255
|
+
## Rotating the private key
|
|
256
|
+
|
|
257
|
+
In this order, because deleting first breaks every job already running:
|
|
258
|
+
|
|
259
|
+
1. Generate the new key on the App's settings page.
|
|
260
|
+
2. Update the organization secret `VEGAFACTORY_APP_PRIVATE_KEY` with it, and the broker's Secrets Store secret if the broker is deployed.
|
|
261
|
+
3. Confirm one workflow run mints a token with the new key.
|
|
262
|
+
4. Only then delete the old key in the App's settings.
|
|
263
|
+
|
|
264
|
+
A leaked key is the exception: delete it first and accept the broken jobs, then work back up the list.
|
|
265
|
+
|
|
266
|
+
## Kill switch
|
|
267
|
+
|
|
268
|
+
**Uninstalling the App from the organization revokes every installation token immediately** and makes the next mint fail closed. It needs no key handling and no coordination, so it is the fastest stop and the one to reach for first. Deleting the private key is the narrower alternative — it stops new tokens from being minted while leaving the installation in place, and any token already minted stays valid for the rest of its hour.
|
|
269
|
+
|
|
270
|
+
## Widening a permission
|
|
271
|
+
|
|
272
|
+
Adding a row to the permission table is a dated line in the register the `decisions:` knob names, on the operator's explicit yes. The App is public: its permission set is what every other organization consents to when they install it, and a widening re-asks that consent silently for every one of them. Narrowing needs no register line, only a check that nothing depended on what was removed.
|
|
273
|
+
|
|
274
|
+
## Acceptance drill
|
|
275
|
+
|
|
276
|
+
Live checks require a separately authorized rollout on a throwaway repository after installation
|
|
277
|
+
and credential setup. Local fixtures do not qualify a deployed broker, and a health HTTP 200 response
|
|
278
|
+
is only liveness. Keep current CI runners. Never delete a shared App key or uninstall the shared
|
|
279
|
+
App as a qualification drill. Four checks:
|
|
280
|
+
|
|
281
|
+
1. A job that mints a token and runs `gh issue edit --add-label` leaves an event whose actor is `vegafactory[bot]`, not a human.
|
|
282
|
+
2. A `git push` step in that same job, using the minted token, **fails** — the App has no Contents write.
|
|
283
|
+
3. Controlled installation HTTP 404, key-rotation, expiry and upstream-failure fixtures prove local refusal; disposable-token revocation fixtures prove cleanup. Real shared-App uninstall/key-revocation drills are excluded. Record actual live broker allow/deny and organization-project reach separately before rollout acceptance.
|
|
284
|
+
4. `gh issue comment` against an issue in a **second** repository of the same org, using the minted token, **fails** — the token is scoped by `repositories:` to the one repository the job runs in, not to the installation.
|
|
285
|
+
|
|
286
|
+
Check 2 is the one worth being stubborn about, and check 4 is its twin: level and scope are two different ways a token can be too wide. It is the difference between a token that can edit a label and a token that can rewrite the repository.
|