@piagent/platform 1.51.1 → 1.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/docs/command-reference-vietnamese.md +2 -2
  3. package/docs/distribution-standard.md +6 -6
  4. package/docs/harness-conformance.md +13 -13
  5. package/docs/operator-manual-vietnamese.md +7 -7
  6. package/docs/publishing-for-teams.md +2 -0
  7. package/docs/release-install-policy.md +2 -2
  8. package/docs/team-onboarding.md +2 -2
  9. package/docs-site/architecture.html +1 -1
  10. package/docs-site/assets/search-en.json +1 -1
  11. package/docs-site/assets/search-vi.json +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/context-engine.html +1 -1
  15. package/docs-site/ecosystem.html +1 -1
  16. package/docs-site/en/architecture.html +1 -1
  17. package/docs-site/en/benchmark.html +1 -1
  18. package/docs-site/en/commands.html +1 -1
  19. package/docs-site/en/context-engine.html +1 -1
  20. package/docs-site/en/ecosystem.html +1 -1
  21. package/docs-site/en/index.html +15 -15
  22. package/docs-site/en/mcp-approval.html +1 -1
  23. package/docs-site/en/mcp-auth.html +1 -1
  24. package/docs-site/en/mcp-servers.html +1 -1
  25. package/docs-site/en/mcp.html +1 -1
  26. package/docs-site/en/permissions.html +1 -1
  27. package/docs-site/en/profiles.html +1 -1
  28. package/docs-site/en/quickstart.html +1 -1
  29. package/docs-site/en/runtime.html +1 -1
  30. package/docs-site/en/scope.html +1 -1
  31. package/docs-site/en/security.html +1 -1
  32. package/docs-site/en/team.html +1 -1
  33. package/docs-site/en/update.html +3 -3
  34. package/docs-site/en/whats-new.html +54 -31
  35. package/docs-site/en/windows.html +1 -1
  36. package/docs-site/en/workflows.html +1 -1
  37. package/docs-site/index.html +15 -15
  38. package/docs-site/mcp-approval.html +1 -1
  39. package/docs-site/mcp-auth.html +1 -1
  40. package/docs-site/mcp-servers.html +1 -1
  41. package/docs-site/mcp.html +1 -1
  42. package/docs-site/permissions.html +1 -1
  43. package/docs-site/profiles.html +1 -1
  44. package/docs-site/quickstart.html +1 -1
  45. package/docs-site/releases.json +65 -0
  46. package/docs-site/runtime.html +1 -1
  47. package/docs-site/scope.html +1 -1
  48. package/docs-site/security.html +1 -1
  49. package/docs-site/team.html +1 -1
  50. package/docs-site/update.html +3 -3
  51. package/docs-site/whats-new.html +54 -31
  52. package/docs-site/windows.html +1 -1
  53. package/docs-site/workflows.html +1 -1
  54. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  55. package/package.json +1 -1
  56. package/packages/piagent-core/managed/check-questions.mjs +2 -46
  57. package/packages/piagent-core/managed/copies.mjs +10 -10
  58. package/packages/piagent-core/managed/general-clone.mjs +111 -0
  59. package/packages/piagent-core/managed/helper-roles.mjs +33 -42
  60. package/packages/piagent-core/managed/helper-run.mjs +21 -15
  61. package/packages/piagent-core/managed/helper-settlement.mjs +6 -10
  62. package/packages/piagent-core/managed/language-environment.mjs +6 -2
  63. package/packages/piagent-core/managed/ledger.mjs +11 -10
  64. package/packages/piagent-core/managed/objections.mjs +8 -25
  65. package/packages/piagent-core/managed/role-names.mjs +6 -0
  66. package/packages/piagent-core/managed/session.mjs +13 -13
  67. package/packages/piagent-core/managed/test-scope.mjs +170 -0
  68. package/packages/piagent-core/managed/tool-boundary.mjs +4 -4
  69. package/packages/piagent-core/managed/workflow.mjs +2 -39
  70. package/packages/piagent-core/package.json +1 -1
  71. package/packages/piagent-core/runtime/managed-failure.d.mts +1 -1
  72. package/packages/piagent-core/runtime/managed-failure.mjs +3 -3
  73. package/packages/piagent-webui/dist/client/assets/{App-C9tlGPMb.js → App-DQwj6l-F.js} +1 -1
  74. package/packages/piagent-webui/dist/client/assets/{GatewayApp-Dvo96qoX.js → GatewayApp-CP8GpZsK.js} +3 -3
  75. package/packages/piagent-webui/dist/client/assets/{InspectionApp-CGgHsOyC.js → InspectionApp-BZqu1pjw.js} +1 -1
  76. package/packages/piagent-webui/dist/client/assets/{SessionAgentWorkspace-n8UMMcrP.js → SessionAgentWorkspace-4nJSbzE9.js} +1 -1
  77. package/packages/piagent-webui/dist/client/assets/{SessionInspectorDrawer-B62kkr8J.js → SessionInspectorDrawer-DEb_TBE8.js} +2 -2
  78. package/packages/piagent-webui/dist/client/assets/SettingsNavigation-BF4X5xkR.js +3 -0
  79. package/packages/piagent-webui/dist/client/assets/{SettingsPage-DKg0SfZQ.js → SettingsPage-CS8LsdhS.js} +1 -1
  80. package/packages/piagent-webui/dist/client/assets/{TerminalPanel-BPTVsNjP.js → TerminalPanel-CiF2_woF.js} +1 -1
  81. package/packages/piagent-webui/dist/client/assets/{WorkspaceCodeEditor-BHGFFBc2.js → WorkspaceCodeEditor-OFxSEbBs.js} +1 -1
  82. package/packages/piagent-webui/dist/client/assets/{WorkspaceExplorer-nd4kghwK.js → WorkspaceExplorer-ZYIi-FWr.js} +3 -3
  83. package/packages/piagent-webui/dist/client/assets/{index-BiLVX8S-.js → index-CfJC4Unf.js} +2 -2
  84. package/packages/piagent-webui/dist/client/index.html +1 -1
  85. package/packages/piagent-webui/server/transcript-projection.ts +3 -3
  86. package/packages/piagent-webui/shared/company-copy.ts +3 -1
  87. package/packages/piagent-webui/shared/turn-end.ts +1 -1
  88. package/schemas/piagent-webui/transcript-v1.schema.json +3 -3
  89. package/scripts/runtime-advisory-exceptions.json +27 -27
  90. package/packages/piagent-webui/dist/client/assets/SettingsNavigation-BZijmuPt.js +0 -3
@@ -1,14 +1,18 @@
1
- import { REVIEW_PROMPT, VERIFY_PROMPT, BRIEF_CHECK, BRIEF_ISSUES_JSON, isCheckCommand } from './workflow.mjs';
1
+ import { REVIEW_PROMPT, BRIEF_CHECK, BRIEF_ISSUES_JSON } from './workflow.mjs';
2
2
 
3
3
  // Each helper role runs at most this often per user message (Studio's MaxHelperCalls).
4
4
  export const HELPER_CALLS = 8;
5
5
 
6
6
  // The helpers a main agent may delegate to, in the Harness's order. Each is
7
7
  // optional: only those the company Harness enables (and Agent Watch passes
