@elalfner/relevo 0.0.0-stage → 0.3.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/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/NOTICE +51 -0
- package/README.es.md +90 -0
- package/README.md +89 -2
- package/agents/dev.md +101 -0
- package/agents/qa.md +112 -0
- package/package.json +42 -4
- package/scripts/add-corrections.mjs +21 -0
- package/scripts/app-id.mjs +19 -0
- package/scripts/archive-state.mjs +31 -0
- package/scripts/branch-state.mjs +31 -0
- package/scripts/branch-work.mjs +25 -0
- package/scripts/can-edit-issue.mjs +45 -0
- package/scripts/capture.mjs +22 -0
- package/scripts/check-archive.mjs +19 -0
- package/scripts/check-issue.mjs +41 -0
- package/scripts/checklist.mjs +29 -0
- package/scripts/cli.mjs +28 -0
- package/scripts/commit-change.mjs +50 -0
- package/scripts/conflict-scope.mjs +19 -0
- package/scripts/design-fetch.mjs +103 -0
- package/scripts/device.mjs +12 -0
- package/scripts/digest.mjs +50 -0
- package/scripts/doctor.mjs +47 -0
- package/scripts/fix-guard.mjs +38 -0
- package/scripts/fix-stash.mjs +57 -0
- package/scripts/fix-state.mjs +28 -0
- package/scripts/fix-tasks.mjs +22 -0
- package/scripts/go-local.mjs +49 -0
- package/scripts/go-plan.mjs +82 -0
- package/scripts/init.mjs +109 -0
- package/scripts/intent-to-add.mjs +20 -0
- package/scripts/issue-coverage.mjs +30 -0
- package/scripts/issue-hash.mjs +44 -0
- package/scripts/issue-sections.mjs +42 -0
- package/scripts/label-change.mjs +35 -0
- package/scripts/lib/appid.mjs +12 -0
- package/scripts/lib/archive-check.mjs +29 -0
- package/scripts/lib/archive-state.mjs +15 -0
- package/scripts/lib/branch-work.mjs +13 -0
- package/scripts/lib/branch.mjs +43 -0
- package/scripts/lib/can-edit-issue.mjs +34 -0
- package/scripts/lib/checklist.mjs +68 -0
- package/scripts/lib/cli.mjs +82 -0
- package/scripts/lib/commit-change.mjs +57 -0
- package/scripts/lib/config.mjs +296 -0
- package/scripts/lib/design-fetch.mjs +156 -0
- package/scripts/lib/device.mjs +67 -0
- package/scripts/lib/doctor.mjs +614 -0
- package/scripts/lib/execution-mode.mjs +24 -0
- package/scripts/lib/fix-guard.mjs +23 -0
- package/scripts/lib/fix-stash.mjs +17 -0
- package/scripts/lib/fix.mjs +31 -0
- package/scripts/lib/github.mjs +161 -0
- package/scripts/lib/go-local.mjs +102 -0
- package/scripts/lib/init.mjs +324 -0
- package/scripts/lib/intent-to-add.mjs +32 -0
- package/scripts/lib/issue-check.mjs +63 -0
- package/scripts/lib/issue-coverage.mjs +47 -0
- package/scripts/lib/issue-hash.mjs +25 -0
- package/scripts/lib/issue-parse.mjs +194 -0
- package/scripts/lib/issue-sections.mjs +43 -0
- package/scripts/lib/labels.mjs +65 -0
- package/scripts/lib/language.mjs +9 -0
- package/scripts/lib/logaudit.mjs +123 -0
- package/scripts/lib/messages.mjs +328 -0
- package/scripts/lib/next-step.mjs +144 -0
- package/scripts/lib/openspec-commands.mjs +44 -0
- package/scripts/lib/openspec-status.mjs +16 -0
- package/scripts/lib/plan.mjs +256 -0
- package/scripts/lib/platform-of.mjs +32 -0
- package/scripts/lib/platform-scope.mjs +32 -0
- package/scripts/lib/platforms.mjs +90 -0
- package/scripts/lib/preflight.mjs +71 -0
- package/scripts/lib/protected-paths.mjs +102 -0
- package/scripts/lib/requeue.mjs +23 -0
- package/scripts/lib/review-adapters.mjs +37 -0
- package/scripts/lib/review-brief.mjs +43 -0
- package/scripts/lib/review-events.mjs +56 -0
- package/scripts/lib/review-state.mjs +157 -0
- package/scripts/lib/reviewers.mjs +39 -0
- package/scripts/lib/role-skills.mjs +112 -0
- package/scripts/lib/run-flags.mjs +16 -0
- package/scripts/lib/run-usage.mjs +65 -0
- package/scripts/lib/scan.mjs +73 -0
- package/scripts/lib/ship.mjs +161 -0
- package/scripts/lib/sla.mjs +90 -0
- package/scripts/lib/stack-profile.mjs +169 -0
- package/scripts/lib/staging.mjs +21 -0
- package/scripts/lib/status-data.mjs +109 -0
- package/scripts/lib/status.mjs +199 -0
- package/scripts/lib/tasks-file.mjs +81 -0
- package/scripts/lib/web-review.mjs +98 -0
- package/scripts/lib/yaml-lite.mjs +157 -0
- package/scripts/needs-reviewer.mjs +41 -0
- package/scripts/next-step.mjs +39 -0
- package/scripts/pick-reviewer.mjs +42 -0
- package/scripts/platform-of.mjs +49 -0
- package/scripts/preflight.mjs +32 -0
- package/scripts/render-message.mjs +35 -0
- package/scripts/requeue.mjs +53 -0
- package/scripts/review-adapter.mjs +32 -0
- package/scripts/review-brief.mjs +29 -0
- package/scripts/review-event.mjs +70 -0
- package/scripts/review-sla.mjs +41 -0
- package/scripts/review-state.mjs +171 -0
- package/scripts/review-who.mjs +21 -0
- package/scripts/role-skills.mjs +53 -0
- package/scripts/run-usage.mjs +22 -0
- package/scripts/scan-diff.mjs +16 -0
- package/scripts/ship-check.mjs +54 -0
- package/scripts/spec-ready.mjs +40 -0
- package/scripts/stack-profile.mjs +67 -0
- package/scripts/stage.mjs +14 -0
- package/scripts/status.mjs +47 -0
- package/scripts/usage-limit.mjs +17 -0
- package/scripts/validate-config.mjs +44 -0
- package/scripts/web-review.mjs +134 -0
- package/skills/doctor/SKILL.md +28 -0
- package/skills/fix/SKILL.md +128 -0
- package/skills/go/SKILL.md +242 -0
- package/skills/init/SKILL.md +72 -0
- package/skills/issue/SKILL.md +128 -0
- package/skills/issue/references/update.md +15 -0
- package/skills/review/SKILL.md +157 -0
- package/skills/ship/SKILL.md +128 -0
- package/skills/status/SKILL.md +27 -0
- package/skills/think/SKILL.md +76 -0
- package/stacks/flutter.md +63 -0
- package/stacks/laravel.md +65 -0
- package/stacks/vue.md +70 -0
- package/templates/issue.md +99 -0
- package/templates/relevo.yml +59 -0
- package/templates/workflows/relevo-digest.yml +54 -0
- package/templates/workflows/relevo-go.yml +263 -0
- package/templates/workflows/relevo-queue.yml +72 -0
- package/templates/workflows/relevo-review.yml +60 -0
- package/templates/workflows/relevo-sla.yml +53 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prints the platform an issue belongs to, as JSON. Usage:
|
|
3
|
+
// node scripts/platform-of.mjs --issue <n> [--repo owner/name] [--config relevo.yml]
|
|
4
|
+
// node scripts/platform-of.mjs --input issue.json [--config relevo.yml] (issue.json: { "body": "..." }, for tests)
|
|
5
|
+
// Output: { platform, stack, path, targets }. `fix` and `review` use it to find the stack again from the issue text.
|
|
6
|
+
// exit 0: printed exit 1: the platform cannot be resolved (the reason is on stderr) exit 2: bad usage or invalid relevo.yml
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { loadConfig } from './lib/config.mjs';
|
|
12
|
+
import { resolvePlatform } from './lib/platform-of.mjs';
|
|
13
|
+
|
|
14
|
+
const arg = (name) => {
|
|
15
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
16
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
17
|
+
};
|
|
18
|
+
const fail = (message, code) => {
|
|
19
|
+
console.error(message);
|
|
20
|
+
process.exit(code);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const configPath = resolve(arg('config') ?? 'relevo.yml');
|
|
24
|
+
if (!existsSync(configPath)) fail(`relevo.yml not found at ${configPath}. Run /relevo:init to create it.`, 2);
|
|
25
|
+
const loaded = loadConfig(readFileSync(configPath, 'utf8'));
|
|
26
|
+
if (!loaded.ok) fail(`relevo.yml is invalid:\n${loaded.errors.map((e) => ` ${e.message}`).join('\n')}`, 2);
|
|
27
|
+
|
|
28
|
+
let body;
|
|
29
|
+
if (arg('input')) {
|
|
30
|
+
try {
|
|
31
|
+
body = JSON.parse(readFileSync(arg('input'), 'utf8')).body ?? '';
|
|
32
|
+
} catch (err) {
|
|
33
|
+
fail(`cannot read ${arg('input')}: ${err.message}`, 2);
|
|
34
|
+
}
|
|
35
|
+
} else if (arg('issue')) {
|
|
36
|
+
const repo = arg('repo') ?? process.env.GITHUB_REPOSITORY;
|
|
37
|
+
const args = ['issue', 'view', arg('issue'), '--json', 'body', '--jq', '.body', ...(repo ? ['--repo', repo] : [])];
|
|
38
|
+
try {
|
|
39
|
+
body = execFileSync('gh', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
fail(`cannot read issue ${arg('issue')}: ${String(err.stderr ?? err.message).trim()}`, 2);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
fail('usage: platform-of.mjs --issue <n> [--repo owner/name] [--config relevo.yml]', 2);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const r = resolvePlatform({ platforms: loaded.config.platforms, body });
|
|
48
|
+
if (!r.ok) fail(r.reason === 'missing' ? `the issue has no ${r.message}` : r.message, 1);
|
|
49
|
+
console.log(JSON.stringify({ platform: r.name, stack: r.entry.stack, path: r.entry.path, targets: r.entry.targets }, null, 2));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The checks to make before building or fixing anything. Usage:
|
|
3
|
+
// node scripts/preflight.mjs [--checks config,openspec,openspec-root,clean-tree,gh] [--config relevo.yml]
|
|
4
|
+
// Default checks: config, openspec, openspec-root, clean-tree. `gh` (signed in to GitHub) is asked for by the
|
|
5
|
+
// commands that need it. Prints one block per failure with its fix.
|
|
6
|
+
// exit 0: all passed exit 1: something failed exit 2: unknown check
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { DEFAULT_CHECKS, formatPreflight, runPreflight } from './lib/preflight.mjs';
|
|
12
|
+
|
|
13
|
+
const arg = (name) => {
|
|
14
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
15
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
16
|
+
};
|
|
17
|
+
const checks = arg('checks') ? arg('checks').split(',').map((c) => c.trim()).filter(Boolean) : DEFAULT_CHECKS;
|
|
18
|
+
const run = (command, args) => {
|
|
19
|
+
const r = spawnSync(command, args, { encoding: 'utf8' });
|
|
20
|
+
return { status: r.error ? 127 : r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const result = runPreflight({ checks, readConfig: () => readFileSync(resolve(arg('config') ?? 'relevo.yml'), 'utf8'), run });
|
|
25
|
+
const text = formatPreflight(result);
|
|
26
|
+
if (result.ok) console.log(text);
|
|
27
|
+
else console.error(text);
|
|
28
|
+
process.exit(result.ok ? 0 : 1);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(err.message);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Renders a hand-off message. Usage:
|
|
3
|
+
// echo '{"assignee":"ana","issue":31,...}' | node scripts/render-message.mjs built [--config relevo.yml] [--language es]
|
|
4
|
+
// The language comes from --language, else from relevo.yml (if found), else English.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { loadConfig } from './lib/config.mjs';
|
|
9
|
+
import { MESSAGE_KINDS, renderMessage } from './lib/messages.mjs';
|
|
10
|
+
|
|
11
|
+
const kind = process.argv[2];
|
|
12
|
+
if (!kind || !MESSAGE_KINDS.includes(kind)) {
|
|
13
|
+
console.error(`usage: render-message.mjs <${MESSAGE_KINDS.join('|')}> [--config path] [--language xx] (JSON data on stdin)`);
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
const arg = (name) => {
|
|
17
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
18
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let language = arg('language');
|
|
22
|
+
if (!language) {
|
|
23
|
+
const path = resolve(arg('config') ?? 'relevo.yml');
|
|
24
|
+
if (existsSync(path)) {
|
|
25
|
+
const loaded = loadConfig(readFileSync(path, 'utf8'));
|
|
26
|
+
if (loaded.ok) language = loaded.config.language;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
console.log(renderMessage(kind, JSON.parse(readFileSync(0, 'utf8') || '{}'), language ?? 'en'));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(err.message);
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Re-evaluates the queue. Carries out every decision except `start` (labels, comments) and prints
|
|
3
|
+
// which issues may start now, so a workflow can dispatch one build run for each.
|
|
4
|
+
//
|
|
5
|
+
// node scripts/requeue.mjs --repo owner/name [--config relevo.yml] [--dry-run] [--credential false]
|
|
6
|
+
// node scripts/requeue.mjs --input state.json [--config relevo.yml]
|
|
7
|
+
// prints {"start":[12,15],"summary":{...}}
|
|
8
|
+
// exit 0: evaluated exit 2: invalid or missing configuration, or bad arguments
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { loadConfig } from './lib/config.mjs';
|
|
13
|
+
import { applyDecisions, gather, realGh, realGhLinks } from './lib/github.mjs';
|
|
14
|
+
import { planGo } from './lib/plan.mjs';
|
|
15
|
+
import { resolveLabelNames } from './lib/labels.mjs';
|
|
16
|
+
import { ownedByQueue, startable } from './lib/requeue.mjs';
|
|
17
|
+
|
|
18
|
+
const arg = (name) => {
|
|
19
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
20
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
21
|
+
};
|
|
22
|
+
const fail = (m) => {
|
|
23
|
+
console.error(m);
|
|
24
|
+
process.exit(2);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const configPath = resolve(arg('config') ?? 'relevo.yml');
|
|
28
|
+
if (!existsSync(configPath)) fail(`relevo.yml not found at ${configPath}. Run /relevo:init to create it.`);
|
|
29
|
+
const loaded = loadConfig(readFileSync(configPath, 'utf8'));
|
|
30
|
+
if (!loaded.ok) fail(loaded.errors.map((e) => `error: ${e.message}`).join('\n'));
|
|
31
|
+
const { config } = loaded;
|
|
32
|
+
|
|
33
|
+
const repo = arg('repo') ?? process.env.GITHUB_REPOSITORY;
|
|
34
|
+
let state;
|
|
35
|
+
if (arg('input')) {
|
|
36
|
+
state = JSON.parse(readFileSync(arg('input') === '-' ? 0 : arg('input'), 'utf8'));
|
|
37
|
+
} else {
|
|
38
|
+
if (!repo) fail('give --repo owner/name (or set GITHUB_REPOSITORY), or --input state.json');
|
|
39
|
+
state = gather({ gh: realGh, ghLinks: realGhLinks(), repo, config });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The whole queue is planned so that the limits stay right, but the queue only acts on the issues that are
|
|
43
|
+
// already waiting: a freshly ready issue is handled by the `go` run its label started.
|
|
44
|
+
const { decisions: planned } = planGo({ config, repo, ...state });
|
|
45
|
+
const decisions = ownedByQueue(planned, state.issues, resolveLabelNames(config.labels));
|
|
46
|
+
const summary = {};
|
|
47
|
+
decisions.forEach((d) => {
|
|
48
|
+
summary[d.action] = (summary[d.action] ?? 0) + 1;
|
|
49
|
+
});
|
|
50
|
+
const dry = process.argv.includes('--dry-run') || Boolean(arg('input'));
|
|
51
|
+
if (!dry) applyDecisions(decisions, { gh: realGh, repo, config });
|
|
52
|
+
// Without a model credential the cloud cannot build, so nothing is dispatched (and nothing would be commented again every run).
|
|
53
|
+
console.log(JSON.stringify({ start: arg('credential') === 'false' ? [] : startable(decisions), summary }));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prints how `review` works for a platform of relevo.yml, as JSON. Usage:
|
|
3
|
+
// node scripts/review-adapter.mjs --platform web [--target web] [--config relevo.yml]
|
|
4
|
+
// Output: { supported, stack, target, kind, log, capture, driver, needs, reason, platform, needs_autopilot }
|
|
5
|
+
// exit 0: supported exit 1: no adapter for this platform and target (the reason is in the output) exit 2: bad usage or invalid relevo.yml
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { loadConfig } from './lib/config.mjs';
|
|
10
|
+
import { adapterFor, needsAutopilot, targetOf } from './lib/review-adapters.mjs';
|
|
11
|
+
|
|
12
|
+
const arg = (name) => {
|
|
13
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
14
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
15
|
+
};
|
|
16
|
+
const fail = (message) => {
|
|
17
|
+
console.error(message);
|
|
18
|
+
process.exit(2);
|
|
19
|
+
};
|
|
20
|
+
const name = arg('platform');
|
|
21
|
+
if (!name) fail('usage: review-adapter.mjs --platform <name> [--target <target>] [--config relevo.yml]');
|
|
22
|
+
const configPath = resolve(arg('config') ?? 'relevo.yml');
|
|
23
|
+
if (!existsSync(configPath)) fail(`relevo.yml not found at ${configPath}. Run /relevo:init to create it.`);
|
|
24
|
+
const loaded = loadConfig(readFileSync(configPath, 'utf8'));
|
|
25
|
+
if (!loaded.ok) fail(`relevo.yml is invalid:\n${loaded.errors.map((e) => ` ${e.message}`).join('\n')}`);
|
|
26
|
+
const platform = loaded.config.platforms[name];
|
|
27
|
+
if (!platform) fail(`there is no platform "${name}" in relevo.yml (platforms: ${Object.keys(loaded.config.platforms).join(', ') || 'none'})`);
|
|
28
|
+
|
|
29
|
+
const target = arg('target') ?? targetOf(platform);
|
|
30
|
+
const adapter = adapterFor({ stack: platform.stack, target });
|
|
31
|
+
console.log(JSON.stringify({ ...adapter, platform: name, needs_autopilot: needsAutopilot(adapter) }));
|
|
32
|
+
process.exit(adapter.supported ? 0 : 1);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prints a short brief of an issue for the person who reviews it. Usage:
|
|
3
|
+
// node scripts/review-brief.mjs --issue 42 --title "Birthday list" --body-file issue.md [--built-file comment.md] [--language en|es]
|
|
4
|
+
// exit 0: printed exit 2: bad arguments
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { reviewBrief } from './lib/review-brief.mjs';
|
|
8
|
+
|
|
9
|
+
const arg = (name) => {
|
|
10
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
11
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
12
|
+
};
|
|
13
|
+
const number = arg('issue');
|
|
14
|
+
const title = arg('title');
|
|
15
|
+
const bodyFile = arg('body-file');
|
|
16
|
+
if (!number || title === undefined || !bodyFile) {
|
|
17
|
+
console.error('usage: review-brief.mjs --issue <n> --title "<title>" --body-file <file> [--built-file <file>] [--language en|es]');
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
const read = (f) => {
|
|
21
|
+
try {
|
|
22
|
+
return readFileSync(f, 'utf8');
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(`cannot read ${f}: ${err.message}`);
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const built = arg('built-file') ? read(arg('built-file')) : '';
|
|
29
|
+
console.log(reviewBrief({ issue: { number, title, body: read(bodyFile) }, built, language: arg('language') ?? 'en' }));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Reacts to a pull request review or push. Reads the GitHub Actions event (GITHUB_EVENT_NAME and
|
|
3
|
+
// GITHUB_EVENT_PATH), updates the issue label and posts the hand-off.
|
|
4
|
+
// node scripts/review-event.mjs --repo owner/name [--config relevo.yml] [--event-name n] [--event-path file] [--dry-run]
|
|
5
|
+
// exit 0: handled or nothing to do exit 2: invalid configuration or bad arguments
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { issueFromBranch } from './lib/branch.mjs';
|
|
10
|
+
import { loadConfig } from './lib/config.mjs';
|
|
11
|
+
import { realGh } from './lib/github.mjs';
|
|
12
|
+
import { renderMessage } from './lib/messages.mjs';
|
|
13
|
+
import { planReviewEvent } from './lib/review-events.mjs';
|
|
14
|
+
|
|
15
|
+
const arg = (name) => {
|
|
16
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
17
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
18
|
+
};
|
|
19
|
+
const fail = (m) => {
|
|
20
|
+
console.error(m);
|
|
21
|
+
process.exit(2);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const configPath = resolve(arg('config') ?? 'relevo.yml');
|
|
25
|
+
if (!existsSync(configPath)) fail(`relevo.yml not found at ${configPath}. Run /relevo:init to create it.`);
|
|
26
|
+
const loaded = loadConfig(readFileSync(configPath, 'utf8'));
|
|
27
|
+
if (!loaded.ok) fail(loaded.errors.map((e) => `error: ${e.message}`).join('\n'));
|
|
28
|
+
const { config } = loaded;
|
|
29
|
+
|
|
30
|
+
const repo = arg('repo') ?? process.env.GITHUB_REPOSITORY;
|
|
31
|
+
const eventName = arg('event-name') ?? process.env.GITHUB_EVENT_NAME;
|
|
32
|
+
const eventPath = arg('event-path') ?? process.env.GITHUB_EVENT_PATH;
|
|
33
|
+
if (!repo || !eventName || !eventPath) fail('give --repo, and the event through GITHUB_EVENT_NAME and GITHUB_EVENT_PATH (or --event-name and --event-path)');
|
|
34
|
+
const payload = JSON.parse(readFileSync(eventPath, 'utf8'));
|
|
35
|
+
|
|
36
|
+
const prData = payload.pull_request;
|
|
37
|
+
const number = issueFromBranch(prData?.head?.ref);
|
|
38
|
+
if (!number) {
|
|
39
|
+
console.log(JSON.stringify({ action: 'ignored', reason: 'not a Relevo feature branch' }));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let event;
|
|
44
|
+
if (eventName === 'pull_request_review') {
|
|
45
|
+
event = { kind: 'review', state: String(payload.review.state).toLowerCase(), reviewer: payload.review.user.login, body: payload.review.body, url: payload.review.html_url };
|
|
46
|
+
} else if (eventName === 'pull_request' && payload.action === 'synchronize') {
|
|
47
|
+
event = { kind: 'push' };
|
|
48
|
+
} else {
|
|
49
|
+
console.log(JSON.stringify({ action: 'ignored', reason: `event ${eventName} is not handled` }));
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const pr = { number: prData.number, url: prData.html_url, author: prData.user.login };
|
|
54
|
+
const labels = JSON.parse(realGh(['api', `repos/${repo}/issues/${number}`])).labels.map((l) => (typeof l === 'string' ? l : l.name));
|
|
55
|
+
const decision = planReviewEvent({ config, event, pr, issue: { number, labels } });
|
|
56
|
+
if (!decision) {
|
|
57
|
+
console.log(JSON.stringify({ action: 'none' }));
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!process.argv.includes('--dry-run')) {
|
|
62
|
+
const edit = ['issue', 'edit', String(number), '--repo', repo];
|
|
63
|
+
decision.labels.add.forEach((l) => edit.push('--add-label', l));
|
|
64
|
+
decision.labels.remove.forEach((l) => edit.push('--remove-label', l));
|
|
65
|
+
if (edit.length > 5) realGh(edit);
|
|
66
|
+
if (decision.comment) {
|
|
67
|
+
realGh(['issue', 'comment', String(number), '--repo', repo, '--body', renderMessage(decision.comment.kind, decision.comment.data, config.language)]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
console.log(JSON.stringify(decision));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Sends the review reminder, then the escalation. Usage:
|
|
3
|
+
// node scripts/review-sla.mjs --repo owner/name [--config relevo.yml] [--dry-run]
|
|
4
|
+
// node scripts/review-sla.mjs --input prs.json [--now ISO] [--config relevo.yml] (never writes)
|
|
5
|
+
// Prints the decisions as JSON. exit 0: evaluated exit 2: invalid configuration or bad arguments
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { loadConfig } from './lib/config.mjs';
|
|
10
|
+
import { realGh } from './lib/github.mjs';
|
|
11
|
+
import { gatherPrs, planSla, slaCommands } from './lib/sla.mjs';
|
|
12
|
+
|
|
13
|
+
const arg = (name) => {
|
|
14
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
15
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
16
|
+
};
|
|
17
|
+
const fail = (m) => {
|
|
18
|
+
console.error(m);
|
|
19
|
+
process.exit(2);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const configPath = resolve(arg('config') ?? 'relevo.yml');
|
|
23
|
+
if (!existsSync(configPath)) fail(`relevo.yml not found at ${configPath}. Run /relevo:init to create it.`);
|
|
24
|
+
const loaded = loadConfig(readFileSync(configPath, 'utf8'));
|
|
25
|
+
if (!loaded.ok) fail(loaded.errors.map((e) => `error: ${e.message}`).join('\n'));
|
|
26
|
+
const { config } = loaded;
|
|
27
|
+
|
|
28
|
+
const repo = arg('repo') ?? process.env.GITHUB_REPOSITORY;
|
|
29
|
+
const now = arg('now') ? new Date(arg('now')) : new Date();
|
|
30
|
+
let prs;
|
|
31
|
+
if (arg('input')) {
|
|
32
|
+
prs = JSON.parse(readFileSync(arg('input') === '-' ? 0 : arg('input'), 'utf8'));
|
|
33
|
+
} else {
|
|
34
|
+
if (!repo) fail('give --repo owner/name (or set GITHUB_REPOSITORY), or --input prs.json');
|
|
35
|
+
prs = gatherPrs({ gh: realGh, repo, config });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const decisions = planSla({ config, prs, now });
|
|
39
|
+
const dry = process.argv.includes('--dry-run') || Boolean(arg('input'));
|
|
40
|
+
if (!dry) decisions.forEach((d) => slaCommands(d, { repo, config }).forEach((cmd) => realGh(cmd)));
|
|
41
|
+
console.log(JSON.stringify({ decisions }, null, 2));
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Keeps the state of an on-device review and decides whether a pull request may be opened.
|
|
3
|
+
// review-state.mjs init <change-dir> [--autopilot off|precheck|verify] build the checklist from the change's scenarios
|
|
4
|
+
// review-state.mjs set <id> <ok|fail|pending> [note ...]
|
|
5
|
+
// review-state.mjs sync <change-dir> after a fix: keep confirmed lines, retest failed ones, add new scenarios
|
|
6
|
+
// review-state.mjs capture <path> record a screenshot that was taken
|
|
7
|
+
// review-state.mjs audit [<log-file>|-] [--package <id>] [--pid <n>]
|
|
8
|
+
// audit the device log (stdin by default), write the report;
|
|
9
|
+
// with --package only that app's lines are audited
|
|
10
|
+
// review-state.mjs drain append the device log (stdin) to evidence/device.log; the skill clears the device's log after it
|
|
11
|
+
// review-state.mjs audit --accumulated audit evidence/device.log plus whatever is on stdin
|
|
12
|
+
// review-state.mjs confirm-env the person confirmed in writing that the app does not point to production data
|
|
13
|
+
// review-state.mjs precheck <n> ok|fail [note] the agent's own result for a line (autopilot precheck or verify)
|
|
14
|
+
// review-state.mjs auto <n> <how ...> the agent verified a line automatically (autopilot verify only)
|
|
15
|
+
// review-state.mjs auto-list print the "Verified automatically" section for the pull request body
|
|
16
|
+
// review-state.mjs waive <n> <reason ...> the person waives finding n of the audit, with a written reason
|
|
17
|
+
// review-state.mjs waived print the "Waived findings" section for the pull request body
|
|
18
|
+
// review-state.mjs agree the person agrees to open the pull request
|
|
19
|
+
// review-state.mjs show print the state as JSON
|
|
20
|
+
// review-state.mjs decide exit 0 when the pull request may be opened, 1 when not
|
|
21
|
+
// The state lives in evidence/review-state.json (override with --state). That folder is never committed.
|
|
22
|
+
|
|
23
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { checklistFromChange } from './lib/checklist.mjs';
|
|
26
|
+
import { auditLog, auditReport, filterByApp } from './lib/logaudit.mjs';
|
|
27
|
+
import { applyWaivers, autoItem, autoSection, confirmEnv, decidePr, newState, precheckItem, setItem, syncState, waivedSection, waiveFinding } from './lib/review-state.mjs';
|
|
28
|
+
|
|
29
|
+
const argv = process.argv.slice(2);
|
|
30
|
+
const si = argv.indexOf('--state');
|
|
31
|
+
const statePath = si === -1 ? 'evidence/review-state.json' : argv[si + 1];
|
|
32
|
+
const args = si === -1 ? argv : argv.filter((_, i) => i !== si && i !== si + 1);
|
|
33
|
+
const [command, ...rest] = args;
|
|
34
|
+
|
|
35
|
+
const fail = (m) => {
|
|
36
|
+
console.error(m);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
};
|
|
39
|
+
const load = () => {
|
|
40
|
+
if (!existsSync(statePath)) fail(`no review in progress (${statePath} not found): run "init" first`);
|
|
41
|
+
return JSON.parse(readFileSync(statePath, 'utf8'));
|
|
42
|
+
};
|
|
43
|
+
const save = (state) => {
|
|
44
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
45
|
+
writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
switch (command) {
|
|
50
|
+
case 'init': {
|
|
51
|
+
if (!rest[0] || !existsSync(rest[0])) fail('usage: review-state.mjs init <change-directory>');
|
|
52
|
+
const ai = rest.indexOf('--autopilot');
|
|
53
|
+
const state = newState(checklistFromChange(rest[0]), { autopilot: ai === -1 ? 'off' : rest[ai + 1] });
|
|
54
|
+
save(state);
|
|
55
|
+
console.log(`${state.items.length} checklist item(s) in ${statePath} (autopilot: ${state.autopilot})`);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case 'sync': {
|
|
59
|
+
if (!rest[0] || !existsSync(rest[0])) fail('usage: review-state.mjs sync <change-directory>');
|
|
60
|
+
const state = syncState(load(), checklistFromChange(rest[0]));
|
|
61
|
+
save(state);
|
|
62
|
+
const pending = state.items.filter((i) => i.status === 'pending').length;
|
|
63
|
+
console.log(`${state.items.length} checklist item(s), ${pending} to test again; the log audit and the agreement must be repeated`);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'set': {
|
|
67
|
+
const [id, status, ...note] = rest;
|
|
68
|
+
const state = setItem(load(), Number(id), status, note.join(' ') || null);
|
|
69
|
+
save(state);
|
|
70
|
+
console.log(`item ${id}: ${status}`);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'capture': {
|
|
74
|
+
if (!rest[0]) fail('usage: review-state.mjs capture <path>');
|
|
75
|
+
const state = load();
|
|
76
|
+
state.captures.push(rest[0]);
|
|
77
|
+
save(state);
|
|
78
|
+
console.log(`recorded ${rest[0]}`);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'drain': {
|
|
82
|
+
const file = join(dirname(statePath), 'device.log');
|
|
83
|
+
const text = readFileSync(0, 'utf8');
|
|
84
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
85
|
+
appendFileSync(file, text.endsWith('\n') || text === '' ? text : `${text}\n`);
|
|
86
|
+
console.log(`${text.split('\n').filter(Boolean).length} line(s) added to ${file}`);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case 'audit': {
|
|
90
|
+
const state = load();
|
|
91
|
+
const pi = rest.indexOf('--package');
|
|
92
|
+
const pkg = pi === -1 ? null : rest[pi + 1];
|
|
93
|
+
const pids = rest.flatMap((a, i) => (a === '--pid' ? [rest[i + 1]] : []));
|
|
94
|
+
const positional = rest.filter((a, i) => !a.startsWith('--') && rest[i - 1] !== '--package' && rest[i - 1] !== '--pid');
|
|
95
|
+
const accumulated = rest.includes('--accumulated');
|
|
96
|
+
const kept = accumulated && existsSync(join(dirname(statePath), 'device.log')) ? readFileSync(join(dirname(statePath), 'device.log'), 'utf8') : '';
|
|
97
|
+
const fresh = positional[0] && positional[0] !== '-' ? readFileSync(positional[0], 'utf8') : readFileSync(0, 'utf8');
|
|
98
|
+
const raw = accumulated ? `${kept}${kept && !kept.endsWith('\n') ? '\n' : ''}${fresh}` : fresh;
|
|
99
|
+
if (pi !== -1 && !pkg) fail('--package needs an application id');
|
|
100
|
+
const { text: source, scope } = pkg ? filterByApp(raw, { package: pkg, pids }) : { text: raw, scope: null };
|
|
101
|
+
const audit = auditLog(source, { scope });
|
|
102
|
+
state.audit = { verdict: audit.verdict, counts: audit.counts, findings: audit.findings, scope, ...(audit.reason ? { reason: audit.reason } : {}) };
|
|
103
|
+
applyWaivers(state);
|
|
104
|
+
audit.verdict = state.audit.verdict;
|
|
105
|
+
save(state);
|
|
106
|
+
writeFileSync(join(dirname(statePath), 'review-report.md'), auditReport(audit, { scope }));
|
|
107
|
+
console.log(audit.verdict);
|
|
108
|
+
if (audit.reason) console.log(` ${audit.reason}`);
|
|
109
|
+
audit.findings.slice(0, 10).forEach((f, i) => console.log(` ${i + 1}. ${f.class}: ${f.excerpt}${f.waived ? ` (waived: ${f.reason})` : ''}`));
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'confirm-env': {
|
|
113
|
+
const state = confirmEnv(load());
|
|
114
|
+
save(state);
|
|
115
|
+
console.log('recorded: the person confirmed that the app does not point to production data');
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'precheck': {
|
|
119
|
+
const [id, result, ...note] = rest;
|
|
120
|
+
const state = precheckItem(load(), Number(id), result, note.join(' ') || null);
|
|
121
|
+
save(state);
|
|
122
|
+
console.log(`item ${id}: pre-check ${result} (the person still confirms it)`);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'auto': {
|
|
126
|
+
const [id, ...how] = rest;
|
|
127
|
+
const state = autoItem(load(), Number(id), how.join(' '));
|
|
128
|
+
save(state);
|
|
129
|
+
console.log(`item ${id}: verified automatically`);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case 'auto-list':
|
|
133
|
+
process.stdout.write(autoSection(load()));
|
|
134
|
+
break;
|
|
135
|
+
case 'waive': {
|
|
136
|
+
const [number, ...reason] = rest;
|
|
137
|
+
const state = waiveFinding(load(), number, reason.join(' '));
|
|
138
|
+
save(state);
|
|
139
|
+
writeFileSync(join(dirname(statePath), 'review-report.md'), auditReport(state.audit, { scope: state.audit.scope }));
|
|
140
|
+
console.log(`finding ${number} waived; the verdict is ${state.audit.verdict}`);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'waived':
|
|
144
|
+
process.stdout.write(waivedSection(load()));
|
|
145
|
+
break;
|
|
146
|
+
case 'agree': {
|
|
147
|
+
const state = load();
|
|
148
|
+
state.agreed = true;
|
|
149
|
+
save(state);
|
|
150
|
+
console.log('the person agreed to open the pull request');
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 'show':
|
|
154
|
+
console.log(JSON.stringify(load(), null, 2));
|
|
155
|
+
break;
|
|
156
|
+
case 'decide': {
|
|
157
|
+
const { open, reasons } = decidePr(load());
|
|
158
|
+
if (open) console.log('the pull request may be opened');
|
|
159
|
+
else {
|
|
160
|
+
console.log('the pull request must NOT be opened yet:');
|
|
161
|
+
reasons.forEach((r) => console.log(` - ${r}`));
|
|
162
|
+
}
|
|
163
|
+
process.exit(open ? 0 : 1);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
fail('usage: review-state.mjs <init|set|sync|capture|drain|audit|waive|waived|confirm-env|precheck|auto|auto-list|agree|show|decide> [...]');
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
fail(err.message);
|
|
171
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Says whether the person running the review is the assignee of the issue. Usage:
|
|
3
|
+
// node scripts/review-who.mjs --assignee <login> --login <login>
|
|
4
|
+
// exit 0: the same person (case does not matter) exit 1: different people, prints both exit 2: bad arguments
|
|
5
|
+
|
|
6
|
+
const arg = (name) => {
|
|
7
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
8
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
9
|
+
};
|
|
10
|
+
const assignee = arg('assignee');
|
|
11
|
+
const login = arg('login');
|
|
12
|
+
if (!assignee || !login) {
|
|
13
|
+
console.error('usage: review-who.mjs --assignee <login> --login <login>');
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
if (assignee.toLowerCase() === login.toLowerCase()) {
|
|
17
|
+
console.log(`${login} is the assignee`);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
console.log(`the issue is assigned to ${assignee}, but ${login} is running the review`);
|
|
21
|
+
process.exit(1);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Resolves the extra skills of a role to the files an agent may read. Usage:
|
|
3
|
+
// node scripts/role-skills.mjs --role <dev|qa|issue|review|fix> [--platform <name>] [--base origin/dev] [--config relevo.yml]
|
|
4
|
+
// Prints JSON: { role, platform, base, skills: [ { name, path, scripts } | { name, left_out, reason } ] }.
|
|
5
|
+
// Only skills present on the base branch are used, so a skill added by the branch is never trusted.
|
|
6
|
+
// exit 0: printed (skills may be left out) exit 1: the profile cannot be resolved exit 2: bad usage or invalid relevo.yml
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { loadConfig } from './lib/config.mjs';
|
|
13
|
+
import { resolveRoleSkills } from './lib/role-skills.mjs';
|
|
14
|
+
import { resolveProfile } from './lib/stack-profile.mjs';
|
|
15
|
+
|
|
16
|
+
const arg = (name) => {
|
|
17
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
18
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
19
|
+
};
|
|
20
|
+
const fail = (message, code) => {
|
|
21
|
+
console.error(message);
|
|
22
|
+
process.exit(code);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const role = arg('role');
|
|
26
|
+
if (!role) fail('usage: role-skills.mjs --role <dev|qa|issue|review|fix> [--platform name] [--base origin/dev] [--config relevo.yml]', 2);
|
|
27
|
+
const configPath = resolve(arg('config') ?? 'relevo.yml');
|
|
28
|
+
if (!existsSync(configPath)) fail(`relevo.yml not found at ${configPath}. Run /relevo:init to create it.`, 2);
|
|
29
|
+
const loaded = loadConfig(readFileSync(configPath, 'utf8'));
|
|
30
|
+
if (!loaded.ok) fail(`relevo.yml is invalid:\n${loaded.errors.map((e) => ` ${e.message}`).join('\n')}`, 2);
|
|
31
|
+
|
|
32
|
+
const names = Object.keys(loaded.config.platforms);
|
|
33
|
+
const name = arg('platform') ?? (names.length === 1 ? names[0] : undefined);
|
|
34
|
+
const platform = name ? loaded.config.platforms[name] : undefined;
|
|
35
|
+
if (!platform) fail(`give --platform <${names.join('|')}>`, 2);
|
|
36
|
+
|
|
37
|
+
const base = dirname(configPath);
|
|
38
|
+
const read = (path) => {
|
|
39
|
+
const file = path.startsWith('/') ? path : resolve(base, path);
|
|
40
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
41
|
+
};
|
|
42
|
+
const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
43
|
+
const resolved = resolveProfile({ platform, pluginRoot, read });
|
|
44
|
+
if (!resolved.ok) fail(resolved.errors.join('\n'), 1);
|
|
45
|
+
|
|
46
|
+
const baseRef = arg('base') ?? `origin/${loaded.config.branches.base}`;
|
|
47
|
+
const git = (args) => execFileSync('git', args, { cwd: base, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
48
|
+
try {
|
|
49
|
+
const skills = resolveRoleSkills({ role, profile: resolved.profile, git, baseRef, allowScripts: loaded.config.skills.allow_scripts });
|
|
50
|
+
console.log(JSON.stringify({ role, platform: name, base: baseRef, skills }, null, 2));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
fail(err.message, 2);
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prints one line with what a cloud run used, read from the execution log of the agent runner. Usage:
|
|
3
|
+
// node scripts/run-usage.mjs --file <execution_file> [--subscription]
|
|
4
|
+
// --subscription says that the cost is notional. Prints nothing when the log is missing or has no usage.
|
|
5
|
+
// It never fails: exit 0 always, so a step that posts the line cannot fail a run.
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { readUsage, usageLine } from './lib/run-usage.mjs';
|
|
9
|
+
|
|
10
|
+
const arg = (name) => {
|
|
11
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
12
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let text = '';
|
|
16
|
+
try {
|
|
17
|
+
if (arg('file')) text = readFileSync(arg('file'), 'utf8');
|
|
18
|
+
} catch {
|
|
19
|
+
text = '';
|
|
20
|
+
}
|
|
21
|
+
const line = usageLine(readUsage(text), { subscription: process.argv.includes('--subscription') });
|
|
22
|
+
if (line) console.log(line);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Scans a unified diff on stdin for temporary markers and credentials.
|
|
3
|
+
// Usage: git diff --cached | node scripts/scan-diff.mjs
|
|
4
|
+
// exit 0: clean exit 1: findings (nothing should be committed or pushed)
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { formatFindings, scanDiff } from './lib/scan.mjs';
|
|
8
|
+
|
|
9
|
+
const findings = scanDiff(readFileSync(0, 'utf8'));
|
|
10
|
+
if (findings.length === 0) {
|
|
11
|
+
console.log('no temporary markers or credentials found');
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
console.error(formatFindings(findings));
|
|
15
|
+
console.error(`\n${findings.length} finding(s): do not commit or push until they are removed`);
|
|
16
|
+
process.exit(1);
|