@piagent/platform 1.45.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 +39 -0
- package/README.md +5 -3
- package/README.vi.md +5 -3
- 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 +4 -4
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +2 -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 +15 -15
- 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 +55 -29
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +15 -15
- 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 +83 -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 +55 -29
- 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/benchmark/benchmark-bootstrap.js +3 -1
- package/packages/piagent-core/extensions/agent-home.d.ts +4 -0
- package/packages/piagent-core/extensions/agent-home.js +52 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +5 -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/mcp/mcp-config-layers.js +4 -1
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/managed-failure.mjs +8 -0
- package/packages/piagent-webui/extension/piagent-webui.ts +2 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +4 -1
- package/packages/piagent-webui/gateway/pi-host.ts +2 -2
- package/packages/piagent-webui/ownership/profile-state.ts +10 -1
- package/packages/piagent-webui/server/benchmark-release-monitor.ts +2 -1
- package/scripts/benchmark-runner-support.mjs +2 -1
- package/scripts/company-terminal.mjs +19 -1
- package/scripts/dashboard-update-job.mjs +4 -2
- package/scripts/install-global.sh +113 -5
- package/scripts/model-scope-prune.mjs +2 -1
- package/scripts/personal-pi-config.mjs +2 -1
- package/scripts/personal-pi-restore.mjs +184 -0
- package/scripts/pi-usage-history.mjs +2 -1
- package/scripts/piagent-cli.mjs +49 -9
- package/scripts/piagent-dashboard.mjs +5 -3
- package/scripts/setup.sh +2 -6
- package/scripts/team-doctor.sh +11 -5
- package/scripts/uninstall-global.sh +17 -2
- package/scripts/update-global.mjs +5 -2
|
@@ -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;
|
|
@@ -44,7 +44,10 @@ export function configPathForScope(scope, options = {}) {
|
|
|
44
44
|
case "global":
|
|
45
45
|
return path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "mcp", "mcp.json");
|
|
46
46
|
case "pi-global":
|
|
47
|
-
|
|
47
|
+
// Piagent's own Pi home, not the member's personal pi (see extensions/agent-home.js).
|
|
48
|
+
if (env.PIAGENT_AGENT_DIR) return path.join(env.PIAGENT_AGENT_DIR, "mcp.json");
|
|
49
|
+
if (env.PI_CODING_AGENT_DIR && path.resolve(env.PI_CODING_AGENT_DIR) !== path.resolve(home, ".pi", "agent")) return path.join(env.PI_CODING_AGENT_DIR, "mcp.json");
|
|
50
|
+
return path.join(home, ".piagent", "agent", "mcp.json");
|
|
48
51
|
case "project":
|
|
49
52
|
return path.join(projectPath, ".mcp.json");
|
|
50
53
|
case "pi-project":
|
|
@@ -89,6 +89,14 @@ const CODES = {
|
|
|
89
89
|
'managed-helper-not-configured': ['tool', 'the company Harness does not enable this subagent'],
|
|
90
90
|
'managed-helper-limit': ['tool', 'this subagent already ran 8 times for this message'],
|
|
91
91
|
'managed-helper-disputed': ['tool', 'this subagent objected to the brief twice for this message; the member decides'],
|
|
92
|
+
'managed-objection-open': ['tool', 'a helper objected that the brief rests on a wrong premise; decide it with resolve_objection first'],
|
|
93
|
+
'managed-objection-unknown': ['tool', 'no such objection in this message'],
|
|
94
|
+
'managed-objection-settled': ['tool', 'this objection is already decided'],
|
|
95
|
+
'managed-objection-invalid': ['tool', 'a decision names what changed (accepted) or why the objection does not hold (rejected)'],
|
|
96
|
+
'managed-review-by-harness': ['tool', 'the harness reviews the final patch; the agent does not ask for one'],
|
|
97
|
+
'managed-review-once': ['tool', 'this change was reviewed already; ask again only after fixing blocking findings'],
|
|
98
|
+
'managed-copies-invalid': ['tool', 'several helpers of a role run 2 or 3 at a time, at the member\'s request'],
|
|
99
|
+
'managed-copies-not-asked': ['tool', 'more than one helper of a role runs only when the member asks for it'],
|
|
92
100
|
helper_run_limit_reached: ['tool', 'this subagent already ran 8 times for this message'],
|
|
93
101
|
'managed-helper-cancelled': ['tool', 'the subagent was stopped'],
|
|
94
102
|
'managed-helper-failed': ['failed', 'the subagent ended without an answer'],
|
|
@@ -34,6 +34,7 @@ import { VSCodeHandoff } from "./vscode-handoff.ts";
|
|
|
34
34
|
import { browserCommand } from "../../piagent-core/runtime/host/browser-opener.mjs";
|
|
35
35
|
import { TerminalSessionAdapter } from "./terminal-session-adapter.ts";
|
|
36
36
|
import { isGatewayRuntimeContext } from "../ownership/gateway-runtime-context.ts";
|
|
37
|
+
import { dormantInPersonalPi } from "../../piagent-core/extensions/agent-home.js";
|
|
37
38
|
|
|
38
39
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
39
40
|
const PLATFORM_ROOT = path.resolve(PACKAGE_ROOT, "../..");
|
|
@@ -183,6 +184,7 @@ function readMethod(provider: CoreInspectionProvider, queue: HeldMessageQueue, s
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
export default function piagentWebUiExtension(pi: ExtensionAPI): void {
|
|
187
|
+
if (dormantInPersonalPi()) return;
|
|
186
188
|
const runtimeInstanceId = createWebUiExtensionRuntimeInstanceRef();
|
|
187
189
|
let terminalAdapter: TerminalSessionAdapter | null = null;
|
|
188
190
|
const managers = new Map<string, Manager>();
|
|
@@ -53,6 +53,7 @@ import { CompanyRelay, type CompanyConnector } from "./company-relay.ts";
|
|
|
53
53
|
import { ProviderAuthBroker } from "./provider-auth-broker.ts";
|
|
54
54
|
import { McpAuthBroker } from "./mcp-auth-broker.ts";
|
|
55
55
|
import { AgentStatusFiles } from "./agent-status-files.ts";
|
|
56
|
+
import { agentWatchControlDir, piagentAgentDir } from "../../piagent-core/extensions/agent-home.js";
|
|
56
57
|
import type { ScopedBrokerRouter, RuntimeFactory } from "./session-runtime-factory.ts";
|
|
57
58
|
import { UiPreferencesStore } from "./ui-preferences-store.ts";
|
|
58
59
|
|
|
@@ -249,7 +250,9 @@ export async function startPiagentGateway(options: {
|
|
|
249
250
|
// Agent Watch reads the member's ~/.pi/agent/status. The dashboard's
|
|
250
251
|
// agentDir is that folder; the company Gateway's is its private session
|
|
251
252
|
// folder under Agent Watch, so it writes to the member's folder instead.
|
|
252
|
-
|
|
253
|
+
// Piagent's own home serves the member's dashboard; a test or benchmark home keeps its status beside it.
|
|
254
|
+
const watchRoot = options.managed || state.agentDir === piagentAgentDir() ? agentWatchControlDir() : state.agentDir;
|
|
255
|
+
statusFiles = new AgentStatusFiles(path.join(watchRoot, "status", options.managed ? "company" : "dashboard"), async () => { const catalog = await readCatalog(); if (catalog.state !== "ready") throw new Error("catalog-not-ready"); return catalog; }, () => Boolean(options.managed)); statusFiles.start();
|
|
253
256
|
const projectFolder = async (projectRef: string) => projects.resolve(projectRef)
|
|
254
257
|
?? (await runtimes!.listSessions()).find((info) => typeof info.cwd === "string" && projectRefForCwd(key, info.cwd) === projectRef)?.cwd
|
|
255
258
|
?? (relay ? (await relay.folders()).find((folder) => projectRefForCwd(key, folder) === projectRef) : undefined);
|
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { privateHostRoot } from "../../piagent-core/managed/pi-host.mjs";
|
|
8
|
+
import { agentWatchControlDir } from "../../piagent-core/extensions/agent-home.js";
|
|
8
9
|
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
10
11
|
|
|
@@ -58,8 +59,7 @@ export function installedPiHostRoot(): string {
|
|
|
58
59
|
const home = os.homedir(), hostPackage = path.join("lib", "node_modules", "@earendil-works", "pi-coding-agent");
|
|
59
60
|
let recorded: string | null = null;
|
|
60
61
|
try {
|
|
61
|
-
|
|
62
|
-
recorded = JSON.parse(fs.readFileSync(path.join(agentDir, "piagent-runtime.json"), "utf8")).pi_sdk_root ?? null;
|
|
62
|
+
recorded = JSON.parse(fs.readFileSync(path.join(agentWatchControlDir(home), "piagent-runtime.json"), "utf8")).pi_sdk_root ?? null;
|
|
63
63
|
} catch { /* no record yet */ }
|
|
64
64
|
// npm's default Windows prefixes (%APPDATA%\npm, or beside node.exe) have no lib folder.
|
|
65
65
|
const appData = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
|
|
@@ -47,8 +47,17 @@ function shortControlSocket(agentDir: string): string {
|
|
|
47
47
|
return path.join(socketRoot, `${digest}.sock`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Piagent's own Pi home (~/.piagent/agent), never the member's personal pi.
|
|
51
|
+
// Same rule as piagent-core/extensions/agent-home.js, which this layer cannot import.
|
|
52
|
+
function defaultAgentDir(): string {
|
|
53
|
+
const own = process.env.PIAGENT_AGENT_DIR?.trim(), configured = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
54
|
+
if (own) return own;
|
|
55
|
+
if (configured && path.resolve(configured) !== path.resolve(os.homedir(), ".pi", "agent")) return configured;
|
|
56
|
+
return path.join(os.homedir(), ".piagent", "agent");
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
export function gatewayProfileState(agentDirInput?: string): GatewayProfileState {
|
|
51
|
-
const agentDir = path.resolve(agentDirInput ||
|
|
60
|
+
const agentDir = path.resolve(agentDirInput || defaultAgentDir());
|
|
52
61
|
fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 });
|
|
53
62
|
const agentStat = fs.lstatSync(agentDir);
|
|
54
63
|
if (!agentStat.isDirectory() || agentStat.isSymbolicLink()
|
|
@@ -10,6 +10,7 @@ import { assertBenchmarkLedgerBinding, inspectBenchmarkLedger, validateBenchmark
|
|
|
10
10
|
import { completedBenchmarkRecord } from "../../piagent-core/benchmark/benchmark-record-validation.js";
|
|
11
11
|
import { benchmarkGitEnvironment } from "../../piagent-core/benchmark/benchmark-runtime.js";
|
|
12
12
|
import { redactSensitiveText } from "../../piagent-core/extensions/redaction-core.js";
|
|
13
|
+
import { piagentAgentDir } from "../../piagent-core/extensions/agent-home.js";
|
|
13
14
|
|
|
14
15
|
const MAX_RUNS = 20, MAX_DIRECTORIES = 100, MAX_DIRECTORY_ENTRIES = 5_000, MAX_JSON_BYTES = 32 * 1024 * 1024;
|
|
15
16
|
type Identity = { projectRef: string; runtimeInstanceId: string; sessionRef: string; taskId: string | null; taskRunId: string | null;
|
|
@@ -210,7 +211,7 @@ function releaseReadiness(cwd: string, file: string, repo: ReturnType<typeof rep
|
|
|
210
211
|
export function projectBenchmarkReleaseMonitor(input: { cwd: string; identity: Identity; generatedAt?: string; benchmarkRoot?: string; releaseReportPath?: string }) {
|
|
211
212
|
const generatedAt = input.generatedAt ?? new Date().toISOString();
|
|
212
213
|
try {
|
|
213
|
-
const repo = repository(input.cwd), root = input.benchmarkRoot ?? path.join(
|
|
214
|
+
const repo = repository(input.cwd), root = input.benchmarkRoot ?? path.join(piagentAgentDir(), "benchmarks", "piagent");
|
|
214
215
|
const releasePath = input.releaseReportPath ?? path.join(input.cwd, "plans", "codex-first-product", "evidence", "p7-local-readiness", "report.json");
|
|
215
216
|
const benchmark = benchmarkRuns(input.cwd, root, repo), release = releaseReadiness(input.cwd, releasePath, repo);
|
|
216
217
|
const revision = `release-monitor.${createHash("sha256").update(JSON.stringify({ benchmark, release })).digest("hex")}`;
|
|
@@ -14,6 +14,7 @@ import { acquireBenchmarkRunLock } from "../packages/piagent-core/benchmark/benc
|
|
|
14
14
|
import { recoverBenchmarkObservationCheckpoints } from "../packages/piagent-core/benchmark/benchmark-resume-recovery.js";
|
|
15
15
|
import { benchmarkSurfaceLabel } from "../packages/piagent-core/benchmark/benchmark-core.js";
|
|
16
16
|
import { codexModelName, codexThinkingEffort } from "../packages/piagent-core/benchmark/benchmark-codex.js";
|
|
17
|
+
import { piagentAgentDir } from "../packages/piagent-core/extensions/agent-home.js";
|
|
17
18
|
|
|
18
19
|
export function fail(message, code = 2) {
|
|
19
20
|
const error = new Error(message);
|
|
@@ -96,7 +97,7 @@ export function registeredBenchmarkCustodyRoot(runRoot) {
|
|
|
96
97
|
|
|
97
98
|
export function defaultOutputRoot(bootstrapMetadata) {
|
|
98
99
|
if (bootstrapMetadata?.defaultOutputRoot) return bootstrapMetadata.defaultOutputRoot;
|
|
99
|
-
const agentRoot =
|
|
100
|
+
const agentRoot = piagentAgentDir();
|
|
100
101
|
return path.join(agentRoot, "benchmarks", "piagent");
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -14,6 +14,7 @@ import { parseFailure } from '../packages/piagent-core/runtime/managed-failure.m
|
|
|
14
14
|
import { checkpointsOf } from '../packages/piagent-core/managed/checkpoints.mjs';
|
|
15
15
|
import { bypassAllowed, effectivePermission } from '../packages/piagent-core/managed/permission.mjs';
|
|
16
16
|
import { memoryOn } from '../packages/piagent-core/managed/memory.mjs';
|
|
17
|
+
import { currentPlan, unfinished } from '../packages/piagent-core/managed/workflow.mjs';
|
|
17
18
|
import { companyFailureText } from '../packages/piagent-webui/shared/company-copy.ts';
|
|
18
19
|
import { headlineText, outcomeText, processItems } from '../packages/piagent-webui/shared/process-copy.ts';
|
|
19
20
|
import { turnEndCopy, turnEndOf } from '../packages/piagent-webui/shared/turn-end.ts';
|
|
@@ -146,6 +147,18 @@ export function planLines(plan) {
|
|
|
146
147
|
...steps.map((p) => ` ${mark[p.status] ?? '☐'} ${p.step}${p.status === 'in_progress' ? ' ← đang làm' : p.status === 'deferred' ? ' (để sau)' : ''}`)];
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
// The panel above the input while a turn works: the checklist while a step
|
|
151
|
+
// is open, and each subagent running now with its job and latest step.
|
|
152
|
+
export function planWidget(plan) {
|
|
153
|
+
return unfinished(plan) ? planLines(plan.plan) : undefined;
|
|
154
|
+
}
|
|
155
|
+
export function helperLines(helpers) {
|
|
156
|
+
const list = Array.isArray(helpers) ? helpers.filter((h) => typeof h?.role === 'string') : [];
|
|
157
|
+
if (!list.length) return undefined;
|
|
158
|
+
const many = (role) => list.filter((h) => h.role === role).length > 1;
|
|
159
|
+
return [`Subagent đang chạy · ${list.length}`, ...list.map((h) => ` ⟳ ${h.role}${many(h.role) ? ` ${h.slot ?? 1}` : ''}${h.job ? ` · ${String(h.job).slice(0, 60)}` : ''}${h.step ? ` — ${String(h.step).slice(0, 70)}` : ''}`)];
|
|
160
|
+
}
|
|
161
|
+
|
|
149
162
|
// A harness note's details, in the shape the shared copy reads.
|
|
150
163
|
export function processInput(details) {
|
|
151
164
|
if (!details || typeof details !== 'object' || typeof details.phase !== 'string') return null;
|
|
@@ -402,7 +415,12 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
402
415
|
const showBranch = async () => { try { const branch = await branchOf(managed()?.cwd ?? ctx.cwd); ctx.ui?.setStatus?.('agent-watch-branch', branch ? `⎇ ${branch}` : undefined); } catch { /* closed meanwhile */ } };
|
|
403
416
|
ctx.ui?.setWorkingMessage?.(WORKING);
|
|
404
417
|
void showBranch();
|
|
418
|
+
// The checklist and the subagents working now stay above the input (setWidget).
|
|
419
|
+
const showPlan = () => ctx.ui?.setWidget?.('agent-watch-plan', planWidget(currentPlan(session.sessionManager)));
|
|
420
|
+
showPlan();
|
|
405
421
|
session.subscribe((event) => {
|
|
422
|
+
if (event?.type === 'tool_execution_end' && event.toolName === 'update_plan' || event?.type === 'agent_end') showPlan();
|
|
423
|
+
if (event?.type === 'managed_helper_state') ctx.ui?.setWidget?.('agent-watch-helpers', helperLines(event.helpers));
|
|
406
424
|
if (event?.type === 'managed_helpers') ctx.ui?.setStatus?.('agent-watch-helpers', event.active ? `Subagent ${event.active}/${event.maximum}` : undefined);
|
|
407
425
|
if (event?.type === 'managed_project_warning') ctx.ui?.notify?.(projectWarningText(event.warnings, event.team, LOCALE), 'warning');
|
|
408
426
|
const end = event?.type === 'managed_turn_end' ? turnEndOf(event.end) : null;
|
|
@@ -413,7 +431,7 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
413
431
|
});
|
|
414
432
|
});
|
|
415
433
|
pi.registerMessageRenderer('agent-watch-review-status', (_message, _options, theme) =>
|
|
416
|
-
box('Review đã cũ: code đổi sau khi review.
|
|
434
|
+
box('Review đã cũ: code đổi sau khi review. Chỉ cần review lại khi đã sửa lỗi blocking.', theme, 'warning'));
|
|
417
435
|
pi.registerMessageRenderer('agent-watch-helper-receipt', (message, _options, theme) =>
|
|
418
436
|
box(String(message.content ?? '').replace(/(\d[\d,]*) tokens/, '$1 token').replace('review is stale', 'review đã cũ'), theme, 'dim'));
|
|
419
437
|
|
|
@@ -59,8 +59,10 @@ if (installed !== to) finish("failed", { reason: "update-version-mismatch", ...(
|
|
|
59
59
|
// the launcher needs the binding imported again in Agent Watch.
|
|
60
60
|
let bindingChanged = false;
|
|
61
61
|
try {
|
|
62
|
-
|
|
63
|
-
const
|
|
62
|
+
// Agent Watch keeps the binding in the personal Pi folder; a test or custom home may hold its own.
|
|
63
|
+
const homes = [process.env.PI_CODING_AGENT_DIR?.trim(), path.join(home, ".pi", "agent")].filter(Boolean);
|
|
64
|
+
const file = homes.map((dir) => path.join(dir, "agent-watch-managed.json")).find((candidate) => fs.existsSync(candidate));
|
|
65
|
+
const binding = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
64
66
|
if (typeof binding?.entrypoint === "string" && typeof binding?.entrypoint_sha256 === "string")
|
|
65
67
|
bindingChanged = crypto.createHash("sha256").update(fs.readFileSync(binding.entrypoint)).digest("hex") !== binding.entrypoint_sha256;
|
|
66
68
|
} catch { /* no company binding on this machine */ }
|