8
- // on) are offered. Scout reads the code, research also reads the web, verify
9
- // re-checks a result with the repository's checks and primary sources,
10
- // review judges the patch. None of them changes files.
11
- export const HELPER_ROLES = Object.freeze(['scout', 'research', 'verify', 'review']);
8
+ // on) are offered. The main agent names a helper by its working name; Studio
9
+ // knows the same helper by its Harness role (its grant). explore reads the
10
+ // code, scout reads the web (and the code), advisor gives a second opinion,
11
+ // review judges the patch. general is the only one that changes files, and
12
+ // only in its own clone of the project (general-clone.mjs). Main does small
13
+ // changes, integration and the repository checks itself.
14
+ import { ROLE_NAMES, HELPER_ROLES, callName } from './role-names.mjs';
15
+ export { ROLE_NAMES, HELPER_ROLES, callName };
12
16
  export const helperRoles = manifest => HELPER_ROLES.filter(role => manifest?.harness?.configuration?.[role]);
13
17
  const WIDE_READING = 'when understanding the task needs more reading than a few batches of files, such as a flow across many modules of an unfamiliar codebase';
14
18
  const NETWORK_COMMANDS = 'run_with_network only when a command itself needs the internet or starts a local server or browser, such as end-to-end tests (the user approves each one)';
