@spences10/pi-harness 0.1.2 → 0.2.1

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/extension.js CHANGED
@@ -1,308 +1,96 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
- import { HARNESS_ASSESSMENT_CUSTOM_TYPE, assessment_active, assessment_context, assessment_tool_names, check_assessment_command, create_assessment_state, format_assessment_record, harness_assess_params_schema, harness_assessment_submit_params_schema, is_assessment_tool_allowed, submit_assessment_record, } from './assessment.js';
3
+ import { HARNESS_ASSESSMENT_CUSTOM_TYPE, assessment_active, assessment_context, create_assessment_state, format_assessment_record, harness_assess_params_schema, harness_assessment_submit_params_schema, submit_assessment_record, } from './assessment.js';
4
4
  import { check_command_allowed, check_path_allowed, } from './enforcement/policy.js';
5
5
  import { HARNESS_SYSTEM_PROMPT } from './prompt.js';
6
- import { harness_paths, read_contract, read_status, } from './runtime/files.js';
6
+ import { harness_paths, read_contract, read_status, shell_quote, } from './runtime/files.js';
7
7
  import { active_harness_context, amend_harness_runtime, create_harness_runtime, format_harness_status_line, format_harness_summary, update_harness_runtime, } from './runtime/index.js';
8
+ import { resolve_harness_cwd } from './runtime/verification.js';
8
9
  import { HARNESS_CUSTOM_TYPE, harness_amend_params_schema, harness_create_params_schema, harness_read_params_schema, harness_update_params_schema, } from './schema.js';
9
- function persist_active_harness(pi, harness_dir) {
10
- pi.appendEntry(HARNESS_CUSTOM_TYPE, {
11
- active_harness_dir: harness_dir,
12
- });
13
- }
14
- function restore_active_harness(ctx) {
15
- const entries = ctx.sessionManager.getEntries();
16
- return entries
17
- .filter((entry) => entry.type === 'custom' &&
18
- entry.customType === HARNESS_CUSTOM_TYPE)
19
- .pop()?.data?.active_harness_dir;
20
- }
21
- function restore_assessment(ctx) {
22
- const entries = ctx.sessionManager.getBranch();
23
- return entries
24
- .filter((entry) => entry.type === 'custom' &&
25
- entry.customType === HARNESS_ASSESSMENT_CUSTOM_TYPE)
26
- .pop()?.data;
27
- }
28
- function update_harness_ui(ctx, harness_dir) {
29
- if (!harness_dir ||
30
- !existsSync(harness_paths(harness_dir).contract)) {
31
- ctx.ui.setStatus('harness', undefined);
32
- ctx.ui.setWidget('harness', undefined);
33
- return;
34
- }
35
- ctx.ui.setStatus('harness', format_harness_status_line(harness_dir));
36
- ctx.ui.setWidget('harness', undefined);
37
- }
38
- function update_assessment_ui(ctx, state) {
39
- if (!state || !assessment_active(state)) {
40
- ctx.ui.setStatus('harness-assessment', undefined);
41
- return;
42
- }
43
- const label = state.status === 'awaiting_approval'
44
- ? '🔎 assess: approval'
45
- : '🔎 assess';
46
- ctx.ui.setStatus('harness-assessment', label);
47
- }
48
- function parse_command(args) {
49
- const [command = 'status', ...rest] = args.trim().split(/\s+/);
50
- return { command, rest: rest.join(' ').trim() };
51
- }
52
- function tool_text(text) {
53
- return { content: [{ type: 'text', text }], details: {} };
54
- }
10
+ const tool_text = (text) => ({
11
+ content: [{ type: 'text', text }],
12
+ details: {},
13
+ });
55
14
  export function should_inject_harness_prompt(event) {
56
- const selected_tools = event.systemPromptOptions?.selectedTools;
57
- return !selected_tools || selected_tools.includes('bash');
15
+ const selected = event.systemPromptOptions?.selectedTools;
16
+ return !selected || selected.includes('bash');
58
17
  }
