@piagent/platform 1.46.0 → 1.46.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/CHANGELOG.md +20 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/en/architecture.md +1 -1
- package/docs/harness-conformance.md +22 -5
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +1 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/architecture.md +1 -1
- package/docs-site/architecture.html +1 -1
- package/docs-site/assets/search-en.json +1 -1
- package/docs-site/assets/search-vi.json +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +9 -9
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/update.html +3 -3
- package/docs-site/en/whats-new.html +27 -13
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +9 -9
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/releases.json +44 -0
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/update.html +3 -3
- package/docs-site/whats-new.html +27 -13
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/managed/copies.mjs +61 -0
- package/packages/piagent-core/managed/grant-share.mjs +9 -1
- package/packages/piagent-core/managed/helper-roles.mjs +5 -3
- package/packages/piagent-core/managed/helper-run.mjs +120 -52
- package/packages/piagent-core/managed/helper-settlement.mjs +5 -1
- package/packages/piagent-core/managed/ledger.mjs +187 -0
- package/packages/piagent-core/managed/objections.mjs +49 -27
- package/packages/piagent-core/managed/patch-snapshot.mjs +9 -0
- package/packages/piagent-core/managed/session.mjs +14 -14
- package/packages/piagent-core/managed/toolchain.mjs +7 -3
- package/packages/piagent-core/managed/workflow.mjs +32 -26
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/managed-failure.mjs +8 -0
- package/scripts/company-terminal.mjs +19 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { briefIssues } from './workflow.mjs';
|
|
4
|
+
|
|
5
|
+
// The objection ledger: what a helper holds against the main agent's work in
|
|
6
|
+
// one run (one member message), and what the main agent decided about it.
|
|
7
|
+
// A helper's objection to its brief, a review finding and a failed verify
|
|
8
|
+
// claim are one kind of item: open until the main agent resolves it with
|
|
9
|
+
// resolve_objection (accepted with what it changed, or rejected with what it
|
|
10
|
+
// checked and why), and a rejection of a required item goes back to the
|
|
11
|
+
// helper once; a disagreement that remains goes to the member. The run's
|
|
12
|
+
// items and decisions are one session entry when the run ends.
|
|
13
|
+
export const LEDGER_ENTRY = 'agent-watch-objections';
|
|
14
|
+
// A brief objection that blocks file changes until it is resolved: the brief
|
|
15
|
+
// rests on a premise the code contradicts, or on a reading the member's words
|
|
16
|
+
// contradict, and the helper pointed at a place that exists.
|
|
17
|
+
const PREMISE = new Set(['wrong_premise', 'conflicts_with_request']);
|
|
18
|
+
const DECISIONS = ['accepted', 'rejected'];
|
|
19
|
+
// After this many refused answers a model that cannot resolve an item goes on;
|
|
20
|
+
// the run reports the item as left without a decision.
|
|
21
|
+
export const LEDGER_REFUSALS = 3;
|
|
22
|
+
const MAX_ITEMS = 60, READ_LIMIT = 5 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
// "file:line" in a piece of evidence; null when it names none.
|
|
25
|
+
export function evidenceRef(text) {
|
|
26
|
+
const m = String(text ?? '').match(/(?:^|[\s(`'"])((?:[\w@.-]+\/)*[\w@.-]*[\w-]\.[A-Za-z0-9]+):(\d+)\b/);
|
|
27
|
+
return m ? { file: m[1], line: Number(m[2]) } : null;
|
|
28
|
+
}
|
|
29
|
+
// Whether the place exists in the project: 'ok', 'missing' (no such file, or
|
|
30
|
+
// the file is shorter), or null when there is nothing to check or the file is
|
|
31
|
+
// outside the project or too large to read.
|
|
32
|
+
export function checkRef(cwd, ref) {
|
|
33
|
+
if (!cwd || !ref?.file || path.isAbsolute(ref.file) || ref.file.split(/[\\/]/).includes('..')) return null;
|
|
34
|
+
let real;
|
|
35
|
+
try { real = fs.realpathSync(path.join(cwd, ref.file)); } catch { return 'missing'; }
|
|
36
|
+
const root = fs.realpathSync(cwd);
|
|
37
|
+
if (real !== root && !real.startsWith(root + path.sep)) return null;
|
|
38
|
+
try {
|
|
39
|
+
const stat = fs.statSync(real);
|
|
40
|
+
if (!stat.isFile()) return 'missing';
|
|
41
|
+
if (!ref.line) return 'ok';
|
|
42
|
+
if (stat.size > READ_LIMIT) return null;
|
|
43
|
+
return fs.readFileSync(real, 'utf8').split('\n').length >= ref.line ? 'ok' : 'missing';
|
|
44
|
+
} catch { return null; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const clip = (value, n) => typeof value === 'string' ? value.trim().slice(0, n) : '';
|
|
48
|
+
export const ledgerOf = run => run?.ledger ?? [];
|
|
49
|
+
const open = run => ledgerOf(run).filter(i => i.status === 'open');
|
|
50
|
+
export const blockingItems = run => open(run).filter(i => i.required && i.source === 'brief');
|
|
51
|
+
|
|
52
|
+
// Adds items from one helper answer. `source`: brief (an objection to the
|
|
53
|
+
// brief), review (blocking and major findings) or verify (failed claims).
|
|
54
|
+
export function addItems(managed, source, role, list, { required } = {}) {
|
|
55
|
+
const run = managed.run;
|
|
56
|
+
if (!run || !list?.length) return [];
|
|
57
|
+
const added = [];
|
|
58
|
+
for (const raw of list) {
|
|
59
|
+
if (run.ledger.length >= MAX_ITEMS) break;
|
|
60
|
+
const evidence = clip(raw.evidence, 500), ref = raw.file ? { file: raw.file, line: raw.line ?? null } : evidenceRef(evidence), place = checkRef(managed.cwd, ref);
|
|
61
|
+
const must = source === 'brief' ? PREMISE.has(raw.kind) && !!evidence && place !== 'missing' : source === 'review' ? raw.kind === 'blocking' : !!required;
|
|
62
|
+
const item = { id: `o${run.ledger.length + 1}`, source, role, kind: raw.kind, detail: clip(raw.detail, 500), evidence, ...(ref ? { ref } : {}), ...(place ? { place } : {}), required: must, status: 'open' };
|
|
63
|
+
run.ledger.push(item); added.push(item);
|
|
64
|
+
}
|
|
65
|
+
return added;
|
|
66
|
+
}
|
|
67
|
+
export const itemText = i => `${i.id} [${i.source === 'brief' ? i.kind : i.source === 'review' ? i.kind : 'failed claim'}] ${i.role}: ${i.detail}${i.evidence ? ` (evidence: ${i.evidence}${i.place === 'missing' ? '; the harness found no such place' : ''})` : ''}`;
|
|
68
|
+
export const setStatus = (item, status, extra = {}) => Object.assign(item, { status, ...extra });
|
|
69
|
+
export function saveLedger(managed) {
|
|
70
|
+
const items = ledgerOf(managed.run);
|
|
71
|
+
if (items.length) try { managed.session.sessionManager.appendCustomEntry(LEDGER_ENTRY, { items, at: new Date().toISOString() }); } catch { /* the report still counts them */ }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// resolve_objection: the main agent's decision on one item, kept with what it
|
|
75
|
+
// checked. An accepted item names what changed; a rejected one says why.
|
|
76
|
+
export function resolveTool(owner) {
|
|
77
|
+
return { name: 'resolve_objection', label: 'Objection',
|
|
78
|
+
description: 'Record your decision on an objection a helper raised (its id, such as o1): a brief objection, a review finding or a failed verify claim. Check it first at the place its evidence names. accepted: you agree; say what you changed (a corrected brief, the plan, the code). rejected: say what you checked (file:line, a command and its result) and why the objection does not hold; a rejected required objection goes back to the helper, and a disagreement that remains goes to the member.',
|
|
79
|
+
parameters: { type: 'object', properties: { id: { type: 'string', maxLength: 10 }, decision: { type: 'string', enum: DECISIONS }, checked: { type: 'string', minLength: 1, maxLength: 300 },
|
|
80
|
+
change: { type: 'string', maxLength: 300 }, reason: { type: 'string', maxLength: 500 } }, required: ['id', 'decision', 'checked'], additionalProperties: false },
|
|
81
|
+
execute: async (_id, args) => {
|
|
82
|
+
const item = ledgerOf(owner.run).find(i => i.id === args?.id);
|
|
83
|
+
if (!item) throw Error(`managed-objection-unknown: no objection ${String(args?.id).slice(0, 10)} in this message; the open ones are ${open(owner.run).map(i => i.id).join(', ') || 'none'}.`);
|
|
84
|
+
if (!['open', 'noted'].includes(item.status)) throw Error(`managed-objection-settled: ${item.id} is already ${item.status}.`);
|
|
85
|
+
if (!DECISIONS.includes(args.decision) || !clip(args.checked, 300)) throw Error('managed-objection-invalid: send decision accepted or rejected, and what you checked.');
|
|
86
|
+
if (args.decision === 'accepted' && !clip(args.change, 300)) throw Error('managed-objection-invalid: an accepted objection names what you changed (the brief, the plan or the code).');
|
|
87
|
+
if (args.decision === 'rejected' && !clip(args.reason, 500)) throw Error('managed-objection-invalid: a rejected objection says why it does not hold.');
|
|
88
|
+
const resolution = { decision: args.decision, checked: clip(args.checked, 300), ...(args.change ? { change: clip(args.change, 300) } : {}), ...(args.reason ? { reason: clip(args.reason, 500) } : {}) };
|
|
89
|
+
setStatus(item, args.decision, { resolution });
|
|
90
|
+
const back = args.decision === 'rejected' && item.required ? ` The ${item.role} subagent judges your reason before the turn ends.` : '';
|
|
91
|
+
return { content: [{ type: 'text', text: `Recorded: ${item.id} ${args.decision}.${back}` }], details: { id: item.id, decision: args.decision } };
|
|
92
|
+
} };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Before an edit or a command that may change files: an open premise
|
|
96
|
+
// objection is resolved first. `answer` names the model answer the call came
|
|
97
|
+
// from, so calls sent together count once.
|
|
98
|
+
export function ledgerGuard(run, answer) {
|
|
99
|
+
const items = blockingItems(run);
|
|
100
|
+
if (!items.length) return;
|
|
101
|
+
run.ledgerRefusals ??= new Set();
|
|
102
|
+
if (!run.ledgerRefusals.has(answer) && run.ledgerRefusals.size >= LEDGER_REFUSALS) { run.ledgerSkipped = true; return; }
|
|
103
|
+
run.ledgerRefusals.add(answer);
|
|
104
|
+
throw Error(`managed-objection-open: resolve ${items.map(i => i.id).join(', ')} with resolve_objection before changing files.\n${items.map(itemText).join('\n')}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The receipt's counts (report version 3): objections to briefs, those the
|
|
108
|
+
// main agent decided, the disagreements handed to the member.
|
|
109
|
+
export function briefCounts(run) {
|
|
110
|
+
const briefs = ledgerOf(run).filter(i => i.source === 'brief');
|
|
111
|
+
return { objections: briefs.length, answered: briefs.filter(i => ['accepted', 'rejected', 'withdrawn'].includes(i.status)).length };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A required rejection the helper has not judged yet; the harness asks it.
|
|
115
|
+
export const toRejudge = run => ledgerOf(run).filter(i => i.source === 'brief' && i.required && i.status === 'rejected' && !i.rejudged);
|
|
116
|
+
export function rejudgeBrief(item) {
|
|
117
|
+
return `You objected to the brief the main agent gave you:\n[${item.kind}] ${item.detail}${item.evidence ? `\nYour evidence: ${item.evidence}` : ''}\n\nThe main agent rejected the objection.\nWhat it checked: ${item.resolution.checked}\nIts reason: ${item.resolution.reason}\n\nJudge its reason against the code and the member's words. When the reason holds, end with an empty brief_issues list. When your objection still holds, repeat it in brief_issues with evidence; the member then decides.`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// One step of the completion gate (true: the gate goes round again). A
|
|
121
|
+
// rejected premise objection goes back to its helper, which withdraws it or
|
|
122
|
+
// keeps it for the member; then the items the main agent never decided are
|
|
123
|
+
// put to it once, with no new review.
|
|
124
|
+
export async function settleLedger(managed, run, { note, send, signal }) {
|
|
125
|
+
const rejected = toRejudge(run)[0];
|
|
126
|
+
if (rejected) {
|
|
127
|
+
rejected.rejudged = true; let reply;
|
|
128
|
+
await note(`Harness: the ${rejected.role} subagent judges the main agent's reason for rejecting ${rejected.id}.`, { phase: 'rejudge', role: rejected.role, id: rejected.id });
|
|
129
|
+
try { reply = (await managed.delegate({ role: rejected.role, task: rejudgeBrief(rejected) }, signal, { harness: true })).content.map(p => p.text).join('\n'); }
|
|
130
|
+
catch { return true; }
|
|
131
|
+
if (!briefIssues(reply)?.length) { setStatus(rejected, 'withdrawn'); return true; }
|
|
132
|
+
setStatus(rejected, 'disputed'); run.disputes.push({ source: rejected.role, brief: true });
|
|
133
|
+
await note(`Harness: the ${rejected.role} subagent keeps its objection; the member decides.\n${itemText(rejected)}`, { phase: 'dispute', role: rejected.role, id: rejected.id });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
const pending = undecided(run);
|
|
137
|
+
if (!pending.length || run.decideAsked) return false;
|
|
138
|
+
run.decideAsked = true;
|
|
139
|
+
await send(decideText(pending), { phase: 'answer', ids: pending.map(i => i.id), issues: pending.slice(0, 10).map(i => ({ kind: i.kind, detail: i.detail })) });
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// What the main agent still has to decide before the turn ends: required
|
|
144
|
+
// items, review findings above minor, brief objections it never answered.
|
|
145
|
+
export const undecided = run => open(run).filter(i => i.required || i.source !== 'verify');
|
|
146
|
+
export function decideText(items) {
|
|
147
|
+
return `Harness: ${items.length} objection(s) have no decision yet:\n${items.map(i => '- ' + itemText(i)).join('\n')}\nCheck each at the place its evidence names, then record your decision with resolve_objection: accepted with what you changed, or rejected with what you checked and why. Minor review findings need nothing; no new review follows.`;
|
|
148
|
+
}
|
|
149
|
+
// The final status line's words about the ledger.
|
|
150
|
+
export function ledgerWords(run) {
|
|
151
|
+
const items = ledgerOf(run), count = s => items.filter(i => i.status === s).length, words = [];
|
|
152
|
+
if (!items.length && !run.minorFindings) return null;
|
|
153
|
+
for (const [status, label] of [['accepted', 'accepted'], ['rejected', 'rejected'], ['withdrawn', 'withdrawn by the helper'], ['cleared', 'cleared by a later check'], ['disputed', 'for the member'], ['open', 'left without a decision'], ['noted', 'noted']])
|
|
154
|
+
if (count(status)) words.push(`${count(status)} ${label}`);
|
|
155
|
+
if (run.minorFindings) words.push(`${run.minorFindings} minor finding(s) listed`);
|
|
156
|
+
return `objections: ${words.join(', ')}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A review of this turn's patch replaces the findings of the one before it:
|
|
160
|
+
// its blocking and major findings are the items, its minor ones only counted.
|
|
161
|
+
// A finding it repeats keeps its item (and the main agent's decision); one it
|
|
162
|
+
// no longer reports is cleared, or withdrawn when the agent had rejected it.
|
|
163
|
+
const sameFinding = (item, f) => (item.ref?.file ?? null) === (f.file || null) && (item.ref?.line === f.line || item.detail === clip(f.issue, 500));
|
|
164
|
+
export function recordFindings(managed, findings) {
|
|
165
|
+
const run = managed.run;
|
|
166
|
+
if (!run || !findings) return;
|
|
167
|
+
// A re-review after a fix read only the files changed since: an item about another file stands.
|
|
168
|
+
const focus = managed.reviewFocus?.files ? new Set(managed.reviewFocus.files) : null;
|
|
169
|
+
const kept = findings.filter(f => f.severity !== 'minor'), live = ledgerOf(run).filter(i => i.source === 'review' && ['open', 'rejected'].includes(i.status)
|
|
170
|
+
&& (!focus || i.kind === 'blocking' || !i.ref?.file || focus.has(i.ref.file)));
|
|
171
|
+
for (const item of live) if (!kept.some(f => f.severity === item.kind && sameFinding(item, f))) setStatus(item, item.status === 'open' ? 'cleared' : 'withdrawn');
|
|
172
|
+
addItems(managed, 'review', 'review', kept.filter(f => !live.some(i => i.kind === f.severity && sameFinding(i, f))).map(f => ({ kind: f.severity, detail: f.issue, evidence: f.evidence, file: f.file || null, line: f.line })));
|
|
173
|
+
run.minorFindings = findings.length - kept.length;
|
|
174
|
+
}
|
|
175
|
+
// The files changed since the last review of this turn's patch, for a
|
|
176
|
+
// re-review after a fix; null when that cannot be told.
|
|
177
|
+
export async function reviewFocus(managed) {
|
|
178
|
+
const before = managed.reviewFiles, now = before ? await managed.changedFiles?.() : null;
|
|
179
|
+
if (!before || !now) return null;
|
|
180
|
+
const files = [...new Set([...before.keys(), ...now.keys()])].filter(name => before.get(name) !== now.get(name)).sort();
|
|
181
|
+
return files.length ? { files: files.slice(0, 200) } : null;
|
|
182
|
+
}
|
|
183
|
+
// The main agent's decisions on items of one source, for the helper that judges them again.
|
|
184
|
+
export function decisionsText(run, source) {
|
|
185
|
+
const decided = ledgerOf(run).filter(i => i.source === source && i.resolution);
|
|
186
|
+
return decided.length ? `\n\nThe main agent's recorded decisions:\n${decided.map(i => `- ${i.detail}: ${i.resolution.decision}; checked: ${i.resolution.checked}${i.resolution.reason ? `; reason: ${i.resolution.reason}` : ''}${i.resolution.change ? `; changed: ${i.resolution.change}` : ''}`).join('\n')}` : '';
|
|
187
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { briefIssues, currentPlan, planText, verifyVerdicts } from './workflow.mjs';
|
|
2
|
+
import { addItems, itemText, ledgerOf, setStatus } from './ledger.mjs';
|
|
3
|
+
export { resolveTool } from './ledger.mjs';
|
|
2
4
|
|
|
3
5
|
// A helper may object to the brief it was given, with evidence. It never
|
|
4
|
-
// decides and never does other work instead: the main agent decides and
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// stops the exchange, and the member decides.
|
|
6
|
+
// decides and never does other work instead: the main agent decides and
|
|
7
|
+
// records its decision in the ledger (ledger.mjs): accepted with what it
|
|
8
|
+
// changed, or rejected with what it checked and why. The same helper
|
|
9
|
+
// objecting a second time in a run stops the exchange, and the member decides.
|
|
8
10
|
|
|
9
11
|
// The member's own words for a helper: the current message and, while a
|
|
10
12
|
// message only continues earlier work ("tiếp tục", a short follow-up), the
|
|
@@ -33,41 +35,61 @@ export function anchorText(manager, harness) {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// Before a helper starts: once a helper objected twice in a run the member
|
|
36
|
-
// decides, so the main agent cannot send it another brief before then.
|
|
37
|
-
//
|
|
38
|
-
|
|
38
|
+
// decides, so the main agent cannot send it another brief before then. An
|
|
39
|
+
// open premise objection of the role is resolved with resolve_objection
|
|
40
|
+
// first; a new brief answers any other objection of the role. A review the
|
|
41
|
+
// main agent asks for itself: none under a required review (the harness
|
|
42
|
+
// reviews the final patch), else once, and again only after blocking findings.
|
|
43
|
+
export function beforeDelegate(run, role, harness, { since = null, review = null, changed = false, asked = false } = {}) {
|
|
39
44
|
if (!run || harness) return;
|
|
40
45
|
if (run.disputes.some(d => d.source === role && d.brief)) throw Error(`managed-helper-disputed: the ${role} subagent objected to your briefs twice for this message, so the member decides. Tell them what it objects to and what you decided; do not delegate to it again before their next message.`);
|
|
41
|
-
|
|
46
|
+
const mine = ledgerOf(run).filter(i => i.source === 'brief' && i.role === role && i.status === 'open');
|
|
47
|
+
const blocked = mine.filter(i => i.required);
|
|
48
|
+
if (blocked.length) throw Error(`managed-objection-open: the ${role} subagent objected to your brief; record your decision with resolve_objection before you brief it again.\n${blocked.map(itemText).join('\n')}`);
|
|
49
|
+
// Reviewers the member asked for (copies.mjs) run at their word.
|
|
50
|
+
if (role === 'review' && !since && changed && !asked) {
|
|
51
|
+
if (run.policy.review === 'require') throw Error('managed-review-by-harness: the harness reviews the final patch when you finish; do not ask for a review yourself. Finish the change and its checks.');
|
|
52
|
+
if (run.mainReviews > 0 && !(review?.stale && review.blocking > 0)) throw Error('managed-review-once: this change was reviewed already. Fix its blocking findings and ask again only then; fix major findings or reject them with resolve_objection; minor findings need nothing.');
|
|
53
|
+
}
|
|
54
|
+
if (role === 'review' && !since) run.mainReviews = (run.mainReviews ?? 0) + 1;
|
|
55
|
+
for (const item of mine) setStatus(item, 'accepted', { resolution: { decision: 'accepted', checked: 'sent the role a new brief', change: 'a new brief' } });
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// it is not relayed.
|
|
58
|
+
// After a helper answered: an objection to the main agent's brief goes into
|
|
59
|
+
// the ledger, is shown to the member and handed to the main agent with the
|
|
60
|
+
// answer. Returns the note the main agent reads under the helper's answer
|
|
61
|
+
// ('' when there is none). The harness's own briefs (its review, its
|
|
62
|
+
// re-check) are fixed text: an objection to one only restates the member's
|
|
63
|
+
// request, so it is not relayed.
|
|
51
64
|
export async function relayObjection(managed, role, reply, harness) {
|
|
52
65
|
const run = managed.run, issues = harness ? null : briefIssues(reply);
|
|
53
66
|
if (!run || !issues?.length) return '';
|
|
54
|
-
const second = run.
|
|
55
|
-
run.
|
|
56
|
-
if (second) run.disputes.push({ source: role, brief: true });
|
|
67
|
+
const second = ledgerOf(run).some(i => i.source === 'brief' && i.role === role), items = addItems(managed, 'brief', role, issues), list = items.map(itemText).join('\n');
|
|
68
|
+
if (second) { run.disputes.push({ source: role, brief: true }); for (const item of items) setStatus(item, 'disputed'); }
|
|
57
69
|
await managed.session.sendCustomMessage({ customType: 'agent-watch-process', display: true,
|
|
58
70
|
content: second ? `Harness: the ${role} subagent objected to a brief a second time for this message; the exchange stops and the member decides.\n${list}`
|
|
59
71
|
: `Harness: the ${role} subagent objected to the brief the main agent gave it:\n${list}`,
|
|
60
|
-
details: { phase: second ? 'dispute' : 'objection', role, by: 'main', issues: issues.map(({ kind, detail }) => ({ kind, detail })) } }, { triggerTurn: false });
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
details: { phase: second ? 'dispute' : 'objection', role, by: 'main', issues: issues.map(({ kind, detail }) => ({ kind, detail })), ids: items.map(i => i.id) } }, { triggerTurn: false });
|
|
73
|
+
if (second) return `\n\n[Harness] The ${role} subagent objected to your brief again. This is its second objection for this message, so the harness ends the exchange: do not delegate to it again before the member's next message. Tell the member plainly what it objects to and what you decided, and let them decide.`;
|
|
74
|
+
const blocking = items.filter(i => i.required);
|
|
75
|
+
return `\n\n[Harness] The ${role} subagent objected to your brief:\n${list}\nCheck each at the place its evidence names, then record your decision with resolve_objection (accepted with what you changed, or rejected with what you checked and why).${blocking.length ? ` Until you do, file changes are refused: ${blocking.map(i => i.id).join(', ')} say${blocking.length === 1 ? 's' : ''} the brief rests on a wrong premise or conflicts with the request.` : ''} A rejected objection goes back to the helper; a second objection from it for this message goes to the member.`;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
// Verify's verdicts, counted for the receipt
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
// Verify's verdicts, counted for the receipt; its failed claims go into the
|
|
79
|
+
// ledger (required under a required verify, noted otherwise). Under a
|
|
80
|
+
// required verify the failed claims are the run's to settle; once the agent
|
|
81
|
+
// has been sent back with them, only the harness's own re-check replaces
|
|
82
|
+
// them, and a claim it no longer fails is cleared.
|
|
83
|
+
export function recordVerdicts(managed, reply, harness, details) {
|
|
84
|
+
const run = managed.run, found = verifyVerdicts(reply);
|
|
70
85
|
details.verdicts = found ? Object.fromEntries(['pass', 'fail', 'unverifiable'].map(s => [s, found.filter(v => v.status === s).length])) : null;
|
|
71
|
-
if (run && found && (harness || !run.claimsAsked))
|
|
86
|
+
if (run && found && (harness || !run.claimsAsked)) {
|
|
87
|
+
run.verifyFails = found.filter(v => v.status === 'fail');
|
|
88
|
+
const failed = new Set(run.verifyFails.map(v => v.claim)), required = run.policy.verify === 'require';
|
|
89
|
+
for (const item of ledgerOf(run)) if (item.source === 'verify' && ['open', 'rejected', 'noted'].includes(item.status) && !failed.has(item.detail)) setStatus(item, 'cleared');
|
|
90
|
+
const known = new Set(ledgerOf(run).filter(i => i.source === 'verify' && i.status !== 'cleared').map(i => i.detail));
|
|
91
|
+
const added = addItems(managed, 'verify', 'verify', run.verifyFails.filter(v => !known.has(v.claim)).map(v => ({ kind: 'fail', detail: v.claim, evidence: v.evidence })), { required });
|
|
92
|
+
if (!required) for (const item of added) setStatus(item, 'noted');
|
|
93
|
+
}
|
|
72
94
|
return found ? ' · ' + Object.entries(details.verdicts).filter(([, n]) => n).map(([s, n]) => `${n} ${s}`).join(', ') : '';
|
|
73
95
|
}
|
|
@@ -176,6 +176,15 @@ export function reviewParts(snapshot, size = REVIEW_PART_BYTES) {
|
|
|
176
176
|
return { parts: parts.length ? parts : [{ files: [], text: `Patch snapshot ${snapshot.digest}: every changed file is too large to review.${skipped}` }], omitted };
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// A re-review after a fix (focus: the files changed since the last review)
|
|
180
|
+
// reads only the parts that hold one of them: the other parts were reviewed
|
|
181
|
+
// and have not changed. Without such a part, or in one part, all of it.
|
|
182
|
+
export function focusParts(review, focus) {
|
|
183
|
+
if (!focus?.files?.length || review.parts.length < 2) return review;
|
|
184
|
+
const files = new Set(focus.files), kept = review.parts.filter(part => part.files?.some(file => files.has(file)));
|
|
185
|
+
return kept.length ? { ...review, parts: kept } : review;
|
|
186
|
+
}
|
|
187
|
+
|
|
179
188
|
// One answer from the review of every part: each part's summary, then the
|
|
180
189
|
// findings of all parts (most severe first) and their objections to the brief
|
|
181
190
|
// in one JSON block. `parsed`: every part gave its JSON.
|
|
@@ -8,7 +8,7 @@ import { ensureSearchTools, trustSystemCertificates } from './toolchain.mjs';
|
|
|
8
8
|
import { inlineExtensions, managedResourceLoader, projectInstructions } from './resource-loader.mjs';
|
|
9
9
|
import { fetchPublicPage, fetchLimits } from './web-fetch.mjs'; import { guardFetch } from './fetch-guard.mjs'; import { UNTRUSTED_RULE } from './untrusted.mjs';
|
|
10
10
|
import { searchForRole } from './web-search.mjs';
|
|
11
|
-
import { shareGrant, releaseGrant } from './grant-share.mjs';
|
|
11
|
+
import { shareGrant, releaseGrant, slotArgs } from './grant-share.mjs';
|
|
12
12
|
import { managedThinkingLevels, nearestLevel } from './capabilities.mjs';
|
|
13
13
|
import { nativeManagedModel, isVendor, piProvider, studioAPI, studioPath } from './native-catalog.mjs';
|
|
14
14
|
import { fetchOriginTool } from './repository-operation.mjs';
|
|
@@ -24,7 +24,7 @@ import { readPatchDigest, readPatchSnapshot, readChangedFiles, readHead } from '
|
|
|
24
24
|
import { memberTurn } from './continuation.mjs';
|
|
25
25
|
import { workflowPolicy, repositoryChecks, sessionPlanTool, currentPlan, workflowPrompt, RunProcess, completionGate, pendingBaseline, pendingPaths, pendingHead, processEditTools } from './workflow.mjs';
|
|
26
26
|
import { workflowContextTool, workflowContextExtension } from './workflow-context.mjs';
|
|
27
|
-
import { beforeDelegate } from './objections.mjs';
|
|
27
|
+
import { beforeDelegate, resolveTool } from './objections.mjs'; import { fanOut } from './copies.mjs';
|
|
28
28
|
import { HELPER_CALLS, HELPER_ROLES, helperRoles, helperPrompt, webPrompt, delegateDescription, mainWebLookups, limitedWebTools } from './helper-roles.mjs'; import { delegateParameters, delegateArgs, SPEC_PROMPT } from './helper-settlement.mjs';
|
|
29
29
|
import { loadAgentResources } from './agent-skills.mjs';
|
|
30
30
|
import { loadPiagentThemes } from '../runtime/terminal/terminal-style.mjs';
|
|
@@ -75,8 +75,9 @@ async function patiently(ask, times) {
|
|
|
75
75
|
catch (error) { if (attempt >= times || !BUSY.test(String(error?.message))) throw error; await sleep(1500 * attempt); }
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
function verifyGrant(grant, manifest,
|
|
79
|
-
|
|
78
|
+
function verifyGrant(grant, manifest, key) {
|
|
79
|
+
const { role, slot = 1 } = slotArgs(key); // a helper beside another of its role: role#slot (grant-share.mjs)
|
|
80
|
+
if (!grant || !sameAuthority(grant, manifest.authority) || grant.role !== role || (grant.slot ?? 1) !== slot || !Number.isSafeInteger(grant.fence) || grant.fence < 1
|
|
80
81
|
|| !/^as_run_[a-f0-9-]{36}_[\w-]{43}$/.test(grant.token) || !(['claude', 'codex'].includes(grant.provider) || isVendor(grant.provider))
|
|
81
82
|
|| !manifest.models.some(m => m.id === grant.model_id && m.owned_by === grant.provider && m.provider_model_id === grant.provider_model_id)) throw Error('managed-grant-invalid');
|
|
82
83
|
}
|
|
@@ -128,7 +129,7 @@ export class ManagedSession {
|
|
|
128
129
|
if (roles.length) customTools.push({ name: 'delegate', label: 'Subagent', description: delegateDescription(roles) + SPEC_PROMPT,
|
|
129
130
|
parameters: delegateParameters(), // the brief and its structured fields (helper-settlement.mjs)
|
|
130
131
|
execute: (_id, args, signal) => self.delegate(delegateArgs(self, args), signal) });
|
|
131
|
-
customTools.push(sessionPlanTool(self));
|
|
132
|
+
customTools.push(sessionPlanTool(self), ...(roles.length ? [resolveTool(self)] : [])); // the objection ledger's decisions (ledger.mjs)
|
|
132
133
|
// The member decides what the request leaves open: numbered options, or their own words.
|
|
133
134
|
customTools.push(workflowContextTool(() => self.session.sessionManager));
|
|
134
135
|
customTools.push(askTool(self), ...memoryToolsFor(self, agentDir)); // team memory's tools, once Studio answered memory here (memory.mjs)
|
|
@@ -459,20 +460,19 @@ export class ManagedSession {
|
|
|
459
460
|
this.review = { ...this.review, stale: true };
|
|
460
461
|
this.session.sessionManager.appendCustomEntry('agent-watch-review', this.review);
|
|
461
462
|
await this.session.sendCustomMessage({ customType: 'agent-watch-review-status', display: true,
|
|
462
|
-
content: 'Review is stale: code changed after review.
|
|
463
|
+
content: 'Review is stale: code changed after review. Ask for another review only after fixing blocking findings; under a required review the harness reviews the final patch.', details: this.review }, { triggerTurn: false });
|
|
463
464
|
}
|
|
464
465
|
// `harness`: the completion gate asks, not the main agent (its brief, its turn).
|
|
465
|
-
async delegate({ role, task, title, since }, signal, { harness = false } = {}) {
|
|
466
|
+
async delegate({ role, task, title, since, ...more }, signal, { harness = false } = {}) {
|
|
466
467
|
if (!this.grant || !HELPER_ROLES.includes(role) || typeof task !== 'string' || !task.trim() || task.length > 12000 || this.helpers.has(role)) throw Error('managed-helper-unavailable');
|
|
467
468
|
const enabled = helperRoles(this.manifest);
|
|
468
469
|
if (!enabled.includes(role)) throw Error(`managed-helper-not-configured: the company Harness has no ${role} subagent${enabled.length ? `; use ${enabled.join(', ')}` : ''}.`);
|
|
469
|
-
// Studio gives a run each helper role once. Say so at once instead of
|
|
470
|
-
// letting the main agent retry a call that cannot succeed.
|
|
471
470
|
// Studio lets each helper role run HELPER_CALLS times per run (one user
|
|
472
|
-
// message)
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
471
|
+
// message), each helper the member asked for counted (copies.mjs). Say so at once.
|
|
472
|
+
const fan = fanOut(this.session?.sessionManager, role, more, harness);
|
|
473
|
+
if ((this.helperCalls.get(role) ?? 0) + (fan?.n ?? 1) > HELPER_CALLS) throw Error(`managed-helper-limit: the ${role} subagent already ran ${HELPER_CALLS} times for this user message. Go on with what it returned, or use it again after the next user message.`);
|
|
474
|
+
beforeDelegate(this.run, role, harness, { since, review: this.review, changed: (this.ownPaths?.size ?? 0) > 0, asked: !!fan });
|
|
475
|
+
const job = this.runHelper(role, task, signal, harness, title, role === 'review' && typeof since === 'string' && since.trim() ? since.trim() : null, fan); this.helpers.set(role, job); this.publishHelpers();
|
|
476
476
|
// While a helper works (a verify running a long suite) the main agent is
|
|
477
477
|
// quiet: a beat each minute tells the dashboard's watchdog, which stops a
|
|
478
478
|
// turn after 15 minutes without an event, that the turn is working.
|
|
@@ -490,7 +490,7 @@ export class ManagedSession {
|
|
|
490
490
|
notify(event) {
|
|
491
491
|
for (const listener of [...this.listeners]) { try { listener(event); } catch { /* a listener's failure is its own */ } }
|
|
492
492
|
}
|
|
493
|
-
runHelper(role, task, signal, harness, title, since = null) { return runHelper(this, role, task, signal, harness, { verifyGrant, patiently, title: typeof title === 'string' ? title : '', since }); }
|
|
493
|
+
runHelper(role, task, signal, harness, title, since = null, fan = null) { return runHelper(this, role, task, signal, harness, { verifyGrant, patiently, title: typeof title === 'string' ? title : '', since, fan }); }
|
|
494
494
|
async dispose() {
|
|
495
495
|
if (this.disposed) return;
|
|
496
496
|
this.disposed = true;
|
|
@@ -74,13 +74,17 @@ export function searchToolPath(name, userHome = os.homedir()) {
|
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
76
|
export async function ensureSearchTools(sdkRoot, userHome = os.homedir(), timeoutMs = 60_000) {
|
|
77
|
-
const missing = ['rg', 'fd'].filter(name => !searchToolPath(name, userHome));
|
|
78
|
-
if (!missing.length) return;
|
|
79
77
|
let manager;
|
|
80
78
|
try { manager = await import(pathToFileURL(path.join(sdkRoot, 'dist/utils/tools-manager.js')).href); } catch { return; }
|
|
79
|
+
// The company terminal looks fd up the way Pi does (its bin folder, then a
|
|
80
|
+
// PATH of /usr/bin:/bin) and, offline, never downloads it: without fd there
|
|
81
|
+
// @ file completion shows nothing, even with fd under /opt/homebrew/bin.
|
|
82
|
+
const missing = ['rg', 'fd'].filter(name => !searchToolPath(name, userHome) || !manager.getToolPath?.(name));
|
|
83
|
+
if (!missing.length) return;
|
|
81
84
|
await Promise.all(missing.map(async name => {
|
|
82
85
|
try {
|
|
83
|
-
|
|
86
|
+
// Pi 1.3 takes a status callback here; the former `silent` flag (true) made it throw before downloading.
|
|
87
|
+
const found = await Promise.race([manager.ensureTool(name), new Promise(resolve => setTimeout(resolve, timeoutMs).unref())]);
|
|
84
88
|
if (typeof found === 'string' && path.isAbsolute(found) && executable(found)) downloaded.set(name, found);
|
|
85
89
|
} catch { /* grep/find fall back to the built-in search */ }
|
|
86
90
|
}));
|
|
@@ -6,6 +6,7 @@ import { TsDiagnostics } from './ts-diagnostics.mjs';
|
|
|
6
6
|
import { checkpointsOf } from './checkpoints.mjs';
|
|
7
7
|
import { discoverDeliveryContext } from './delivery-context.mjs';
|
|
8
8
|
import { LANE_LIMITS, patchStats, changeLane, lanePolicy } from './risk-lanes.mjs';
|
|
9
|
+
import { briefCounts, decisionsText, ledgerGuard, ledgerOf, ledgerWords, reviewFocus, saveLedger, setStatus, settleLedger } from './ledger.mjs';
|
|
9
10
|
|
|
10
11
|
// The process layer of the company harness: a plan for multi-step work, the
|
|
11
12
|
// repository's checks after a code change, an independent review of the patch.
|
|
@@ -149,8 +150,10 @@ export function workflowPrompt(policy, checks) {
|
|
|
149
150
|
const list = checks.commands.length ? `${checks.commands.map(c => '`' + c + '`').join(', ')}${checks.source === 'detected' ? ' (detected; the repository declares none in AGENTS.md "## Checks")' : ''}` : 'none declared: choose the tests, type check or build that cover the change';
|
|
150
151
|
lines.push(`- Verify: after changing code, run the repository checks that cover the change and fix failures before you finish. Repository checks: ${list}.${policy.verify === 'require' ? ' The turn does not end until a check has passed on the final code.' : ''}`);
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
// What each severity asks: blocking is fixed or argued, major decided, minor only listed.
|
|
154
|
+
const severities = ' Blocking findings: fix them, or reject one with resolve_objection and the reviewer judges your reason; a disagreement that remains goes to the member. Major findings: fix them or reject them with resolve_objection; no new review follows. Minor findings are listed for the member and need nothing.';
|
|
155
|
+
if (policy.review !== 'off') lines.push(policy.review === 'require' ? `- Review: when you finish, the harness reviews the final patch with an independent reviewer, and after a fix only what you changed; do not ask for a review yourself.${severities}`
|
|
156
|
+
: `- Review: when your code change is finished, ask the review helper (delegate role "review") once; ask again only after fixing blocking findings.${severities}`);
|
|
154
157
|
if (policy.lanes && (policy.verify !== 'off' || policy.review !== 'off')) {
|
|
155
158
|
const L = LANE_LIMITS;
|
|
156
159
|
lines.push(`- Size: the process follows the size of your change. A small change (at most ${L.tinyFiles} files and ${L.tinyLines} changed lines, no migration, auth, security, CI, dependency or AGENTS.md file) ends after its checks, without a review: do not ask for one. A risky change (a complex request, one of those files, more than ${L.riskyFiles} files or ${L.riskyLines} lines) must pass the checks${policy.review !== 'off' ? ' and the harness review' : ''} even where they are only suggested.`);
|
|
@@ -242,11 +245,12 @@ export class RunProcess {
|
|
|
242
245
|
Object.assign(this, { policy, request: String(request ?? ''), complex, checks, startDigest: null, mutations: 0, lastCheck: null, checksRun: 0, checksFailed: 0,
|
|
243
246
|
reviews: 0, blocking: 0, blockingOpen: 0, fixLoops: 0, planUpdated: false, planRefusals: new Set(), planSkipped: false, unplannedChange: false,
|
|
244
247
|
unknownTools: 0, changed: false, verified: false, docsOnly: false, approvalDeclined: false, reviewed: false, reviewUnavailable: false, outcome: null, legacyOutcome: null,
|
|
245
|
-
//
|
|
246
|
-
// claims verify marked failed,
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
// The objection ledger (ledger.mjs), disagreements handed to the
|
|
249
|
+
// member, the claims verify marked failed, the reviews the main agent
|
|
250
|
+
// asked for, and where the harness last sent the agent back (to read
|
|
251
|
+
// its answer and see whether the code moved).
|
|
252
|
+
ledger: [], disputes: [], verifyFails: [], claimsAsked: false, claimsAt: null, claimsRechecked: false, claimsDone: false, mainReviews: 0, minorFindings: 0,
|
|
253
|
+
sentAt: null, sentDigest: null, rejudged: false, reviewSettled: false, decideAsked: false,
|
|
250
254
|
// The patch's risk lane (workflow.lanes) and the main agent's own web lookups.
|
|
251
255
|
lane: null, laneStats: null, laneDigest: null, webLookups: 0 });
|
|
252
256
|
}
|
|
@@ -295,7 +299,7 @@ export class RunProcess {
|
|
|
295
299
|
verified: this.verified, reviews: count(this.reviews), reviewed: this.reviewed, blocking: count(this.blocking), blocking_open: Math.min(this.blockingOpen, this.blocking, 1000),
|
|
296
300
|
fix_loops: this.fixLoops, outcome: (version === 3 ? this.outcome : this.legacyOutcome) ?? fallback,
|
|
297
301
|
...(version >= 2 ? { plan_skipped: this.planSkipped, unknown_tools: count(this.unknownTools) } : {}),
|
|
298
|
-
...(version === 3 ? { objections: count(this.objections
|
|
302
|
+
...(version === 3 ? { objections: count(briefCounts(this).objections), objections_answered: count(briefCounts(this).answered), disputes: count(this.disputes.length) } : {}) };
|
|
299
303
|
}
|
|
300
304
|
}
|
|
301
305
|
|
|
@@ -355,15 +359,18 @@ export async function completionGate(managed, run, signal) {
|
|
|
355
359
|
}
|
|
356
360
|
if (policy.review === 'require' && !fresh && !run.reviewSettled) {
|
|
357
361
|
// After a round of findings the reviewer also reads the agent's answer to them.
|
|
358
|
-
const answer = run.sentAt !== null ? answerSince(run.sentAt) : '';
|
|
359
|
-
|
|
362
|
+
const answer = run.sentAt !== null ? answerSince(run.sentAt) + decisionsText(run, 'review') : '';
|
|
363
|
+
// After a fix the reviewer checks its blocking findings and the files changed since.
|
|
364
|
+
const focus = run.sentAt !== null ? await reviewFocus(managed) : null, previous = (review?.findings ?? []).filter(f => f.severity === 'blocking').map((f, i) => `${i + 1}. ${findingText(f)}`).join('\n');
|
|
365
|
+
const brief = `Review the current patch against the member's request.\n\nChecks: ${run.verified ? 'a repository check passed on this code' : 'no check has passed on this code'}.${answer ? `\n\nThe main agent's answer to your previous blocking findings (the code changed after it):\n${answer}` : ''}${focus ? `\n\nSince your last review only these files changed: ${focus.files.join(', ')}. Judge whether each previous blocking finding is fixed (repeat it when it is not) and review the changes in these files; the rest of the patch was reviewed and has not changed.${previous ? `\nYour previous blocking findings:\n${previous}` : ''}` : ''}`;
|
|
366
|
+
managed.reviewFocus = focus;
|
|
360
367
|
try { await managed.delegate({ role: 'review', task: brief }, signal, { harness: true }); }
|
|
361
368
|
catch (error) {
|
|
362
369
|
run.reviewUnavailable = !stopped(); run.reviewUnavailableReason = reviewUnavailableReason(error?.message); interrupted = stopped();
|
|
363
370
|
// Why the review could not run, as a code only (the report says "failed").
|
|
364
371
|
if (run.reviewUnavailable) try { session.sessionManager.appendCustomEntry('agent-watch-failure-detail', { role: 'review', code: failureCode(error?.message) }); } catch { /* the report still says it */ }
|
|
365
372
|
break;
|
|
366
|
-
}
|
|
373
|
+
} finally { managed.reviewFocus = null; }
|
|
367
374
|
continue;
|
|
368
375
|
}
|
|
369
376
|
if (policy.review === 'require' && fresh && (review.blocking ?? 0) > 0 && !run.reviewSettled) {
|
|
@@ -373,7 +380,7 @@ export async function completionGate(managed, run, signal) {
|
|
|
373
380
|
// reviewer reads that answer once and judges its findings again.
|
|
374
381
|
run.rejudged = true;
|
|
375
382
|
await note('Harness: the main agent answered the review instead of changing the code; the reviewer judges its answer.', { phase: 'rejudge', role: 'review' });
|
|
376
|
-
const brief = `Judge your previous blocking findings on this patch again. The main agent answered them without changing the code:\n${answerSince(run.sentAt) || '(no answer)'}\n\nYour previous blocking findings:\n${list}\n\nKeep a finding as blocking when the answer does not hold against the code; leave it out when the answer is right.`;
|
|
383
|
+
const brief = `Judge your previous blocking findings on this patch again. The main agent answered them without changing the code:\n${answerSince(run.sentAt) || '(no answer)'}${decisionsText(run, 'review')}\n\nYour previous blocking findings:\n${list}\n\nKeep a finding as blocking when the answer does not hold against the code; leave it out when the answer is right.`;
|
|
377
384
|
try { await managed.delegate({ role: 'review', task: brief }, signal, { harness: true }); }
|
|
378
385
|
catch { if (stopped()) { interrupted = true; break; } run.reviewSettled = true; }
|
|
379
386
|
continue;
|
|
@@ -381,11 +388,12 @@ export async function completionGate(managed, run, signal) {
|
|
|
381
388
|
if (run.sentDigest === digest) {
|
|
382
389
|
// It heard the answer and kept its findings: the member decides.
|
|
383
390
|
run.reviewSettled = true; run.disputes.push({ source: 'review' });
|
|
391
|
+
for (const item of ledgerOf(run)) if (item.source === 'review' && item.kind === 'blocking' && ['open', 'rejected'].includes(item.status)) setStatus(item, 'disputed');
|
|
384
392
|
await note(`Harness: the main agent and the review subagent still disagree after two rounds; the member decides.\n${list}`, { phase: 'dispute', role: 'review', findings: blocking.slice(0, 10) });
|
|
385
393
|
} else if (run.fixLoops >= policy.maxFixLoops) run.reviewSettled = true;
|
|
386
394
|
else {
|
|
387
395
|
run.fixLoops += 1; run.sentDigest = digest; run.sentAt = session.messages.length;
|
|
388
|
-
await send(`Harness review of the current patch found ${blocking.length} blocking issue(s):\n${list}\nFix them, or
|
|
396
|
+
await send(`Harness review of the current patch found ${blocking.length} blocking issue(s):\n${list}\nFix them, or reject one with resolve_objection (what you checked and why): a rejection without a code change goes back to the reviewer, who judges it. After your fix the reviewer checks these findings and the files you changed, not the whole patch again.`,
|
|
389
397
|
{ phase: 'review', loop: run.fixLoops, maxLoops: policy.maxFixLoops, findings: blocking.slice(0, 10) });
|
|
390
398
|
continue;
|
|
391
399
|
}
|
|
@@ -398,30 +406,25 @@ export async function completionGate(managed, run, signal) {
|
|
|
398
406
|
const fails = run.verifyFails, list = fails.map((v, i) => `${i + 1}. ${v.claim}${v.evidence ? `\n Evidence: ${v.evidence}` : ''}`).join('\n'), claims = fails.slice(0, 10).map(v => ({ claim: v.claim }));
|
|
399
407
|
if (!run.claimsAsked) {
|
|
400
408
|
run.claimsAsked = true; run.claimsAt = session.messages.length;
|
|
401
|
-
await send(`Harness: the verify subagent marked ${fails.length} claim(s) as failed:\n${list}\nFix the cause, or
|
|
409
|
+
await send(`Harness: the verify subagent marked ${fails.length} claim(s) as failed:\n${list}\nFix the cause, or reject a verdict with resolve_objection (what you checked and why). The verify subagent then checks these claims again with your answer.`, { phase: 'claims', claims });
|
|
402
410
|
continue;
|
|
403
411
|
}
|
|
404
412
|
if (!run.claimsRechecked) {
|
|
405
413
|
run.claimsRechecked = true;
|
|
406
414
|
await note("Harness: the verify subagent checks the failed claims again with the main agent's answer.", { phase: 'rejudge', role: 'verify' });
|
|
407
|
-
try { await managed.delegate({ role: 'verify', task: `Check these claims again on the current code; they were marked failed before:\n${list}\n\nThe main agent's answer:\n${answerSince(run.claimsAt) || '(no answer)'}\n\nA claim passes only on evidence you see yourself. It stays failed when it does not hold and the result the member asked for depends on it. When the answer shows the claim is not part of what the member asked and the result does not depend on it, mark it unverifiable and say why.` }, signal, { harness: true }); }
|
|
415
|
+
try { await managed.delegate({ role: 'verify', task: `Check these claims again on the current code; they were marked failed before:\n${list}\n\nThe main agent's answer:\n${answerSince(run.claimsAt) || '(no answer)'}${decisionsText(run, 'verify')}\n\nA claim passes only on evidence you see yourself. It stays failed when it does not hold and the result the member asked for depends on it. When the answer shows the claim is not part of what the member asked and the result does not depend on it, mark it unverifiable and say why.` }, signal, { harness: true }); }
|
|
408
416
|
catch { if (stopped()) { interrupted = run.changed; break; } }
|
|
409
417
|
continue;
|
|
410
418
|
}
|
|
411
419
|
run.claimsDone = true; run.disputes.push({ source: 'verify' });
|
|
420
|
+
for (const item of ledgerOf(run)) if (item.source === 'verify' && ['open', 'accepted', 'rejected'].includes(item.status)) setStatus(item, 'disputed');
|
|
412
421
|
await note(`Harness: the main agent and the verify subagent still disagree after two rounds; the member decides.\n${list}`, { phase: 'dispute', role: 'verify', claims });
|
|
413
422
|
}
|
|
414
423
|
// Plan "require": a checklist left with open steps is the next round's
|
|
415
424
|
// (continuation.mjs), after this run's checks and review.
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
if (
|
|
419
|
-
run.objectionsAsked = true;
|
|
420
|
-
await send(`Harness: ${open.length} objection(s) to a brief got no corrected brief:\n${open.map(o => `- ${o.role} subagent: ${o.issues.map(i => `[${i.kind}] ${i.detail}`).join('; ')}`).join('\n')}\nIn your answer to the member, say in one or two sentences what you decided about ${open.length === 1 ? 'it' : 'each'} and why.`,
|
|
421
|
-
{ phase: 'answer', role: open[0].role, issues: open.flatMap(o => o.issues).slice(0, 10) });
|
|
422
|
-
for (const o of open) o.answered = true;
|
|
423
|
-
continue;
|
|
424
|
-
}
|
|
425
|
+
// A rejected premise objection goes back to its helper; then what the
|
|
426
|
+
// main agent never decided is asked once (ledger.mjs).
|
|
427
|
+
if (await settleLedger(managed, run, { note, send, signal })) continue;
|
|
425
428
|
break;
|
|
426
429
|
}
|
|
427
430
|
// However the code was changed, a complex task under plan "require" that
|
|
@@ -434,14 +437,15 @@ export async function completionGate(managed, run, signal) {
|
|
|
434
437
|
session.sessionManager.appendCustomEntry(BASELINE_ENTRY, { digest: settled ? digest : run.startDigest, settled, outcome: run.outcome, ...(managed.base ? { head: managed.base } : {}),
|
|
435
438
|
...(settled ? {} : { paths: [...(managed.ownPaths ?? [])].slice(0, 2000) }) });
|
|
436
439
|
if (run.checkpoint) await checkpointsOf(managed).finish(run);
|
|
437
|
-
|
|
440
|
+
saveLedger(managed);
|
|
441
|
+
if (!run.changed && !run.disputes.length && !ledgerOf(run).length) return;
|
|
438
442
|
const finalPlan = currentPlan(session.sessionManager), planOpen = run.planUpdated ? finalPlan?.plan?.filter(p => p.status === 'pending' || p.status === 'in_progress').length ?? 0 : 0;
|
|
439
443
|
const status = { verified: run.verified, reviewed: run.reviewed, blockingOpen: run.blockingOpen, checks: run.checksRun, fixLoops: run.fixLoops,
|
|
440
444
|
...(planOpen ? { planOpen } : {}), ...(run.planSkipped ? { planSkipped: true } : {}), ...(run.disputes.length ? { disputes: run.disputes.length } : {}), ...(run.changed ? {} : { unchanged: true }) };
|
|
441
445
|
const words = [...(run.changed ? [run.verified ? 'a check passed on the final code' : policy.verify === 'off' ? null : run.docsOnly ? 'only documentation changed: no check needed' : 'no check passed on the final code',
|
|
442
446
|
run.reviewed ? (run.blockingOpen ? `review: ${run.blockingOpen} blocking issue(s) still open` : 'review: no blocking issue') : policy.review === 'off' ? null : run.reviewUnavailable ? `review could not run (${run.reviewUnavailableReason})` : 'not reviewed',
|
|
443
447
|
run.planSkipped ? 'code was changed before the required plan existed' : null, planOpen ? `plan: ${planOpen} step(s) not completed` : null] : []),
|
|
444
|
-
run.disputes.length ? `${run.disputes.length} disagreement(s) between the main agent and a subagent go to the member` : null].filter(Boolean);
|
|
448
|
+
run.disputes.length ? `${run.disputes.length} disagreement(s) between the main agent and a subagent go to the member` : null, ledgerWords(run)].filter(Boolean);
|
|
445
449
|
await session.sendCustomMessage({ customType: 'agent-watch-process', content: `Process status for this turn: ${run.changed ? 'code changed' : 'no code changed'}; ${words.join('; ') || 'no process step configured'}.`, display: true,
|
|
446
450
|
details: { phase: 'final', outcome: run.outcome, ...status, ...(run.reviewUnavailable ? { reviewUnavailable: run.reviewUnavailableReason } : {}), policy: { verify: policy.verify, review: policy.review },
|
|
447
451
|
...(configured.lanes && run.lane ? { lane: run.lane, ...(run.laneStats ? { size: run.laneStats } : {}) } : {}) } }, { triggerTurn: false });
|
|
@@ -457,6 +461,8 @@ export function processEditTools(session, tools) {
|
|
|
457
461
|
return { ...tool, execute: async (...args) => {
|
|
458
462
|
const plan = currentPlan(session.session.sessionManager);
|
|
459
463
|
session.run?.beforeEdit(tool.name, plan, session.mainAnswers);
|
|
464
|
+
// An open premise objection is resolved before files change (ledger.mjs).
|
|
465
|
+
if (tool.name !== 'bash' || !isReadOnlyCommand(args[1]?.command)) ledgerGuard(session.run, session.mainAnswers);
|
|
460
466
|
// While a required plan is missing, a command that changes files
|
|
461
467
|
// counts as an edit: the code is compared after each command.
|
|
462
468
|
const watch = tool.name === 'bash' && !!session.run?.planMissing(plan) && !session.run.unplannedChange && session.run.startDigest !== null;
|