@@ -24,7 +28,7 @@ export function mainWebLookups(manifest) {
24
28
  export function limitedWebTools(tools, currentRun, limit) {
25
29
  return tools.map(tool => ({ ...tool, execute: async (...args) => {
26
30
  const run = currentRun();
27
- if (run && ++run.webLookups > limit) throw Error(`managed-web-lookups-spent: you used your ${limit} quick web lookup(s) for this message. Hand the rest of the web work to the research helper (delegate role "research").`);
31
+ if (run && ++run.webLookups > limit) throw Error(`managed-web-lookups-spent: you used your ${limit} quick web lookup(s) for this message. Hand the rest of the web work to the scout helper (delegate role "scout").`);
28
32
  return tool.execute(...args);
29
33
  } }));
30
34
  }
@@ -34,15 +38,16 @@ export function webPrompt(roles, lookups = 0) {
34
38
  if (!roles.includes('research'))
35
39
  return ` Shell commands run without network: use web_search for current information or documentation, web_fetch to read a public https page, and ${NETWORK_COMMANDS}.`;
36
40
  return lookups > 0
37
- ? ` Shell commands run without network. For one quick fact (a version, a flag, one documentation page) use web_search or web_fetch yourself, at most ${lookups} call(s) per member message; hand anything wider on the web (several sources, comparisons, news, research) to the research helper, and use ${NETWORK_COMMANDS}.`
38
- : ` Shell commands run without network and you have no web tools: hand anything on the web (current information, news, documentation, releases, error messages) to the research helper, and use ${NETWORK_COMMANDS}.`;
41
+ ? ` Shell commands run without network. For one quick fact (a version, a flag, one documentation page) use web_search or web_fetch yourself, at most ${lookups} call(s) per member message; hand anything wider on the web (several sources, comparisons, news, research) to the scout helper, and use ${NETWORK_COMMANDS}.`
42
+ : ` Shell commands run without network and you have no web tools: hand anything on the web (current information, news, documentation, releases, error messages) to the scout helper, and use ${NETWORK_COMMANDS}.`;
39
43
  }
40
44
  export function helperPrompt(roles) {
41
45
  const lines = [];
42
- if (roles.includes('scout')) lines.push(`Use the scout helper (delegate role "scout") ${WIDE_READING}, and work from its file:line findings; do small lookups yourself.`);
43
- if (roles.includes('research')) lines.push(roles.includes('scout') ? 'Use the research helper (delegate role "research") for anything on the web (current information, news, library and API docs, releases, error messages), together with the code when needed; give it the question and the answer you need, and work from its summary.'
44
- : `Use the research helper (delegate role "research") for anything on the web (current information, news, documentation) and ${WIDE_READING}; work from its findings and do small code lookups yourself.`);
45
- if (roles.includes('verify')) lines.push('Before you report a non-trivial result as done, you may have the verify helper (delegate role "verify") confirm it independently: it runs the repository checks on the current code and checks claims against the code and primary sources, answering pass, fail or unverifiable with evidence.');
46
+ if (roles.includes('scout')) lines.push(`Use the explore helper (delegate role "explore") ${WIDE_READING}, and work from its file:line findings; do small lookups yourself.`);
47
+ if (roles.includes('research')) lines.push(roles.includes('scout') ? 'Use the scout helper (delegate role "scout") for anything on the web (current information, news, library and API docs, releases, error messages), together with the code when needed; give it the question and the answer you need, and work from its summary.'
48
+ : `Use the scout helper (delegate role "scout") for anything on the web (current information, news, documentation) and ${WIDE_READING}; work from its findings and do small code lookups yourself.`);
49
+ if (roles.includes('advisor')) lines.push('Use the advisor helper (delegate role "advisor") for a second opinion before a hard or hard-to-reverse decision (a design, a plan, a suspected root cause): give it the question, your options and what you know; it answers with a recommendation and the risks.');
50
+ if (roles.includes('general')) lines.push('Make small changes yourself. Use the general helper (delegate role "general") for large work, or work that can proceed apart from you: it works in its own clone of the repository, one general helper at a time, and cannot touch your files. It sees only committed work (commit first if it needs your changes); its commits come back as a git ref that you merge or cherry-pick yourself. You then run the repository checks and, for a non-trivial change, ask the review helper.');
46
51
  // Helpers read the member's request beside the brief and may object to it.
47
52
  if (lines.length) lines.push('A helper sees the member\'s request next to your brief and may object to the brief with evidence (brief_issues); each objection gets an id. You decide, but check each one at the place its evidence names and record your decision with resolve_objection: accepted with what you changed (a corrected brief, the plan, the code), or rejected with what you checked and why. An objection that the brief rests on a wrong premise or conflicts with the request refuses file changes until you decide it; a rejected one goes back to the helper, and a disagreement that remains goes to the member, as does a second objection from the same helper for one message.');
48
53
  // More than one helper of a role, only at the member's word (copies.mjs).
@@ -50,44 +55,30 @@ export function helperPrompt(roles) {
50
55
  return lines.length ? ' ' + lines.join(' ') : '';
51
56
  }
52
57
  const HELPER_USES = {
53
- scout: () => 'scout: before changing an area you do not know that spans more files than you can read in a few batches (where something is computed and every caller, how a flow crosses modules). It reads only the project, no web, and answers with file:line facts; your own context stays free for the change.',
54
- research: roles => roles.includes('scout') ? 'research: anything on the web (current information, news, library or API documentation, releases, error messages), with the code when needed. It searches and reads the pages; you get its summary with sources.'
55
- : 'research: anything on the web (current information, news, documentation; it searches and reads the pages, you get its summary with sources), and before changing an area you do not know that spans more files than you can read in a few batches (where something is computed and every caller, how a flow crosses modules); your own context stays free for the change.',
56
- verify: () => 'verify: an independent check of a result: it runs the repository checks on the current code (offline, and the project is read-only for its commands) and confirms claims, yours or a helper\'s, against the code and primary sources. It answers pass, fail or unverifiable per claim, with evidence.',
58
+ scout: () => 'explore: before changing an area you do not know that spans more files than you can read in a few batches (where something is computed and every caller, how a flow crosses modules). It reads only the project, no web, and answers with file:line facts; your own context stays free for the change.',
59
+ research: roles => roles.includes('scout') ? 'scout: anything on the web (current information, news, library or API documentation, releases, error messages), with the code when needed. It searches and reads the pages; you get its summary with sources.'
60
+ : 'scout: anything on the web (current information, news, documentation; it searches and reads the pages, you get its summary with sources), and before changing an area you do not know that spans more files than you can read in a few batches (where something is computed and every caller, how a flow crosses modules); your own context stays free for the change.',
61
+ advisor: () => 'advisor: a second opinion on a hard or hard-to-reverse decision. It reads the project, has no web, and answers with a recommendation, the options it rejected and the risks.',
62
+ general: () => 'general: large work, or work that can proceed apart from you. It edits and runs checks in its own clone of the repository (never your files), commits there, and its commits come back as the git ref it names, which you merge or cherry-pick yourself. It sees only committed work. One at a time.',
57
63
  review: () => 'review: an independent review of the current patch (this turn\'s changes, commits made in the turn included). To review work committed in earlier turns, pass since: the commit, branch or HEAD~N it started from; when this turn changed nothing, it reviews the branch\'s commits since it left the default branch. Ask once, when your change is finished, and again only after fixing blocking findings; under a required review the harness reviews the final patch and refuses your own call.',
58
64
  };
59
65
  export function delegateDescription(roles) {
60
- return `Hand work to a helper with its own context. Helpers read the project but cannot change files or call helpers. ${roles.map(role => HELPER_USES[role](roles)).join(' ')} A lookup that one grep or read answers stays with you. Write the task as a brief the helper can act on alone: the goal, what to find or check, where to start and what you already know or ruled out, and the answer you need (file:line facts). One call per helper runs at a time; each can be called again, up to ${HELPER_CALLS} times per user message, for example a second scout question on what the first answer opened.`;
66
+ return `Hand work to a helper with its own context. Helpers read the project and cannot call helpers; only general changes files, in its own clone. ${roles.map(role => HELPER_USES[role](roles)).join(' ')} A lookup that one grep or read answers stays with you. Write the task as a brief the helper can act on alone: the goal, what to find or check, where to start and what you already know or ruled out, and the answer you need (file:line facts). One call per helper runs at a time; each can be called again, up to ${HELPER_CALLS} times per user message, for example a second scout question on what the first answer opened.`;
61
67
  }
62
- // Scout and research end with their objections to the brief, if any.
68
+ // Explore, scout and advisor end with their objections to the brief, if any.
63
69
  const OBJECTIONS = `${BRIEF_CHECK} End your answer with one JSON object in a \`\`\`json block: {${BRIEF_ISSUES_JSON}}, with an empty list when the brief is sound. You cannot change files or delegate.`;
64
70
  const RESEARCH_PROMPT = `Research the assigned question. Read only the needed source; use web_search or web_fetch for external documentation. Answer with facts the main agent can act on: file:line, short quotes, what calls what, and what you checked and found nothing in. ${OBJECTIONS}`;
65
71
  const SCOUT_PROMPT = `Scout the codebase for the assigned question. Search first (grep, find), then read only the parts that matter. Answer with facts the main agent can act on: file:line, short quotes, what calls what, and where you looked and found nothing. You have no web access: when the answer needs external documentation, say so. ${OBJECTIONS}`;
72
+ const ADVISOR_PROMPT = `Advise on the assigned decision. Read the project as needed. Give a recommendation, the options you rejected and why, and the risks, as points the main agent can act on (file:line where one rests on code). Say what you could not check. You have no web access. ${OBJECTIONS}`;
73
+ const GENERAL_PROMPT = `You are the general helper: you do the assigned work in your own clone of the repository, which is your working folder. Nothing you do there reaches the main agent's files. Make the change, run the checks that cover it, and COMMIT your finished work (git add -A, git commit): only committed work is returned. Do not rewrite or delete history before the starting commit. Answer with what you changed (file:line), the checks you ran and their result, and what you left undone. ${BRIEF_CHECK} End your answer with one JSON object in a \`\`\`json block: {${BRIEF_ISSUES_JSON}}, with an empty list when the brief is sound. You cannot delegate.`;
66
74
  export const READ_TOOLS = ['read', 'grep', 'find', 'ls'];
67
- // What each helper may use. Every helper's project is read-only; verify also
68
- // runs commands there (offline, writes only to its temporary directory).
75
+ export const WRITE_TOOLS = [...READ_TOOLS, 'bash', 'edit', 'write'];
76
+ // What each helper may use. The project is read-only to every helper except
77
+ // general, which writes only its own clone (`writes`).
69
78
  export const HELPER_SETUP = {
70
- scout: { web: false, commands: false, prompt: SCOUT_PROMPT, label: 'Scout' },
71
- research: { web: true, commands: false, prompt: RESEARCH_PROMPT, label: 'Research' },
72
- verify: { web: true, commands: true, prompt: VERIFY_PROMPT, label: 'Verify' },
79
+ scout: { web: false, commands: false, prompt: SCOUT_PROMPT, label: 'Explore' },
80
+ research: { web: true, commands: false, prompt: RESEARCH_PROMPT, label: 'Scout' },
81
+ advisor: { web: false, commands: false, prompt: ADVISOR_PROMPT, label: 'Advisor' },
82
+ general: { web: false, commands: true, writes: true, prompt: GENERAL_PROMPT, label: 'General' },
73
83
  review: { web: false, commands: false, prompt: REVIEW_PROMPT, label: 'Review' },
74
84
  };
75
- // A repository check a helper ran counts for the run like one the main
76
- // agent ran, when the code was the same before and after it: the helper
77
- // cannot write, but the main agent may edit beside it in the same answer.
78
- export function countedCheck(managed, tool) {
79
- if (tool.name !== 'bash') return tool;
80
- return { ...tool, execute: async (...args) => {
81
- // Whether anything in the folder moved during the check (another
82
- // conversation's edit too: the suite ran on the whole folder); the check
83
- // then counts for this conversation's own code.
84
- const folder = async () => { try { return await (managed.folderDigest ?? managed.digest).call(managed); } catch { return null; } };
85
- const counted = managed.run && isCheckCommand(args[1]?.command, managed.checks.commands), before = counted ? await folder() : null;
86
- let ok = false;
87
- try { const result = await tool.execute(...args); ok = !result?.isError; return result; }
88
- finally {
89
- const after = counted && before !== null ? await folder() : null;
90
- if (after !== null && after === before) await managed.run.afterTool('bash', args[1], ok, () => managed.digest(), managed);
91
- }
92
- } };
93
- }
@@ -4,19 +4,19 @@
4
4
  // refusal, starts once more: Studio then gives the new helper the next model
5
5
  // of its role that serves (an auto role's list).
6
6
  import { fenced, HELPER_UNTRUSTED_RULE } from './untrusted.mjs';
7
- import { relevantChecks } from './check-evidence.mjs';
8
7
  import { jobTitle } from './call-purpose.mjs';
9
8
  import { ManagedToolBoundary } from './tool-boundary.mjs';
10
9
  import { managedResourceLoader } from './resource-loader.mjs';
11
10
  import { shareGrant, releaseGrant, slotArgs, baseRole } from './grant-share.mjs';
12
11
  import { describeFailure, failureCode, failureText } from '../runtime/managed-failure.mjs';
13
12
  import { reviewParts, focusParts, mergeReviews, REVIEW_PARALLEL, emptySnapshot, readBranchBase, readPatchDigest, readPatchSnapshot, resolveReviewBase } from './patch-snapshot.mjs';
14
- import { reviewFindings, briefIssues, verifyVerdicts } from './workflow.mjs';
13
+ import { reviewFindings, briefIssues } from './workflow.mjs';
15
14
  import { currentWorkflow, helperWorkflowPrompt } from './workflow-context.mjs';
16
- import { anchorText, relayObjection, recordVerdicts } from './objections.mjs';
15
+ import { anchorText, relayObjection } from './objections.mjs';
17
16
  import { recordFindings } from './ledger.mjs';
18
17
  import { copyNote, mergeCopies } from './copies.mjs';
19
- import { HELPER_CALLS, HELPER_SETUP, READ_TOOLS, countedCheck } from './helper-roles.mjs';
18
+ import { HELPER_CALLS, HELPER_SETUP, READ_TOOLS, WRITE_TOOLS } from './helper-roles.mjs';
19
+ import { cloneProject, discardClone, takeBack, takeBackText, sweepText, recordGeneral } from './general-clone.mjs';
20
20
  import { trackToolFailures } from './tool-failures.mjs';
21
21
  import { helperMemoryTools, renewer } from './memory.mjs';
22
22
  import { answeredSettlement, failedSettlement, settleHelper, OUTCOME_WORDS } from './helper-settlement.mjs';
@@ -88,7 +88,7 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
88
88
  let results;
89
89
  try { results = await Promise.all(Array.from({ length: fan.n }, (_, i) => answerUnder(managed, i ? `${role}#${i + 1}` : role, task, both, harness, snapshot, parts, opts, [copyNote(role, i, fan)]).catch(error => { stop.abort(); throw error; }))); }
90
90
  catch (error) { throw signal?.aborted ? Error('managed-helper-cancelled') : error; }
91
- const merged = mergeCopies(role, setup.label, results, { findingsOf: reviewFindings, issuesOf: briefIssues, verdictsOf: verifyVerdicts });
91
+ const merged = mergeCopies(role, setup.label, results, { findingsOf: reviewFindings, issuesOf: briefIssues });
92
92
  result = { ...results[0], reply: merged.reply, usage: merged.usage, review: merged.findings ? merged : null, stale: results.some(r => r.stale), report: null };
93
93
  }
94
94
  const { reply, usage, review, stale, from, grant } = result;
@@ -104,10 +104,9 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
104
104
  if (!from) { managed.review = details; if (!stale) { recordFindings(managed, findings); managed.reviewFiles = await managed.changedFiles?.(); } }
105
105
  managed.session.sessionManager.appendCustomEntry('agent-watch-review', details);
106
106
  }
