@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,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dev-implement reclaim: release an orphaned claim. The operator runs this after
|
|
3
|
+
// dev-status flags a working issue as possibly-orphaned — it resets working →
|
|
4
|
+
// ready and unassigns, so a fresh session can claim it cleanly. It NEVER claims
|
|
5
|
+
// or resumes: a takeover still needs the operator's explicit handover to a new
|
|
6
|
+
// session, and it never touches the issue's worktree — the branch and its
|
|
7
|
+
// checkout stay exactly where they are, so the session that picks the issue up
|
|
8
|
+
// resumes in them (worktree.mjs restore re-adds a checkout that went missing). It fails closed and refuses to release a claim whose ledger is still
|
|
9
|
+
// fresh (the session may be alive) unless --force is passed.
|
|
10
|
+
//
|
|
11
|
+
// Exit codes: 0 released · 2 refused/blocked (reasons printed). Read-verify runs
|
|
12
|
+
// before any mutation, so a block never leaves a half-released issue.
|
|
13
|
+
// Usage: node reclaim.mjs --issue <n> [--repo o/r] [--orphan-hours 6] [--force] [--json]
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { GhUnavailable, findMarkerComment, ghJson, parseFlags, renderResult } from './lib/gh.mjs';
|
|
20
|
+
const policyUrl = new URL('./effective-policy.mjs', import.meta.url);
|
|
21
|
+
const { resolveState, readWorkflowLabels, loadConfiguredPolicy } = await import(existsSync(policyUrl) ? policyUrl.href : new URL('../../dev-setup/scripts/effective-policy.mjs', import.meta.url).href);
|
|
22
|
+
|
|
23
|
+
// stdio mode for a discarded fd, hoisted out of quote-adjacency: SkillSpector reads the
|
|
24
|
+
// bare word beside its own closing quote as a removal cue and fails closed on the whole
|
|
25
|
+
// file (skill-maintainer's standards.md, known behaviours). Same value, same behaviour.
|
|
26
|
+
const DISCARD = 'ignore';
|
|
27
|
+
|
|
28
|
+
const ageHours = (iso, now) => Math.floor((now - Date.parse(iso)) / 3_600_000);
|
|
29
|
+
|
|
30
|
+
// Read-verify: the deterministic facts that must hold before a release. Returns
|
|
31
|
+
// { blocks, plan } — plan is the mutation to run when blocks is empty.
|
|
32
|
+
export function evaluateReclaim({ issue, comments, devMd = '', configuredPolicy = null, orphanHours = 6, force = false, now = Date.now() }) {
|
|
33
|
+
const blocks = [];
|
|
34
|
+
const labels = (issue.labels ?? []).map((l) => l.name);
|
|
35
|
+
|
|
36
|
+
if (issue.state && issue.state !== 'open') blocks.push(`issue is ${issue.state} — only an open working issue can be reclaimed`);
|
|
37
|
+
let labelMap;
|
|
38
|
+
try { labelMap = configuredPolicy?.policy.values['workflow-labels'] ?? readWorkflowLabels(devMd); } catch (error) { blocks.push(error.message); }
|
|
39
|
+
if (configuredPolicy) blocks.push(...configuredPolicy.blocks);
|
|
40
|
+
const resolvedState = resolveState(labels, labelMap);
|
|
41
|
+
blocks.push(...resolvedState.blocks);
|
|
42
|
+
if (resolvedState.state !== 'working') blocks.push("issue is not 'working' — nothing to reclaim");
|
|
43
|
+
|
|
44
|
+
const moved = findMarkerComment(comments, 'ledger')?.comment?.updated_at ?? null;
|
|
45
|
+
const ageH = moved ? ageHours(moved, now) : null;
|
|
46
|
+
if (!force && ageH !== null && ageH < orphanHours) {
|
|
47
|
+
blocks.push(`ledger moved ${ageH}h ago (< orphan threshold ${orphanHours}h) — this claim may be live; pass --force to release anyway`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const assignees = (issue.assignees ?? []).map((a) => a.login);
|
|
51
|
+
return {
|
|
52
|
+
blocks,
|
|
53
|
+
plan: { labelMap, state: resolvedState.state, removeAssignees: assignees, ledgerAgeHours: ageH },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ghRun(args) {
|
|
58
|
+
try {
|
|
59
|
+
return execFileSync(process.env.VSK_GH || 'gh', args, { encoding: 'utf8', stdio: [DISCARD, 'pipe', 'pipe'] });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const stderr = error.stderr?.toString().trim() || error.message;
|
|
62
|
+
throw new GhUnavailable(`gh ${args.join(' ')} failed: ${stderr}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function gatherAndReclaim(flags) {
|
|
67
|
+
const repo = flags.repo || ghJson(['repo', 'view', '--json', 'nameWithOwner']).nameWithOwner;
|
|
68
|
+
const issueNumber = flags.issue;
|
|
69
|
+
// Interpolated paths and messages are concatenated, not template literals: a backtick at a
|
|
70
|
+
// shell-word start whose first inner token carries the interpolation trips SkillSpector's
|
|
71
|
+
// bounded parser for the whole skill (skill-maintainer's standards.md, known behaviours).
|
|
72
|
+
const raw = ghJson(['api', 'repos/' + repo + '/issues/' + issueNumber]);
|
|
73
|
+
const comments = ghJson(['api', 'repos/' + repo + '/issues/' + issueNumber + '/comments', '--paginate']);
|
|
74
|
+
const devMd = readFileSync(flags['dev-md'] || '.vegastack/dev.md', 'utf8');
|
|
75
|
+
const configuredPolicy = loadConfiguredPolicy({ home: homedir(), repo, devMd });
|
|
76
|
+
const orphanRaw = Number(flags['orphan-hours']);
|
|
77
|
+
const { blocks, plan } = evaluateReclaim({
|
|
78
|
+
issue: { state: raw.state, labels: raw.labels, assignees: raw.assignees },
|
|
79
|
+
comments,
|
|
80
|
+
devMd, configuredPolicy,
|
|
81
|
+
orphanHours: Number.isFinite(orphanRaw) && orphanRaw >= 1 ? orphanRaw : 6,
|
|
82
|
+
force: Boolean(flags.force),
|
|
83
|
+
});
|
|
84
|
+
if (blocks.length > 0) return { blocks, warns: [] };
|
|
85
|
+
|
|
86
|
+
const WORKING = plan.labelMap.working, READY = plan.labelMap.ready;
|
|
87
|
+
|
|
88
|
+
// Mutations, only past a clean read-verify. Label swap first (frees the
|
|
89
|
+
// claim), then unassign, then the operator-visible note.
|
|
90
|
+
ghRun(['issue', 'edit', String(issueNumber), '-R', repo, '--remove-label', WORKING, '--add-label', READY]);
|
|
91
|
+
for (const login of plan.removeAssignees) {
|
|
92
|
+
ghRun(['issue', 'edit', String(issueNumber), '-R', repo, '--remove-assignee', login]);
|
|
93
|
+
}
|
|
94
|
+
const age = plan.ledgerAgeHours === null ? 'no ledger comment was ever written' : `ledger silent ${plan.ledgerAgeHours}h`;
|
|
95
|
+
const note = `**Claim released.** \`${WORKING}\` → \`${READY}\`, unassigned — ${age}. The prior session did not hand back; its branch and ledger stand. Resume it (dev-implement, the operator's handover) or let a fresh session claim it.`;
|
|
96
|
+
ghRun(['issue', 'comment', String(issueNumber), '-R', repo, '--body', note]);
|
|
97
|
+
return { blocks: [], warns: [`released #${issueNumber}: ${WORKING} → ${READY}, unassigned (${age})`] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
101
|
+
if (invokedDirectly) {
|
|
102
|
+
const flags = parseFlags(process.argv.slice(2), ['json', 'force']);
|
|
103
|
+
let outcome;
|
|
104
|
+
if (!flags.issue) {
|
|
105
|
+
outcome = { blocks: ['usage: reclaim.mjs --issue <n> [--repo o/r] [--orphan-hours 6] [--force] [--json]'], warns: [] };
|
|
106
|
+
} else {
|
|
107
|
+
try {
|
|
108
|
+
outcome = gatherAndReclaim(flags);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
outcome = { blocks: [error instanceof GhUnavailable ? `cannot verify: ${error.message}` : `reclaim error: ${error.message}`], warns: [] };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const { exitCode, text } = renderResult('reclaim', outcome, { json: Boolean(flags.json) });
|
|
114
|
+
console.log(text);
|
|
115
|
+
process.exit(exitCode);
|
|
116
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Pure recovery decisions and bounded source reconciliation. Runtime owners alone
|
|
3
|
+
// perform ownership transitions, source fetches and execution.
|
|
4
|
+
import { validateLesson } from './learning.mjs';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { evaluateApprovals, readApprovalSources, readPages, scopeDigest, gatherConsolidatedApproval } from './lib/approval.mjs';
|
|
8
|
+
const sha = /^[a-f0-9]{40}$/;
|
|
9
|
+
const digest = /^[a-f0-9]{64}$/;
|
|
10
|
+
const uuid = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
11
|
+
const taskId = /^[1-9]\d*-T[1-9]\d*$/;
|
|
12
|
+
export const canonicalRecovery = value => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a],[b]) => a.localeCompare(b))) : item);
|
|
13
|
+
const same = (a,b) => canonicalRecovery(a) === canonicalRecovery(b);
|
|
14
|
+
const hash = body => createHash('sha256').update(body).digest('hex');
|
|
15
|
+
const unique = values => Array.isArray(values) && new Set(values).size === values.length;
|
|
16
|
+
export function compareTaskIds(expected, actual) {
|
|
17
|
+
return { missing: [...new Set(expected)].filter(id => !actual.includes(id)), unknown: [...new Set(actual)].filter(id => !expected.includes(id)) };
|
|
18
|
+
}
|
|
19
|
+
export function isPreparationSubset(ids, allowed) {
|
|
20
|
+
return unique(ids) && ids.length > 0 && ids.every(id => taskId.test(id) && allowed.preparation.includes(id) && !allowed.live.includes(id));
|
|
21
|
+
}
|
|
22
|
+
export function localApprovalBinding(wire) {
|
|
23
|
+
const source = wire?.source, n = Number(source?.commentId);
|
|
24
|
+
if (!wire?.approvalId || source?.kind !== 'github-comment' || !source.repositoryId || !source.issueNodeId || !Number.isSafeInteger(n) || n <= 0 || String(n) !== source.commentId || !digest.test(source.bodySha256)) throw Error('invalid canonical approval source');
|
|
25
|
+
return { approvalId:wire.approvalId, commentId:n, bodySha256:source.bodySha256 };
|
|
26
|
+
}
|
|
27
|
+
export function validateRecoveryPacket(packet) {
|
|
28
|
+
const keys=['schemaVersion','repo','issue','briefRef','planRef','approvalIds','approvalBindings','recordBinding','taskIds','completed','lastVerifiedCommit','openFindings','rulings','commentCursor','pendingRunIds','learning'];
|
|
29
|
+
if (!packet || Object.keys(packet).length!==keys.length || keys.some(key=>!Object.hasOwn(packet,key))) throw Error('unknown or missing recovery packet field');
|
|
30
|
+
if (Buffer.byteLength(JSON.stringify(packet))>128*1024) throw Error('recovery packet exceeds bound');
|
|
31
|
+
if (packet?.schemaVersion !== 3 || !/^[^/\s]+\/[^/\s]+$/.test(packet.repo) || !Number.isSafeInteger(packet.issue) || packet.issue <= 0) throw Error('unsupported recovery packet; regenerate from verified source');
|
|
32
|
+
if (!unique(packet.taskIds) || !packet.taskIds.length || packet.taskIds.some(id => !taskId.test(id) || !id.startsWith(packet.issue + '-T'))) throw Error('invalid recovery task identities');
|
|
33
|
+
if (!Array.isArray(packet.approvalBindings) || !packet.approvalBindings.length || !same(packet.approvalIds, packet.approvalBindings.map(row => row.approvalId))) throw Error('canonical authority unavailable');
|
|
34
|
+
packet.approvalBindings.forEach(localApprovalBinding);
|
|
35
|
+
if(new Set(packet.approvalBindings.map(canonicalRecovery)).size!==packet.approvalBindings.length)throw Error('duplicate recovery authority');
|
|
36
|
+
if (packet.recordBinding !== null) localApprovalBinding(packet.recordBinding);
|
|
37
|
+
for (const [key, kind] of [['briefRef','brief'],['planRef','plan']]) {
|
|
38
|
+
const ref=packet[key];
|
|
39
|
+
if (ref?.repo !== packet.repo || ref.issue !== packet.issue || ref.kind !== kind || !ref.artifactId || !Number.isSafeInteger(ref.rev) || !digest.test(ref.digest)) throw Error('invalid approved artifact');
|
|
40
|
+
}
|
|
41
|
+
if (!sha.test(packet.lastVerifiedCommit) || !Array.isArray(packet.completed) || !unique(packet.completed.map(row=>row.taskId)) || packet.completed.some(row=>!packet.taskIds.includes(row.taskId)||!sha.test(row.headSha)||typeof row.evidenceUrl!=='string'||!row.evidenceUrl)) throw Error('invalid completed recovery work');
|
|
42
|
+
if (!packet.commentCursor?.id || !Number.isFinite(Date.parse(packet.commentCursor.updatedAt)) || !Array.isArray(packet.pendingRunIds) || !Array.isArray(packet.openFindings) || !Array.isArray(packet.rulings) || !Array.isArray(packet.learning) || packet.learning.length>32 || Buffer.byteLength(JSON.stringify(packet.learning))>16384) throw Error('invalid bounded recovery context');
|
|
43
|
+
packet.learning.forEach(validateLesson);
|
|
44
|
+
return packet;
|
|
45
|
+
}
|
|
46
|
+
export function reconcileRecovery(packet, current) {
|
|
47
|
+
const blocks=[], sourceRefs=[];
|
|
48
|
+
try { validateRecoveryPacket(packet); } catch(error) { return {outstandingTaskIds:[],blocks:[error.message],sourceRefs}; }
|
|
49
|
+
const outstandingTaskIds=packet.taskIds.filter(id=>!packet.completed.some(row=>row.taskId===id));
|
|
50
|
+
if (current.historyComplete !== true) blocks.push('complete current authority history unavailable');
|
|
51
|
+
if (!current.approval || current.approval.blocks?.length || !same(current.approval.approvalBindings,packet.approvalBindings.map(localApprovalBinding))) blocks.push('canonical approval changed or unavailable');
|
|
52
|
+
if (!same(current.briefRef,packet.briefRef)||!same(current.planRef,packet.planRef)||!same(current.taskIds,packet.taskIds)) blocks.push('approved scope changed; preserve prior completed work and reconcile');
|
|
53
|
+
const comments=Array.isArray(current.comments)?current.comments:[];
|
|
54
|
+
const cursor=comments.find(row=>String(row.id)===String(packet.commentCursor.id));
|
|
55
|
+
if (!cursor || !Number.isFinite(Date.parse(cursor.updated_at))) blocks.push('comment cursor unavailable');
|
|
56
|
+
for (const row of comments) {
|
|
57
|
+
if (Date.parse(row.updated_at)>Date.parse(packet.commentCursor.updatedAt) || [packet.planRef.artifactId,packet.briefRef.artifactId].includes(row.node_id)) sourceRefs.push({id:String(row.id),updatedAt:row.updated_at,bodySha256:hash(row.body)});
|
|
58
|
+
if (Date.parse(row.updated_at)>Date.parse(packet.commentCursor.updatedAt) && (/^<!-- vsk:v1 type=(correction|ruling|handback)\b/m.test(row.body) || (current.operators??[]).includes(row.user?.login) && !/^<!-- vsk:v1 type=(plan|approval)\b/m.test(row.body)) && !(current.reconciledComments??[]).some(pin=>String(pin.id)===String(row.id)&&pin.bodySha256===hash(row.body))) blocks.push('new or edited instruction requires reconciliation: '+row.id);
|
|
59
|
+
}
|
|
60
|
+
for (const row of packet.completed) if (!(current.verifiedCompleted??[]).some(verified=>same(row,verified))) blocks.push('completed commit/evidence unavailable: '+row.taskId);
|
|
61
|
+
if (!current.verifiedCommit || current.verifiedCommit!==packet.lastVerifiedCommit) blocks.push('last verified commit missing or outside current ancestry');
|
|
62
|
+
if (current.conflictingFindings?.length || current.conflictingRulings?.length) blocks.push('contradictory current findings or rulings');
|
|
63
|
+
return {outstandingTaskIds,blocks,sourceRefs};
|
|
64
|
+
}
|
|
65
|
+
/** @param {{outstandingTaskIds?: string[], pendingDelivery?: unknown[], blocks?: string[]}} input */
|
|
66
|
+
export function chooseResumeAction({outstandingTaskIds=[],pendingDelivery=[],blocks=[]}) {
|
|
67
|
+
if (blocks.length) return {action:'refuse',taskIds:[],reason:blocks.join('; ')};
|
|
68
|
+
if (outstandingTaskIds.length) return {action:'resume-task',taskIds:[...outstandingTaskIds],reason:'verified-outstanding-work'};
|
|
69
|
+
if (pendingDelivery.length) return {action:'retry-delivery',taskIds:[],reason:'pending-delivery'};
|
|
70
|
+
return {action:'handback',taskIds:[],reason:'verified-work-complete'};
|
|
71
|
+
}
|
|
72
|
+
export function evaluateSharedRecovery({task,stopProof,checkpoint,targetMachine,policy,pendingEffects,recovery}) {
|
|
73
|
+
const refuse=reason=>({action:'refuse',reason}), wait=reason=>({action:'wait',reason});
|
|
74
|
+
if (!task || !recovery || recovery.schemaVersion!==2) return wait('verified remote recovery unavailable');
|
|
75
|
+
if (task.state==='completed') return refuse('completed work cannot replay');
|
|
76
|
+
if (!same(task.approvalBindings,recovery.approvalBindings)||task.taskKey!==recovery.taskKey||task.runId!==recovery.runId||task.generation!==recovery.generation||task.scopeDigest!==recovery.scopeDigest||task.approvalDigest!==recovery.approvalDigest) return refuse('recovery task identity differs');
|
|
77
|
+
try { recovery.approvalBindings.forEach(localApprovalBinding); } catch { return refuse('canonical authority unavailable'); }
|
|
78
|
+
if (!policy?.current || !same(policy.approvalBindings,recovery.approvalBindings)) return wait('fresh canonical authority and policy required');
|
|
79
|
+
if (!targetMachine?.registered || !same(targetMachine.execution,recovery.execution)) return refuse('original qualified execution identity unavailable');
|
|
80
|
+
if (!stopProof || !['process-exit','verified-reboot','operator-confirmed'].includes(stopProof.kind)) return wait('verified stop proof required');
|
|
81
|
+
if (stopProof.machineId!==task.machineId||stopProof.installationId!==task.installationId||stopProof.sessionId!==task.sessionId||stopProof.generation!==task.generation||!stopProof.runIds?.includes(task.runId)) return refuse('stop proof owner or generation differs');
|
|
82
|
+
if (!policy.ownerMachine || stopProof.hostBindingDigest!==policy.ownerMachine.hostBindingDigest || stopProof.sessionId===policy.ownerMachine.sessionId && stopProof.bootIdDigest!==policy.ownerMachine.bootIdDigest) return refuse('stop proof host or boot differs');
|
|
83
|
+
if (!checkpoint || !same(checkpoint,task.checkpoint)||!same(checkpoint,recovery.checkpoint)||checkpoint.runId!==task.runId||checkpoint.scopeDigest!==task.scopeDigest||checkpoint.repo!==task.repo) return wait('exact available checkpoint required');
|
|
84
|
+
if (!['qualified-managed-only','reconciled'].includes(recovery.remoteEffectCoverage?.kind)) return wait('remote effect coverage unresolved');
|
|
85
|
+
const effects=[...(recovery.effects??[]),...(pendingEffects??[])];
|
|
86
|
+
if (effects.some(row=>row.kind!=='telemetry-push'&&!['acknowledged','cancelled-before-send'].includes(row.state))) return wait('blocking remote effects unresolved');
|
|
87
|
+
if (recovery.joins?.some(row=>row.state!=='accepted')) return wait('parent child integration unresolved');
|
|
88
|
+
return {action:targetMachine.machineId===task.machineId&&targetMachine.installationId===task.installationId&&targetMachine.sessionId===task.sessionId?'resume-original':'transfer',reason:'verified-recovery-predicates'};
|
|
89
|
+
}
|
|
90
|
+
const stoppedGroupBinding = task => ({taskKey:task.taskKey,runId:task.runId,generation:task.generation,ownerToken:task.ownerToken,machineId:task.machineId,installationId:task.installationId,sessionId:task.sessionId});
|
|
91
|
+
const stoppedGroupCandidate = task => ({host:task.host,repo:task.repo,issue:task.issue,repositoryNodeId:task.repositoryNodeId,issueNodeId:task.issueNodeId,scopeDigest:task.scopeDigest,approvalDigest:task.approvalDigest,approvalBindings:task.approvalBindings,runId:task.runId,stage:task.stage,paths:task.paths,resources:task.resources,independent:task.independent,parentTaskKey:task.parentTaskKey,parentBinding:task.parentBinding??null,approvedTaskIds:task.approvedTaskIds});
|
|
92
|
+
export function evaluateStoppedGroupRecovery(input) {
|
|
93
|
+
const refuse=reason=>({action:'refuse',reason,request:null}),wait=reason=>({action:'wait',reason,request:null});
|
|
94
|
+
if(!input||typeof input!=='object'||Array.isArray(input))return refuse('stopped group recovery input unavailable');
|
|
95
|
+
const {operationId,expectedHead,parentTaskKey,groupPlan,groupsDigest,approvedGroups,classifications,members}=input;
|
|
96
|
+
if(!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(operationId)||!sha.test(expectedHead)||!digest.test(parentTaskKey)||!digest.test(groupsDigest)||groupPlan?.kind!=='plan'||!digest.test(groupPlan.digest))return refuse('stopped group recovery identity unavailable');
|
|
97
|
+
if(!Array.isArray(approvedGroups)||!approvedGroups.length||!Array.isArray(classifications)||classifications.length!==approvedGroups.length||!Array.isArray(members))return refuse('complete approved stopped group required');
|
|
98
|
+
if(hash(canonicalRecovery(approvedGroups))!==groupsDigest)return refuse('stopped group digest differs from approved groups');
|
|
99
|
+
const expectedChildren=approvedGroups.map(group=>({issue:Number(/^#([1-9]\d*)$/.exec(group?.members?.length===1?group.members[0]:'')?.[1]),files:group?.files}));
|
|
100
|
+
if(expectedChildren.some(row=>!Number.isSafeInteger(row.issue)||row.issue<=0||!Array.isArray(row.files)||!row.files.length||!unique(row.files)))return refuse('approved stopped group declaration unavailable');
|
|
101
|
+
const stateRef=ref=>ref&&Object.keys(ref).sort().join(',')==='blobSha256,commitSha,kind,operationId'&&ref.kind==='state-receipt'&&uuid.test(ref.operationId)&&sha.test(ref.commitSha)&&digest.test(ref.blobSha256);
|
|
102
|
+
const retained=[];
|
|
103
|
+
for(let index=0;index<classifications.length;index++){
|
|
104
|
+
const row=classifications[index],group=approvedGroups[index],expected=expectedChildren[index],kind=row?.classification?.kind;
|
|
105
|
+
if(!row||Object.keys(row).sort().join(',')!=='classification,groupId,issue,issueNodeId,taskKey'||row.groupId!==group?.id||row.issue!==expected.issue||typeof row.issueNodeId!=='string'||!/^[A-Za-z0-9_-]+$/.test(row.issueNodeId)||!digest.test(row.taskKey))return refuse('stopped group classification identity differs');
|
|
106
|
+
if(kind==='retained'){
|
|
107
|
+
if(Object.keys(row.classification).sort().join(',')!=='expected,kind'||row.classification.expected?.taskKey!==row.taskKey) return refuse('retained stopped group classification differs');
|
|
108
|
+
retained.push(row);
|
|
109
|
+
}else if(kind==='completed'){
|
|
110
|
+
if(Object.keys(row.classification).sort().join(',')!=='acceptedScope,joinEvidence,kind'||!stateRef(row.classification.acceptedScope)||!stateRef(row.classification.joinEvidence))return refuse('completed stopped group evidence differs');
|
|
111
|
+
}else if(kind==='no-shared-task'){
|
|
112
|
+
if(Object.keys(row.classification).sort().join(',')!=='kind')return refuse('no-task stopped group classification differs');
|
|
113
|
+
}else return refuse('unknown stopped group classification');
|
|
114
|
+
}
|
|
115
|
+
if(!retained.length||!unique(classifications.map(row=>row.groupId))||!unique(classifications.map(row=>row.issue))||!unique(classifications.map(row=>row.issueNodeId))||!unique(classifications.map(row=>row.taskKey))||members.length!==retained.length+1)return refuse('stopped group classifications are incomplete or repeated');
|
|
116
|
+
if(!unique(members.map(row=>row?.task?.taskKey))||!unique(members.map(row=>row?.task?.runId))||!unique(members.map(row=>row?.task?.issue)))return refuse('stopped group members must be unique');
|
|
117
|
+
const parent=members.find(row=>row?.task?.taskKey===parentTaskKey);
|
|
118
|
+
if(!parent||parent.task.parentTaskKey!==null||parent.task.parentBinding!=null)return refuse('one top-level stopped group parent required');
|
|
119
|
+
const childIssues=members.filter(row=>row!==parent).map(row=>row.task.issue).sort((a,b)=>a-b),retainedIssues=retained.map(row=>row.issue).sort((a,b)=>a-b);
|
|
120
|
+
if(!same(childIssues,retainedIssues))return refuse('stopped group retained-member projection differs');
|
|
121
|
+
for(const row of members){
|
|
122
|
+
const task=row?.task,recovery=task?.recovery;
|
|
123
|
+
if(!row||Object.keys(row).sort().join(',')!=='candidate,expected,stateCommit,task')return refuse('unknown or missing stopped group member field');
|
|
124
|
+
if(!task||row.stateCommit!==expectedHead)return refuse('stopped group members do not share the expected head');
|
|
125
|
+
if(!['stopped','blocked'].includes(task.state)||!task.stopProof)return wait('every stopped group member needs verified stop evidence');
|
|
126
|
+
if(task.schemaVersion!==1&&task.schemaVersion!==2)return refuse('unsupported stopped group task schema');
|
|
127
|
+
if(!recovery||recovery.schemaVersion!==2)return refuse('unsupported stopped group recovery schema');
|
|
128
|
+
if(!same(row.expected,stoppedGroupBinding(task))||!same(row.candidate,stoppedGroupCandidate(task)))return refuse('stopped group owner or candidate changed');
|
|
129
|
+
if(task.taskKey!==recovery.taskKey||task.runId!==recovery.runId||task.generation!==recovery.generation||task.scopeDigest!==recovery.scopeDigest||task.approvalDigest!==recovery.approvalDigest||!same(task.approvalBindings,recovery.approvalBindings))return refuse('stopped group recovery identity differs');
|
|
130
|
+
const checkpoint=task.checkpoint,stop=task.stopProof;
|
|
131
|
+
if(!checkpoint||!same(checkpoint,recovery.checkpoint)||checkpoint.runId!==task.runId||checkpoint.repo!==task.repo||checkpoint.scopeDigest!==task.scopeDigest)return wait('exact stopped group checkpoint unavailable');
|
|
132
|
+
if(stop.machineId!==task.machineId||stop.installationId!==task.installationId||stop.sessionId!==task.sessionId||stop.generation!==task.generation||!stop.runIds?.includes(task.runId))return refuse('stopped group stop owner or generation differs');
|
|
133
|
+
if(task.parentTaskKey!==null){
|
|
134
|
+
if(task.parentTaskKey!==parentTaskKey||!same(task.parentBinding,stoppedGroupBinding(parent.task)))return refuse('stopped group child original parent differs');
|
|
135
|
+
const approved=expectedChildren.find(group=>group.issue===task.issue),classification=retained.find(group=>group.issue===task.issue);
|
|
136
|
+
if(!approved||!classification||!same(task.paths,approved.files)||!same(row.expected,classification.classification.expected)||task.issueNodeId!==classification.issueNodeId||task.taskKey!==classification.taskKey)return refuse('stopped group child scope differs from approved group');
|
|
137
|
+
}
|
|
138
|
+
if(recovery.remoteEffectCoverage?.kind==='unmanaged-possible'||[...(recovery.effects??[])].some(effect=>effect.kind!=='telemetry-push'&&!['acknowledged','cancelled-before-send'].includes(effect.state)))return wait('stopped group blocking effects unresolved');
|
|
139
|
+
if((recovery.joins??[]).some(join=>!['accepted','prepared'].includes(join.state)))return refuse('stopped group join history is not recoverable');
|
|
140
|
+
}
|
|
141
|
+
const request={schemaVersion:1,kind:'recover-stopped-group',operationId,expectedHead,parentTaskKey,groupPlan,groupsDigest,members:members.map(({expected,candidate})=>({expected:structuredClone(expected),candidate:structuredClone(candidate)})).sort((a,b)=>a.expected.taskKey.localeCompare(b.expected.taskKey))};
|
|
142
|
+
return {action:'recover-stopped-group',reason:'verified-complete-stopped-group',request};
|
|
143
|
+
}
|
|
144
|
+
// The provider reader is transport, not a cached approval verdict. Re-read old
|
|
145
|
+
// authoritative comments too: cursor filtering alone misses edited old records.
|
|
146
|
+
export async function readRecoverySources(packet,{readJson,operators,checkout,readCompletionEvidence,consolidatedRequest}) {
|
|
147
|
+
validateRecoveryPacket(packet);
|
|
148
|
+
const prefix='repos/'+packet.repo+'/issues/'+packet.issue;
|
|
149
|
+
const brief=await readJson(['api',prefix]);
|
|
150
|
+
const comments=await readPages(readJson,['api',prefix+'/comments']);
|
|
151
|
+
const sourceComments=await readApprovalSources(comments,readJson);
|
|
152
|
+
const approval=consolidatedRequest ? await gatherConsolidatedApproval({...consolidatedRequest,operators,readJson}) : evaluateApprovals({repo:packet.repo,issue:packet.issue,brief,comments,sourceComments,operators,requiredScope:'brief+plan'});
|
|
153
|
+
const plan=comments.find(row=>row.node_id===packet.planRef.artifactId);
|
|
154
|
+
const ref=(value,kind)=>value?{repo:packet.repo,issue:packet.issue,kind,artifactId:value.node_id,rev:Number(/\brev=(\d+)/.exec(value.body)?.[1]),digest:scopeDigest(value.body,kind)}:null;
|
|
155
|
+
const git=args=>{const result=spawnSync('git',args,{cwd:checkout,encoding:'utf8',timeout:10000,maxBuffer:1024*1024});return result.status===0&&!result.error;};
|
|
156
|
+
const verifiedCompleted=[];
|
|
157
|
+
for (const row of packet.completed) if (git(['cat-file','-e',row.headSha+'^{commit}'])&&git(['merge-base','--is-ancestor',row.headSha,packet.lastVerifiedCommit])&&await readCompletionEvidence(row,packet)) verifiedCompleted.push(row);
|
|
158
|
+
return {historyComplete:true,approval,comments,operators,briefRef:ref(brief,'brief'),planRef:ref(plan,'plan'),taskIds:[...(plan?.body??'').matchAll(/<!--\s*task-id:([1-9]\d*-T[1-9]\d*)\s*-->/g)].map(match=>match[1]),verifiedCompleted,verifiedCommit:git(['merge-base','--is-ancestor',packet.lastVerifiedCommit,'HEAD'])?packet.lastVerifiedCommit:null};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// A task's approved Interfaces/Consumes sentence defines its preparation
|
|
162
|
+
// prerequisites. Ambiguous prose refuses instead of falling back to the issue's
|
|
163
|
+
// full blocker list or caller-provided prerequisite counts.
|
|
164
|
+
export function preparationTaskContracts(planBody, ids, selectedFiles) {
|
|
165
|
+
const rows=[...planBody.matchAll(/^-\s*\[[ x]\].*<!--\s*task-id:([1-9]\d*-T[1-9]\d*)\s*-->.*$/gim)];
|
|
166
|
+
if(!unique(ids)||!ids.length)throw Error('exact preparation task IDs required');
|
|
167
|
+
return ids.map(id=>{
|
|
168
|
+
const index=rows.findIndex(row=>row[1]===id);
|
|
169
|
+
if(index<0||rows.filter(row=>row[1]===id).length!==1)throw Error('preparation task missing or duplicate');
|
|
170
|
+
const section=planBody.slice(rows[index].index,rows[index+1]?.index??planBody.length);
|
|
171
|
+
const interfaces=/^\s*- Interfaces\s*[—–-]\s*(.*)$/m.exec(section)?.[1];
|
|
172
|
+
if(!interfaces||!new RegExp('Preparation(?: task)?\\s*'+id.replace('-','\\-')+'\\b','i').test(interfaces))throw Error('task is not explicitly preparation');
|
|
173
|
+
const consumes=/\bConsumes\s+(.+?)(?:\.\s+Produces|;\s*produces|$)/i.exec(interfaces)?.[1]??'';
|
|
174
|
+
let prerequisites=[...consumes.matchAll(/#([1-9]\d*)\b/g)].map(row=>Number(row[1]));
|
|
175
|
+
if(!prerequisites.length){
|
|
176
|
+
const sentence=planBody.split(/\n\n/).find(part=>part.includes(id)&&/approved preparation with satisfied/.test(part));
|
|
177
|
+
prerequisites=[...(sentence??'').matchAll(/satisfied\s+#([1-9]\d*)\s+code-contract/g)].map(row=>Number(row[1]));
|
|
178
|
+
}
|
|
179
|
+
if(!prerequisites.length)throw Error('approved preparation prerequisite mapping unavailable');
|
|
180
|
+
const files=selectedFiles(planBody,[id]);
|
|
181
|
+
if(!files.length)throw Error('approved preparation files unavailable');
|
|
182
|
+
return {id,files,prerequisiteIssues:[...new Set(prerequisites)]};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
export function evaluatePreparation({parentApproval,childApproval,taskIds,taskPrerequisites,current}) {
|
|
186
|
+
const blocks=[];
|
|
187
|
+
if(!parentApproval?.ok||parentApproval.blocks?.length||!childApproval?.preparation||childApproval.blocks?.length)blocks.push('fresh evaluated preparation scope required');
|
|
188
|
+
const expected=childApproval?.preparation, binding=expected?.approvalBinding;
|
|
189
|
+
if(!binding||!same(childApproval.approvalBindings,[binding])||!same(childApproval.approvalIds,[binding.approvalId])||!same(parentApproval.approvalBindings,[binding]))blocks.push('canonical preparation authority differs');
|
|
190
|
+
if(!unique(taskIds)||!taskIds.length||!same(taskIds,expected?.taskIds)||!same(taskPrerequisites?.approvalBinding,binding)||!same(taskPrerequisites?.parent,expected?.parent)||!same(taskPrerequisites?.plan,expected?.plan)||!same(taskPrerequisites?.tasks,expected?.tasks))blocks.push('exact preparation task contract differs');
|
|
191
|
+
if(!current?.ownership||!current?.policyCurrent)blocks.push('preparation ownership or current policy unavailable');
|
|
192
|
+
if(!Array.isArray(expected?.pendingEffects)||expected.pendingEffects.length)blocks.push('accepted preparation prerequisites unresolved');
|
|
193
|
+
if(current?.operation&&!['edit','check','review','integrate'].includes(current.operation))blocks.push('preparation does not grant live operations');
|
|
194
|
+
return {blocks,warns:[],mode:'preparation',approvalBindings:childApproval?.approvalBindings??[],recordBinding:childApproval?.recordBinding??null,taskIds:blocks.length?[]:taskIds};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
export function readBoundedRecoveryInput(stream=process.stdin,maxBytes=65536,timeoutMs=350) {
|
|
199
|
+
return new Promise((resolveInput,reject)=>{
|
|
200
|
+
let bytes=0,finished=false;const chunks=[];
|
|
201
|
+
const done=(error)=>{if(finished)return;finished=true;clearTimeout(timer);stream.removeListener('data',data);stream.removeListener('end',end);stream.removeListener('error',failed);stream.pause();if(error)reject(error);else{try{resolveInput(new TextDecoder('utf-8',{fatal:true}).decode(Buffer.concat(chunks)));}catch(error){reject(error);}}};
|
|
202
|
+
const data=chunk=>{const value=Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);bytes+=value.length;if(bytes>maxBytes)done(Error('recovery input exceeds bound'));else chunks.push(value);};
|
|
203
|
+
const end=()=>done(),failed=error=>done(error),timer=setTimeout(()=>done(Error('recovery input deadline exceeded')),timeoutMs);
|
|
204
|
+
stream.on('data',data).once('end',end).once('error',failed);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The packaged dispatcher owns processes/private records. This helper never
|
|
209
|
+
// imports TypeScript source or accepts an arbitrary check command.
|
|
210
|
+
export function taskCheckpointArguments({runId,taskId: id,write=false}) {
|
|
211
|
+
if(!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(runId)||!taskId.test(id))throw Error('exact task checkpoint identities required');
|
|
212
|
+
return ['dispatch','--checkpoint-task',id,'--run-id',runId,...(write?['--once']:['--dry-run']),'--json'];
|
|
213
|
+
}
|