@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,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dev-implement guard: the evidence comment's required shape. Structure blocks;
|
|
3
|
+
// nothing here warns.
|
|
4
|
+
//
|
|
5
|
+
// Exit codes: 0 pass · 2 blocked (this guard has no warn class).
|
|
6
|
+
// Usage: node evidence-check.mjs --file <evidence.md> --json
|
|
7
|
+
import { compareTaskIds } from './recovery.mjs';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { GhUnavailable, findMarkerComment, ghJson, parseFlags, parseMarker, renderResult } from './lib/gh.mjs';
|
|
12
|
+
|
|
13
|
+
const REQUIRED_SECTIONS = [
|
|
14
|
+
[/\*\*Done:\*\*/, '**Done:** section'],
|
|
15
|
+
[/\*\*Tests:\*\*/, '**Tests:** section (command → fresh result)'],
|
|
16
|
+
[/\*\*Review:\*\*/, '**Review:** section (mode + verdict/adjudications)'],
|
|
17
|
+
[/\*\*Changelog:\*\*/, '**Changelog:** section (entry, or none with a holding reason)'],
|
|
18
|
+
[/\*\*Docs:\*\*/, '**Docs:** section (brief/plan revisions in sync, or unchanged)'],
|
|
19
|
+
[/\*\*Not done/, '**Not done / limits:** section (the honest list)'],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function checkEvidence(text) {
|
|
23
|
+
const blocks = [];
|
|
24
|
+
|
|
25
|
+
const marker = parseMarker(text);
|
|
26
|
+
if (!marker || marker.keys.type !== 'evidence') {
|
|
27
|
+
blocks.push('missing evidence marker (<!-- vsk:v1 type=evidence rev=n branch=... sha=... -->)');
|
|
28
|
+
} else {
|
|
29
|
+
if (!marker.keys.branch) blocks.push('evidence marker missing branch=');
|
|
30
|
+
if (!/^[0-9a-f]{7,}$/.test(marker.keys.sha ?? '')) blocks.push('evidence marker missing a real sha=');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const [pattern, label] of REQUIRED_SECTIONS) {
|
|
34
|
+
if (!pattern.test(text)) blocks.push(`missing ${label}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!/Branch:\s*\S+\s*@\s*[0-9a-f]{7,}/.test(text)) blocks.push('missing "Branch: <name> @ <sha7>" tail line');
|
|
38
|
+
|
|
39
|
+
return { blocks, warns: [] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The plan comment's checkboxes and the ledger's completed tasks are two records
|
|
43
|
+
// of the same fact, and the checkbox — a second write, to a different comment
|
|
44
|
+
// than the ledger the session already maintains — is the one that silently rots.
|
|
45
|
+
// At hand-back the ledger's completed tasks must be reflected in the plan's
|
|
46
|
+
// checkboxes, or the operator reads a false x/y. Distinct task numbers with a
|
|
47
|
+
// `complete` line in the ledger, versus `[x]` boxes in the plan.
|
|
48
|
+
export function checkTaskConsistency(comments) {
|
|
49
|
+
const blocks = [];
|
|
50
|
+
const warns = [];
|
|
51
|
+
const plan = findMarkerComment(comments, 'plan')?.comment?.body;
|
|
52
|
+
const ledger = findMarkerComment(comments, 'ledger')?.comment?.body;
|
|
53
|
+
|
|
54
|
+
// No plan comment, or a plan with no checkboxes (e.g. a research issue), is
|
|
55
|
+
// nothing to reconcile — not a violation. No ledger yet is the same.
|
|
56
|
+
if (!plan || !ledger) return { blocks, warns };
|
|
57
|
+
const rows = [...plan.matchAll(/^-\s*\[([ x])\].*$/gim)];
|
|
58
|
+
if (!rows.length) return { blocks, warns };
|
|
59
|
+
const ids = rows.map(row => /<!--\s*task-id:([1-9]\d*-T[1-9]\d*)\s*-->/.exec(row[0])?.[1]);
|
|
60
|
+
if (ids.some(id => !id) || new Set(ids).size !== ids.length) {
|
|
61
|
+
blocks.push('plan task identities missing or duplicate; regenerate only from newly approved scope');
|
|
62
|
+
return { blocks, warns };
|
|
63
|
+
}
|
|
64
|
+
const planDone = rows.flatMap((row, i) => row[1].toLowerCase() === 'x' ? [ids[i]] : []);
|
|
65
|
+
const completed = [...ledger.matchAll(/^-\s*(?:Task\s+)?([1-9]\d*-T[1-9]\d*):\s*complete\b/gim)].map(row => row[1]);
|
|
66
|
+
if (/^-\s*Task\s+\d+:\s*complete\b/im.test(ledger)) blocks.push('legacy numeric ledger completion lacks approved task identity');
|
|
67
|
+
const difference = compareTaskIds(planDone, completed);
|
|
68
|
+
if (difference.missing.length || difference.unknown.length) blocks.push('task identity mismatch: plan-only [' + difference.missing.join(', ') + ']; ledger-only [' + difference.unknown.join(', ') + ']');
|
|
69
|
+
return { blocks, warns };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fetch the issue's comments and reconcile plan checkboxes against the ledger.
|
|
73
|
+
// Fails closed: an unreachable gh blocks rather than passing silently.
|
|
74
|
+
function taskConsistencyFromIssue({ issue, repo: repoFlag }) {
|
|
75
|
+
const repo = repoFlag || ghJson(['repo', 'view', '--json', 'nameWithOwner']).nameWithOwner;
|
|
76
|
+
const comments = ghJson(['api', 'repos/' + repo + '/issues/' + issue + '/comments', '--paginate']);
|
|
77
|
+
return checkTaskConsistency(comments);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
81
|
+
if (invokedDirectly) {
|
|
82
|
+
const flags = parseFlags(process.argv.slice(2));
|
|
83
|
+
let outcome;
|
|
84
|
+
if (!flags.file) {
|
|
85
|
+
outcome = { blocks: ['usage: evidence-check.mjs --file <evidence.md> [--issue <n> --repo <o/r>] [--json]'], warns: [] };
|
|
86
|
+
} else {
|
|
87
|
+
try {
|
|
88
|
+
outcome = checkEvidence(readFileSync(flags.file, 'utf8'));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
outcome = { blocks: [`cannot read evidence: ${error.message}`], warns: [] };
|
|
91
|
+
}
|
|
92
|
+
// Consistency check only when an issue is named — keeps the file-shape check
|
|
93
|
+
// network-free by default. A gh failure fails closed onto the block list.
|
|
94
|
+
if (flags.issue) {
|
|
95
|
+
try {
|
|
96
|
+
const consistency = taskConsistencyFromIssue(flags);
|
|
97
|
+
outcome.blocks.push(...consistency.blocks);
|
|
98
|
+
outcome.warns.push(...consistency.warns);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
outcome.blocks.push(error instanceof GhUnavailable ? `cannot verify plan/ledger consistency: ${error.message}` : `consistency check error: ${error.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const { exitCode, text } = renderResult('evidence-check', outcome, { json: Boolean(flags.json) });
|
|
105
|
+
console.log(text);
|
|
106
|
+
process.exit(exitCode);
|
|
107
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dev-implement evidence-upload: put one screenshot into the shared evidence
|
|
3
|
+
// repo through the GitHub contents API. Dry-run by default — it prints the PUT
|
|
4
|
+
// it would make (path and size, never the bytes) — and sends only under
|
|
5
|
+
// --write. The request body ({message, content}) travels over gh's stdin
|
|
6
|
+
// (`--input -`), so the base64 payload never touches argv or any output line.
|
|
7
|
+
// A 409 on the first PUT (a concurrent upload chose the same name) is retried
|
|
8
|
+
// once under a fresh name. Symlinks, non-image extensions, and empty files are
|
|
9
|
+
// refused before any network call.
|
|
10
|
+
//
|
|
11
|
+
// Exit codes: 0 planned or sent · 1 sent after a 409 retry (warn) · 2 refused
|
|
12
|
+
// (reasons printed). Usage:
|
|
13
|
+
// node evidence-upload.mjs --repo <o/r> --issue <n> --file <png>
|
|
14
|
+
// [--evidence-repo <o/r>] [--dev-md <path>] [--write] [--json]
|
|
15
|
+
// The evidence repo comes from dev.md's `evidence-repo:` knob; --evidence-repo
|
|
16
|
+
// overrides it. The uploaded path is <repo-name>/<issue>/<timestamp>-<name>.
|
|
17
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { basename, extname, resolve } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { GhUnavailable, ghJson, parseFlags, renderResult } from './lib/gh.mjs';
|
|
21
|
+
|
|
22
|
+
export const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
|
|
23
|
+
const REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
24
|
+
const ISSUE = /^\d+$/;
|
|
25
|
+
|
|
26
|
+
// The contents-API path for one file in the evidence repo — the one place it is built.
|
|
27
|
+
// Interpolated paths and messages are concatenated, not template literals: a backtick at a
|
|
28
|
+
// shell-word start whose first inner token carries the interpolation trips SkillSpector's
|
|
29
|
+
// bounded parser for the whole skill (skill-maintainer's standards.md, known behaviours).
|
|
30
|
+
const apiPathFor = (evidenceRepo, path) => 'repos/' + evidenceRepo + '/contents/' + path;
|
|
31
|
+
|
|
32
|
+
// First `evidence-repo:` knob line in dev.md; a trailing `# comment` is ignored
|
|
33
|
+
// because the value stops at whitespace.
|
|
34
|
+
export function evidenceRepoFrom(devMd) {
|
|
35
|
+
const match = /^evidence-repo:\s*(\S+)/m.exec(devMd ?? '');
|
|
36
|
+
return match ? match[1] : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// UTC YYYYMMDD-HHMMSS — the collision-avoiding prefix on every evidence file.
|
|
40
|
+
export function timestamp(now) {
|
|
41
|
+
return now.toISOString().replace(/[-:]/g, '').replace('T', '-').slice(0, 15);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Pure plan: file checks by metadata only (lstat — the file's contents are
|
|
45
|
+
// never read here), evidence repo resolution, target path and size. `put` is
|
|
46
|
+
// null whenever anything blocks.
|
|
47
|
+
export function plan({ repo, issue, file, evidenceRepo, devMd, now = new Date() }) {
|
|
48
|
+
const blocks = [];
|
|
49
|
+
// repo and issue become path segments in the evidence repo, so each is held
|
|
50
|
+
// to its own shape — a stray `../` would otherwise land the file elsewhere.
|
|
51
|
+
if (!repo) blocks.push('--repo <o/r> is required (the repo the evidence belongs to)');
|
|
52
|
+
else if (!REPO.test(repo)) blocks.push(`--repo ${repo} is not <owner>/<name>`);
|
|
53
|
+
if (!issue) blocks.push('--issue <n> is required');
|
|
54
|
+
else if (!ISSUE.test(String(issue))) blocks.push(`--issue ${issue} is not an issue number`);
|
|
55
|
+
if (!file) blocks.push('--file <png> is required');
|
|
56
|
+
|
|
57
|
+
let bytes = 0;
|
|
58
|
+
if (file) {
|
|
59
|
+
let stat = null;
|
|
60
|
+
try {
|
|
61
|
+
stat = lstatSync(file);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
blocks.push(`cannot read ${file}: ${error.code || error.message}`);
|
|
64
|
+
}
|
|
65
|
+
if (stat) {
|
|
66
|
+
if (stat.isSymbolicLink()) {
|
|
67
|
+
blocks.push(file + ' is a symlink — evidence is uploaded from a regular file so the bytes sent are the bytes named');
|
|
68
|
+
} else if (!stat.isFile()) {
|
|
69
|
+
blocks.push(file + ' is not a regular file');
|
|
70
|
+
} else {
|
|
71
|
+
const ext = extname(file).toLowerCase();
|
|
72
|
+
if (!IMAGE_EXTENSIONS.includes(ext)) {
|
|
73
|
+
blocks.push((ext || '(no extension)') + ' is not an image extension (png, jpg, jpeg, webp, gif)');
|
|
74
|
+
}
|
|
75
|
+
if (stat.size === 0) blocks.push(file + ' is empty (0 bytes)');
|
|
76
|
+
bytes = stat.size;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const target = evidenceRepo || evidenceRepoFrom(devMd);
|
|
82
|
+
if (!target) blocks.push('no evidence repo: dev.md has no evidence-repo: line and --evidence-repo was not passed');
|
|
83
|
+
else if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(target)) blocks.push(`evidence repo "${target}" is not owner/name — it becomes an API path segment, so it is checked before use`);
|
|
84
|
+
|
|
85
|
+
if (blocks.length > 0) return { blocks, put: null };
|
|
86
|
+
|
|
87
|
+
const path = repo.slice(repo.indexOf('/') + 1) + '/' + issue + '/' + timestamp(now) + '-' + basename(file);
|
|
88
|
+
return {
|
|
89
|
+
blocks,
|
|
90
|
+
put: { evidenceRepo: target, path, bytes, apiPath: apiPathFor(target, path), message: `evidence #${issue}` },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The retry name after a 409: `-r2` after the timestamp, so a concurrent
|
|
95
|
+
// upload's file and this one both survive under the same issue directory.
|
|
96
|
+
export function retryPath(path) {
|
|
97
|
+
return path.replace(/(\/\d{8}-\d{6})-/, '$1-r2-');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// One PUT through gh with the JSON body on stdin. A 409 (a concurrent upload
|
|
101
|
+
// chose the same name) is retried once under retryPath; any other failure,
|
|
102
|
+
// or a second one, propagates to the caller.
|
|
103
|
+
export function upload(put, contentBase64, { gh } = {}) {
|
|
104
|
+
const warns = [];
|
|
105
|
+
const send = (apiPath) => ghJson(['api', '-X', 'PUT', apiPath, '--input', '-'], {
|
|
106
|
+
gh,
|
|
107
|
+
input: JSON.stringify({ message: put.message, content: contentBase64 }),
|
|
108
|
+
});
|
|
109
|
+
let path = put.path;
|
|
110
|
+
let response;
|
|
111
|
+
let attempts = 1;
|
|
112
|
+
try {
|
|
113
|
+
response = send(put.apiPath);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (!(error instanceof GhUnavailable) || error.httpStatus !== 409) throw error;
|
|
116
|
+
path = retryPath(put.path);
|
|
117
|
+
warns.push(`first PUT hit HTTP 409 (a concurrent upload chose the same name) — retried as ${path}`);
|
|
118
|
+
attempts = 2;
|
|
119
|
+
response = send(apiPathFor(put.evidenceRepo, path));
|
|
120
|
+
}
|
|
121
|
+
return { attempts, path, url: response?.content?.html_url ?? null, warns };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Plan, then (only under --write) read the bytes and send them. The base64
|
|
125
|
+
// string exists only inside the request body handed to gh's stdin.
|
|
126
|
+
export function run(flags, { gh, now = new Date() } = {}) {
|
|
127
|
+
const devMdPath = flags['dev-md'] || '.vegastack/dev.md';
|
|
128
|
+
let devMd = '';
|
|
129
|
+
if (!flags['evidence-repo']) {
|
|
130
|
+
try {
|
|
131
|
+
devMd = readFileSync(devMdPath, 'utf8');
|
|
132
|
+
} catch {
|
|
133
|
+
return { blocks: [`cannot read ${devMdPath} (pass --dev-md <path> or --evidence-repo <o/r>)`], warns: [], upload: null };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const planned = plan({
|
|
137
|
+
repo: flags.repo,
|
|
138
|
+
issue: flags.issue,
|
|
139
|
+
file: flags.file,
|
|
140
|
+
evidenceRepo: flags['evidence-repo'],
|
|
141
|
+
devMd,
|
|
142
|
+
now,
|
|
143
|
+
});
|
|
144
|
+
if (planned.blocks.length > 0) return { blocks: planned.blocks, warns: [], upload: null };
|
|
145
|
+
|
|
146
|
+
const { put } = planned;
|
|
147
|
+
const summary = { evidenceRepo: put.evidenceRepo, path: put.path, bytes: put.bytes };
|
|
148
|
+
if (!flags.write) return { blocks: [], warns: [], upload: { ...summary, mode: 'dry-run', attempts: 0, url: null } };
|
|
149
|
+
|
|
150
|
+
const sent = upload(put, readFileSync(flags.file).toString('base64'), { gh });
|
|
151
|
+
return {
|
|
152
|
+
blocks: [],
|
|
153
|
+
warns: sent.warns,
|
|
154
|
+
upload: { ...summary, path: sent.path, mode: 'sent', attempts: sent.attempts, url: sent.url },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
159
|
+
if (invokedDirectly) {
|
|
160
|
+
const flags = parseFlags(process.argv.slice(2), ['json', 'write']);
|
|
161
|
+
let outcome;
|
|
162
|
+
try {
|
|
163
|
+
outcome = run(flags);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
outcome = {
|
|
166
|
+
blocks: [error instanceof GhUnavailable ? `cannot upload: ${error.message}` : `evidence-upload error: ${error.message}`],
|
|
167
|
+
warns: [],
|
|
168
|
+
upload: null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const { exitCode, text } = renderResult('evidence-upload', outcome, { json: false });
|
|
172
|
+
if (flags.json) {
|
|
173
|
+
const { blocks, warns, upload: result } = outcome;
|
|
174
|
+
console.log(JSON.stringify({ guard: 'evidence-upload', ok: blocks.length === 0, blocks, warns, upload: result }, null, 2));
|
|
175
|
+
} else {
|
|
176
|
+
const lines = [text];
|
|
177
|
+
if (outcome.upload) lines.splice(1, 0, ' ' + outcome.upload.mode + ': ' + outcome.upload.path + ' (' + outcome.upload.bytes + ' bytes)');
|
|
178
|
+
console.log(lines.join('\n'));
|
|
179
|
+
}
|
|
180
|
+
process.exit(exitCode);
|
|
181
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bounded lessons reuse ordinary-work evidence. No model, network or vendor memory.
|
|
3
|
+
const sha=/^[a-f0-9]{40}$/;
|
|
4
|
+
const digest=/^[a-f0-9]{64}$/;
|
|
5
|
+
const closed=(row,keys)=>row&&typeof row==='object'&&!Array.isArray(row)&&Object.keys(row).length===keys.length&&keys.every(key=>Object.hasOwn(row,key));
|
|
6
|
+
const safePath=value=>typeof value==='string'&&value.length>0&&!value.startsWith('/')&&!value.includes('\\')&&!/[\0\r\n]/.test(value)&&!value.split('/').some(part=>['.','..',''].includes(part));
|
|
7
|
+
const nativePath=path=>/(?:^|\/)\.(?:claude|codex)\/.*memory|(?:^|\/)MEMORY\.md$/i.test(path);
|
|
8
|
+
const protectedPath=path=>/(?:^|\/)(?:AGENTS\.md|CLAUDE\.md|org\.md|dev\.md|decisions\.md|settings\.json|factory\.json)$/.test(path);
|
|
9
|
+
export function validateLesson(row) {
|
|
10
|
+
if (!closed(row,['id','repo','taskId','scopeDigest','sourceSha','statement','evidenceRefs','targetPaths','undoRef','state','supersedes'])||!/^lesson-[a-f0-9]{32}$/.test(row.id)||!/^\S+\/\S+$/.test(row.repo)||!/^\d+-T\d+$/.test(row.taskId)||!digest.test(row.scopeDigest)||!sha.test(row.sourceSha)||typeof row.statement!=='string'||!row.statement.trim()||Buffer.byteLength(row.statement)>768||/[\0\r]/.test(row.statement)) throw Error('invalid lesson identity');
|
|
11
|
+
if (!['observed','validated','adopted','rejected','reverted'].includes(row.state)||!Array.isArray(row.targetPaths)||!row.targetPaths.length||row.targetPaths.length>32||row.targetPaths.some(path=>!safePath(path)||nativePath(path))||new Set(row.targetPaths).size!==row.targetPaths.length||typeof row.undoRef!=='string'||!/^git:[a-f0-9]{40}$/.test(row.undoRef)||!Array.isArray(row.supersedes)||row.supersedes.length>32||row.supersedes.some(id=>!/^lesson-[a-f0-9]{32}$/.test(id)||id===row.id)) throw Error('invalid reversible lesson');
|
|
12
|
+
if (!Array.isArray(row.evidenceRefs)||!row.evidenceRefs.length||row.evidenceRefs.length>8||row.evidenceRefs.some(ref=>!closed(ref,['kind','ref','sha','passed'])||!['check','review','measurement'].includes(ref.kind)||typeof ref.ref!=='string'||ref.ref.length>256||!sha.test(ref.sha)||typeof ref.passed!=='boolean')) throw Error('invalid lesson evidence');
|
|
13
|
+
return row;
|
|
14
|
+
}
|
|
15
|
+
export function evaluateLesson(candidate,context) {
|
|
16
|
+
const answer=(action,reason)=>({action,reason});
|
|
17
|
+
if (context?.protectedChange || candidate?.targetPaths?.some(protectedPath)) return answer('propose','protected rules require explicit approval');
|
|
18
|
+
try { validateLesson(candidate); } catch(error) { return answer('reject',error.message); }
|
|
19
|
+
if (context.learningEnabled!==true) return answer('reject','learning disabled or policy unavailable');
|
|
20
|
+
if (candidate.repo!==context.repo||candidate.scopeDigest!==context.scopeDigest||candidate.sourceSha!==context.sourceSha||!context.taskIds?.includes(candidate.taskId)) return answer('reject','lesson scope or source differs');
|
|
21
|
+
if (candidate.targetPaths.some(path=>!context.allowedFiles?.includes(path))||context.paidProviderChange) return answer('propose','outside reversible local authorization');
|
|
22
|
+
if (!['validated','adopted'].includes(candidate.state)||context.reversible!==true||context.improved!==true) return answer('reject','ordinary work improvement and exact undo unverified');
|
|
23
|
+
if (!candidate.evidenceRefs.every(ref=>context.verifiedEvidence?.some(verified=>JSON.stringify(verified)===JSON.stringify(ref)))||!candidate.evidenceRefs.some(ref=>ref.passed&&ref.sha===candidate.sourceSha)) return answer('reject','source-bound evidence unavailable');
|
|
24
|
+
if (context.reviewRequired&&!candidate.evidenceRefs.some(ref=>ref.kind==='review'&&ref.passed&&ref.sha===candidate.sourceSha)) return answer('reject','independent review required');
|
|
25
|
+
if (context.adoption==='propose-only') return answer('propose','policy requires proposal');
|
|
26
|
+
return answer('adopt','verified reversible improvement');
|
|
27
|
+
}
|
|
28
|
+
export function selectLessons(packet,{repo,scopeDigest,sourceSha,maxItems=3,maxBytes=2048}) {
|
|
29
|
+
if (!Array.isArray(packet?.learning)||packet.learning.length>32||Buffer.byteLength(JSON.stringify(packet.learning))>16384) return [];
|
|
30
|
+
const count=Math.min(3,Math.max(0,maxItems)),limit=Math.min(2048,Math.max(0,maxBytes)),selected=[],seen=new Set();
|
|
31
|
+
const superseded=new Set(packet.learning.filter(row=>['adopted','reverted'].includes(row.state)).flatMap(row=>row.supersedes??[]));
|
|
32
|
+
for (const row of packet.learning) {
|
|
33
|
+
try {validateLesson(row);}catch{continue;}
|
|
34
|
+
if(row.state!=='adopted'||row.repo!==repo||row.scopeDigest!==scopeDigest||row.sourceSha!==sourceSha||superseded.has(row.id))continue;
|
|
35
|
+
const key=JSON.stringify([repo,[...row.targetPaths].sort(),row.evidenceRefs]);
|
|
36
|
+
if(seen.has(key)||selected.some(other=>other.targetPaths.some(path=>row.targetPaths.includes(path))))continue;
|
|
37
|
+
if(selected.length>=count||Buffer.byteLength(JSON.stringify([...selected,row]))>limit)continue;
|
|
38
|
+
seen.add(key);selected.push(row);
|
|
39
|
+
}
|
|
40
|
+
return selected;
|
|
41
|
+
}
|