107
- const verdicts = role === 'verify' ? recordVerdicts(managed, reply, harness, details) : '';
108
107
  const report = result.report ?? answeredSettlement(role, reply, details, managed.helperSpecs?.get(role) ?? {}); details.outcome = report.outcome;
109
108
  await managed.session.sendCustomMessage({ customType: 'agent-watch-helper-receipt', display: true,
110
- content: `${setup.label}: ${usage.toLocaleString('en-US')} tokens${verdicts}${fan ? ` · ${fan.n} helpers` : review ? ` · ${parts.parts.length} parts` : ''}${snapshot ? (stale ? ' · review is stale' : ' · patch ' + snapshot.digest.slice(0, 12)) : ''} · ${OUTCOME_WORDS[report.outcome]}.`, details }, { triggerTurn: false });
109
+ content: `${setup.label}: ${usage.toLocaleString('en-US')} tokens${fan ? ` · ${fan.n} helpers` : review ? ` · ${parts.parts.length} parts` : ''}${snapshot ? (stale ? ' · review is stale' : ' · patch ' + snapshot.digest.slice(0, 12)) : ''} · ${OUTCOME_WORDS[report.outcome]}.`, details }, { triggerTurn: false });
111
110
  // An objection to the brief comes with the answer; a stale review has none to act on.
112
111
  const objection = stale ? '' : await relayObjection(managed, role, reply, harness);
113
112
  const ending = `Outcome: ${report.outcome}${report.outcome === 'unverifiable' ? ` (${report.summary})` : ''}.\n\n`;
@@ -137,7 +136,7 @@ async function answerUnder(managed, key, task, signal, harness, snapshot, parts,
137
136
  try { verifyGrant(grant, managed.manifest, key); }
138
137
  catch (error) { try { await managed.broker.request('close', slotArgs(key)); } catch { /* closes with the run */ } throw error; }
139
138
  } catch (error) { throw Error(`managed-helper-failed: ${describeFailure(role, failureText(error))}`); }
140
- let boundary, report = null;
139
+ let boundary, report = null, clone = null, liveRoot = null;
141
140
  const sessions = new Set(), watched = new Set(), spec = managed.helperSpecs?.get(role) ?? {};
142
141
  const renew = renewer(managed, key, g => verifyGrant(g, managed.manifest, key));
