razzou-harness 0.1.2 → 0.2.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/CHANGELOG.md +85 -0
- package/README.md +215 -25
- package/dist/agents/claude-code.js +64 -14
- package/dist/anthropic/usage.js +53 -0
- package/dist/ci/harness-ci.js +38 -0
- package/dist/cli.js +125 -16
- package/dist/daemon/architects.js +6 -2
- package/dist/daemon/design.js +4 -0
- package/dist/daemon/items.js +24 -4
- package/dist/daemon/manager.js +108 -59
- package/dist/daemon/orchestrator.js +579 -91
- package/dist/daemon/requests.js +10 -3
- package/dist/daemon/review.js +334 -0
- package/dist/dashboard/server.js +34 -6
- package/dist/design/owner-time.js +72 -0
- package/dist/design/schemas.js +95 -4
- package/dist/github/app.js +28 -4
- package/dist/github/artifacts.js +35 -0
- package/dist/github/forge.js +95 -0
- package/dist/github/setup.js +60 -10
- package/dist/review/cost-estimate.js +25 -0
- package/dist/review/paid-calls.js +68 -0
- package/dist/roles.js +8 -0
- package/dist/setup/doctor.js +81 -10
- package/dist/setup/init.js +3 -2
- package/dist/setup/setup.js +378 -0
- package/dist/state/events.js +5 -2
- package/dist/state/state.js +158 -29
- package/dist/web/THIRD-PARTY-LICENSES.md +1 -1
- package/dist/web/assets/{index-D27gAaUJ.css → index-CMViLVQH.css} +1 -1
- package/dist/web/assets/index-DLttwGM6.js +96 -0
- package/dist/web/index.html +2 -2
- package/package.json +11 -4
- package/roles/architect.md +24 -5
- package/roles/cost.md +43 -0
- package/roles/manager.md +4 -3
- package/roles/security.md +37 -0
- package/src/ci/harness-ci.ts +42 -1
- package/templates/harness.yml +64 -0
- package/dist/web/assets/index-DtxDOPz5.js +0 -96
package/dist/ci/harness-ci.js
CHANGED
|
@@ -181,6 +181,42 @@ export function prKind(author, branch, bot) {
|
|
|
181
181
|
return 'Harness';
|
|
182
182
|
return 'Owner';
|
|
183
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* A plan's kind, from its features' (v3 §3.1): CODE with no `infra` feature, INFRA with
|
|
186
|
+
* only `infra` features, MIXED otherwise. A feature without a kind is an `app` one. The
|
|
187
|
+
* same rule as the daemon's `planKind`, kept here so this file stands alone.
|
|
188
|
+
*/
|
|
189
|
+
export function planKindOf(features) {
|
|
190
|
+
const infra = features.filter((f) => f.kind === 'infra').length;
|
|
191
|
+
return infra === 0 ? 'CODE' : infra === features.length ? 'INFRA' : 'MIXED';
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The plan-kind rules of a Plan PR (v3 §3.1), from the plan file on the base branch: a
|
|
195
|
+
* tag in the title must be the plan's kind, and a CODE plan stays off the Terraform
|
|
196
|
+
* folder, so infrastructure can't reach main as code. The tag grants nothing, and a
|
|
197
|
+
* title without one, from an older daemon, passes.
|
|
198
|
+
*/
|
|
199
|
+
async function planKindProblems(input, config) {
|
|
200
|
+
const [, module, plan] = /^plan\/([^/]+)\/([^/]+)$/.exec(input.branch) ?? [];
|
|
201
|
+
if (!module || !plan)
|
|
202
|
+
return [];
|
|
203
|
+
const file = `harness/modules/${module}/plans/${plan}.json`;
|
|
204
|
+
const definition = parseJson(await input.base.read(file));
|
|
205
|
+
if (!definition || !Array.isArray(definition.features))
|
|
206
|
+
return [`${file} is missing or unreadable on the base branch, so the plan's kind is unknown`];
|
|
207
|
+
const kind = planKindOf(definition.features);
|
|
208
|
+
const problems = [];
|
|
209
|
+
const tag = /(?:^|\s)-(CODE|INFRA|MIXED)-(?:\s|$)/.exec(input.title)?.[1];
|
|
210
|
+
if (tag && tag !== kind)
|
|
211
|
+
problems.push(`the title says -${tag}-, but ${module}/${plan} is a ${kind} plan on the base branch: its features' kinds decide`);
|
|
212
|
+
if (kind === 'CODE' && config?.terraform && typeof config.terraform === 'object') {
|
|
213
|
+
const dir = (config.terraform.dir || 'iac').replace(/\/+$/, '');
|
|
214
|
+
const touched = input.changedPaths.filter((p) => p === dir || p.startsWith(`${dir}/`));
|
|
215
|
+
if (touched.length > 0)
|
|
216
|
+
problems.push(`a CODE plan may not change ${dir}/: infrastructure gets plans of its own (v3 §3.1); changed: ${touched.join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
return problems;
|
|
219
|
+
}
|
|
184
220
|
/** The `verify` job: the problems that make a PR fail, or none. */
|
|
185
221
|
export async function verify(input) {
|
|
186
222
|
const config = parseJson(await input.base.read('harness/config.json'));
|
|
@@ -195,6 +231,8 @@ export async function verify(input) {
|
|
|
195
231
|
for (const problem of await lockfileProblems(input.head))
|
|
196
232
|
problems.push(`package-lock.json: ${problem}`);
|
|
197
233
|
}
|
|
234
|
+
if (kind === 'Plan')
|
|
235
|
+
problems.push(...(await planKindProblems(input, config)));
|
|
198
236
|
if (kind === 'Arch') {
|
|
199
237
|
const rules = await protectionRules(input.head);
|
|
200
238
|
for (const path of input.changedPaths.filter((p) => !archMayChange(p, rules))) {
|
package/dist/cli.js
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
6
|
import { parseArgs } from 'node:util';
|
|
6
7
|
import { computeLock, directoryReader } from './ci/harness-ci.js';
|
|
7
8
|
import { isAlive, packageRoot, readDaemonInfo, runDaemon, version } from './daemon/main.js';
|
|
8
9
|
import { ConfigSchema, requiredChecks } from './design/schemas.js';
|
|
10
|
+
import { loadAppCredentials } from './github/app.js';
|
|
9
11
|
import { applyRepositoryRules, checkApp, createApp, installCi } from './github/setup.js';
|
|
10
12
|
import { doctor } from './setup/doctor.js';
|
|
11
13
|
import { CHECKLIST, init } from './setup/init.js';
|
|
12
14
|
import { recover } from './setup/recover.js';
|
|
15
|
+
import { setup } from './setup/setup.js';
|
|
13
16
|
import { sleep } from './util.js';
|
|
14
17
|
const HELP = `harness ${version}
|
|
15
18
|
|
|
16
19
|
Usage: harness <command> [--dir <project>]
|
|
17
20
|
|
|
21
|
+
setup [--repo <owner/name>] [--owner <login>]
|
|
22
|
+
Set up this machine and the project: every step the harness can do, asking only for
|
|
23
|
+
what is yours (a login, a token, a click on GitHub); run it again any time
|
|
18
24
|
init [--repo <owner/name>] [--owner <login>]
|
|
19
25
|
Write the harness's files into this repository, and print the setup checklist
|
|
20
26
|
up Run the daemon for the project (foreground) and open the dashboard
|
|
21
27
|
down Stop the running daemon; sandboxes keep their state
|
|
22
28
|
status Show plans, features and the pipeline
|
|
23
29
|
resume Lift a pause of the pipeline
|
|
30
|
+
mode [manual|swarm]
|
|
31
|
+
Show the mode, or switch it and apply the mode's rules on main with your gh:
|
|
32
|
+
in Swarm the daemon merges plans (docs/architecture/proposal-v3.md)
|
|
33
|
+
budget --owner-hours <n> | --agent-hours <n>
|
|
34
|
+
Set the hours of your time for the whole project, or the agent-hours threshold
|
|
24
35
|
retry <m/p/f> Grant one more attempt to an escalated feature (--note: guidance)
|
|
25
36
|
module new <id> <title>
|
|
26
37
|
Start a module: its Architect's sandbox and conversation
|
|
@@ -38,8 +49,8 @@ Usage: harness <command> [--dir <project>]
|
|
|
38
49
|
Check that the App is installed on the repository and can get a token
|
|
39
50
|
install-ci [--owner <login>]
|
|
40
51
|
Write the harness workflow, its CI script and CODEOWNERS into the project
|
|
41
|
-
github-setup --repo <owner/repo>
|
|
42
|
-
Apply merge settings and the main ruleset, with your own gh login
|
|
52
|
+
github-setup --repo <owner/repo> [--mode manual|swarm]
|
|
53
|
+
Apply merge settings and the main ruleset of the mode (the daemon's, by default), with your own gh login
|
|
43
54
|
lock Write harness/lock.json, the hashes of every protected file and field
|
|
44
55
|
doctor [--probe] [--build-images]
|
|
45
56
|
Check this machine and the project, and set up the providers;
|
|
@@ -58,6 +69,9 @@ async function main() {
|
|
|
58
69
|
name: { type: 'string' },
|
|
59
70
|
repo: { type: 'string' },
|
|
60
71
|
owner: { type: 'string' },
|
|
72
|
+
mode: { type: 'string' },
|
|
73
|
+
'owner-hours': { type: 'string' },
|
|
74
|
+
'agent-hours': { type: 'string' },
|
|
61
75
|
probe: { type: 'boolean' },
|
|
62
76
|
'build-images': { type: 'boolean' },
|
|
63
77
|
help: { type: 'boolean', short: 'h' },
|
|
@@ -75,24 +89,65 @@ async function main() {
|
|
|
75
89
|
case 'status':
|
|
76
90
|
return status(harnessDir);
|
|
77
91
|
case 'resume':
|
|
78
|
-
return ownerAction(harnessDir, '/api/resume', {});
|
|
92
|
+
return void await ownerAction(harnessDir, '/api/resume', {});
|
|
93
|
+
case 'mode': {
|
|
94
|
+
if (!argument) {
|
|
95
|
+
const file = join(harnessDir, 'state.json');
|
|
96
|
+
const state = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : undefined;
|
|
97
|
+
console.log(state?.mode ?? 'manual');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (argument !== 'manual' && argument !== 'swarm')
|
|
101
|
+
throw new Error('usage: harness mode [manual|swarm]');
|
|
102
|
+
const result = await ownerAction(harnessDir, '/api/mode', { mode: argument });
|
|
103
|
+
// GitHub's rules on main differ per mode, and only the owner's gh may change them (v3 §2.1): done here, in the same command.
|
|
104
|
+
const repo = projectConfig(projectDir)?.project.repo;
|
|
105
|
+
if (result.error || !repo)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
await applyRules(projectDir, harnessDir, repo, argument);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.log(`GitHub's rules on main were not changed: ${error.message}\nRun \`harness github-setup --repo ${repo}\` with gh logged in as an admin of the repository.`);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
case 'budget': {
|
|
116
|
+
const hours = (key) => (values[key] === undefined ? undefined : Number(values[key]));
|
|
117
|
+
const budgets = { ownerHours: hours('owner-hours'), agentHours: hours('agent-hours') };
|
|
118
|
+
if ((budgets.ownerHours === undefined && budgets.agentHours === undefined) || [budgets.ownerHours, budgets.agentHours].some((h) => h !== undefined && (!Number.isFinite(h) || h <= 0))) {
|
|
119
|
+
throw new Error('usage: harness budget --owner-hours <n> | --agent-hours <n>, with positive numbers');
|
|
120
|
+
}
|
|
121
|
+
const info = readDaemonInfo(harnessDir);
|
|
122
|
+
if (info && isAlive(info.pid))
|
|
123
|
+
return void (await ownerAction(harnessDir, '/api/budgets', budgets));
|
|
124
|
+
// The daemon is down: the hours wait for it in .harness/setup.json (v4 §5).
|
|
125
|
+
if (budgets.agentHours !== undefined)
|
|
126
|
+
throw new Error('the agent-hours threshold needs the daemon running: `harness up`');
|
|
127
|
+
const file = join(harnessDir, 'setup.json');
|
|
128
|
+
const pending = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
|
|
129
|
+
mkdirSync(harnessDir, { recursive: true });
|
|
130
|
+
writeFileSync(file, `${JSON.stringify({ ...pending, ownerHours: budgets.ownerHours }, null, 2)}\n`);
|
|
131
|
+
console.log(`${budgets.ownerHours} hours of your time: the daemon records it when it starts (harness up).`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
79
134
|
case 'retry':
|
|
80
135
|
if (!argument)
|
|
81
136
|
throw new Error('usage: harness retry <module/plan/feature>');
|
|
82
|
-
return ownerAction(harnessDir, '/api/retry', { feature: argument, note: values.note });
|
|
137
|
+
return void await ownerAction(harnessDir, '/api/retry', { feature: argument, note: values.note });
|
|
83
138
|
case 'message':
|
|
84
139
|
if (!argument)
|
|
85
140
|
throw new Error('usage: harness message <text>');
|
|
86
|
-
return ownerAction(harnessDir, '/api/message', { text: positionals.slice(1).join(' ') });
|
|
141
|
+
return void await ownerAction(harnessDir, '/api/message', { text: positionals.slice(1).join(' ') });
|
|
87
142
|
case 'answer':
|
|
88
143
|
if (!argument || positionals.length < 3)
|
|
89
144
|
throw new Error('usage: harness answer <request-id> <text>');
|
|
90
|
-
return ownerAction(harnessDir, '/api/answer', { id: argument, text: positionals.slice(2).join(' ') });
|
|
145
|
+
return void await ownerAction(harnessDir, '/api/answer', { id: argument, text: positionals.slice(2).join(' ') });
|
|
91
146
|
case 'module': {
|
|
92
147
|
const [, id, ...title] = positionals.slice(1);
|
|
93
148
|
if (argument !== 'new' || !id)
|
|
94
149
|
throw new Error('usage: harness module new <id> <title>');
|
|
95
|
-
return ownerAction(harnessDir, '/api/modules', { id, title: title.join(' ') });
|
|
150
|
+
return void await ownerAction(harnessDir, '/api/modules', { id, title: title.join(' ') });
|
|
96
151
|
}
|
|
97
152
|
case 'attach':
|
|
98
153
|
return attach(harnessDir, argument);
|
|
@@ -100,7 +155,7 @@ async function main() {
|
|
|
100
155
|
const version = Number((positionals[2] ?? '').replace(/^v/, ''));
|
|
101
156
|
if (!argument || !Number.isInteger(version) || version < 1)
|
|
102
157
|
throw new Error('usage: harness accept <module> <vN>');
|
|
103
|
-
return ownerAction(harnessDir, '/api/accept', { module: argument, version });
|
|
158
|
+
return void await ownerAction(harnessDir, '/api/accept', { module: argument, version });
|
|
104
159
|
}
|
|
105
160
|
case 'actions':
|
|
106
161
|
return listActions(harnessDir);
|
|
@@ -108,7 +163,7 @@ async function main() {
|
|
|
108
163
|
const [module, action] = (argument ?? '').split('/');
|
|
109
164
|
if (!module || !action)
|
|
110
165
|
throw new Error('usage: harness done <module>/<action> [--note <text>]');
|
|
111
|
-
return ownerAction(harnessDir, '/api/actions/done', { module, action, note: values.note });
|
|
166
|
+
return void await ownerAction(harnessDir, '/api/actions/done', { module, action, note: values.note });
|
|
112
167
|
}
|
|
113
168
|
case 'github-app':
|
|
114
169
|
return githubApp(argument, values);
|
|
@@ -120,12 +175,29 @@ async function main() {
|
|
|
120
175
|
}
|
|
121
176
|
case 'github-setup': {
|
|
122
177
|
if (!values.repo)
|
|
123
|
-
throw new Error('usage: harness github-setup --repo <owner/repo>');
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
178
|
+
throw new Error('usage: harness github-setup --repo <owner/repo> [--mode manual|swarm]');
|
|
179
|
+
if (values.mode !== undefined && values.mode !== 'manual' && values.mode !== 'swarm')
|
|
180
|
+
throw new Error('--mode is manual or swarm');
|
|
181
|
+
return applyRules(projectDir, harnessDir, values.repo, values.mode);
|
|
182
|
+
}
|
|
183
|
+
case 'setup': {
|
|
184
|
+
if (process.stdin.isTTY !== true)
|
|
185
|
+
throw new Error('harness setup needs a terminal: it asks for what only you can give. `harness doctor` lists what is missing.');
|
|
186
|
+
return setup({
|
|
187
|
+
projectDir,
|
|
188
|
+
packageDir: packageRoot,
|
|
189
|
+
version,
|
|
190
|
+
...(values.repo ? { repo: values.repo } : {}),
|
|
191
|
+
...(values.owner ? { owner: values.owner } : {}),
|
|
192
|
+
say: (line) => console.log(line),
|
|
193
|
+
ask: (question) => ask(question),
|
|
194
|
+
open: (url) => spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).on('error', () => { }).unref(),
|
|
195
|
+
interactive: (file, args) => spawnSync(file, args, { stdio: 'inherit' }).status ?? 1,
|
|
196
|
+
up: async () => {
|
|
197
|
+
await runDaemon(projectDir);
|
|
198
|
+
process.exit(0);
|
|
199
|
+
},
|
|
200
|
+
});
|
|
129
201
|
}
|
|
130
202
|
case 'doctor':
|
|
131
203
|
return runDoctor(projectDir, values);
|
|
@@ -158,6 +230,42 @@ async function main() {
|
|
|
158
230
|
console.log(HELP);
|
|
159
231
|
}
|
|
160
232
|
}
|
|
233
|
+
async function ask(question) {
|
|
234
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
235
|
+
try {
|
|
236
|
+
return (await rl.question(question)).trim();
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
rl.close();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** The project's configuration, when the directory has one. */
|
|
243
|
+
function projectConfig(projectDir) {
|
|
244
|
+
const configFile = join(projectDir, 'harness', 'config.json');
|
|
245
|
+
return existsSync(configFile) ? ConfigSchema.parse(JSON.parse(readFileSync(configFile, 'utf8'))) : undefined;
|
|
246
|
+
}
|
|
247
|
+
/** Merge settings and the ruleset of the mode on main, with the owner's own gh: the daemon's mode, unless `mode` says otherwise (v3 §2.1). */
|
|
248
|
+
async function applyRules(projectDir, harnessDir, repo, mode) {
|
|
249
|
+
const config = projectConfig(projectDir);
|
|
250
|
+
const required = config ? requiredChecks(config) : ['verify', 'checks'];
|
|
251
|
+
const stateFile = join(harnessDir, 'state.json');
|
|
252
|
+
const snapshot = existsSync(stateFile) ? JSON.parse(readFileSync(stateFile, 'utf8')) : undefined;
|
|
253
|
+
const effective = mode ?? snapshot?.mode ?? 'manual';
|
|
254
|
+
let options = { mode: effective };
|
|
255
|
+
if (effective === 'swarm') {
|
|
256
|
+
const statuses = (config?.swarm ?? ConfigSchema.shape.swarm.parse(undefined)).requiredStatuses;
|
|
257
|
+
let appId;
|
|
258
|
+
try {
|
|
259
|
+
appId = loadAppCredentials().appId;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
appId = undefined;
|
|
263
|
+
}
|
|
264
|
+
options = { mode: effective, statuses, ...(appId !== undefined ? { appId } : {}) };
|
|
265
|
+
}
|
|
266
|
+
for (const line of await applyRepositoryRules(repo, required, options))
|
|
267
|
+
console.log(`set: ${line}`);
|
|
268
|
+
}
|
|
161
269
|
async function githubApp(action, values) {
|
|
162
270
|
if (action === 'create') {
|
|
163
271
|
if (!values.org)
|
|
@@ -249,6 +357,7 @@ async function ownerAction(harnessDir, path, body) {
|
|
|
249
357
|
});
|
|
250
358
|
const result = (await response.json());
|
|
251
359
|
console.log(result.done ? 'done' : result.error ?? 'nothing to do');
|
|
360
|
+
return result;
|
|
252
361
|
}
|
|
253
362
|
/** The Architect's terminal, attached in this one. Detaching leaves the Architect running. */
|
|
254
363
|
function attach(harnessDir, module) {
|
|
@@ -116,7 +116,7 @@ export class Architects {
|
|
|
116
116
|
return folder;
|
|
117
117
|
};
|
|
118
118
|
copyFileSync(join(o.options.packageDir, 'roles', 'architect.md'), join(staging('harness'), 'architect.md'));
|
|
119
|
-
writeFileSync(join(staging('harness'), 'welcome.md'), welcome(module));
|
|
119
|
+
writeFileSync(join(staging('harness'), 'welcome.md'), welcome(module, o.state.budgets.ownerHours !== undefined));
|
|
120
120
|
for (const file of ['architect.md', 'welcome.md'])
|
|
121
121
|
await o.driver.upload(sandbox, join(staging('harness'), file), p.harness);
|
|
122
122
|
await o.uploadAgentFiles(sandbox, 'architect', dir);
|
|
@@ -264,13 +264,16 @@ export class Architects {
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
/** The first prompt of a session: a new module, or one whose sandbox was recreated without its conversation. */
|
|
267
|
-
function welcome(module) {
|
|
267
|
+
function welcome(module, budgeted) {
|
|
268
268
|
const latest = module.proposals.at(-1);
|
|
269
|
+
// The owner's time is budgeted: the Architect designs owner actions within it (v3 §4.1).
|
|
270
|
+
const budget = budgeted ? ['The owner budgets their time: call get_owner_time before you propose, and estimate every owner action.'] : [];
|
|
269
271
|
if (!latest) {
|
|
270
272
|
return clip([
|
|
271
273
|
`[harness] You are the Architect of the new module "${module.module}": ${module.title}.`,
|
|
272
274
|
'Read README.md, ARCHITECTURE.md and the other modules\' designs in harness/modules/ first.',
|
|
273
275
|
'Then greet the owner in two lines and wait: they will tell you what this module is for. Then use the grill-me skill.',
|
|
276
|
+
...budget,
|
|
274
277
|
].join('\n'), 2000);
|
|
275
278
|
}
|
|
276
279
|
const where = {
|
|
@@ -285,5 +288,6 @@ function welcome(module) {
|
|
|
285
288
|
'Your sandbox was recreated, and your earlier conversation is gone.',
|
|
286
289
|
`Your proposals are in harness/modules/${module.module}/proposals/. ${where}`,
|
|
287
290
|
'Read them, README.md and ARCHITECTURE.md, then greet the owner in two lines and wait.',
|
|
291
|
+
...budget,
|
|
288
292
|
].join('\n'), 2000);
|
|
289
293
|
}
|
package/dist/daemon/design.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { archMayChange, computeLock, protectionRules } from '../ci/harness-ci.js';
|
|
2
|
+
import { moduleOwnerTime, ownerTimeProblems } from '../design/owner-time.js';
|
|
2
3
|
import { checkScripts, definitionHash, loadDesign } from '../design/schemas.js';
|
|
3
4
|
import { HARNESS } from '../git/repo.js';
|
|
4
5
|
import { clip } from '../util.js';
|
|
@@ -54,6 +55,9 @@ export async function submitDesign(o, module, summary, head) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
// Once the owner budgets their time, every owner action is estimated, and the module fits what is available (v3 §4.1).
|
|
59
|
+
if (mine)
|
|
60
|
+
problems.push(...ownerTimeProblems(await o.ownerTime(), moduleOwnerTime(mine, o.state)));
|
|
57
61
|
if (problems.length > 0 || !mine)
|
|
58
62
|
return { ok: false, problems };
|
|
59
63
|
// The new checks, run once on the repository as it is: nothing is built yet, so they should fail.
|
package/dist/daemon/items.js
CHANGED
|
@@ -2,10 +2,12 @@ import { definitionHash } from '../design/schemas.js';
|
|
|
2
2
|
import { countedAttempts, featureKey, planKey } from '../state/state.js';
|
|
3
3
|
import { clip, tail } from '../util.js';
|
|
4
4
|
import { attemptRefName, refOf } from './refs.js';
|
|
5
|
-
|
|
5
|
+
/** What waits for a Manager: the items of its module, or of its plan alone for a plan's Manager (v3 §6.4). */
|
|
6
|
+
export function pendingItems(o, scope) {
|
|
7
|
+
const { module, plan: only } = scope;
|
|
6
8
|
const questions = [];
|
|
7
9
|
const others = [];
|
|
8
|
-
for (const feature of Object.values(o.state.features).filter((f) => f.module === module)) {
|
|
10
|
+
for (const feature of Object.values(o.state.features).filter((f) => f.module === module && (only === undefined || f.plan === only))) {
|
|
9
11
|
const last = feature.attempts.at(-1);
|
|
10
12
|
if (!last)
|
|
11
13
|
continue;
|
|
@@ -22,7 +24,7 @@ export function pendingItems(o, module) {
|
|
|
22
24
|
}
|
|
23
25
|
const rounds = o.design.config.brakes.ciFixRounds;
|
|
24
26
|
const deployRounds = o.design.config.brakes.deployFixRounds;
|
|
25
|
-
for (const plan of Object.values(o.state.plans).filter((p) => p.module === module)) {
|
|
27
|
+
for (const plan of Object.values(o.state.plans).filter((p) => p.module === module && (only === undefined || p.plan === only))) {
|
|
26
28
|
if ((plan.status === 'deploy_failed' || plan.status === 'verify_failed') && (plan.deployFixRounds ?? 0) < deployRounds) {
|
|
27
29
|
const key = planKey(plan);
|
|
28
30
|
const what = plan.status === 'deploy_failed' ? 'DEPLOY FAILED' : 'POST-DEPLOY CHECKS FAILED';
|
|
@@ -202,7 +204,25 @@ export function mailText(o, manager, mail) {
|
|
|
202
204
|
return [`[harness] ANSWER from the owner (request ${mail.requestId})`, '', `You asked: ${clip(question, 800)}`, '', `Answer: ${mail.text}`].join('\n');
|
|
203
205
|
}
|
|
204
206
|
const module = o.design.modules.find((m) => m.id === manager.module);
|
|
205
|
-
const
|
|
207
|
+
const features = (p) => p.features.map((f) => `${f.id} "${f.title}"`).join(', ');
|
|
208
|
+
if (manager.plan) {
|
|
209
|
+
// A plan's Manager, in Swarm (v3 §6.4): its plan, and where the module's other plans stand.
|
|
210
|
+
const mine = module?.plans.find((p) => p.id === manager.plan);
|
|
211
|
+
const others = (module?.plans ?? []).filter((p) => p.id !== manager.plan).map((p) => {
|
|
212
|
+
const status = o.state.plans[`${manager.module}/${p.id}`]?.status;
|
|
213
|
+
const where = status === 'completed' ? 'complete' : status ? `${status.replace(/_/g, ' ')}, with its own Manager` : 'not started';
|
|
214
|
+
return `- ${p.id} "${p.title}": ${where}`;
|
|
215
|
+
});
|
|
216
|
+
return [
|
|
217
|
+
`[harness] WELCOME: you are the Manager of plan ${manager.plan} "${mine?.title ?? ''}" of module "${manager.module}".`,
|
|
218
|
+
'',
|
|
219
|
+
`Its features: ${mine ? features(mine) : '(none)'}.`,
|
|
220
|
+
...(others.length > 0 ? ['', "The module's other plans, which are not yours:", ...others] : []),
|
|
221
|
+
'',
|
|
222
|
+
`The plan is harness/modules/${manager.module}/plans/${manager.plan}.json in your clone, next to the others. Read it now, with ARCHITECTURE.md and README.md if they exist, so you're ready for questions and reviews. Then reply in one line and wait for the next item.`,
|
|
223
|
+
].join('\n');
|
|
224
|
+
}
|
|
225
|
+
const plans = (module?.plans ?? []).map((p) => `- ${p.id} "${p.title}": ${features(p)}`);
|
|
206
226
|
return [
|
|
207
227
|
`[harness] WELCOME: you are the Manager of module "${manager.module}".`,
|
|
208
228
|
'',
|