@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,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Decides whether a pull request may be merged. Usage:
|
|
3
|
+
// node scripts/ship-check.mjs --repo owner/name --branch feature/31-x [--invoker login] [--config relevo.yml] [--labels a,b] [--baseline <head sha>] [--issue 31]
|
|
4
|
+
// --labels the issue's labels; with them the issue must be pr-open
|
|
5
|
+
// --baseline the head recorded when ship started: an approval given on it still counts after the archive commit
|
|
6
|
+
// node scripts/ship-check.mjs --input pr.json --invoker login (a saved `gh pr view --json ...`; null for no PR)
|
|
7
|
+
// Prints the decision as JSON. exit 0: may be merged exit 1: must stop exit 2: bad arguments or configuration
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { loadConfig } from './lib/config.mjs';
|
|
12
|
+
import { realGh } from './lib/github.mjs';
|
|
13
|
+
import { shipDecision } from './lib/ship.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
|
+
|
|
29
|
+
const FIELDS = 'number,state,isDraft,mergeable,title,author,reviews,statusCheckRollup,headRefName,headRefOid,baseRefName';
|
|
30
|
+
let pr;
|
|
31
|
+
let invoker = arg('invoker');
|
|
32
|
+
|
|
33
|
+
if (arg('input')) {
|
|
34
|
+
pr = JSON.parse(readFileSync(arg('input'), 'utf8'));
|
|
35
|
+
} else {
|
|
36
|
+
const repo = arg('repo') ?? process.env.GITHUB_REPOSITORY;
|
|
37
|
+
const branch = arg('branch');
|
|
38
|
+
if (!repo || !branch) fail('usage: ship-check.mjs --repo owner/name --branch <branch> [--invoker login] [--config relevo.yml]');
|
|
39
|
+
const found = JSON.parse(realGh(['pr', 'list', '--repo', repo, '--head', branch, '--state', 'all', '--json', 'number,state', '--limit', '5']));
|
|
40
|
+
const open = found.find((p) => p.state === 'OPEN') ?? found[0];
|
|
41
|
+
pr = open ? JSON.parse(realGh(['pr', 'view', String(open.number), '--repo', repo, '--json', FIELDS])) : null;
|
|
42
|
+
invoker ??= realGh(['api', 'user', '--jq', '.login']).trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const decision = shipDecision({
|
|
46
|
+
config: loaded.config,
|
|
47
|
+
pr,
|
|
48
|
+
invoker,
|
|
49
|
+
baseline: arg('baseline'),
|
|
50
|
+
labels: arg('labels') === undefined ? undefined : arg('labels').split(',').map((l) => l.trim()).filter(Boolean),
|
|
51
|
+
issue: arg('issue'),
|
|
52
|
+
});
|
|
53
|
+
console.log(JSON.stringify(decision, null, 2));
|
|
54
|
+
process.exit(decision.allowed ? 0 : 1);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Says whether OpenSpec considers the specification of a change complete. Usage:
|
|
3
|
+
// node scripts/spec-ready.mjs --change <change_id>
|
|
4
|
+
// node scripts/spec-ready.mjs --input status.json (a saved `openspec status --change <id> --json`)
|
|
5
|
+
// Complete means every artifact is done (or skipped, for a change that declares skip_specs).
|
|
6
|
+
// exit 0: complete exit 1: artifacts missing (named on stdout) exit 2: bad arguments or OpenSpec failed
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { pendingArtifacts, planningComplete } from './lib/openspec-status.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
|
+
|
|
21
|
+
let status;
|
|
22
|
+
try {
|
|
23
|
+
if (arg('input')) status = JSON.parse(readFileSync(arg('input'), 'utf8'));
|
|
24
|
+
else {
|
|
25
|
+
if (!arg('change')) fail('usage: spec-ready.mjs --change <change_id> (or --input status.json)');
|
|
26
|
+
const r = spawnSync('openspec', ['status', '--change', arg('change'), '--json'], { encoding: 'utf8' });
|
|
27
|
+
if (r.error || r.status !== 0) fail(`openspec status failed${r.stderr ? `: ${r.stderr.trim()}` : ''}`);
|
|
28
|
+
status = JSON.parse(r.stdout);
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
fail(`could not read the status: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (planningComplete(status)) {
|
|
35
|
+
console.log('the specification is complete');
|
|
36
|
+
} else {
|
|
37
|
+
const pending = pendingArtifacts(status);
|
|
38
|
+
console.log(pending.length ? `artifacts not done: ${pending.join(', ')}` : 'OpenSpec reported no artifacts');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prints the resolved profile of a platform, as JSON. Usage:
|
|
3
|
+
// node scripts/stack-profile.mjs --platform <name> [--config relevo.yml] [--plugin-root <dir>] [--role issue]
|
|
4
|
+
// With `--role issue` it prints only the questions the stack has for the issue writer ({ platform, stack, questions });
|
|
5
|
+
// without it the questions are not part of the output, so dev and qa never receive them.
|
|
6
|
+
// The profile is the plugin's stacks/<stack>.md plus the repository's .relevo/stacks/<stack>.md, with the
|
|
7
|
+
// `platforms.<name>.checks` of relevo.yml applied. The agents receive this output; they do not look for profiles.
|
|
8
|
+
// exit 0: printed exit 1: the stack has no profile (or a profile is invalid) exit 2: bad usage or invalid relevo.yml
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { dirname, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { loadConfig } from './lib/config.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 pluginRoot = resolve(arg('plugin-root') ?? resolve(dirname(fileURLToPath(import.meta.url)), '..'));
|
|
26
|
+
const configPath = resolve(arg('config') ?? 'relevo.yml');
|
|
27
|
+
if (!existsSync(configPath)) fail(`relevo.yml not found at ${configPath}. Run /relevo:init to create it.`, 2);
|
|
28
|
+
|
|
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
|
+
if (!name) fail(`usage: stack-profile.mjs --platform <${names.join('|')}> [--config relevo.yml] [--plugin-root dir]`, 2);
|
|
35
|
+
const platform = loaded.config.platforms[name];
|
|
36
|
+
if (!platform) fail(`unknown platform "${name}"; the platforms of relevo.yml are: ${names.join(', ')}`, 2);
|
|
37
|
+
|
|
38
|
+
const base = dirname(configPath);
|
|
39
|
+
const read = (path) => {
|
|
40
|
+
const file = path.startsWith('/') ? path : resolve(base, path);
|
|
41
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
42
|
+
};
|
|
43
|
+
const result = resolveProfile({ platform, pluginRoot, read });
|
|
44
|
+
if (!result.ok) fail(result.errors.join('\n'), 1);
|
|
45
|
+
|
|
46
|
+
const { profile } = result;
|
|
47
|
+
if (arg('role') === 'issue') {
|
|
48
|
+
console.log(JSON.stringify({ platform: name, stack: profile.stack, questions: profile.questions }, null, 2));
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
if (arg('role') !== undefined) fail('--role can only be "issue"', 2);
|
|
52
|
+
console.log(JSON.stringify({
|
|
53
|
+
platform: name,
|
|
54
|
+
stack: profile.stack,
|
|
55
|
+
path: platform.path,
|
|
56
|
+
targets: platform.targets,
|
|
57
|
+
target_tools: Object.fromEntries(platform.targets.map((t) => [t, profile.targets[t] ?? ''])),
|
|
58
|
+
commands: profile.commands,
|
|
59
|
+
generated: profile.generated,
|
|
60
|
+
tools: profile.tools,
|
|
61
|
+
requires_skills: profile.requires_skills,
|
|
62
|
+
skills: profile.skills,
|
|
63
|
+
layers: profile.layers,
|
|
64
|
+
ignored: profile.ignored,
|
|
65
|
+
overridden: profile.overridden,
|
|
66
|
+
body: profile.body,
|
|
67
|
+
}, null, 2));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stages the named files, and nothing else. Usage: node scripts/stage.mjs path [path ...]
|
|
3
|
+
// Refuses ".", "-A" and similar: every commit must name the files it includes.
|
|
4
|
+
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { explicitStageArgs } from './lib/staging.mjs';
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const args = explicitStageArgs(process.argv.slice(2));
|
|
10
|
+
execFileSync('git', args, { stdio: 'inherit' });
|
|
11
|
+
} catch (err) {
|
|
12
|
+
console.error(err.message);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Who is waiting on what. Read-only: it never changes an issue, label or branch.
|
|
3
|
+
// node scripts/status.mjs --repo owner/name [--me login] [--json] [--config relevo.yml]
|
|
4
|
+
// node scripts/status.mjs --input state.json [--me login] [--now ISO] [--json]
|
|
5
|
+
// exit 0: reported 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, realGhLinks } from './lib/github.mjs';
|
|
11
|
+
import { buildStatus, renderStatus } from './lib/status.mjs';
|
|
12
|
+
import { gatherStatus } from './lib/status-data.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 = (m) => {
|
|
19
|
+
console.error(m);
|
|
20
|
+
process.exit(2);
|
|
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.`);
|
|
25
|
+
const loaded = loadConfig(readFileSync(configPath, 'utf8'));
|
|
26
|
+
if (!loaded.ok) fail(loaded.errors.map((e) => `error: ${e.message}`).join('\n'));
|
|
27
|
+
const { config } = loaded;
|
|
28
|
+
|
|
29
|
+
const repo = arg('repo') ?? process.env.GITHUB_REPOSITORY;
|
|
30
|
+
let data;
|
|
31
|
+
if (arg('input')) {
|
|
32
|
+
data = 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 state.json');
|
|
35
|
+
data = gatherStatus({ gh: realGh, ghLinks: realGhLinks(), repo, config });
|
|
36
|
+
}
|
|
37
|
+
let me = arg('me') ?? null;
|
|
38
|
+
if (!me && !arg('input')) {
|
|
39
|
+
try {
|
|
40
|
+
me = JSON.parse(realGh(['api', 'user'])).login;
|
|
41
|
+
} catch {
|
|
42
|
+
me = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const report = buildStatus({ config, ...data, me, now: arg('now') ? new Date(arg('now')) : new Date() });
|
|
47
|
+
console.log(process.argv.includes('--json') ? JSON.stringify(report, null, 2) : renderStatus(report, config.language));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Did a cloud run end because a usage limit, a quota or the credit was reached? Usage:
|
|
3
|
+
// node scripts/usage-limit.mjs --file <execution_file>
|
|
4
|
+
// exit 0: the log shows it exit 1: it does not, or the file is missing or unreadable
|
|
5
|
+
// The final step of the build job uses it to say "label the issue ready again later" instead of the generic text.
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { reachedUsageLimit } from './lib/run-usage.mjs';
|
|
9
|
+
|
|
10
|
+
const i = process.argv.indexOf('--file');
|
|
11
|
+
let text = '';
|
|
12
|
+
try {
|
|
13
|
+
if (i !== -1) text = readFileSync(process.argv[i + 1], 'utf8');
|
|
14
|
+
} catch {
|
|
15
|
+
text = '';
|
|
16
|
+
}
|
|
17
|
+
process.exit(reachedUsageLimit(text) ? 0 : 1);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Validates relevo.yml. Usage: node scripts/validate-config.mjs [path] [--json] [--effective]
|
|
3
|
+
// exit 0: valid exit 1: invalid exit 2: file not found
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { loadConfig } from './lib/config.mjs';
|
|
9
|
+
import { profileLookups } from './lib/stack-profile.mjs';
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const flags = new Set(args.filter((a) => a.startsWith('--')));
|
|
13
|
+
const target = resolve(args.find((a) => !a.startsWith('--')) ?? 'relevo.yml');
|
|
14
|
+
|
|
15
|
+
if (!existsSync(target)) {
|
|
16
|
+
console.error(`relevo.yml not found at ${target}. Run /relevo:init to create it.`);
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Targets and `checks` are checked against the profile of each stack, when a profile can be read.
|
|
21
|
+
const read = (path) => {
|
|
22
|
+
const file = path.startsWith('/') ? path : resolve(dirname(target), path);
|
|
23
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
24
|
+
};
|
|
25
|
+
const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const result = loadConfig(readFileSync(target, 'utf8'), profileLookups({ pluginRoot, read }));
|
|
27
|
+
|
|
28
|
+
if (result.ok) {
|
|
29
|
+
result.config.qa.rules_from.forEach((file) => {
|
|
30
|
+
if (!existsSync(resolve(dirname(target), file))) {
|
|
31
|
+
result.warnings.push({ path: 'qa.rules_from', message: `qa.rules_from points to "${file}", which does not exist` });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (flags.has('--json')) {
|
|
37
|
+
console.log(JSON.stringify({ ok: result.ok, errors: result.errors, warnings: result.warnings }, null, 2));
|
|
38
|
+
} else {
|
|
39
|
+
result.errors.forEach((e) => console.error(`error: ${e.message}`));
|
|
40
|
+
result.warnings.forEach((w) => console.error(`warning: ${w.message}`));
|
|
41
|
+
if (result.ok) console.log(`${target} is valid`);
|
|
42
|
+
}
|
|
43
|
+
if (result.ok && flags.has('--effective')) console.log(JSON.stringify(result.config, null, 2));
|
|
44
|
+
process.exit(result.ok ? 0 : 1);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs a review script in a browser of its own and reports what it checked. Usage:
|
|
3
|
+
// node scripts/web-review.mjs --script evidence/review-web.mjs --url http://localhost:5173 \
|
|
4
|
+
// [--allow-origins https://staging.example.com] [--secret-env TEST_USER,TEST_PASS] [--out evidence] [--headed]
|
|
5
|
+
// The script is the agent's, written from the scenarios and reviewed by QA. Its default export receives
|
|
6
|
+
// { page, context, baseUrl, line, credentials }: line(n, how, fn) runs fn and records the checklist line n as
|
|
7
|
+
// auto-ok or auto-fail; credentials(name) gives a test credential taken from the environment variable `name`,
|
|
8
|
+
// which must be in --secret-env. The browser has a clean profile and can reach only localhost, the page under
|
|
9
|
+
// review and the allowed origins; every secret value is redacted from what is written.
|
|
10
|
+
// Writes <out>/web-log.txt (the log the audit reads, with --package web) and <out>/web-result.json.
|
|
11
|
+
// exit 0: the script ran (lines can still have failed) exit 1: the script was refused, Playwright is missing,
|
|
12
|
+
// the browser could not start, or the script failed outside a line exit 2: bad arguments
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { join, resolve } from 'node:path';
|
|
17
|
+
import { pathToFileURL } from 'node:url';
|
|
18
|
+
import { formatBrowserLog, isAllowedOrigin, redact, scanReviewScript } from './lib/web-review.mjs';
|
|
19
|
+
|
|
20
|
+
const RUN_LIMIT_MS = 120000;
|
|
21
|
+
|
|
22
|
+
const arg = (name) => {
|
|
23
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
24
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
25
|
+
};
|
|
26
|
+
const list = (name) => (arg(name) ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
27
|
+
const stop = (code, message) => {
|
|
28
|
+
console.error(message);
|
|
29
|
+
process.exit(code);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const scriptPath = arg('script');
|
|
33
|
+
const baseUrl = arg('url');
|
|
34
|
+
if (!scriptPath || !baseUrl || !existsSync(scriptPath)) stop(2, 'usage: web-review.mjs --script <file> --url <url> [--allow-origins a,b] [--secret-env A,B] [--out evidence] [--headed]');
|
|
35
|
+
let origin;
|
|
36
|
+
try {
|
|
37
|
+
origin = new URL(baseUrl);
|
|
38
|
+
} catch {
|
|
39
|
+
stop(2, `--url is not a URL: ${baseUrl}`);
|
|
40
|
+
}
|
|
41
|
+
const allowed = list('allow-origins');
|
|
42
|
+
const secretNames = list('secret-env');
|
|
43
|
+
const out = resolve(arg('out') ?? 'evidence');
|
|
44
|
+
|
|
45
|
+
const problems = scanReviewScript(readFileSync(scriptPath, 'utf8'));
|
|
46
|
+
if (problems.length > 0) stop(1, `the review script was refused before any browser started:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
|
|
47
|
+
|
|
48
|
+
const secrets = Object.fromEntries(secretNames.filter((n) => process.env[n]).map((n) => [n, process.env[n]]));
|
|
49
|
+
const hide = (text) => redact(text, secrets);
|
|
50
|
+
|
|
51
|
+
function loadPlaywright() {
|
|
52
|
+
for (const base of [join(process.cwd(), 'package.json'), import.meta.url]) {
|
|
53
|
+
try {
|
|
54
|
+
return createRequire(base)('playwright');
|
|
55
|
+
} catch { /* try the next place */ }
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const playwright = loadPlaywright();
|
|
60
|
+
if (!playwright) stop(1, 'Playwright cannot be resolved from this project: install it with "npm install -D playwright" and "npx playwright install chromium"');
|
|
61
|
+
|
|
62
|
+
const events = [];
|
|
63
|
+
const results = [];
|
|
64
|
+
let scriptError = null;
|
|
65
|
+
let pagesLoaded = 0;
|
|
66
|
+
let browser;
|
|
67
|
+
|
|
68
|
+
const line = async (n, how, fn) => {
|
|
69
|
+
if (!Number.isInteger(n) || n < 1) throw new Error('line(n, how, fn) needs the number of a checklist line');
|
|
70
|
+
try {
|
|
71
|
+
await fn();
|
|
72
|
+
results.push({ n, status: 'auto-ok', how: String(how) });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
results.push({ n, status: 'auto-fail', how: String(how), error: String(error?.message ?? error).split('\n')[0] });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const credentials = (name) => {
|
|
78
|
+
if (!secretNames.includes(name)) throw new Error(`credentials("${name}"): the name is not in --secret-env`);
|
|
79
|
+
if (!(name in secrets)) throw new Error(`credentials("${name}"): the environment variable is not set`);
|
|
80
|
+
return secrets[name];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
async function run() {
|
|
84
|
+
try {
|
|
85
|
+
browser = await playwright.chromium.launch({ headless: !process.argv.includes('--headed') });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
stop(1, `the browser could not start: ${String(error.message).split('\n')[0]}`);
|
|
88
|
+
}
|
|
89
|
+
const context = await browser.newContext({ acceptDownloads: false, serviceWorkers: 'block' });
|
|
90
|
+
await context.route('**/*', (route) => {
|
|
91
|
+
const request = route.request();
|
|
92
|
+
if (isAllowedOrigin(request.url(), { baseUrl, allowed })) return route.continue();
|
|
93
|
+
events.push({ type: 'blocked', text: `${request.method()} ${request.url()}` });
|
|
94
|
+
return route.abort('blockedbyclient');
|
|
95
|
+
});
|
|
96
|
+
const page = await context.newPage();
|
|
97
|
+
// "Failed to load resource" repeats what the request and response events already say (and the blocked ones).
|
|
98
|
+
page.on('console', (m) => { if (m.type() === 'error' && !m.text().startsWith('Failed to load resource')) events.push({ type: 'console', text: m.text() }); });
|
|
99
|
+
page.on('pageerror', (e) => events.push({ type: 'pageerror', text: `${e.name}: ${e.message}` }));
|
|
100
|
+
page.on('requestfailed', (r) => {
|
|
101
|
+
const reason = r.failure()?.errorText ?? 'failed';
|
|
102
|
+
if (!reason.includes('BLOCKED_BY_CLIENT')) events.push({ type: 'requestfailed', text: `${r.method()} ${r.url()} ${reason}` });
|
|
103
|
+
});
|
|
104
|
+
page.on('response', (r) => {
|
|
105
|
+
if (r.request().isNavigationRequest() && r.request().frame() === page.mainFrame() && r.status() < 400) pagesLoaded += 1;
|
|
106
|
+
});
|
|
107
|
+
page.on('response', (r) => { if (r.status() >= 400) events.push({ type: 'response', text: `${r.status()} ${r.request().method()} ${r.url()}` }); });
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const mod = await import(pathToFileURL(resolve(scriptPath)).href);
|
|
111
|
+
if (typeof mod.default !== 'function') throw new Error('the script must export a default async function');
|
|
112
|
+
await mod.default({ page, context, baseUrl, line, credentials });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
scriptError = String(error?.message ?? error).split('\n')[0];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
scriptError = `the run took more than ${RUN_LIMIT_MS / 1000} seconds and was stopped`;
|
|
120
|
+
}, RUN_LIMIT_MS);
|
|
121
|
+
let backstop;
|
|
122
|
+
await Promise.race([run(), new Promise((r) => { backstop = setTimeout(r, RUN_LIMIT_MS + 500); })]);
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
clearTimeout(backstop);
|
|
125
|
+
if (browser) await browser.close().catch(() => {});
|
|
126
|
+
|
|
127
|
+
mkdirSync(out, { recursive: true });
|
|
128
|
+
// A browser that never loaded a page proves nothing: the log is left empty so the audit is inconclusive.
|
|
129
|
+
writeFileSync(join(out, 'web-log.txt'), pagesLoaded === 0 ? '' : hide(formatBrowserLog(events, { origin: origin.host })));
|
|
130
|
+
writeFileSync(join(out, 'web-result.json'), hide(`${JSON.stringify({ url: baseUrl, lines: results, blocked: events.filter((e) => e.type === 'blocked').length, pagesLoaded, scriptError }, null, 2)}\n`));
|
|
131
|
+
results.sort((a, b) => a.n - b.n).forEach((r) => console.log(hide(`line ${r.n}: ${r.status} - ${r.how}${r.error ? ` (${r.error})` : ''}`)));
|
|
132
|
+
console.log(`${events.length} event(s) written to ${join(out, 'web-log.txt')}; audit it with: review-state.mjs audit --package web`);
|
|
133
|
+
if (scriptError) stop(1, hide(`the script failed outside a line: ${scriptError}`));
|
|
134
|
+
process.exit(0);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doctor
|
|
3
|
+
description: Checks that this machine, the repository and GitHub are ready for Relevo and reports every check as pass, fail or warning, with the fix next to each failure. It covers the required tools (OpenSpec, git, the GitHub CLI, the toolchain and the targets of each platform's stack profile), the skills that /relevo:think invokes, the extra skills of each role, relevo.yml, labels and branches, branch protection and approvals, the automation secret and workflows, and a CI check. Read-only: it never changes anything. Use when asked to check, diagnose or verify the Relevo setup.
|
|
4
|
+
argument-hint: ""
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Relevo doctor
|
|
8
|
+
|
|
9
|
+
Report whether Relevo can run here. This command only reads; it never changes anything.
|
|
10
|
+
|
|
11
|
+
Scripts live in `${CLAUDE_PLUGIN_ROOT}/scripts/`. Run them with `node`. They hold the checks; use them instead of judging the setup yourself.
|
|
12
|
+
|
|
13
|
+
## What you must never do
|
|
14
|
+
|
|
15
|
+
- Never run a command that writes: no `gh` call except the reads inside the script, no `git add`, no install, no file edit. If something is wrong, tell the person the fix; do not apply it. `/relevo:init` is what applies fixes.
|
|
16
|
+
- Never read, print or ask for the value of a secret. The script lists secret names only.
|
|
17
|
+
- Never hide a failure or turn it into a pass.
|
|
18
|
+
|
|
19
|
+
## Steps
|
|
20
|
+
|
|
21
|
+
1. Run `node ${CLAUDE_PLUGIN_ROOT}/scripts/doctor.mjs` from the repository root. It finds the repository itself; pass `--repo owner/name` only when it cannot.
|
|
22
|
+
2. Show its output as it is: each check is `pass`, `FAIL` or `warn`, and each failure and warning has its fix on the next line.
|
|
23
|
+
3. If there are failures, say which command fixes most of them: `/relevo:init` for labels, branches, protection, workflows and the ignore line; the install commands shown for tools and skills; adding the model credential to the repository secrets by hand.
|
|
24
|
+
4. If the output says branch protection is not available, say plainly that approving and merging are then enforced only by the written confirmation `ship` asks for, not by the platform.
|
|
25
|
+
|
|
26
|
+
## Exit status
|
|
27
|
+
|
|
28
|
+
`doctor.mjs` exits 0 when no check failed (warnings allowed) and 1 when at least one failed. Report the result in one line after the output.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fix
|
|
3
|
+
description: Corrects a defect found in a built issue, specification first. It writes the corrected behaviour into the change's specification and task list, then runs the dev and qa agents, and only then commits and pushes. The issue keeps its place in the workflow. Use only when asked to fix a specific issue, with a description of what is wrong. It pushes code, so it never starts on its own.
|
|
4
|
+
argument-hint: "[issue-number] [what is wrong]"
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Relevo fix
|
|
9
|
+
|
|
10
|
+
Correct a defect in the issue named at the start of `$ARGUMENTS`. The first word is the issue number; everything after it is the description of what is wrong. If there is no description, ask the person what is wrong before doing anything else.
|
|
11
|
+
|
|
12
|
+
The order matters and is checked by a script: **the specification is updated first, the code second.** In this session you never write application code yourself; the `relevo:dev` agent does.
|
|
13
|
+
|
|
14
|
+
Scripts live in `${CLAUDE_PLUGIN_ROOT}/scripts/`. Run them with `node`. They hold the rules that must not vary (which state an issue returns to, the specification-first check, the tasks file edit, scanning, messages); use them instead of deciding those things yourself.
|
|
15
|
+
|
|
16
|
+
## What you must never do
|
|
17
|
+
|
|
18
|
+
- Never edit application code in this session, and never edit anything outside the change folder before the developer has run.
|
|
19
|
+
- Never change the project rules file (`qa.rules_from`) to make a defect fit. If the defect is a breach of a rule, the rule stays and the code is fixed.
|
|
20
|
+
- Never commit or push if QA still rejects after the retries: save the changes in a stash with `fix-stash.mjs` and tell the person.
|
|
21
|
+
- Never approve, merge or open a pull request, never use `git add .` or `-A`, never force-push, and never push to a branch other than the issue's own.
|
|
22
|
+
|
|
23
|
+
## Steps
|
|
24
|
+
|
|
25
|
+
### 0. Preflight
|
|
26
|
+
|
|
27
|
+
Stop with a clear message, changing nothing, if any of these fails:
|
|
28
|
+
|
|
29
|
+
- `node ${CLAUDE_PLUGIN_ROOT}/scripts/validate-config.mjs relevo.yml` exits non-zero. Show its output.
|
|
30
|
+
- `openspec --version` does not run. Tell the person to run `/relevo:doctor`.
|
|
31
|
+
- The working tree has uncommitted changes.
|
|
32
|
+
- `node ${CLAUDE_PLUGIN_ROOT}/scripts/fix-state.mjs --current "<the issue's labels, comma separated>"` exits 1. It prints why a fix cannot run in the issue's state. Keep its output: it tells you the state the issue returns to (`to`) and whom to notify.
|
|
33
|
+
|
|
34
|
+
Find the repository (`$GITHUB_REPOSITORY`, or `gh repo view --json nameWithOwner -q .nameWithOwner`), the issue's branch (`git branch -r --list "origin/feature/<issue>-*"`) and its change folder (`openspec/changes/<issue>-*`). If the branch is not on the remote, stop and say so: there is nothing to correct yet.
|
|
35
|
+
|
|
36
|
+
Switch to the branch: `git fetch origin <branch>`, `git switch <branch>`, `git pull --ff-only origin <branch>`.
|
|
37
|
+
|
|
38
|
+
Then check two things before reading the defect:
|
|
39
|
+
|
|
40
|
+
- **Is there code to correct?** `node ${CLAUDE_PLUGIN_ROOT}/scripts/branch-work.mjs --base <base> --change <change>`. If it exits 1 the branch holds only the specification: change nothing, tell the person to delete the remote branch and apply the label relevo:ready again, and stop.
|
|
41
|
+
- **Is there saved work?** `node ${CLAUDE_PLUGIN_ROOT}/scripts/fix-stash.mjs find --issue <issue>`. If it prints a stash, a previous fix was rejected by QA and its work was saved (in this clone only). Tell the person which iteration it was and offer to restore it. If they agree, run `fix-stash.mjs pop --issue <issue>`; when it exits 1 it names the conflicting files and keeps the stash: show them and stop. After a restore, read the specification and the corrections already written, and go on from step 3 (Develop). To start from scratch instead, the person must confirm in writing before you run `fix-stash.mjs drop --issue <issue>`.
|
|
42
|
+
|
|
43
|
+
### 1. Understand the defect
|
|
44
|
+
|
|
45
|
+
Read the description, the change's specification (`openspec/changes/<change>/specs/`) and the project rules file (`qa.rules_from` in `relevo.yml`). Decide which of these it is:
|
|
46
|
+
|
|
47
|
+
- the specification was wrong or missing a case: the specification needs correcting;
|
|
48
|
+
- the code does not do what the specification says: the specification still gets a scenario for the exact case that failed, because that scenario is what QA will verify;
|
|
49
|
+
- the defect breaks a project rule: say so to the person, and write a scenario that restores compliance. Do not touch the rules file.
|
|
50
|
+
|
|
51
|
+
If you cannot tell what the correct behaviour should be, ask the person. They are here.
|
|
52
|
+
|
|
53
|
+
### 2. Specification first
|
|
54
|
+
|
|
55
|
+
Update the change, writing no application code:
|
|
56
|
+
|
|
57
|
+
1. In the right capability under `openspec/changes/<change>/specs/`, add a `#### Scenario:` with `WHEN`/`THEN` lines that describe the corrected behaviour (or rewrite the scenario that was wrong, and say why in `design.md`).
|
|
58
|
+
2. In `design.md`, record a decision only if one changed. In `proposal.md`, change the scope only if it changed.
|
|
59
|
+
3. Add the correction tasks, one per distinct correction and each verifiable:
|
|
60
|
+
```bash
|
|
61
|
+
node ${CLAUDE_PLUGIN_ROOT}/scripts/add-corrections.mjs openspec/changes/<change>/tasks.md "<task>" ["<task>" ...]
|
|
62
|
+
```
|
|
63
|
+
It appends `## N. Review corrections [Iteration K]` with unchecked tasks and prints the iteration number `K`.
|
|
64
|
+
4. `openspec validate "<change>"`. If it fails, fix the artifacts and validate again, at most twice; then stop and tell the person.
|
|
65
|
+
5. Run the guard:
|
|
66
|
+
```bash
|
|
67
|
+
node ${CLAUDE_PLUGIN_ROOT}/scripts/fix-guard.mjs openspec/changes/<change>
|
|
68
|
+
```
|
|
69
|
+
If it exits 1 it prints what is missing. Correct the specification and run it again. **Do not launch the developer until it exits 0.**
|
|
70
|
+
6. Check the task list: `node ${CLAUDE_PLUGIN_ROOT}/scripts/fix-tasks.mjs --file openspec/changes/<change>/tasks.md --stage before`. The only unchecked tasks may be the corrections just added, and one of them must start with `Test:` (a regression test that fails without the fix), unless every correction ends with `(device only)` because it shows only on a device. If it exits 1, fix the task list and run it again.
|
|
71
|
+
|
|
72
|
+
Do not commit yet: the whole fix is committed together once QA passes.
|
|
73
|
+
|
|
74
|
+
### 3. Develop
|
|
75
|
+
|
|
76
|
+
Find the platform of the issue again from its text, then resolve the profile and the extra skills for it:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
node ${CLAUDE_PLUGIN_ROOT}/scripts/platform-of.mjs --issue <issue>
|
|
80
|
+
node ${CLAUDE_PLUGIN_ROOT}/scripts/stack-profile.mjs --platform <platform>
|
|
81
|
+
node ${CLAUDE_PLUGIN_ROOT}/scripts/role-skills.mjs --role fix --platform <platform>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If `platform-of.mjs` exits 1, change nothing more and tell the person what it printed (the issue no longer names a valid platform: they correct the `Platform:` line and run `/relevo:fix` again). If `stack-profile.mjs` fails, show its message and stop.
|
|
85
|
+
|
|
86
|
+
Launch the `relevo:dev` agent in **build** mode. Give it the change folder, the project rules files, the resolved profile, the folder of the platform (`path`), the skill paths and the base branch. The original tasks are all done, so the only pending ones are the corrections, and those are all it should touch.
|
|
87
|
+
|
|
88
|
+
Read its report. If its open questions are not "none", they mean the specification is still unclear: ask the person, update the specification and run the guard again.
|
|
89
|
+
|
|
90
|
+
### 4. QA, with retries
|
|
91
|
+
|
|
92
|
+
Launch the `relevo:qa` agent on the result, with the same change folder, rules files, profile, folder of the platform, skill paths (for `fix`, those of `dev` and `qa` are in the output of `role-skills.mjs --role fix`) and base branch. Its first line is `QA PASSED` or `QA REJECTED`.
|
|
93
|
+
|
|
94
|
+
On `QA REJECTED` you may retry up to `qa.retries` times. Each retry launches `relevo:dev` in **fix** mode with the complete QA report, then `relevo:qa` again with the previous report, so it checks each earlier finding.
|
|
95
|
+
|
|
96
|
+
If QA still rejects after the retries: **make no commit and no push.** Save the work with `node ${CLAUDE_PLUGIN_ROOT}/scripts/fix-stash.mjs save --issue <issue> --iteration <K>` (the tree is left clean, so the next `fix` can start), show the person the findings, say that `/relevo:fix <issue>` offers the saved work back, leave the issue's labels untouched, and stop.
|
|
97
|
+
|
|
98
|
+
### 5. Commit
|
|
99
|
+
|
|
100
|
+
Before committing, check that every task is done: `node ${CLAUDE_PLUGIN_ROOT}/scripts/fix-tasks.mjs --file openspec/changes/<change>/tasks.md --stage after`. If it exits 1, a correction was left open: do not commit, and go back to step 3 for it.
|
|
101
|
+
|
|
102
|
+
Commit exactly the files the developer reported plus the files changed under `openspec/changes/<change>/`, naming each one, through the script. Never use `git add .` or `-A`; the script refuses them.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
node ${CLAUDE_PLUGIN_ROOT}/scripts/commit-change.mjs --message "fix: <short summary> (#<issue>)" --within "<path>" --change "<change>" -- <paths>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The script stages the paths, refuses protected paths and files outside the folder of the platform, scans what is staged for temporary markers and credentials, and commits only when it finds nothing. If it exits 1 it has unstaged the paths and printed the file and line: show them to the person and stop without pushing. The changes stay in the working tree. Do not add links to this session or attribution lines to the message.
|
|
109
|
+
|
|
110
|
+
### 6. Push
|
|
111
|
+
|
|
112
|
+
`git push origin <branch>` (a normal push, never forced).
|
|
113
|
+
|
|
114
|
+
### 7. The issue
|
|
115
|
+
|
|
116
|
+
Use what `fix-state.mjs` printed in step 0.
|
|
117
|
+
|
|
118
|
+
1. If `labels` is not empty, apply it with `gh issue edit` (it returns a `qa-failed` issue to `built`, and a `changes-requested` one to `pr-open`). In every other state the labels do not change.
|
|
119
|
+
2. Decide who is told:
|
|
120
|
+
- `notify` is `assignee`: the issue's assignee, with the ask `Your next step: /relevo:review <issue>`.
|
|
121
|
+
- `notify` is `reviewer`: find the pull request (`gh pr list --head <branch> --json number,reviews`), take the people who requested changes, re-request each one with `gh pr edit <number> --add-reviewer <login>`, and mention them with the ask `please re-review`.
|
|
122
|
+
- otherwise: mention nobody.
|
|
123
|
+
3. Post one comment built by `node ${CLAUDE_PLUGIN_ROOT}/scripts/render-message.mjs fix_done` with: `issue`, `title`, `iteration` (the number from step 2), `summary` (one line of what changed), `qa` (`QA PASSED`), `commit` (the short hash), and `recipient` and `ask` when someone is told.
|
|
124
|
+
4. If `evidence/review-state.json` exists (a review is in progress), reconcile it with the new specification: `node ${CLAUDE_PLUGIN_ROOT}/scripts/review-state.mjs sync openspec/changes/<change>`. Lines already confirmed stay confirmed, lines that failed are tested again, new scenarios are added, and the log audit and the agreement are repeated. Tell the person to restart the app and resume with `/relevo:review <issue>`, and that they can write `retest <numbers>` for any confirmed line the fix may have affected, because QA only audited this correction.
|
|
125
|
+
|
|
126
|
+
## Report
|
|
127
|
+
|
|
128
|
+
Finish with one short block: the issue, the iteration, what changed, the QA verdict, the commit, and the issue's state before and after.
|