143
142
  try {
@@ -146,20 +145,24 @@ async function answerUnder(managed, key, task, signal, harness, snapshot, parts,
146
145
  // Studio's logs name the job by its title (the brief's first line without one).
147
146
  (managed.jobTitles ??= new WeakMap()).set(runtime, jobTitle(task, title));
148
147
  const setup = HELPER_SETUP[role], memory = helperMemoryTools(managed, () => grant, renew);
149
- boundary = new ManagedToolBoundary({ cwd: managed.cwd, sdkRoot: managed.sdk, protectedRoots: managed.protectedRoots, readOnly: true, commands: setup.commands }); boundary.unsandboxed = () => alwaysBypass(managed);
150
- const checks = role === 'verify' ? `\n\nRepository checks: ${managed.checks.commands.length ? relevantChecks(managed, managed.checks.commands).map(c => '`' + c + '`').join(', ') : 'none declared or detected; choose the tests, type check or build that cover the claims'}.${managed.run?.fullAsked || managed.run?.scopeChoice === 'full' ? '' : ' Run only the tests that cover the claims: the member decides about a whole-suite run.'}` : '';
148
+ // General writes, but only in its own clone of the project (general-clone.mjs); the others read the project.
149
+ if (setup.writes) { clone = await cloneProject(managed); (managed.liveClones ??= new Set()).add(liveRoot = clone.root); recordGeneral(managed, clone, 'running'); }
150
+ const cwd = clone?.dir ?? managed.cwd;
151
+ boundary = new ManagedToolBoundary({ cwd, sdkRoot: managed.sdk, protectedRoots: managed.protectedRoots, readOnly: !setup.writes, commands: setup.commands, ...(clone ? { identity: managed.boundary.setup.identity, readRoots: [clone.objects] } : {}) });
152
+ // Always bypass would run a command on the host, outside the clone.
153
+ if (!setup.writes) boundary.unsandboxed = () => alwaysBypass(managed);
151
154
  // One helper session answers the brief (with one part of the patch for a
152
155
  // review in parts); its reply and tokens. A helper runs at its role's
153
156
  // level from Studio (fixed by the Harness, or its main agent's).
154
157
  const ask = async (extra, note = '', who = key) => {
155
- const { session } = await managed.api.createAgentSession({ cwd: managed.cwd, model, modelRuntime: runtime, thinkingLevel: grant.effort || 'off',
158
+ const { session } = await managed.api.createAgentSession({ cwd, model, modelRuntime: runtime, thinkingLevel: grant.effort || 'off',
156
159
  settingsManager: managed.api.SettingsManager.inMemory({ retry: { enabled: false, provider: { maxRetries: 0 } }, cacheWarming: 'off' }),
157
- sessionManager: managed.api.SessionManager.inMemory(managed.cwd), noTools: 'builtin', tools: [...READ_TOOLS, ...(setup.commands ? ['bash'] : []), ...(setup.web ? ['web_search', 'web_fetch'] : []), ...memory.map(tool => tool.name)],
158
- customTools: [...unsandboxedBash(managed, boundary.tools(managed.api)).filter(tool => READ_TOOLS.includes(tool.name) || setup.commands && tool.name === 'bash').map(tool => countedCheck(managed, tool)),
160
+ sessionManager: managed.api.SessionManager.inMemory(cwd), noTools: 'builtin', tools: [...(setup.writes ? WRITE_TOOLS : READ_TOOLS), ...(setup.web ? ['web_search', 'web_fetch'] : []), ...memory.map(tool => tool.name)],
161
+ customTools: [...(setup.writes ? boundary.tools(managed.api).filter(tool => WRITE_TOOLS.includes(tool.name)) : unsandboxedBash(managed, boundary.tools(managed.api)).filter(tool => READ_TOOLS.includes(tool.name))),
159
162
  ...(setup.web ? managed.webTools(runtime, key) : []), ...memory], resourceLoader: managedResourceLoader(managed.api, { systemPrompt: setup.prompt + HELPER_UNTRUSTED_RULE }) });
160
163
  sessions.add(session); trackToolFailures(managed, session, role); watched.add(who); watchHelper(managed, who, session, task, title);
161
164
  const abort = () => void session.abort(); signal?.addEventListener('abort', abort, { once: true });
162
- try { if (signal?.aborted) throw Error('managed-helper-cancelled'); await session.prompt(anchorText(managed.session.sessionManager, harness) + task + note + helperWorkflowPrompt(currentWorkflow(managed.session.sessionManager)) + checks + extra, { expandPromptTemplates: false }); }
165
+ try { if (signal?.aborted) throw Error('managed-helper-cancelled'); await session.prompt(anchorText(managed.session.sessionManager, harness) + task + note + helperWorkflowPrompt(currentWorkflow(managed.session.sessionManager)) + extra, { expandPromptTemplates: false }); }
163
166
  finally { signal?.removeEventListener('abort', abort); }
164
167
  const messages = session.messages.filter(m => m.role === 'assistant');
165
168
  // The main agent (and the member) learn why a helper failed, not just that it did.
@@ -191,9 +194,10 @@ async function answerUnder(managed, key, task, signal, harness, snapshot, parts,
191
194
  let answers;
192
195
  try { answers = await Promise.all(notes.map((note, i) => once(note, i ? `${role}#${i + 1}` : key))); }
193
196
  catch (error) { for (const session of sessions) void session.abort(); throw error; }
194
- const merged = mergeCopies(role, setup.label, answers, { findingsOf: reviewFindings, issuesOf: briefIssues, verdictsOf: verifyVerdicts });
197
+ const merged = mergeCopies(role, setup.label, answers, { findingsOf: reviewFindings, issuesOf: briefIssues });
195
198
  ({ reply, usage } = merged); review = merged.findings ? merged : null;
196
199
  }
200
+ if (clone) { const back = await takeBack(managed, clone); recordGeneral(managed, clone, 'returned', back); reply += takeBackText(clone, back) + sweepText(managed.orphans ?? { removed: [], unproven: [] }); managed.orphans = null; clone = null; }
197
201
  const from = snapshot?.from ?? null;
198
202
  const stale = snapshot ? (await (from ? readPatchDigest(managed.boundary, null, from.base) : managed.patchDigest())) !== snapshot.digest : false;
199
203
  const findings = snapshot ? (review ? review.findings : reviewFindings(reply)) : null;
@@ -201,6 +205,8 @@ async function answerUnder(managed, key, task, signal, harness, snapshot, parts,
201
205
  return { reply, usage, review, stale, from, grant, report };
202
206
  } catch (error) { report = failedSettlement(role, error, signal, spec); throw error; }
203
207
  finally {
208
+ if (clone) { discardClone(clone); recordGeneral(managed, clone, 'discarded'); }
209
+ managed.liveClones?.delete(liveRoot);
204
210
  for (const session of sessions) session.dispose(); await boundary?.dispose();
205
211
  await settleHelper(managed, renew.lease, report); // with the role's current token, before it closes
206
212
  await closeRole(managed, key);
@@ -1,14 +1,14 @@
1
1
  // How a helper's attempt ended (Agent Studio 0.34+, POST /v1/managed/settle):
2
2
  // one report per attempt, sent with the helper's own run token before its
3
3
  // role closes. succeeded: the helper did its job and answered in the form its
4
- // role needs (verify's verdicts, review's findings); failed: it could not
4
+ // role needs (review's findings); failed: it could not
5
5
  // (an error); cancelled: the member or the main agent stopped it;
6
6
  // unverifiable: it answered without what its role needs, or its review went
7
7
  // stale. Studio reads a helper role that closed without a report as
8
8
  // unverifiable: absence is never success or failure.
9
9
  import { CLIENT_AGENT } from './client-agent.mjs';
10
10
  import { hasCapability } from './studio-capabilities.mjs';
11
- import { HELPER_ROLES } from './helper-roles.mjs';
11
+ import { ROLE_NAMES } from './helper-roles.mjs';
12
12
 
13
13
  // The task as the main agent gives it: five optional fields beside the brief.
14
14
  export const SPEC_FIELDS = Object.freeze({
@@ -20,7 +20,7 @@ export const SPEC_FIELDS = Object.freeze({
20
20
  });
21
21
  const field = description => ({ type: 'string', maxLength: 1000, description });
22
22
  export function delegateParameters() {
23
- return { type: 'object', properties: { role: { type: 'string', enum: HELPER_ROLES },
23
+ return { type: 'object', properties: { role: { type: 'string', enum: Object.keys(ROLE_NAMES) },
24
24
  title: { type: 'string', maxLength: 120, description: 'One line naming the job, for the company logs (for example "Find where login tokens are stored").' },
25
25
  task: { type: 'string', minLength: 1, maxLength: 12000 },
26
26
  ...Object.fromEntries(Object.entries(SPEC_FIELDS).map(([name, description]) => [name, field(description)])),
@@ -37,7 +37,7 @@ export const SPEC_PROMPT = ' Give a helper target and acceptance at least (also
37
37
  // brief; the main agent's task follows. The spec is kept for the settlement.
38
38
  export function delegateArgs(self, args) {
39
39
  const spec = Object.fromEntries(Object.keys(SPEC_FIELDS).map(k => [k, typeof args?.[k] === 'string' ? args[k].trim().slice(0, 1000) : '']).filter(([, v]) => v));
40
- (self.helperSpecs ??= new Map()).set(args?.role, spec);
40
+ (self.helperSpecs ??= new Map()).set(ROLE_NAMES[args?.role], spec);
41
41
  if (!Object.keys(spec).length) return args;
42
42
  const head = Object.entries(spec).map(([k, v]) => `${k[0].toUpperCase()}${k.slice(1)}: ${v}`).join('\n');
43
43
  return { ...args, task: `${head}\n\n${args.task}`.slice(0, 12000) };
@@ -58,18 +58,14 @@ export function answeredSettlement(role, reply, details = {}, spec = {}) {
58
58
  const summary = clip(typeof parsed?.summary === 'string' && parsed.summary.trim() ? parsed.summary : prose || `${role} answered.`, 600);
59
59
  const evidence = [], files = new Set();
60
60
  let outcome = 'succeeded';
61
- if (role === 'verify') {
62
- const verdicts = Array.isArray(parsed?.verdicts) ? parsed.verdicts : null;
63
- if (!verdicts) outcome = 'unverifiable';
64
- for (const v of verdicts ?? []) evidence.push(clip(`${v?.status ?? '?'}: ${v?.claim ?? ''} — ${v?.evidence ?? ''}`, 300));
65
- } else if (role === 'review') {
61
+ if (role === 'review') {
66
62
  if (details.stale) outcome = 'unverifiable';
67
63
  else if (details.parsed === false) outcome = 'unverifiable';
68
64
  for (const f of details.findings ?? []) { evidence.push(clip(`${f.severity} ${f.file ?? ''}:${f.line ?? ''} ${f.issue ?? ''}`, 300)); if (f.file) files.add(String(f.file)); }
69
65
  }
70
66
  for (const m of String(reply).matchAll(FILE_LINE)) {
71
67
  files.add(m[1].replace(/^\.\//, ''));
72
- if (role !== 'verify' && role !== 'review' && evidence.length < 8) evidence.push(clip(`${m[1]}:${m[2]}`, 300));
68
+ if (role !== 'review' && evidence.length < 8) evidence.push(clip(`${m[1]}:${m[2]}`, 300));
73
69
  }
74
70
  return { outcome, summary: details.stale ? 'The patch changed during review; this review is stale.' : summary,
75
71
  evidence: [...new Set(evidence.filter(Boolean))].slice(0, 8), files: [...files].filter(f => f.length <= 300).slice(0, 50), ...(Object.keys(spec ?? {}).length ? { spec } : {}) };
@@ -121,10 +121,14 @@ function linkGradleDistributions(source, target) {
121
121
  }
122
122
  }
123
123
 
124
+ // The per-project package cache a sandboxed command may write.
125
+ export function cacheDirectory(userHome, repositoryTop) {
126
+ const root = process.platform === 'linux' ? path.join(process.env.XDG_CACHE_HOME || path.join(userHome, '.cache'), 'piagent/sandbox') : path.join(userHome, 'Library/Caches/Piagent/sandbox');
127
+ return path.join(root, createHash('sha256').update(repositoryTop).digest('hex').slice(0, 24));
128
+ }
124
129
  export function languageEnvironment({ userHome, repositoryTop, home }) {
125
130
  const linux = process.platform === 'linux';
126
- const root = linux ? path.join(process.env.XDG_CACHE_HOME || path.join(userHome, '.cache'), 'piagent/sandbox') : path.join(userHome, 'Library/Caches/Piagent/sandbox');
127
- const cache = path.join(root, createHash('sha256').update(repositoryTop).digest('hex').slice(0, 24));
131
+ const cache = cacheDirectory(userHome, repositoryTop), root = path.dirname(cache);
128
132
  fs.mkdirSync(cache, { recursive: true, mode: 0o700 });
129
133
  fs.chmodSync(cache, 0o700);
130
134
  const now = new Date(); try { fs.utimesSync(cache, now, now); } catch { /* best effort */ }
@@ -1,11 +1,12 @@
1
+ import { callName } from './role-names.mjs';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { briefIssues } from './workflow.mjs';
4
5
 
5
6
  // The objection ledger: what a helper holds against the main agent's work in
6
7
  // one run (one member message), and what the main agent decided about it.
7
- // A helper's objection to its brief, a review finding and a failed verify
8
- // claim are one kind of item: open until the main agent resolves it with
8
+ // A helper's objection to its brief and a review finding are one kind
9
+ // of item: open until the main agent resolves it with
9
10
  // resolve_objection (accepted with what it changed, or rejected with what it
10
11
  // checked and why), and a rejection of a required item goes back to the
11
12
  // helper once; a disagreement that remains goes to the member. The run's
@@ -50,7 +51,7 @@ const open = run => ledgerOf(run).filter(i => i.status === 'open');
50
51
  export const blockingItems = run => open(run).filter(i => i.required && i.source === 'brief');
51
52
 
52
53
  // Adds items from one helper answer. `source`: brief (an objection to the
53
- // brief), review (blocking and major findings) or verify (failed claims).
54
+ // brief), review (blocking and major findings)
54
55
  export function addItems(managed, source, role, list, { required } = {}) {
55
56
  const run = managed.run;
56
57
  if (!run || !list?.length) return [];
@@ -64,7 +65,7 @@ export function addItems(managed, source, role, list, { required } = {}) {
64
65
  }
65
66
  return added;
66
67
  }
67
- export const itemText = i => `${i.id} [${i.source === 'brief' ? i.kind : i.source === 'review' ? i.kind : 'failed claim'}] ${i.role}: ${i.detail}${i.evidence ? ` (evidence: ${i.evidence}${i.place === 'missing' ? '; the harness found no such place' : ''})` : ''}`;
68
+ export const itemText = i => `${i.id} [${i.source === 'brief' ? i.kind : i.source === 'review' ? i.kind : 'failed claim'}] ${callName(i.role)}: ${i.detail}${i.evidence ? ` (evidence: ${i.evidence}${i.place === 'missing' ? '; the harness found no such place' : ''})` : ''}`;
68
69
  export const setStatus = (item, status, extra = {}) => Object.assign(item, { status, ...extra });
69
70
  export function saveLedger(managed) {
70
71
  const items = ledgerOf(managed.run);
@@ -75,7 +76,7 @@ export function saveLedger(managed) {
75
76
  // checked. An accepted item names what changed; a rejected one says why.
76
77
  export function resolveTool(owner) {
77
78
  return { name: 'resolve_objection', label: 'Objection',
78
- description: 'Record your decision on an objection a helper raised (its id, such as o1): a brief objection, a review finding or a failed verify claim. Check it first at the place its evidence names. accepted: you agree; say what you changed (a corrected brief, the plan, the code). rejected: say what you checked (file:line, a command and its result) and why the objection does not hold; a rejected required objection goes back to the helper, and a disagreement that remains goes to the member.',
79
+ description: 'Record your decision on an objection a helper raised (its id, such as o1): a brief objection or a review finding. Check it first at the place its evidence names. accepted: you agree; say what you changed (a corrected brief, the plan, the code). rejected: say what you checked (file:line, a command and its result) and why the objection does not hold; a rejected required objection goes back to the helper, and a disagreement that remains goes to the member.',
79
80
  parameters: { type: 'object', properties: { id: { type: 'string', maxLength: 10 }, decision: { type: 'string', enum: DECISIONS }, checked: { type: 'string', minLength: 1, maxLength: 300 },
80
81
  change: { type: 'string', maxLength: 300 }, reason: { type: 'string', maxLength: 500 } }, required: ['id', 'decision', 'checked'], additionalProperties: false },
81
82
  execute: async (_id, args) => {
@@ -87,7 +88,7 @@ export function resolveTool(owner) {
87
88
  if (args.decision === 'rejected' && !clip(args.reason, 500)) throw Error('managed-objection-invalid: a rejected objection says why it does not hold.');
88
89
  const resolution = { decision: args.decision, checked: clip(args.checked, 300), ...(args.change ? { change: clip(args.change, 300) } : {}), ...(args.reason ? { reason: clip(args.reason, 500) } : {}) };
89
90
  setStatus(item, args.decision, { resolution });
90
- const back = args.decision === 'rejected' && item.required ? ` The ${item.role} subagent judges your reason before the turn ends.` : '';
91
+ const back = args.decision === 'rejected' && item.required ? ` The ${callName(item.role)} subagent judges your reason before the turn ends.` : '';
91
92
  return { content: [{ type: 'text', text: `Recorded: ${item.id} ${args.decision}.${back}` }], details: { id: item.id, decision: args.decision } };
92
93
  } };
93
94
  }
@@ -125,12 +126,12 @@ export async function settleLedger(managed, run, { note, send, signal }) {
125
126
  const rejected = toRejudge(run)[0];
126
127
  if (rejected) {
127
128
  rejected.rejudged = true; let reply;
128
- await note(`Harness: the ${rejected.role} subagent judges the main agent's reason for rejecting ${rejected.id}.`, { phase: 'rejudge', role: rejected.role, id: rejected.id });
129
- try { reply = (await managed.delegate({ role: rejected.role, task: rejudgeBrief(rejected) }, signal, { harness: true })).content.map(p => p.text).join('\n'); }
129
+ await note(`Harness: the ${callName(rejected.role)} subagent judges the main agent's reason for rejecting ${rejected.id}.`, { phase: 'rejudge', role: rejected.role, id: rejected.id });
130
+ try { reply = (await managed.delegate({ role: callName(rejected.role), task: rejudgeBrief(rejected) }, signal, { harness: true })).content.map(p => p.text).join('\n'); }
130
131
  catch { return true; }
131
132
  if (!briefIssues(reply)?.length) { setStatus(rejected, 'withdrawn'); return true; }
132
133
  setStatus(rejected, 'disputed'); run.disputes.push({ source: rejected.role, brief: true });
133
- await note(`Harness: the ${rejected.role} subagent keeps its objection; the member decides.\n${itemText(rejected)}`, { phase: 'dispute', role: rejected.role, id: rejected.id });
134
+ await note(`Harness: the ${callName(rejected.role)} subagent keeps its objection; the member decides.\n${itemText(rejected)}`, { phase: 'dispute', role: rejected.role, id: rejected.id });
134
135
  return true;
135
136
  }
136
137
  const asked = run.decidedAsked ??= new Set(), pending = undecided(run).filter(item => !asked.has(item.id));
@@ -142,7 +143,7 @@ export async function settleLedger(managed, run, { note, send, signal }) {
142
143
 
143
144
  // What the main agent still has to decide before the turn ends: required
144
145
  // items, review findings above minor, brief objections it never answered.
145
- export const undecided = run => open(run).filter(i => i.required || i.source !== 'verify');
146
+ export const undecided = run => open(run);
146
147
  export function decideText(items) {
147
148
  return `Harness: ${items.length} objection(s) have no decision yet:\n${items.map(i => '- ' + itemText(i)).join('\n')}\nCheck each at the place its evidence names, then record your decision with resolve_objection: accepted with what you changed, or rejected with what you checked and why. Minor review findings need nothing; no new review follows.`;
148
149
  }
@@ -1,4 +1,5 @@
1
- import { briefIssues, currentPlan, planText, verifyVerdicts } from './workflow.mjs';
1
+ import { callName } from './helper-roles.mjs';
2
+ import { briefIssues, currentPlan, planText } from './workflow.mjs';
2
3
  import { addItems, itemText, ledgerOf, setStatus } from './ledger.mjs';
3
4
  export { resolveTool } from './ledger.mjs';
4
5
 
@@ -42,10 +43,10 @@ export function anchorText(manager, harness) {
42
43
  // reviews the final patch), else once, and again only after blocking findings.
43
44
  export function beforeDelegate(run, role, harness, { since = null, review = null, changed = false, asked = false } = {}) {
44
45
  if (!run || harness) return;
45
- if (run.disputes.some(d => d.source === role && d.brief)) throw Error(`managed-helper-disputed: the ${role} subagent objected to your briefs twice for this message, so the member decides. Tell them what it objects to and what you decided; do not delegate to it again before their next message.`);
46
+ if (run.disputes.some(d => d.source === role && d.brief)) throw Error(`managed-helper-disputed: the ${callName(role)} subagent objected to your briefs twice for this message, so the member decides. Tell them what it objects to and what you decided; do not delegate to it again before their next message.`);
46
47
  const mine = ledgerOf(run).filter(i => i.source === 'brief' && i.role === role && i.status === 'open');
47
48
  const blocked = mine.filter(i => i.required);
48
- if (blocked.length) throw Error(`managed-objection-open: the ${role} subagent objected to your brief; record your decision with resolve_objection before you brief it again.\n${blocked.map(itemText).join('\n')}`);
49
+ if (blocked.length) throw Error(`managed-objection-open: the ${callName(role)} subagent objected to your brief; record your decision with resolve_objection before you brief it again.\n${blocked.map(itemText).join('\n')}`);
49
50
  // Reviewers the member asked for (copies.mjs) run at their word.
50
51
  if (role === 'review' && !since && changed && !asked) {
51
52
  if (run.policy.review === 'require') throw Error('managed-review-by-harness: the harness reviews the final patch when you finish; do not ask for a review yourself. Finish the change and its checks.');
@@ -67,29 +68,11 @@ export async function relayObjection(managed, role, reply, harness) {
67
68
  const second = ledgerOf(run).some(i => i.source === 'brief' && i.role === role), items = addItems(managed, 'brief', role, issues), list = items.map(itemText).join('\n');
68
69
  if (second) { run.disputes.push({ source: role, brief: true }); for (const item of items) setStatus(item, 'disputed'); }
69
70
  await managed.session.sendCustomMessage({ customType: 'agent-watch-process', display: true,
70
- content: second ? `Harness: the ${role} subagent objected to a brief a second time for this message; the exchange stops and the member decides.\n${list}`
71
- : `Harness: the ${role} subagent objected to the brief the main agent gave it:\n${list}`,
71
+ content: second ? `Harness: the ${callName(role)} subagent objected to a brief a second time for this message; the exchange stops and the member decides.\n${list}`
72
+ : `Harness: the ${callName(role)} subagent objected to the brief the main agent gave it:\n${list}`,
72
73
  details: { phase: second ? 'dispute' : 'objection', role, by: 'main', issues: issues.map(({ kind, detail }) => ({ kind, detail })), ids: items.map(i => i.id) } }, { triggerTurn: false });
73
- if (second) return `\n\n[Harness] The ${role} subagent objected to your brief again. This is its second objection for this message, so the harness ends the exchange: do not delegate to it again before the member's next message. Tell the member plainly what it objects to and what you decided, and let them decide.`;
74
+ if (second) return `\n\n[Harness] The ${callName(role)} subagent objected to your brief again. This is its second objection for this message, so the harness ends the exchange: do not delegate to it again before the member's next message. Tell the member plainly what it objects to and what you decided, and let them decide.`;
74
75
  const blocking = items.filter(i => i.required);
75
- return `\n\n[Harness] The ${role} subagent objected to your brief:\n${list}\nCheck each at the place its evidence names, then record your decision with resolve_objection (accepted with what you changed, or rejected with what you checked and why).${blocking.length ? ` Until you do, file changes are refused: ${blocking.map(i => i.id).join(', ')} say${blocking.length === 1 ? 's' : ''} the brief rests on a wrong premise or conflicts with the request.` : ''} A rejected objection goes back to the helper; a second objection from it for this message goes to the member.`;
76
+ return `\n\n[Harness] The ${callName(role)} subagent objected to your brief:\n${list}\nCheck each at the place its evidence names, then record your decision with resolve_objection (accepted with what you changed, or rejected with what you checked and why).${blocking.length ? ` Until you do, file changes are refused: ${blocking.map(i => i.id).join(', ')} say${blocking.length === 1 ? 's' : ''} the brief rests on a wrong premise or conflicts with the request.` : ''} A rejected objection goes back to the helper; a second objection from it for this message goes to the member.`;
76
77
  }
77
78
 
78
- // Verify's verdicts, counted for the receipt; its failed claims go into the
79
- // ledger (required under a required verify, noted otherwise). Under a
80
- // required verify the failed claims are the run's to settle; once the agent
81
- // has been sent back with them, only the harness's own re-check replaces
82
- // them, and a claim it no longer fails is cleared.
83
- export function recordVerdicts(managed, reply, harness, details) {
84
- const run = managed.run, found = verifyVerdicts(reply);
85
- details.verdicts = found ? Object.fromEntries(['pass', 'fail', 'unverifiable'].map(s => [s, found.filter(v => v.status === s).length])) : null;
86
- if (run && found && (harness || !run.claimsAsked)) {
87
- run.verifyFails = found.filter(v => v.status === 'fail');
88
- const failed = new Set(run.verifyFails.map(v => v.claim)), required = run.policy.verify === 'require';
89
- for (const item of ledgerOf(run)) if (item.source === 'verify' && ['open', 'rejected', 'noted'].includes(item.status) && !failed.has(item.detail)) setStatus(item, 'cleared');
90
- const known = new Set(ledgerOf(run).filter(i => i.source === 'verify' && i.status !== 'cleared').map(i => i.detail));
91
- const added = addItems(managed, 'verify', 'verify', run.verifyFails.filter(v => !known.has(v.claim)).map(v => ({ kind: 'fail', detail: v.claim, evidence: v.evidence })), { required });
92
- if (!required) for (const item of added) setStatus(item, 'noted');
93
- }
94
- return found ? ' · ' + Object.entries(details.verdicts).filter(([, n]) => n).map(([s, n]) => `${n} ${s}`).join(', ') : '';
95
- }
@@ -0,0 +1,6 @@
1
+ // A helper's working name (what the main agent calls it) and its Harness role
2
+ // (Studio's grant). No imports: ledger.mjs and copies.mjs need these names
3
+ // without loading helper-roles.mjs, which imports workflow.mjs (a cycle).
4
+ export const ROLE_NAMES = Object.freeze({ explore: 'scout', scout: 'research', advisor: 'advisor', general: 'general', review: 'review' });
5
+ export const HELPER_ROLES = Object.freeze(Object.values(ROLE_NAMES));
6
+ export const callName = role => Object.keys(ROLE_NAMES).find(name => ROLE_NAMES[name] === role) ?? role;
@@ -14,18 +14,15 @@ import { nativeManagedModel, isVendor, piProvider, studioAPI, studioPath } from
14
14
  import { fetchOriginTool } from './repository-operation.mjs';
15
15
  import { describeFailure, failureCode, failureText } from '../runtime/managed-failure.mjs';
16
16
  import { enableCompanyTurnRetry, wrapRoleStreams, conversationSession } from './request-stream.mjs'; import { StudioLink, closeRun } from './studio-link.mjs';
17
- import { trackToolFailures } from './tool-failures.mjs';
18
- import { fastServed, fastSetter, fastState, recordFast, restoreFast } from './fast-mode.mjs';
19
- import { runHelper } from './helper-run.mjs';
20
- import { askTool } from './member-questions.mjs';
21
- import { setTimeout as sleep } from 'node:timers/promises';
17
+ import { trackToolFailures } from './tool-failures.mjs'; import { fastServed, fastSetter, fastState, recordFast, restoreFast } from './fast-mode.mjs';
18
+ import { runHelper } from './helper-run.mjs'; import { sweepGeneral } from './general-clone.mjs'; import { askTool } from './member-questions.mjs';
22
19
  import { stageCompaction, compactEarly, backOffFailedCompaction, trackCompaction, boundRequestImages } from './compaction.mjs';
23
20
  import { readPatchDigest, readPatchSnapshot, readChangedFiles, readHead, readRepositoryHeads } from './patch-snapshot.mjs';
24
- import { memberTurn } from './continuation.mjs';
21
+ import { memberTurn } from './continuation.mjs'; import { setTimeout as sleep } from 'node:timers/promises';
25
22
  import { workflowPolicy, repositoryChecks, sessionPlanTool, currentPlan, workflowPrompt, RunProcess, completionGate, pendingBaseline, pendingPaths, pendingHead, processEditTools } from './workflow.mjs';
26
23
  import { workflowContextTool, workflowContextExtension } from './workflow-context.mjs';
27
24
  import { beforeDelegate, resolveTool } from './objections.mjs'; import { fanOut, withDroppedNote } from './copies.mjs'; import { withMemberMcp } from './member-mcp.mjs';
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';
25
+ import { HELPER_CALLS, HELPER_ROLES, ROLE_NAMES, callName, helperRoles, helperPrompt, webPrompt, delegateDescription, mainWebLookups, limitedWebTools } from './helper-roles.mjs'; import { delegateParameters, delegateArgs, SPEC_PROMPT } from './helper-settlement.mjs';
29
26
  import { loadAgentResources } from './agent-skills.mjs';
30
27
  import { loadPiagentThemes } from '../runtime/terminal/terminal-style.mjs';
31
28
  import { alwaysBypass, restorePermission, recordBypass, permissionSetter } from './permission.mjs';
@@ -463,19 +460,21 @@ export class ManagedSession {
463
460
  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 });
464
461
  }
465
462
  // `harness`: the completion gate asks, not the main agent (its brief, its turn).
466
- async delegate({ role, task, title, since, ...more }, signal, { harness = false } = {}) {
463
+ async delegate({ role: name, task, title, since, ...more }, signal, { harness = false } = {}) {
464
+ const role = ROLE_NAMES[name]; // the helper's working name -> its Harness role
467
465
  if (!this.grant || !HELPER_ROLES.includes(role) || typeof task !== 'string' || !task.trim() || task.length > 12000 || this.helpers.has(role)) throw Error('managed-helper-unavailable');
468
466
  const enabled = helperRoles(this.manifest);
469
- if (!enabled.includes(role)) throw Error(`managed-helper-not-configured: the company Harness has no ${role} subagent${enabled.length ? `; use ${enabled.join(', ')}` : ''}.`);
467
+ if (!enabled.includes(role)) throw Error(`managed-helper-not-configured: the company Harness has no ${name} subagent${enabled.length ? `; use ${enabled.map(callName).join(', ')}` : ''}.`);
468
+ if (role === 'general' && (more.copies || more.split)) throw Error('managed-general-single: the general helper runs one at a time, each in its own clone; call it again for the next piece of work after merging the first.');
469
+ if (role === 'general') this.orphans = sweepGeneral(this);
470
470
  // Studio lets each helper role run HELPER_CALLS times per run (one user
471
471
  // message), each helper the member asked for counted (copies.mjs). Say so at once.
472
472
  const asked = fanOut(this.session?.sessionManager, role, more, harness), fan = asked?.dropped ? null : asked;
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.`);
473
+ if ((this.helperCalls.get(role) ?? 0) + (fan?.n ?? 1) > HELPER_CALLS) throw Error(`managed-helper-limit: the ${name} 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
474
  beforeDelegate(this.run, role, harness, { since, review: this.review, changed: (this.ownPaths?.size ?? 0) > 0, asked: !!fan });
475
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
- // While a helper works (a verify running a long suite) the main agent is
477
- // quiet: a beat each minute tells the dashboard's watchdog, which stops a
478
- // turn after 15 minutes without an event, that the turn is working.
476
+ // While a helper works (a general helper running a long suite) the main agent is quiet: a beat each
477
+ // minute tells the dashboard's watchdog (it stops a turn after 15 silent minutes) that the turn is working.
479
478
  const beat = setInterval(() => this.notify({ type: 'managed_helper_progress', role }), this.helperBeatMs ?? HELPER_BEAT_MS); beat.unref?.();
480
479
  try { return withDroppedNote(await job, role, asked); } finally { clearInterval(beat); this.helpers.delete(role); this.publishHelpers(); }
481
480
  }
@@ -495,6 +494,7 @@ export class ManagedSession {
495
494
  if (this.disposed) return;
496
495
  this.disposed = true;
497
496
  await this.session?.abort(); await Promise.allSettled([...this.helpers.values()]);
497
+ for (const root of this.liveClones ?? []) fs.rmSync(root, { recursive: true, force: true });
498
498
  this.session?.dispose(); await this.boundary?.dispose(); await this.broker.dispose();
499
499
  }
500
500
  }