mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
* fix(capy): the install trailer says behind=<n> and its fix, so a stale install shows in every relayed trailer (field list 2 item 13, #2378) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy): a lease records its holder's start identity, so a pid reused after a wake is reclaimed; a skipped session-start upgrade ends fail behind, never ok (GBRA-105, GBRA-106) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): stale needs proof (unreadable or malformed identities keep the lease), ps identity is timezone-independent, and reclaim never deletes a concurrent renewal Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(autoplan): enforced round cap and spend budget, deferred close, public export without machine paths, opt-in durable checkpoints (field list 2 W5-W7) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): one directory lock for publish, prune and reclaim; reclaim rereads the record judged stale, so a renewal is never deleted or hidden Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): the directory lock names its holder; broken only when the holder is dead or its pid reused, unlock removes only its own lock, touch exits 3 under a live holder Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): breakers serialize on .lock.break and reread the lock before removing it; gstack-skill-start reports a lease the held lock kept out Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(lease): name the double-crash limit of breaking a dead lock Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy): --for review-voice install profile, no-Drive bootstrap command, reviewer prompt trailer (field list 2 W11) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit89bf556c11) * test: pin review-voice row, helpers row and the no-Drive fallback in existing install tests Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit7f6c70459a) * test: the DRIVER template names the reviewer trailer relay (pregate lane) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit2545d9a6d2) * test(context-budget): capy skill eager ceiling for the review-voice subagent paragraph (W11; ratchet protocol, capture value) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(eval-plan): prereg diagnostics dry run (W8) and .gstack/eval-models.json lint (W9), WIP Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitb509096218) * feat(eval-plan): W8/W9 tests, docs/eval-models.md, troubleshooting anchors, /eval-plan steps Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit9335e45de2) * test(eval-plan): eval-plan/SKILL.md names only defined codes and accepted flags Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commita7a974d268) * test(review-voice): the launch line points at the verdict's bun-only directory (#3233's launch contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(third-party-actions): the Aside drive-offer detector matches the product name, not the English idiom (paid run 38988927989 tpa-absent-linux false positive) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): export refuses artifact paths outside the run before writing; a deferred close keeps earlier undecided findings; reopening CEO grants Eng its rerun (review of #3234) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(autoplan-budget): bind the reservation expectation to lib/autoplan-budget's default estimate (test-of-test ratchet) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): the docs observer accepts the spawned-prefixed gstack-skill-start --help read Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate capy-review-voice out of the Windows subset (POSIX launcher contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for field list 2 wave A Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.115.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
132 lines
7.4 KiB
TypeScript
Executable File
132 lines
7.4 KiB
TypeScript
Executable File
#!/usr/bin/env bun
|
|
/**
|
|
* gstack-artifact — validate, print and acknowledge the unattended artifact contract.
|
|
*
|
|
* gstack-artifact validate <run.json> [--json] completion check for a run directory
|
|
* gstack-artifact validate --public <dir> [--private <run.json>] [--json]
|
|
* a public run copy: provenance, recorded machine roots
|
|
* gstack-artifact validate <file> --as <name> [--run <id>] [--json]
|
|
* one artifact against its schema
|
|
* gstack-artifact schema <name> | --list JSON Schema this version validates
|
|
* gstack-artifact ack <run.json> --consumer <id> [--json] idempotent consumption report (P7)
|
|
* gstack-artifact urgent <findings.jsonl> the `## URGENT, outside this plan` block
|
|
*
|
|
* Query command: no GSTACK_RESULT line. `--json` means JSON only on stdout and
|
|
* diagnostics on stderr. Logic: lib/headless-artifacts.ts; codes: lib/result-codes.ts.
|
|
*/
|
|
import * as path from 'node:path';
|
|
import {
|
|
ARTIFACT_NAMES, EXIT, ackRun, exitTableHelp, jsonSchema, readJsonl, renderUrgentBlock,
|
|
validateFile, validateRun, type ArtifactName, type ValidationError,
|
|
} from '../lib/headless-artifacts';
|
|
import { resultLine } from '../lib/result-codes';
|
|
import { resolveStateRoot } from '../lib/state-root';
|
|
import { validatePublic } from '../lib/autoplan-public';
|
|
|
|
const HELP = `gstack-artifact — the unattended artifact contract (docs/unattended.md)
|
|
|
|
validate <run.json> [--json] run-directory completion check
|
|
validate --public <dir> [--private <run.json>] a public run copy (gstack-autoplan export --public): provenance digests;
|
|
with --private (on the exporting machine) any recorded root fails
|
|
(ARTIFACT_MACHINE_PATH, file:line); home-path patterns WARN
|
|
validate <file> --as <name> [--run <id>] [--json] one artifact against its schema
|
|
schema <name> | --list JSON Schema for ${ARTIFACT_NAMES.join(', ')}
|
|
ack <run.json> --consumer <id> [--json] set run.json.consumed_by (idempotent) and log the ack
|
|
urgent <findings.jsonl> render the URGENT block (\`None.\` when empty)
|
|
|
|
Without --json: one error line per problem on stderr, each ending in (<CODE>),
|
|
then ARTIFACT_VALID: run=<id> status=<status>[ attested=<n> when voices bound on attestation] or ARTIFACT_INVALID: <n> error(s) on stdout.
|
|
${exitTableHelp()}`;
|
|
|
|
function flag(args: string[], name: string): string | undefined {
|
|
const i = args.indexOf(name);
|
|
return i >= 0 ? args[i + 1] : undefined;
|
|
}
|
|
function usage(message: string): number {
|
|
process.stderr.write(`gstack-artifact: ${message}\n${HELP}\n`);
|
|
return EXIT.usage;
|
|
}
|
|
function printErrors(errors: ValidationError[]): void {
|
|
for (const e of errors) process.stderr.write(resultLine('gstack-artifact validate', e.code, `${e.message} at ${e.path}`) + '\n');
|
|
}
|
|
|
|
function main(argv: string[]): number {
|
|
const [cmd, ...rest] = argv;
|
|
const json = rest.includes('--json');
|
|
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { process.stdout.write(HELP + '\n'); return cmd ? EXIT.ok : EXIT.usage; }
|
|
|
|
if (cmd === 'schema') {
|
|
if (rest.includes('--list')) { process.stdout.write(ARTIFACT_NAMES.join('\n') + '\n'); return EXIT.ok; }
|
|
const name = rest[0];
|
|
if (!name || !ARTIFACT_NAMES.includes(name as ArtifactName)) return usage(`schema needs one of: ${ARTIFACT_NAMES.join(', ')}`);
|
|
process.stdout.write(JSON.stringify(jsonSchema(name as ArtifactName), null, 2) + '\n');
|
|
return EXIT.ok;
|
|
}
|
|
|
|
if (cmd === 'validate' && flag(rest, '--public') !== undefined) {
|
|
const dir = flag(rest, '--public')!;
|
|
const result = validatePublic(dir, flag(rest, '--private'));
|
|
if (json) process.stdout.write(JSON.stringify({ valid: result.valid, run: result.run ?? null, public: dir, errors: result.errors, warnings: result.warnings }) + '\n');
|
|
else {
|
|
printErrors(result.errors);
|
|
for (const w of result.warnings) process.stderr.write(w + '\n');
|
|
process.stdout.write(result.valid ? `ARTIFACT_VALID: public run=${result.run} warnings=${result.warnings.length}${flag(rest, '--private') ? '' : ' (recorded roots unchecked: pass --private <run.json> on the exporting machine)'}\n` : `ARTIFACT_INVALID: ${result.errors.length} error(s)\n`);
|
|
}
|
|
return result.valid ? EXIT.ok : EXIT.fail;
|
|
}
|
|
|
|
if (cmd === 'validate') {
|
|
const file = rest.find(a => !a.startsWith('--') && a !== flag(rest, '--as') && a !== flag(rest, '--run'));
|
|
if (!file) return usage('validate needs a file');
|
|
const as = flag(rest, '--as');
|
|
if (as !== undefined && !ARTIFACT_NAMES.includes(as as ArtifactName)) return usage(`--as needs one of: ${ARTIFACT_NAMES.join(', ')}`);
|
|
if (as) {
|
|
const errors = validateFile(file, as as ArtifactName, flag(rest, '--run'));
|
|
if (json) process.stdout.write(JSON.stringify({ valid: errors.length === 0, file, as, errors }) + '\n');
|
|
else { printErrors(errors); process.stdout.write(errors.length ? `ARTIFACT_INVALID: ${errors.length} error(s)\n` : `ARTIFACT_VALID: ${as} ${path.basename(file)}\n`); }
|
|
return errors.length ? EXIT.fail : EXIT.ok;
|
|
}
|
|
const result = validateRun(file);
|
|
if (json) {
|
|
process.stdout.write(JSON.stringify({ valid: result.valid, run: result.manifest?.run ?? null, status: result.manifest?.status ?? null, run_dir: result.runDir, attested: result.attested ?? 0, errors: result.errors }) + '\n');
|
|
} else {
|
|
printErrors(result.errors);
|
|
process.stdout.write(result.valid
|
|
? `ARTIFACT_VALID: run=${result.manifest!.run} status=${result.manifest!.status}${result.attested ? ` attested=${result.attested}` : ''}\n`
|
|
: `ARTIFACT_INVALID: ${result.errors.length} error(s)\n`);
|
|
}
|
|
return result.valid ? EXIT.ok : EXIT.fail;
|
|
}
|
|
|
|
if (cmd === 'ack') {
|
|
const file = rest.find(a => !a.startsWith('--') && a !== flag(rest, '--consumer'));
|
|
const consumer = flag(rest, '--consumer');
|
|
if (!file || !consumer) return usage('ack needs <run.json> --consumer <id>');
|
|
const validation = validateRun(file);
|
|
if (!validation.valid) {
|
|
printErrors(validation.errors);
|
|
process.stderr.write('gstack-artifact ack: refusing to acknowledge an invalid run; nothing written\n');
|
|
return EXIT.refused;
|
|
}
|
|
const analyticsPath = path.join(resolveStateRoot(), 'analytics', 'autoplan-timing.jsonl');
|
|
const result = ackRun(file, consumer, { analyticsPath, ephemeral: process.env.GSTACK_EPHEMERAL === '1' });
|
|
if (result.analytics === 'skipped_ephemeral') process.stderr.write('ack: analytics skipped (state root is ephemeral; set GSTACK_STATE_ROOT)\n');
|
|
if (json) process.stdout.write(JSON.stringify({ run: validation.manifest!.run, consumer, ...result }) + '\n');
|
|
else process.stdout.write(`ARTIFACT_ACK: run=${validation.manifest!.run} consumer=${consumer} ${result.added ? 'recorded' : 'already recorded'} analytics=${result.analytics}\n`);
|
|
return EXIT.ok;
|
|
}
|
|
|
|
if (cmd === 'urgent') {
|
|
const file = rest[0];
|
|
if (!file) return usage('urgent needs <findings.jsonl>');
|
|
const read = readJsonl(file);
|
|
if (read.errors.length) { printErrors(read.errors); return EXIT.fail; }
|
|
process.stdout.write(renderUrgentBlock(read.rows));
|
|
return EXIT.ok;
|
|
}
|
|
|
|
return usage(`unknown command ${cmd}`);
|
|
}
|
|
|
|
process.exitCode = main(process.argv.slice(2));
|