@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +5 -3
  3. package/README.vi.md +5 -3
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +6 -6
  6. package/docs/en/architecture.md +1 -1
  7. package/docs/harness-conformance.md +22 -5
  8. package/docs/languages.json +4 -4
  9. package/docs/operator-manual-vietnamese.md +7 -7
  10. package/docs/publishing-for-teams.md +2 -0
  11. package/docs/release-install-policy.md +2 -2
  12. package/docs/team-onboarding.md +2 -2
  13. package/docs/vi/architecture.md +1 -1
  14. package/docs-site/architecture.html +1 -1
  15. package/docs-site/assets/search-en.json +1 -1
  16. package/docs-site/assets/search-vi.json +1 -1
  17. package/docs-site/benchmark.html +1 -1
  18. package/docs-site/commands.html +1 -1
  19. package/docs-site/context-engine.html +1 -1
  20. package/docs-site/ecosystem.html +1 -1
  21. package/docs-site/en/architecture.html +1 -1
  22. package/docs-site/en/benchmark.html +1 -1
  23. package/docs-site/en/commands.html +1 -1
  24. package/docs-site/en/context-engine.html +1 -1
  25. package/docs-site/en/ecosystem.html +1 -1
  26. package/docs-site/en/index.html +15 -15
  27. package/docs-site/en/mcp-approval.html +1 -1
  28. package/docs-site/en/mcp-auth.html +1 -1
  29. package/docs-site/en/mcp-servers.html +1 -1
  30. package/docs-site/en/mcp.html +1 -1
  31. package/docs-site/en/permissions.html +1 -1
  32. package/docs-site/en/profiles.html +1 -1
  33. package/docs-site/en/quickstart.html +1 -1
  34. package/docs-site/en/runtime.html +1 -1
  35. package/docs-site/en/scope.html +1 -1
  36. package/docs-site/en/security.html +1 -1
  37. package/docs-site/en/team.html +1 -1
  38. package/docs-site/en/update.html +3 -3
  39. package/docs-site/en/whats-new.html +55 -29
  40. package/docs-site/en/windows.html +1 -1
  41. package/docs-site/en/workflows.html +1 -1
  42. package/docs-site/index.html +15 -15
  43. package/docs-site/mcp-approval.html +1 -1
  44. package/docs-site/mcp-auth.html +1 -1
  45. package/docs-site/mcp-servers.html +1 -1
  46. package/docs-site/mcp.html +1 -1
  47. package/docs-site/permissions.html +1 -1
  48. package/docs-site/profiles.html +1 -1
  49. package/docs-site/quickstart.html +1 -1
  50. package/docs-site/releases.json +83 -0
  51. package/docs-site/runtime.html +1 -1
  52. package/docs-site/scope.html +1 -1
  53. package/docs-site/security.html +1 -1
  54. package/docs-site/team.html +1 -1
  55. package/docs-site/update.html +3 -3
  56. package/docs-site/whats-new.html +55 -29
  57. package/docs-site/windows.html +1 -1
  58. package/docs-site/workflows.html +1 -1
  59. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  60. package/package.json +1 -1
  61. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +3 -1
  62. package/packages/piagent-core/extensions/agent-home.d.ts +4 -0
  63. package/packages/piagent-core/extensions/agent-home.js +52 -0
  64. package/packages/piagent-core/extensions/piagent-guard.ts +5 -1
  65. package/packages/piagent-core/managed/copies.mjs +61 -0
  66. package/packages/piagent-core/managed/grant-share.mjs +9 -1
  67. package/packages/piagent-core/managed/helper-roles.mjs +5 -3
  68. package/packages/piagent-core/managed/helper-run.mjs +120 -52
  69. package/packages/piagent-core/managed/helper-settlement.mjs +5 -1
  70. package/packages/piagent-core/managed/ledger.mjs +187 -0
  71. package/packages/piagent-core/managed/objections.mjs +49 -27
  72. package/packages/piagent-core/managed/patch-snapshot.mjs +9 -0
  73. package/packages/piagent-core/managed/session.mjs +14 -14
  74. package/packages/piagent-core/managed/toolchain.mjs +7 -3
  75. package/packages/piagent-core/managed/workflow.mjs +32 -26
  76. package/packages/piagent-core/mcp/mcp-config-layers.js +4 -1
  77. package/packages/piagent-core/package.json +1 -1
  78. package/packages/piagent-core/runtime/managed-failure.mjs +8 -0
  79. package/packages/piagent-webui/extension/piagent-webui.ts +2 -0
  80. package/packages/piagent-webui/gateway/gateway-service.ts +4 -1
  81. package/packages/piagent-webui/gateway/pi-host.ts +2 -2
  82. package/packages/piagent-webui/ownership/profile-state.ts +10 -1
  83. package/packages/piagent-webui/server/benchmark-release-monitor.ts +2 -1
  84. package/scripts/benchmark-runner-support.mjs +2 -1
  85. package/scripts/company-terminal.mjs +19 -1
  86. package/scripts/dashboard-update-job.mjs +4 -2
  87. package/scripts/install-global.sh +113 -5
  88. package/scripts/model-scope-prune.mjs +2 -1
  89. package/scripts/personal-pi-config.mjs +2 -1
  90. package/scripts/personal-pi-restore.mjs +184 -0
  91. package/scripts/pi-usage-history.mjs +2 -1
  92. package/scripts/piagent-cli.mjs +49 -9
  93. package/scripts/piagent-dashboard.mjs +5 -3
  94. package/scripts/setup.sh +2 -6
  95. package/scripts/team-doctor.sh +11 -5
  96. package/scripts/uninstall-global.sh +17 -2
  97. 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, role) {
79
- if (!grant || !sameAuthority(grant, manifest.authority) || grant.role !== role || !Number.isSafeInteger(grant.fence) || grant.fence < 1
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. Obtain a new review for the current patch.', details: this.review }, { triggerTurn: false });
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). Say so at once instead of asking for a grant it refuses.
473
- if ((this.helperCalls.get(role) ?? 0) >= 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);
475
- const job = this.runHelper(role, task, signal, harness, title, role === 'review' && typeof since === 'string' && since.trim() ? since.trim() : null); this.helpers.set(role, job); this.publishHelpers();
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
- const found = await Promise.race([manager.ensureTool(name, true), new Promise(resolve => setTimeout(resolve, timeoutMs).unref())]);
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
- if (policy.review !== 'off') lines.push(policy.review === 'require' ? '- Review: the harness reviews the final patch with an independent reviewer; blocking findings come back to you to fix, or to answer when a finding is wrong: the reviewer then judges your answer, and a disagreement that remains goes to the member.'
153
- : '- Review: before you finish a code change, ask the review helper (delegate role "review") to review the patch, then fix blocking findings.');
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
- // Helper objections to a brief, disagreements handed to the member, the
246
- // claims verify marked failed, and where the harness last sent the
247
- // agent back (to read its answer and see whether the code moved).
248
- objections: [], disputes: [], verifyFails: [], claimsAsked: false, claimsAt: null, claimsRechecked: false, claimsDone: false,
249
- sentAt: null, sentDigest: null, rejudged: false, reviewSettled: false, objectionsAsked: false,
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.length), objections_answered: count(this.objections.filter(o => o.answered && !o.disputed).length), disputes: count(this.disputes.length) } : {}) };
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
- 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}` : ''}`;
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 explain why an issue is not real: an answer without a code change goes back to the reviewer, who judges it. The patch is reviewed again after your change.`,
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 explain why a verdict is wrong. The verify subagent then checks these claims again with your answer.`, { phase: 'claims', claims });
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
- // An objection no corrected brief followed is answered to the member, once.
417
- const open = run.objections.filter(o => !o.answered && !o.disputed);
418
- if (open.length && !run.objectionsAsked) {
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
- if (!run.changed && !run.disputes.length) return;
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
- return path.join(env.PI_CODING_AGENT_DIR || path.join(home, ".pi", "agent"), "mcp.json");
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":
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.45.0",
3
+ "version": "1.46.1",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -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
- statusFiles = new AgentStatusFiles((options.managed ? path.join(os.homedir(), ".pi", "agent", "status", "company") : path.join(state.agentDir, "status", "dashboard")), async () => { const catalog = await readCatalog(); if (catalog.state !== "ready") throw new Error("catalog-not-ready"); return catalog; }, () => Boolean(options.managed)); statusFiles.start();
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
- const agentDir = process.env.PI_CODING_AGENT_DIR || path.join(home, ".pi", "agent");
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 || process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
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(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"), "benchmarks", "piagent");
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 = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
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. Cần review lại cho thay đổi hiện tại.', theme, 'warning'));
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
- const agentDir = process.env.PI_CODING_AGENT_DIR?.trim() || path.join(home, ".pi", "agent");
63
- const binding = JSON.parse(fs.readFileSync(path.join(agentDir, "agent-watch-managed.json"), "utf8"));
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 */ }