@hmharness/agent 0.10.0 → 0.12.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/dist/pipeline.d.ts +30 -1
- package/dist/pipeline.js +55 -2
- package/dist/project.js +7 -0
- package/package.json +2 -2
package/dist/pipeline.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runLoop, type LoopResult, type ProviderConfig, type Registry, type ToolContext } from '@hmharness/kernel';
|
|
2
|
-
|
|
2
|
+
import type { DeviceTestOptions, DeviceTestStep } from '@hmharness/domain-harmony';
|
|
3
|
+
export type PipelineStage = 'plan' | 'code' | 'test' | 'review' | 'judge' | 'device';
|
|
3
4
|
/** stage labels used in runStage (repairer = the repair-loop role) */
|
|
4
5
|
export type StageRole = PipelineStage | 'repairer';
|
|
5
6
|
export interface StageRecord {
|
|
@@ -20,6 +21,12 @@ export interface PipelineReport {
|
|
|
20
21
|
finalVerdict: 'PASS' | 'FAIL' | 'none';
|
|
21
22
|
stages: StageRecord[];
|
|
22
23
|
repairsUsed: number;
|
|
24
|
+
/** set when opts.release ran on a PASS verdict (ADR-0008) */
|
|
25
|
+
release?: {
|
|
26
|
+
projectId: string;
|
|
27
|
+
version: string;
|
|
28
|
+
checkpointId: string;
|
|
29
|
+
};
|
|
23
30
|
}
|
|
24
31
|
export interface PipelineOptions {
|
|
25
32
|
task: string;
|
|
@@ -38,6 +45,28 @@ export interface PipelineOptions {
|
|
|
38
45
|
signal?: AbortSignal;
|
|
39
46
|
/** injectable loop (tests); default kernel runLoop */
|
|
40
47
|
runLoopImpl?: typeof runLoop;
|
|
48
|
+
/** V3 device gate (ADR-0007): when set, run an on-device install/launch/
|
|
49
|
+
* log-marker/uninstall pass after the test stage and feed the four steps
|
|
50
|
+
* to the judge as mechanical evidence. Pure command execution - no model
|
|
51
|
+
* turns. */
|
|
52
|
+
deviceGate?: {
|
|
53
|
+
hdc: string;
|
|
54
|
+
target?: string;
|
|
55
|
+
hap: string;
|
|
56
|
+
bundle: string;
|
|
57
|
+
ability: string;
|
|
58
|
+
expectLog: string;
|
|
59
|
+
/** injectable runner (tests); default = domain-harmony runDeviceTest */
|
|
60
|
+
runDeviceTestImpl?: (o: DeviceTestOptions) => Promise<DeviceTestStep[]>;
|
|
61
|
+
};
|
|
62
|
+
/** V3 release slice (ADR-0008): when the final verdict is PASS, bind the
|
|
63
|
+
* work to the workspace's Project Runtime (M8): attach the run, checkpoint
|
|
64
|
+
* the tree, and record a release pinned to that checkpoint. FAIL/budget
|
|
65
|
+
* never release - no verdict, no version. */
|
|
66
|
+
release?: {
|
|
67
|
+
version: string;
|
|
68
|
+
notes?: string;
|
|
69
|
+
};
|
|
41
70
|
}
|
|
42
71
|
/** Pull the judge's verdict line out of the final text; absence = FAIL
|
|
43
72
|
* (an judge that did not follow the contract did not render a verdict). */
|
package/dist/pipeline.js
CHANGED
|
@@ -73,6 +73,7 @@ export async function runPipeline(opts) {
|
|
|
73
73
|
let repairs = 0;
|
|
74
74
|
let spent = 0;
|
|
75
75
|
let status = 'completed';
|
|
76
|
+
let releaseTick;
|
|
76
77
|
const startedAt = new Date().toISOString();
|
|
77
78
|
const runStage = async (stage, attempt, directive) => {
|
|
78
79
|
// maxTurns is only a soft checkpoint in the kernel loop - the HARD caps
|
|
@@ -107,6 +108,35 @@ export async function runPipeline(opts) {
|
|
|
107
108
|
catch { /* best-effort persistence */ }
|
|
108
109
|
return rec;
|
|
109
110
|
};
|
|
111
|
+
/** Device gate: run the four-step on-device pass, record it as a 'device'
|
|
112
|
+
* stage (no model turns spent), and mirror it into the repair context.
|
|
113
|
+
* Defined BEFORE the try block - the try body calls it (TDZ otherwise). */
|
|
114
|
+
const runDeviceGate = async () => {
|
|
115
|
+
const g = opts.deviceGate;
|
|
116
|
+
const runner = g.runDeviceTestImpl ?? (await import('@hmharness/domain-harmony')).runDeviceTest;
|
|
117
|
+
let steps;
|
|
118
|
+
try {
|
|
119
|
+
steps = await runner({ hdc: g.hdc, ...(g.target ? { target: g.target } : {}), hap: g.hap, bundle: g.bundle, ability: g.ability, expectLog: g.expectLog });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
steps = [{ step: 'device-gate', pass: false, detail: String(err).slice(0, 300) }];
|
|
123
|
+
}
|
|
124
|
+
const allPass = steps.every((s) => s.pass);
|
|
125
|
+
const rec = {
|
|
126
|
+
stage: 'device',
|
|
127
|
+
attempt: repairs + 1,
|
|
128
|
+
verdict: allPass ? 'PASS' : 'FAIL',
|
|
129
|
+
text: steps.map((s) => `${s.pass ? 'PASS' : 'FAIL'} ${s.step}: ${s.detail}`).join('\n').slice(0, 4000),
|
|
130
|
+
turns: 0,
|
|
131
|
+
toolUses: 0,
|
|
132
|
+
reason: 'final',
|
|
133
|
+
};
|
|
134
|
+
stages.push(rec);
|
|
135
|
+
try {
|
|
136
|
+
await writeFile(join(dir, `stage-${String(stages.length).padStart(2, '0')}-device.json`), JSON.stringify(rec, null, 2) + '\n', 'utf8');
|
|
137
|
+
}
|
|
138
|
+
catch { /* best-effort */ }
|
|
139
|
+
};
|
|
110
140
|
try {
|
|
111
141
|
// 1. plan
|
|
112
142
|
const plan = await runStage('plan', 1, DIRECTIVES.plan(opts.task, ''));
|
|
@@ -126,6 +156,10 @@ export async function runPipeline(opts) {
|
|
|
126
156
|
status = 'budget';
|
|
127
157
|
return await finish();
|
|
128
158
|
}
|
|
159
|
+
// 3b. device gate (V3 slice, ADR-0007): mechanical on-device evidence,
|
|
160
|
+
// zero model turns; judge sees it in the evidence summary
|
|
161
|
+
if (opts.deviceGate)
|
|
162
|
+
await runDeviceGate();
|
|
129
163
|
let review = await runStage('review', 1, DIRECTIVES.review(opts.task, ''));
|
|
130
164
|
if (spent >= totalBudget) {
|
|
131
165
|
status = 'budget';
|
|
@@ -141,6 +175,8 @@ export async function runPipeline(opts) {
|
|
|
141
175
|
break;
|
|
142
176
|
}
|
|
143
177
|
testOut = await runStage('test', repairs + 1, DIRECTIVES.test(opts.task, `Plan:\n${plan.text.slice(0, 1500)}\nRepair ${repairs} applied - re-verify.`));
|
|
178
|
+
if (opts.deviceGate)
|
|
179
|
+
await runDeviceGate();
|
|
144
180
|
review = await runStage('review', repairs + 1, DIRECTIVES.review(opts.task, `Repair ${repairs} was applied; focus on it.`));
|
|
145
181
|
if (spent >= totalBudget) {
|
|
146
182
|
status = 'budget';
|
|
@@ -148,7 +184,23 @@ export async function runPipeline(opts) {
|
|
|
148
184
|
}
|
|
149
185
|
judge = await runStage('judge', repairs + 1, DIRECTIVES.judge(opts.task, evidenceSummary(stages)));
|
|
150
186
|
}
|
|
151
|
-
|
|
187
|
+
const verdict = judge && judge.verdict !== 'n/a' ? judge.verdict : 'FAIL';
|
|
188
|
+
// V3 release slice: only a PASS verdict earns a version (ADR-0008)
|
|
189
|
+
if (verdict === 'PASS' && opts.release) {
|
|
190
|
+
try {
|
|
191
|
+
const P = await import("./project.js");
|
|
192
|
+
const proj = await P.projectFor(opts.home, opts.ctx.cwd);
|
|
193
|
+
await P.attachRun(opts.home, proj, pipelineId);
|
|
194
|
+
const cp = await P.checkpointProject(opts.home, proj, `pipeline ${pipelineId}`);
|
|
195
|
+
await P.releaseProject(opts.home, proj, opts.release.version, opts.release.notes ?? `pipeline ${pipelineId} VERDICT: PASS (${stages.length} stages, ${repairs} repairs)`);
|
|
196
|
+
releaseTick = { projectId: proj.projectId, version: opts.release.version, checkpointId: cp.id };
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
// release is bookkeeping - record the failure, never fail the pipeline
|
|
200
|
+
stages.push({ stage: 'judge', attempt: 0, verdict: 'n/a', text: `release binding failed: ${String(err).slice(0, 200)}`, turns: 0, toolUses: 0, reason: 'final' });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return await finish(verdict);
|
|
152
204
|
}
|
|
153
205
|
catch (err) {
|
|
154
206
|
status = 'error';
|
|
@@ -165,6 +217,7 @@ export async function runPipeline(opts) {
|
|
|
165
217
|
finalVerdict: verdict,
|
|
166
218
|
stages,
|
|
167
219
|
repairsUsed: repairs,
|
|
220
|
+
...(releaseTick ? { release: releaseTick } : {}),
|
|
168
221
|
};
|
|
169
222
|
try {
|
|
170
223
|
await writeFile(join(dir, 'pipeline.report.json'), JSON.stringify(report, null, 2) + '\n', 'utf8');
|
|
@@ -174,6 +227,6 @@ export async function runPipeline(opts) {
|
|
|
174
227
|
}
|
|
175
228
|
}
|
|
176
229
|
function evidenceSummary(stages) {
|
|
177
|
-
const relevant = stages.filter((s) => s.stage === 'test' || s.stage === 'review');
|
|
230
|
+
const relevant = stages.filter((s) => s.stage === 'test' || s.stage === 'review' || s.stage === 'device');
|
|
178
231
|
return relevant.map((s) => `[${s.stage} #${s.attempt}] ${s.text.slice(0, 600)}`).join('\n\n').slice(0, 3000);
|
|
179
232
|
}
|
package/dist/project.js
CHANGED
|
@@ -192,6 +192,13 @@ export async function checkpointProject(home, rec, label) {
|
|
|
192
192
|
else {
|
|
193
193
|
const rel = `artifacts/cp-${Date.now().toString(36)}`;
|
|
194
194
|
const dest = join(projDir(home, rec.projectId), rel);
|
|
195
|
+
// the copy fallback cannot handle the workspace containing the destination
|
|
196
|
+
// (workspace == home in degenerate setups) - say so plainly instead of
|
|
197
|
+
// letting cp throw EINVAL halfway through
|
|
198
|
+
const norm = (p) => resolve(p).replace(/[\\/]+$/, '').toLowerCase();
|
|
199
|
+
if (norm(dest).startsWith(norm(rec.workspace) + '\\') || norm(dest).startsWith(norm(rec.workspace) + '/')) {
|
|
200
|
+
throw new Error(`checkpoint copy fallback requires the project workspace (${rec.workspace}) to be outside HMH_HOME (${home})`);
|
|
201
|
+
}
|
|
195
202
|
await mkdir(dest, { recursive: true });
|
|
196
203
|
await cp(rec.workspace, dest, { recursive: true, filter: (src) => !COPY_SKIP.has(src.split(/[\\/]/).pop() ?? '') });
|
|
197
204
|
let files = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "hmharness agent execution layer: base tools, system prompt, sub-agent spawn, and the shared task runner that frontends (cli, web) drive.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"build": "tsc -p tsconfig.build.json"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@hmharness/domain-harmony": "0.
|
|
18
|
+
"@hmharness/domain-harmony": "0.11.0",
|
|
19
19
|
"@hmharness/domain-ops": "0.8.0",
|
|
20
20
|
"@hmharness/evolution": "0.9.0",
|
|
21
21
|
"@hmharness/kernel": "0.9.0",
|