59
- function is_terminal_harness(harness_dir) {
60
- if (!harness_dir ||
61
- !existsSync(harness_paths(harness_dir).status)) {
62
- return false;
63
- }
64
- const status = read_status(harness_dir).status;
65
- return status === 'completed' || status === 'failed';
66
- }
67
- export default async function harness(pi) {
18
+ export default function harness(pi) {
68
19
  let active_harness_dir;
69
- let assessment_state;
70
- let decision_prompt_open = false;
71
- function set_active_harness(harness_dir, ctx) {
72
- active_harness_dir = harness_dir;
73
- persist_active_harness(pi, harness_dir);
74
- update_harness_ui(ctx, harness_dir);
75
- }
76
- function persist_assessment(state, ctx) {
77
- assessment_state = state;
78
- pi.appendEntry(HARNESS_ASSESSMENT_CUSTOM_TYPE, state);
79
- update_assessment_ui(ctx, state);
20
+ let assessment;
21
+ function update_ui(ctx) {
22
+ ctx.ui.setStatus('harness', active_harness_dir &&
23
+ existsSync(harness_paths(active_harness_dir).status)
24
+ ? format_harness_status_line(active_harness_dir)
25
+ : undefined);
26
+ ctx.ui.setStatus('harness-assessment', assessment_active(assessment) ? '🔎 assess' : undefined);
80
27
  }
81
- function start_assessment(task, source, ctx) {
82
- const normalized_task = task.trim();
83
- if (assessment_active(assessment_state) &&
84
- assessment_state?.task === normalized_task) {
85
- return assessment_state;
86
- }
87
- const tools_before_assessment = assessment_active(assessment_state)
88
- ? assessment_state?.tools_before_assessment
89
- : pi.getActiveTools();
90
- const state = create_assessment_state(normalized_task, source, tools_before_assessment ?? pi.getActiveTools());
91
- persist_assessment(state, ctx);
92
- pi.setActiveTools(assessment_tool_names(state.tools_before_assessment));
93
- return state;
28
+ function set_active(dir, ctx) {
29
+ active_harness_dir = dir;
30
+ pi.appendEntry(HARNESS_CUSTOM_TYPE, { active_harness_dir: dir });
31
+ update_ui(ctx);
94
32
  }
95
- function restore_tools_after_assessment(state) {
96
- pi.setActiveTools(state.tools_before_assessment);
33
+ function start_assessment(task, ctx) {
34
+ assessment = create_assessment_state(task, 'user');
35
+ pi.appendEntry(HARNESS_ASSESSMENT_CUSTOM_TYPE, assessment);
36
+ update_ui(ctx);
37
+ return assessment;
97
38
  }
98
- function complete_assessment(decision, ctx) {
99
- const current = assessment_state;
100
- const record = current?.record;
101
- if (!current ||
102
- current.status !== 'awaiting_approval' ||
103
- !record) {
104
- ctx.ui.notify('No assessment is awaiting approval', 'warning');
105
- return;
106
- }
107
- if (decision === 'direct' &&
108
- active_harness_dir &&
109
- !is_terminal_harness(active_harness_dir)) {
110
- ctx.ui.notify('Direct work cannot bypass the active harness; submit an approved harness contract to amend it', 'warning');
111
- return;
112
- }
113
- if (decision === 'harness') {
114
- if (!record.proposed_contract) {
115
- ctx.ui.notify('This assessment has no proposed harness contract', 'warning');
116
- return;
117
- }
118
- const approved = {
119
- ...current,
120
- status: 'approved',
121
- decision,
122
- };
123
- if (active_harness_dir &&
124
- !is_terminal_harness(active_harness_dir)) {
125
- if (record.proposed_contract.cwd ||
126
- record.proposed_contract.forbidden_paths?.length ||
127
- record.proposed_contract.forbidden_commands?.length) {
128
- ctx.ui.notify('Outer-policy changes require a separate harness contract', 'warning');
129
- return;
130
- }
131
- const contract = amend_harness_runtime({
132
- harness_dir: active_harness_dir,
133
- reason: `Approved assessment ${approved.id} v${approved.version}`,
134
- requested_by: 'user',
135
- task: record.proposed_contract.task,
136
- allowed_paths: record.proposed_contract.allowed_paths,
137
- validation_commands: record.proposed_contract.validation_commands,
138
- allow_test_changes: record.proposed_contract.allow_test_changes,
139
- escalation_rules: record.proposed_contract.escalation_rules,
140
- planner_model: record.proposed_contract.planner_model,
141
- planner_thinking: record.proposed_contract.planner_thinking,
142
- executor_model: record.proposed_contract.executor_model,
143
- executor_thinking: record.proposed_contract.executor_thinking,
144
- reviewer_model: record.proposed_contract.reviewer_model,
145
- reviewer_thinking: record.proposed_contract.reviewer_thinking,
146
- });
147
- persist_assessment(approved, ctx);
148
- restore_tools_after_assessment(approved);
149
- set_active_harness(active_harness_dir, ctx);
150
- return {
151
- message: `The user approved assessment ${approved.id} v${approved.version}. Resume harness ${contract.id} with approved scaffold v${contract.scaffold.version} using the execute-harness skill. Harness approval does not authorize Factory; use Factory only after a direct user request to evaluate it.`,
152
- trigger: true,
153
- };
154
- }
155
- persist_assessment(approved, ctx);
156
- restore_tools_after_assessment(approved);
157
- const { harness_dir, contract } = create_harness_runtime(record.proposed_contract, ctx.cwd);
158
- set_active_harness(harness_dir, ctx);
159
- return {
160
- message: `The user approved assessment ${approved.id} v${approved.version}. Execute harness ${contract.id} from ${harness_dir} using the execute-harness skill. Harness approval does not authorize Factory; use Factory only after a direct user request to evaluate it.`,
161
- trigger: true,
162
- };
163
- }
164
- const completed = {
165
- ...current,
166
- status: decision === 'reject' ? 'rejected' : 'approved',
167
- decision,
168
- };
169
- persist_assessment(completed, ctx);
170
- restore_tools_after_assessment(completed);
171
- if (decision === 'direct') {
172
- return {
173
- message: `The user approved direct work from assessment ${completed.id} v${completed.version}. Proceed only with the smallest vertical slice recorded in the assessment.`,
174
- trigger: true,
175
- };
176
- }
177
- return {
178
- message: `The user rejected assessment ${completed.id} v${completed.version}. Do not implement the candidate work.`,
179
- trigger: false,
180
- };
181
- }
182
- function send_decision_message(result) {
183
- pi.sendMessage({
184
- customType: 'harness-assessment-decision',
185
- content: result.message,
186
- display: true,
187
- }, {
188
- triggerTurn: result.trigger,
189
- deliverAs: 'followUp',
190
- });
39
+ function require_active(dir) {
40
+ if (!active_harness_dir ||
41
+ resolve(dir) !== resolve(active_harness_dir))
42
+ throw new Error('Use /harness use <dir> to select this harness first');
191
43
  }
192
- async function prompt_for_assessment_decision(ctx) {
193
- const current = assessment_state;
194
- if (decision_prompt_open ||
195
- current?.status !== 'awaiting_approval' ||
196
- !current.record ||
197
- !ctx.hasUI) {
198
- return;
199
- }
200
- decision_prompt_open = true;
201
- try {
202
- const harness_is_active = active_harness_dir &&
203
- !is_terminal_harness(active_harness_dir);
204
- const options = [
205
- ...(current.record.proposed_contract
206
- ? [
207
- harness_is_active
208
- ? 'Approve and amend active harness'
209
- : 'Create and execute harness',
210
- ]
211
- : []),
212
- ...(harness_is_active ? [] : ['Proceed with direct work']),
213
- 'Reject the proposed work',
214
- 'Keep assessing',
215
- ];
216
- const choice = await ctx.ui.select(`Assessment ${current.id} v${current.version}`, options);
217
- if (assessment_state?.id !== current.id ||
218
- assessment_state.version !== current.version) {
219
- return;
220
- }
221
- const decision = choice?.startsWith('Create') || choice?.startsWith('Approve')
222
- ? 'harness'
223
- : choice?.startsWith('Proceed')
224
- ? 'direct'
225
- : choice?.startsWith('Reject')
226
- ? 'reject'
227
- : undefined;
228
- if (!decision)
229
- return;
230
- const result = complete_assessment(decision, ctx);
231
- if (result)
232
- send_decision_message(result);
233
- }
234
- finally {
235
- decision_prompt_open = false;
44
+ function restore(ctx) {
45
+ active_harness_dir = undefined;
46
+ assessment = undefined;
47
+ for (const entry of ctx.sessionManager.getBranch()) {
48
+ if (entry.type !== 'custom')
49
+ continue;
50
+ if (entry.customType === HARNESS_CUSTOM_TYPE)
51
+ active_harness_dir = entry.data?.active_harness_dir;
52
+ if (entry.customType === HARNESS_ASSESSMENT_CUSTOM_TYPE)
53
+ assessment = entry.data;
236
54
  }
55
+ if (active_harness_dir &&
56
+ existsSync(harness_paths(active_harness_dir).status) &&
57
+ read_status(active_harness_dir).status === 'completed')
58
+ set_active(undefined, ctx);
59
+ update_ui(ctx);
237
60
  }
238
61
  pi.registerCommand('assess', {
239
- description: 'Assess candidate work before choosing direct execution, a harness, or rejection',
62
+ description: 'Optional investigation and recommendation; does not lock tools or create an approval gate',
240
63
  handler: async (args, ctx) => {
241
- const trimmed = args.trim();
242
- if (!trimmed || trimmed === 'status') {
243
- if (!assessment_state) {
244
- ctx.ui.notify('No harness assessment', 'warning');
245
- return;
246
- }
247
- ctx.ui.notify(format_assessment_record(assessment_state), 'info');
248
- return;
64
+ const task = args.trim();
65
+ if (!task || task === 'status') {
66
+ ctx.ui.notify(assessment
67
+ ? format_assessment_record(assessment)
68
+ : 'No assessment', 'info');
249
69
  }
250
- if (trimmed === 'approve' || trimmed.startsWith('approve ')) {
251
- const requested = trimmed.slice('approve'.length).trim();
252
- if (requested === 'harness' || requested === 'direct') {
253
- const result = complete_assessment(requested, ctx);
254
- if (result)
255
- send_decision_message(result);
256
- return;
257
- }
258
- await prompt_for_assessment_decision(ctx);
259
- return;
70
+ else if (task === 'clear') {
71
+ assessment = undefined;
72
+ pi.appendEntry(HARNESS_ASSESSMENT_CUSTOM_TYPE, undefined);
73
+ update_ui(ctx);
260
74
  }
261
- if (trimmed === 'reject') {
262
- const result = complete_assessment('reject', ctx);
263
- if (result)
264
- send_decision_message(result);
265
- return;
75
+ else if (/^(approve|reject)(?:\s|$)/.test(task)) {
76
+ ctx.ui.notify('Assessment is advisory. No approval roundtrip is required; use /harness create when useful.', 'info');
266
77
  }
267
- if (trimmed === 'clear') {
268
- if (assessment_state && assessment_active(assessment_state)) {
269
- restore_tools_after_assessment(assessment_state);
270
- }
271
- assessment_state = undefined;
272
- pi.appendEntry(HARNESS_ASSESSMENT_CUSTOM_TYPE, undefined);
273
- update_assessment_ui(ctx, undefined);
274
- ctx.ui.notify('Cleared harness assessment', 'info');
275
- return;
78
+ else {
79
+ pi.sendUserMessage(assessment_context(start_assessment(task, ctx)));
276
80
  }
277
- const state = start_assessment(trimmed, 'user', ctx);
278
- pi.sendUserMessage(`Assess this candidate work without modifying the repository. Use repository evidence and existing primitives before recommending direct work, a harness, or rejection.\n\n${state.task}`);
279
81
  },
280
82
  });
281
83
  pi.registerCommand('harness', {
282
- description: 'Assess, run, review, and inspect ephemeral /tmp task harnesses',
84
+ description: 'Create, run, review, or inspect an optional task harness',
283
85
  handler: async (args, ctx) => {
284
- const { command, rest } = parse_command(args);
86
+ const [command = 'status', ...words] = args.trim().split(/\s+/);
87
+ const rest = words.join(' ');
285
88
  if (command === 'clear') {
286
- set_active_harness(undefined, ctx);
287
- ctx.ui.notify('Cleared active harness', 'info');
288
- return;
289
- }
290
- if (command === 'use') {
291
- if (!rest) {
292
- ctx.ui.notify('Usage: /harness use <dir>', 'warning');
293
- return;
294
- }
295
- set_active_harness(rest, ctx);
296
- ctx.ui.notify(`Active harness: ${rest}`, 'info');
297
- return;
298
- }
299
- if (command === 'status') {
300
- const harness_dir = rest || active_harness_dir;
301
- if (!harness_dir) {
302
- ctx.ui.notify('No active harness', 'warning');
303
- return;
304
- }
305
- ctx.ui.notify(format_harness_summary(harness_dir), 'info');
89
+ const had_active_harness = Boolean(active_harness_dir);
90
+ set_active(undefined, ctx);
91
+ ctx.ui.notify(had_active_harness
92
+ ? 'Harness cleared.'
93
+ : 'No active harness to clear.', 'info');
306
94
  return;
307
95
  }
308
96
  if (command === 'create') {
@@ -310,194 +98,187 @@ export default async function harness(pi) {
310
98
  ctx.ui.notify('Usage: /harness create <task>', 'warning');
311
99
  return;
312
100
  }
313
- const state = start_assessment(rest, 'harness-create', ctx);
314
- pi.sendUserMessage(`Assess this task before creating a harness. Do not treat candidate capabilities as implementation commitments.\n\n${state.task}`);
101
+ set_active(undefined, ctx);
102
+ pi.sendUserMessage(`Create a task harness with harness_create for: ${rest}. Use the project's normal acceptance checks and a suitable scope. No assessment or extra approval roundtrip is required. Do not expand the user-authorized task.`);
315
103
  return;
316
104
  }
317
- if (command === 'run') {
318
- const harness_dir = rest || active_harness_dir;
319
- if (!harness_dir) {
320
- ctx.ui.notify('Usage: /harness run <dir>', 'warning');
321
- return;
322
- }
323
- set_active_harness(harness_dir, ctx);
324
- pi.sendUserMessage(`Use the execute-harness skill to run this harness until validation completes: ${harness_dir}. Harness execution does not authorize Factory; use Factory only after a direct user request to evaluate it. If the team tool is available, create or reuse a team and create a task for this harness. Use one mutating teammate only when useful; use a worktree only when isolation and merge ownership are explicit. Then inspect team status/results before reporting.`);
105
+ const dir = rest || active_harness_dir;
106
+ if (!dir) {
107
+ ctx.ui.notify('No active harness. Use /harness create <task>.', 'info');
325
108
  return;
326
109
  }
327
- if (command === 'review') {
328
- const harness_dir = rest || active_harness_dir;
329
- if (!harness_dir) {
330
- ctx.ui.notify('Usage: /harness review <dir>', 'warning');
331
- return;
332
- }
333
- set_active_harness(harness_dir, ctx);
334
- pi.sendUserMessage(`Use the review-harness skill to review drift, shortcuts, validation evidence, and remaining risks for: ${harness_dir}`);
335
- return;
110
+ if (command === 'use' ||
111
+ command === 'run' ||
112
+ command === 'review') {
113
+ if (!existsSync(harness_paths(dir).contract))
114
+ throw new Error('Harness contract does not exist');
115
+ set_active(dir, ctx);
116
+ if (command === 'use')
117
+ ctx.ui.notify(`Active harness: ${dir}`, 'info');
118
+ else
119
+ pi.sendUserMessage(`Use the ${command === 'review' ? 'review' : 'execute'}-harness skill for ${dir}. Work directly unless the user has authorized peer collaboration; Team Mode only coordinates existing sessions.`);
336
120
  }
337
- ctx.ui.notify('Usage: /harness create|run|review|status|use|clear', 'info');
121
+ else if (command === 'status')
122
+ ctx.ui.notify(format_harness_summary(dir), 'info');
123
+ else
124
+ ctx.ui.notify('Usage: /harness create|run|review|status|use|clear', 'info');
338
125
  },
339
126
  });
340
127
  pi.registerTool({
341
128
  name: 'harness_assess',
342
- label: 'Start Harness Assessment',
343
- description: 'Enter read-only assessment before deciding whether candidate work should run directly, use a harness, or be rejected.',
344
- promptSnippet: 'Enter read-only evidence-gated assessment before adopting broad or risky work',
345
- promptGuidelines: [
346
- 'Call harness_assess by itself; after it activates, use only the remaining read-only tools.',
347
- ],
129
+ label: 'Assess Task',
130
+ description: 'Optional advisory investigation. Does not restrict tools or require another approval.',
348
131
  parameters: harness_assess_params_schema,
349
- async execute(_tool_call_id, params, _signal, _on_update, ctx) {
350
- const state = start_assessment(params.task, 'agent', ctx);
351
- return tool_text(assessment_context(state));
132
+ executionMode: 'sequential',
133
+ async execute(_id, params, _signal, _update, ctx) {
134
+ return tool_text(assessment_context(start_assessment(params.task, ctx)));
352
135
  },
353
136
  });
354
137
  pi.registerTool({
355
138
  name: 'harness_assessment_submit',
356
- label: 'Submit Harness Assessment',
357
- description: 'Submit repository evidence and one recommendation for direct work, a harness contract, or rejection. Direct user approval is still required.',
139
+ label: 'Record Assessment',
140
+ description: 'Record an optional recommendation; does not create or approve a harness.',
358
141
  parameters: harness_assessment_submit_params_schema,
359
- async execute(_tool_call_id, params, _signal, _on_update, ctx) {
360
- if (params.proposed_contract?.cwd &&
361
- resolve(params.proposed_contract.cwd) !== resolve(ctx.cwd)) {
362
- throw new Error('Proposed harness cwd must match the assessed project');
363
- }
364
- const current = assessment_state ??
365
- start_assessment(params.task, 'agent', ctx);
366
- const submitted = submit_assessment_record(current, params);
367
- persist_assessment(submitted, ctx);
368
- return tool_text(`${format_assessment_record(submitted)}\n\nWaiting for direct user approval.`);
142
+ executionMode: 'sequential',
143
+ async execute(_id, params, _signal, _update, ctx) {
144
+ assessment = submit_assessment_record(assessment ?? create_assessment_state(params.task, 'agent'), params);
145
+ pi.appendEntry(HARNESS_ASSESSMENT_CUSTOM_TYPE, assessment);
146
+ update_ui(ctx);
147
+ return tool_text(format_assessment_record(assessment));
369
148
  },
370
149
  });
371
150
  pi.registerTool({
372
151
  name: 'harness_create',
373
152
  label: 'Create Harness',
374
- description: 'Request an ephemeral /tmp task harness. An approved assessment is required before creation.',
375
- promptSnippet: 'Request constrained execution only after evidence-gated assessment and direct user approval',
153
+ description: 'Create a task contract with accepted validation commands and optional scope. Assessment is not required.',
376
154
  parameters: harness_create_params_schema,
377
- async execute(_tool_call_id, params, _signal, _on_update, ctx) {
378
- const state = start_assessment(params.task, 'harness-create', ctx);
379
- return tool_text(`Harness not created. Complete and obtain approval for assessment ${state.id}.`);
155
+ executionMode: 'sequential',
156
+ async execute(_id, params, _signal, _update, ctx) {
157
+ if (active_harness_dir)
158
+ throw new Error('A harness is already active; the user can /harness clear or /harness create a new task');
159
+ if (params.cwd && resolve(params.cwd) !== resolve(ctx.cwd))
160
+ throw new Error('Harness cwd must match the current project');
161
+ const { harness_dir } = create_harness_runtime(params, ctx.cwd);
162
+ if (assessment_active(assessment)) {
163
+ assessment = undefined;
164
+ pi.appendEntry(HARNESS_ASSESSMENT_CUSTOM_TYPE, undefined);
165
+ }
166
+ set_active(harness_dir, ctx);
167
+ return tool_text(format_harness_summary(harness_dir));
380
168
  },
381
169
  });
382
170
  pi.registerTool({
383
171
  name: 'harness_amend',
384
- label: 'Amend Harness Scaffold',
385
- description: 'Amend the active execution scaffold with an audited version change. Cannot weaken the outer runtime policy.',
386
- promptSnippet: 'Amend bounded task scope, validation, test policy, or model strategy when evidence changes',
172
+ label: 'Amend Strategy',
173
+ description: 'Adjust the execution plan without removing accepted checks or expanding the accepted maximum scope.',
387
174
  parameters: harness_amend_params_schema,
388
- async execute(_tool_call_id, params, _signal, _on_update, ctx) {
389
- const contract = amend_harness_runtime(params);
390
- set_active_harness(params.harness_dir, ctx);
391
- return tool_text(`Amended harness ${contract.id} scaffold to v${contract.scaffold.version}`);
175
+ executionMode: 'sequential',
176
+ async execute(_id, params, _signal, _update, ctx) {
177
+ require_active(params.harness_dir);
178
+ amend_harness_runtime({ ...params, requested_by: 'planner' });
179
+ update_ui(ctx);
180
+ return tool_text(format_harness_summary(params.harness_dir));
392
181
  },
393
182
  });
394
183
  pi.registerTool({
395
184
  name: 'harness_update',
396
185
  label: 'Update Harness',
397
- description: 'Update harness status, phase, notes, and validation evidence.',
186
+ description: 'Record progress. Requesting completed runs accepted checks and the drift guard through normal tool hooks before recording success.',
398
187
  parameters: harness_update_params_schema,
399
- async execute(_tool_call_id, params, _signal, _on_update, ctx) {
400
- const status_file = update_harness_runtime(params);
401
- set_active_harness(params.harness_dir, ctx);
402
- return tool_text(`Updated harness ${status_file.id}: ${status_file.status}`);
188
+ executionMode: 'sequential',
189
+ async execute(_id, params, signal, _update, ctx) {
190
+ require_active(params.harness_dir);
191
+ try {
192
+ const status = await update_harness_runtime(params, {
193
+ cwd: ctx.cwd,
194
+ signal,
195
+ run: async (command, cwd, run_signal) => {
196
+ const outcome = await ctx.executeTool('bash', { command: `cd -- ${shell_quote(cwd)} && ${command}` }, { signal: run_signal });
197
+ return {
198
+ passed: outcome.isError === false,
199
+ output: JSON.stringify(outcome.result),
200
+ };
201
+ },
202
+ });
203
+ return tool_text(`Harness ${status.status}. Evidence: ${harness_paths(params.harness_dir).outcome_markdown}`);
204
+ }
205
+ finally {
206
+ update_ui(ctx);
207
+ }
403
208
  },
404
209
  });
405
210
  pi.registerTool({
406
211
  name: 'harness_read',
407
212
  label: 'Read Harness',
408
- description: 'Read a harness contract, status, and runtime file locations.',
213
+ description: 'Read the current task contract and outcome locations.',
409
214
  parameters: harness_read_params_schema,
410
- async execute(_tool_call_id, params) {
215
+ async execute(_id, params) {
411
216
  return tool_text(format_harness_summary(params.harness_dir));
412
217
  },
413
218
  });
414
- pi.on('session_start', async (_event, ctx) => {
415
- active_harness_dir = restore_active_harness(ctx);
416
- assessment_state = restore_assessment(ctx);
417
- if (is_terminal_harness(active_harness_dir)) {
418
- set_active_harness(undefined, ctx);
419
- }
420
- if (assessment_active(assessment_state)) {
421
- pi.setActiveTools(assessment_tool_names(pi.getActiveTools()));
422
- }
423
- update_harness_ui(ctx, active_harness_dir);
424
- update_assessment_ui(ctx, assessment_state);
425
- });
219
+ pi.on('session_start', async (_event, ctx) => restore(ctx));
220
+ pi.on('session_tree', async (_event, ctx) => restore(ctx));
426
221
  pi.on('input', async (event, ctx) => {
427
222
  if (event.source !== 'extension' &&
428
- is_terminal_harness(active_harness_dir)) {
429
- set_active_harness(undefined, ctx);
430
- }
223
+ active_harness_dir &&
224
+ existsSync(harness_paths(active_harness_dir).status) &&
225
+ ['completed', 'failed'].includes(read_status(active_harness_dir).status))
226
+ set_active(undefined, ctx);
431
227
  });
432
228
  pi.on('before_agent_start', async (event) => {
433
- let system_prompt = event.systemPrompt;
434
- if (should_inject_harness_prompt(event)) {
435
- system_prompt = `${system_prompt}\n\n${HARNESS_SYSTEM_PROMPT}`;
436
- }
437
- const current_assessment = assessment_state;
438
- if (current_assessment && assessment_active(current_assessment)) {
439
- system_prompt = `${system_prompt}\n\n${assessment_context(current_assessment)}`;
229
+ const contexts = [
230
+ should_inject_harness_prompt(event)
231
+ ? HARNESS_SYSTEM_PROMPT
232
+ : '',
233
+ assessment_active(assessment) && assessment
234
+ ? assessment_context(assessment)
235
+ : '',
236
+ active_harness_dir &&
237
+ existsSync(harness_paths(active_harness_dir).contract)
238
+ ? active_harness_context(active_harness_dir)
239
+ : '',
240
+ ].filter(Boolean);
241
+ if (contexts.length) {
242
+ event.systemPromptOptions.sections.harness =
243
+ contexts.join('\n\n');
440
244
  }
441
- if (active_harness_dir &&
442
- existsSync(harness_paths(active_harness_dir).contract)) {
443
- system_prompt = `${system_prompt}\n\n${active_harness_context(active_harness_dir)}`;
245
+ else {
246
+ delete event.systemPromptOptions.sections.harness;
444
247
  }
445
- return { systemPrompt: system_prompt };
446
248
  });
447
249
  pi.on('tool_call', async (event, ctx) => {
448
- if (event.toolName === 'harness_create') {
449
- const input = event.input;
450
- const task = input.task?.trim() || 'Unspecified harness request';
451
- const state = start_assessment(task, 'harness-create', ctx);
452
- return {
453
- block: true,
454
- reason: `Harness creation requires direct approval of an assessment. Assessment ${state.id} is active; investigate and call harness_assessment_submit.`,
455
- };
456
- }
457
- const current_assessment = assessment_state;
458
- if (current_assessment && assessment_active(current_assessment)) {
459
- if (!is_assessment_tool_allowed(event.toolName)) {
460
- return {
461
- block: true,
462
- reason: `Assessment ${current_assessment.id}: tool ${event.toolName} is not read-only and is disabled until the user decides.`,
463
- };
464
- }
465
- if (event.toolName === 'bash') {
466
- const input = event.input;
467
- const result = check_assessment_command(input.command ?? '');
468
- if (!result.ok) {
469
- return { block: true, reason: result.reason };
470
- }
471
- }
472
- }
473
250
  if (!active_harness_dir ||
474
- !existsSync(harness_paths(active_harness_dir).contract)) {
251
+ !existsSync(harness_paths(active_harness_dir).contract))
475
252
  return;
476
- }
477
253
  const contract = read_contract(active_harness_dir);
478
254
  if (event.toolName === 'bash') {
479
- const input = event.input;
480
- const command = input.command ?? '';
481
- const result = check_command_allowed(contract, command);
255
+ const result = check_command_allowed(contract, String(event.input.command ?? ''));
482
256
  if (!result.ok)
483
257
  return { block: true, reason: result.reason };
484
258
  }
485
- if (event.toolName === 'edit' || event.toolName === 'write') {
259
+ if (event.toolName === 'write' || event.toolName === 'edit') {
486
260
  const input = event.input;
487
- if (!input.path)
488
- return;
489
- const result = check_path_allowed(contract, input.path);
261
+ const path = input.path ?? input.file_path ?? input.filePath;
262
+ if (typeof path !== 'string')
263
+ return {
264
+ block: true,
265
+ reason: 'Harness mutation requires a known file path',
266
+ };
267
+ let cwd;
268
+ try {
269
+ cwd = resolve_harness_cwd(contract, ctx.cwd);
270
+ }
271
+ catch (error) {
272
+ return { block: true, reason: String(error) };
273
+ }
274
+ const result = check_path_allowed({ ...contract, policy: { ...contract.policy, cwd } }, path);
490
275
  if (!result.ok)
491
276
  return { block: true, reason: result.reason };
492
277
  }
493
278
  });
494
- pi.on('agent_end', async (_event, ctx) => {
495
- await prompt_for_assessment_decision(ctx);
496
- });
497
279
  pi.on('session_shutdown', async (_event, ctx) => {
498
280
  ctx.ui.setStatus('harness', undefined);
499
281
  ctx.ui.setStatus('harness-assessment', undefined);
500
- ctx.ui.setWidget('harness', undefined);
501
282
  });
502
283
  }
503
284
  //# sourceMappingURL=extension.js.map