@tea-agent/loop-agent 0.44.0-next.13 → 0.44.0-next.14

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 (145) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +12 -12
  3. package/dist/adapters/loop-agent.js +34 -0
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +5 -5
  6. package/dist/cli/program.js +1 -4
  7. package/dist/commands/init-legacy-audit.js +330 -0
  8. package/dist/commands/init-upgrade.js +644 -104
  9. package/dist/commands/init.js +685 -330
  10. package/dist/commands/operator-guide.js +99 -0
  11. package/dist/commands/operator.js +16 -3
  12. package/dist/executors/pi-sdk-executor.js +3 -1
  13. package/dist/governance/harness.js +2 -7
  14. package/dist/governance/manifest-types.js +28 -9
  15. package/dist/governance/project-capability-scanner.js +263 -0
  16. package/dist/governance/project-config.js +96 -0
  17. package/dist/governance/resource-catalog.js +151 -0
  18. package/dist/infrastructure/console/app-data.js +6 -2
  19. package/dist/shared/operator/capabilities.js +2 -2
  20. package/dist/shared/package-metadata.js +23 -7
  21. package/dist/shared/pi-context-pressure/extension.js +23 -0
  22. package/dist/shared/pi-context-pressure/index.js +1 -0
  23. package/dist/shared/pi-context-pressure/telemetry.js +4 -0
  24. package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
  25. package/dist/shared/update/npm-client.js +49 -1
  26. package/dist/worker/cli.js +3 -5
  27. package/dist/worker/console/chat/browser-automation.js +114 -5
  28. package/dist/worker/console/chat/browser-routes.js +21 -0
  29. package/dist/worker/console/chat/chat-event-store.js +7 -6
  30. package/dist/worker/console/chat/context-insights.js +84 -19
  31. package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
  32. package/dist/worker/console/chat/pet/pet-persist.js +109 -0
  33. package/dist/worker/console/chat/pet/pet-registry.js +142 -0
  34. package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
  35. package/dist/worker/console/chat/pet/pet-routes.js +125 -0
  36. package/dist/worker/console/chat/pet/pet-service.js +262 -0
  37. package/dist/worker/console/chat/pet/pet-state.js +99 -0
  38. package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
  39. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +2 -2
  40. package/dist/worker/console/chat/pi-runtime.js +7 -2
  41. package/dist/worker/console/chat/routes.js +43 -7
  42. package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
  43. package/dist/worker/console/chat/session-store.js +20 -3
  44. package/dist/worker/console/console-shutdown.js +34 -0
  45. package/dist/worker/console/server.js +2 -1
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BA7qCL_r.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-Df09ajwL.js → arc-h09p-LAc.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CpF9sw5R.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BtPys8ja.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DZPnTWqT.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BD9D4h1p.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Svn8yBmn.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-B8EbQj-S.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-tENZpcpF.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DBx71Lco.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-1v_QamMz.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BAMCNNBG.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BvuEury-.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-YqFuHp8s.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-ouI5T7Mr.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-Bri0_Izk.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-e8Vhhi4X.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-tSIXGQiJ.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Rl-sF5pt.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Db6ddPz8.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Jwd_Xw5S.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CeC_8InC.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LKPRaNq8.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DlVQytMW.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DnfJZwO-.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-3axVSXca.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-xe04-_L-.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-hpmRINiD.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
  78. package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
  79. package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BF6ksWTX.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DXisdeD7.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BWu65wmu.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-hfNhHqAI.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-D6HTfQcp.js → linear-BKZlWCUU.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-Iv91R1WP.js → mermaid.core-DbTaQjFE.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CfPChwjR.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B7TcoqHz.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-ouyPKMtQ.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B9tuyLbh.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Ck4q9nJs.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-wXLX5Dnp.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D2XWw6_m.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DzxKiug4.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWUqK5Us.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D5Pa4O5g.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-VH7cRnsJ.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C4HM5NoZ.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Bu-4hOlA.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CasFEvvl.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D-gpwPSA.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static/pets/ember/pet.json +167 -0
  105. package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
  106. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
  107. package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
  108. package/dist/worker/console/workspace-context.js +14 -0
  109. package/dist/worker/console/workspace-initialization.js +25 -16
  110. package/dist/worker/delivery/package.js +4 -1
  111. package/dist/worker/observe/static/operator-chrome.js +1 -42
  112. package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
  113. package/dist/workflows/dag/final-verification.js +45 -10
  114. package/dist/workflows/dag/hybrid/sources.js +14 -14
  115. package/dist/workflows/dag/hybrid/templates/backend-test.js +5 -5
  116. package/dist/workflows/dag/hybrid/templates/frontend-test.js +4 -4
  117. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
  118. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
  119. package/dist/workflows/dag/skill-instructions.js +102 -32
  120. package/dist/workflows/dag/skill-snapshot.js +3 -0
  121. package/dist/workflows/dag/skills.js +7 -7
  122. package/dist/workflows/dag/validate.js +2 -2
  123. package/dist/workflows/dag/workspace-checkpoint.js +13 -1
  124. package/docs/architecture/runtime-boundaries.md +28 -22
  125. package/docs/init-surface.manifest.json +43 -295
  126. package/docs/templates/agent-dag.base.json +9 -9
  127. package/docs/templates/agent-dag.final-verification.json +6 -6
  128. package/docs/templates/agent-dag.supervised-implementation.json +12 -12
  129. package/docs/templates/frontend-implementation-dag.json +2 -2
  130. package/docs/templates/frontend-test-dag.json +2 -2
  131. package/docs/templates/hybrid-dag.json +9 -9
  132. package/docs/templates/knowledge-sync-dag.json +8 -8
  133. package/package.json +6 -2
  134. package/resource-catalog.v1.json +1973 -0
  135. package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
  136. package/resources/init-history/legacy-surface.v1.json +33 -0
  137. package/skills/init-update/SKILL.md +34 -0
  138. package/skills/init-update/agents/openai.yaml +6 -0
  139. package/dist/worker/console/static/assets/channel-DiTNLZUm.js +0 -1
  140. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +0 -1
  141. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +0 -1
  142. package/dist/worker/console/static/assets/index-C_8_gWal.css +0 -1
  143. package/dist/worker/console/static/assets/index-DybcVXCu.js +0 -488
  144. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +0 -1
  145. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BB9CzRHB.js +0 -8
@@ -0,0 +1,99 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { checkInitUpdate } from "./init.js";
4
+ import { loadNormalizedProjectConfig } from "../governance/project-config.js";
5
+ import { CONTROLLER_RESOURCE_IDS, createControllerContextProvider, } from "../governance/resource-catalog.js";
6
+ import { LOOP_AGENT_PACKAGE_NAME, readPackageVersion } from "../shared/package-metadata.js";
7
+ async function exists(file) {
8
+ try {
9
+ await access(file);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ export async function buildOperatorGuideDocument(repoRootInput) {
17
+ const repoRoot = path.resolve(repoRootInput);
18
+ const provider = await createControllerContextProvider();
19
+ const packageRoot = path.dirname(provider.list().length > 0
20
+ ? (await provider.readText(CONTROLLER_RESOURCE_IDS.loopAgentBridgeSkill)).absolutePath
21
+ : repoRoot);
22
+ const version = readPackageVersion(path.resolve(packageRoot, "..", "..", "..")) ?? "unknown";
23
+ let reproducibility = "unavailable";
24
+ try {
25
+ reproducibility = (await loadNormalizedProjectConfig(repoRoot)).verification.reproducibility;
26
+ }
27
+ catch {
28
+ // An uninitialized or malformed project is classified below by init assessment.
29
+ }
30
+ let status = "uninitialized";
31
+ let bridge = "missing";
32
+ let next = {
33
+ command: "loop-agent init --repo-root .",
34
+ reason: "initialize the minimal project surface",
35
+ };
36
+ if (await exists(path.join(repoRoot, "harness.json"))) {
37
+ const report = await checkInitUpdate({ repoRoot });
38
+ const bridgeDecision = report.humanDecisions.find((item) => item.reason.includes("bridge-path-conflict"));
39
+ bridge = bridgeDecision
40
+ ? "conflict"
41
+ : await exists(path.join(repoRoot, ".agents", "skills", "loop-agent", "SKILL.md"))
42
+ ? "ready"
43
+ : "missing";
44
+ if (report.humanDecisions.length > 0 || report.modelMergeTasks.length > 0) {
45
+ status = "blocked";
46
+ next = {
47
+ command: "loop-agent init upgrade --repo-root . --json",
48
+ reason: "complete the bounded migration decisions before starting new work",
49
+ };
50
+ }
51
+ else if (report.deterministicActions.length > 0) {
52
+ status = "needs-update";
53
+ next = {
54
+ command: "loop-agent init update --repo-root . --apply-safe --json",
55
+ reason: "apply the deterministic minimal-surface update",
56
+ };
57
+ }
58
+ else {
59
+ status = "ready";
60
+ next = {
61
+ command: "loop-agent task advance <task-id> --profile auto --json",
62
+ reason: "prepare or continue a governed task",
63
+ };
64
+ }
65
+ }
66
+ return {
67
+ schemaVersion: 1,
68
+ kind: "loop-agent-operator-guide",
69
+ controller: {
70
+ packageName: LOOP_AGENT_PACKAGE_NAME,
71
+ version,
72
+ resourceCatalogSha256: provider.identity.catalogDigest,
73
+ },
74
+ project: { repoRoot, status, configReproducibility: reproducibility, bridge },
75
+ dag: {
76
+ entry: "loop-agent task advance",
77
+ longRunningOwner: "console-operation-required",
78
+ builtinSkillSource: "builtin",
79
+ },
80
+ next,
81
+ };
82
+ }
83
+ export function renderOperatorGuideMarkdown(document) {
84
+ return [
85
+ "# loop-agent operator guide",
86
+ "",
87
+ `- Controller: \`${document.controller.packageName}@${document.controller.version}\``,
88
+ `- Project: \`${document.project.status}\``,
89
+ `- Bridge: \`${document.project.bridge}\``,
90
+ `- Verification: \`${document.project.configReproducibility}\``,
91
+ "",
92
+ "## Next",
93
+ "",
94
+ `\`${document.next.command}\``,
95
+ "",
96
+ document.next.reason,
97
+ "",
98
+ ].join("\n");
99
+ }
@@ -1,4 +1,5 @@
1
1
  import { buildOperatorCapabilitiesDocument, operatorFailed, operatorSucceeded, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
2
+ import { buildOperatorGuideDocument, renderOperatorGuideMarkdown, } from "./operator-guide.js";
2
3
  export function parseOperatorArgs(args) {
3
4
  const options = {};
4
5
  const positional = [];
@@ -7,19 +8,31 @@ export function parseOperatorArgs(args) {
7
8
  options.json = true;
8
9
  continue;
9
10
  }
11
+ if (token === "--markdown") {
12
+ options.markdown = true;
13
+ continue;
14
+ }
10
15
  if (token.startsWith("-")) {
11
- throw new Error(`unknown flag ${token}\nusage: operator capabilities --json`);
16
+ throw new Error(`unknown flag ${token}\nusage: operator <capabilities|guide> [--json|--markdown]`);
12
17
  }
13
18
  positional.push(token);
14
19
  }
15
20
  const subcommand = positional[0];
16
21
  if (!subcommand) {
17
- throw new Error("usage: operator <capabilities> --json");
22
+ throw new Error("usage: operator <capabilities|guide> [--json|--markdown]");
18
23
  }
19
24
  return { subcommand, options };
20
25
  }
21
- export async function runOperator(_repoRoot, args) {
26
+ export async function runOperator(repoRoot, args) {
22
27
  const { subcommand, options } = parseOperatorArgs(args);
28
+ if (subcommand === "guide") {
29
+ const guide = await buildOperatorGuideDocument(repoRoot);
30
+ if (options.markdown)
31
+ console.log(renderOperatorGuideMarkdown(guide));
32
+ else
33
+ emit(operatorSucceeded("operator guide", guide), true);
34
+ return;
35
+ }
23
36
  if (subcommand !== "capabilities") {
24
37
  const result = operatorFailed({
25
38
  command: "operator",
@@ -8,7 +8,7 @@ import { computeExtensionToolAllowlistExtras } from "./pi-extension-resolver.js"
8
8
  import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractPiEventStopReason, extractAssistantTextFromPiJson, } from "./pi-executor.js";
9
9
  import { serializeSessionEvent } from "./pi-event-serializer.js";
10
10
  import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
11
- import { createCompactionController, createContextPressureExtension, createSiftBridgeState, inMemoryCompactionSettings, isDagSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, SIFT_RETRIEVE_TOOL_NAME, } from "../shared/pi-context-pressure/index.js";
11
+ import { createCompactionController, createContextPressureExtension, createSiftBridgeState, inMemoryCompactionSettings, isDagSiftEnabled, parseCompactionPolicy, parsePiToolResultPruneConfig, parseSiftBridgeConfig, SIFT_RETRIEVE_TOOL_NAME, } from "../shared/pi-context-pressure/index.js";
12
12
  import { DAG_TELEMETRY_SESSION_ID_ENV, injectWizardLocalTelemetryHeaders, isTelemetrySessionId, } from "../shared/wizard-local-telemetry.js";
13
13
  /** Maximum number of same-session continuations after provider output truncation. */
14
14
  export const PI_SDK_OUTPUT_LIMIT_CONTINUE_MAX = 3;
@@ -292,6 +292,7 @@ async function createSdkSession(sdk, input, shared) {
292
292
  // Prefer SettingsManager.inMemory so harness runs do not depend on ~/.pi or
293
293
  // project .pi/settings.json being present/trusted.
294
294
  const policy = parseCompactionPolicy();
295
+ const pruneSettings = parsePiToolResultPruneConfig();
295
296
  const compaction = {
296
297
  ...PI_RECOMMENDED_COMPACTION,
297
298
  ...inMemoryCompactionSettings(policy),
@@ -321,6 +322,7 @@ async function createSdkSession(sdk, input, shared) {
321
322
  policy,
322
323
  controller,
323
324
  sift,
325
+ ...(pruneSettings.enabled && policy.mode !== "off" ? { prune: pruneSettings.config } : {}),
324
326
  });
325
327
  const telemetryExtension = createDagWizardLocalTelemetryHeadersExtension(input.telemetrySessionId);
326
328
  const extensionFactories = [pressureExtension, telemetryExtension].filter((extension) => Boolean(extension));
@@ -1,9 +1,4 @@
1
- import { readFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { harnessManifestSchema } from '../shared/types.js';
1
+ import { loadNormalizedProjectConfig } from './project-config.js';
4
2
  export async function loadHarnessManifest(repoRoot) {
5
- const manifestPath = path.join(repoRoot, 'harness.json');
6
- const raw = await readFile(manifestPath, 'utf-8');
7
- const parsed = JSON.parse(raw);
8
- return harnessManifestSchema.parse(parsed);
3
+ return (await loadNormalizedProjectConfig(repoRoot)).manifest;
9
4
  }
@@ -209,14 +209,18 @@ export const nightSchedulerPolicySchema = z.object({
209
209
  export const workerPolicySchema = z.object({
210
210
  nightScheduler: nightSchedulerPolicySchema.optional().default({}),
211
211
  });
212
- export const CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR = "harness executors.cursor is no longer supported; remove it and use executors.pi only (Cursor is available only via cursor-prompt sidecar)";
213
- export const harnessManifestSchema = z
212
+ export const verificationCommandsSchema = z
214
213
  .object({
214
+ quick: z.array(z.string().trim().min(1)).optional().default([]),
215
+ standard: z.array(z.string().trim().min(1)).optional().default([]),
216
+ full: z.array(z.string().trim().min(1)).optional().default([]),
217
+ })
218
+ .strict();
219
+ export const CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR = "harness executors.cursor is no longer supported; remove it and use executors.pi only (Cursor is available only via cursor-prompt sidecar)";
220
+ const harnessManifestCommonShape = {
215
221
  $schema: z.string().optional(),
216
- version: z.number(),
217
222
  project: z.string(),
218
223
  adapter: z.string().optional(),
219
- governanceRoot: z.string(),
220
224
  features: z
221
225
  .object({
222
226
  goals: z.boolean().optional().default(true),
@@ -225,9 +229,7 @@ export const harnessManifestSchema = z
225
229
  .default({
226
230
  goals: true,
227
231
  }),
228
- entrypoints: z.record(z.string(), z.string().nullable()),
229
- artifacts: z.record(z.string(), z.string().nullable()),
230
- scripts: z.record(z.string(), z.string().nullable()),
232
+ verification: verificationCommandsSchema.optional().default({}),
231
233
  workflowPolicy: workflowPolicySchema,
232
234
  /** Optional; absent means Night Scheduler uses package defaults when enabled by callers. */
233
235
  workerPolicy: workerPolicySchema.optional(),
@@ -237,8 +239,25 @@ export const harnessManifestSchema = z
237
239
  .strict()
238
240
  .optional()
239
241
  .default({}),
240
- })
241
- .strict()
242
+ };
243
+ const harnessManifestV1Schema = z.object({
244
+ ...harnessManifestCommonShape,
245
+ version: z.literal(1),
246
+ governanceRoot: z.string(),
247
+ entrypoints: z.record(z.string(), z.string().nullable()),
248
+ artifacts: z.record(z.string(), z.string().nullable()),
249
+ scripts: z.record(z.string(), z.string().nullable()),
250
+ }).strict();
251
+ const harnessManifestV2Schema = z.object({
252
+ ...harnessManifestCommonShape,
253
+ version: z.literal(2),
254
+ governanceRoot: z.string().optional().default("docs"),
255
+ entrypoints: z.record(z.string(), z.string().nullable()).optional().default({}),
256
+ artifacts: z.record(z.string(), z.string().nullable()).optional().default({}),
257
+ scripts: z.record(z.string(), z.string().nullable()).optional().default({}),
258
+ }).strict();
259
+ export const harnessManifestSchema = z
260
+ .discriminatedUnion("version", [harnessManifestV1Schema, harnessManifestV2Schema])
242
261
  .superRefine((manifest, ctx) => {
243
262
  // Preserve the empty read-only compatibility projection for callers that
244
263
  // inspect parsed manifests, while rejecting this field in JSON input.
@@ -0,0 +1,263 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { lstat, open, readdir, realpath, stat } from "node:fs/promises";
4
+ import path from "node:path";
5
+ export const PROJECT_CAPABILITY_SCANNER_VERSION = 1;
6
+ const MAX_INPUT_BYTES = 1024 * 1024;
7
+ const RISKY_COMMAND_RE = /(?:^|[^A-Za-z0-9])(deploy|publish|release|upload|push|destroy|apply|login|logout)(?:$|[^A-Za-z0-9])/i;
8
+ const SHELL_COMPOSITION_RE = /(?:&&|\|\||[|;<>`]|\$\(|\r|\n)/;
9
+ const SAFE_NODE_SCRIPT_RE = /^(?:(?:npx|pnpm exec|yarn exec)\s+)?(?:tsc|eslint|biome|prettier|vitest|jest|mocha|ava|vite|next|node --test)(?:\s|$)/i;
10
+ export function classifyVerificationCommand(command) {
11
+ const normalized = command.trim();
12
+ if (!normalized || RISKY_COMMAND_RE.test(normalized) || SHELL_COMPOSITION_RE.test(normalized))
13
+ return "unsafe";
14
+ return "safe";
15
+ }
16
+ function isContained(root, candidate) {
17
+ const relative = path.relative(root, candidate);
18
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
19
+ }
20
+ async function readSafeInput(repoRoot, relativePath) {
21
+ const file = path.resolve(repoRoot, ...relativePath.split("/"));
22
+ if (!isContained(path.resolve(repoRoot), file))
23
+ return null;
24
+ try {
25
+ const [rootReal, fileReal, linkInfo] = await Promise.all([realpath(repoRoot), realpath(file), lstat(file)]);
26
+ if (!isContained(rootReal, fileReal) || !linkInfo.isFile() || linkInfo.isSymbolicLink() || linkInfo.nlink !== 1)
27
+ return null;
28
+ if (linkInfo.size > MAX_INPUT_BYTES)
29
+ return null;
30
+ const noFollow = "O_NOFOLLOW" in fsConstants ? fsConstants.O_NOFOLLOW : 0;
31
+ const handle = await open(file, fsConstants.O_RDONLY | noFollow);
32
+ let before;
33
+ let after;
34
+ let bytes;
35
+ try {
36
+ before = await handle.stat();
37
+ if (!before.isFile() || before.nlink !== 1 || before.size > MAX_INPUT_BYTES)
38
+ return null;
39
+ bytes = await handle.readFile();
40
+ after = await handle.stat();
41
+ }
42
+ finally {
43
+ await handle.close();
44
+ }
45
+ const [pathAfter, fileRealAfter] = await Promise.all([stat(file), realpath(file)]);
46
+ if (!isContained(rootReal, fileRealAfter))
47
+ return null;
48
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs)
49
+ return null;
50
+ if (after.dev !== pathAfter.dev || after.ino !== pathAfter.ino || after.size !== pathAfter.size)
51
+ return null;
52
+ return {
53
+ path: relativePath,
54
+ bytes: bytes.byteLength,
55
+ sha256: createHash("sha256").update(bytes).digest("hex"),
56
+ content: bytes.toString("utf8"),
57
+ };
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function isSafeNodeScript(name, scripts, visiting = new Set()) {
64
+ if (visiting.has(name))
65
+ return false;
66
+ const body = scripts[name];
67
+ if (typeof body !== "string" || classifyVerificationCommand(body) !== "safe")
68
+ return false;
69
+ if (SAFE_NODE_SCRIPT_RE.test(body.trim()))
70
+ return true;
71
+ const delegated = /^(?:npm|pnpm|yarn)\s+(?:run\s+)?([A-Za-z0-9:_-]+)$/.exec(body.trim());
72
+ if (!delegated)
73
+ return false;
74
+ visiting.add(name);
75
+ const safe = isSafeNodeScript(delegated[1], scripts, visiting);
76
+ visiting.delete(name);
77
+ return safe;
78
+ }
79
+ function parseJson(input) {
80
+ if (!input)
81
+ return null;
82
+ try {
83
+ const parsed = JSON.parse(input.content);
84
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ function nodePackageManager(pkg, evidence) {
91
+ if (typeof pkg.packageManager === "string") {
92
+ if (pkg.packageManager.startsWith("pnpm@"))
93
+ return "pnpm";
94
+ if (pkg.packageManager.startsWith("yarn@"))
95
+ return "yarn";
96
+ }
97
+ if (evidence.has("pnpm-lock.yaml"))
98
+ return "pnpm";
99
+ if (evidence.has("yarn.lock"))
100
+ return "yarn";
101
+ return "npm";
102
+ }
103
+ function nodeCommand(pm, script) {
104
+ if (script === "test")
105
+ return `${pm} test`;
106
+ return pm === "npm" ? `npm run ${script}` : `${pm} run ${script}`;
107
+ }
108
+ function workspacePatterns(pkg) {
109
+ const raw = Array.isArray(pkg.workspaces)
110
+ ? pkg.workspaces
111
+ : pkg.workspaces && typeof pkg.workspaces === "object" && Array.isArray(pkg.workspaces.packages)
112
+ ? pkg.workspaces.packages
113
+ : [];
114
+ return raw.filter((item) => typeof item === "string" && item.endsWith("/*") && !item.includes(".."));
115
+ }
116
+ async function discoverWorkspaceCandidates(repoRoot, patterns) {
117
+ const candidates = [];
118
+ for (const pattern of patterns) {
119
+ const parent = pattern.slice(0, -2).replaceAll("\\", "/");
120
+ const parentPath = path.resolve(repoRoot, ...parent.split("/"));
121
+ if (!isContained(path.resolve(repoRoot), parentPath))
122
+ continue;
123
+ let names;
124
+ try {
125
+ names = (await readdir(parentPath, { withFileTypes: true }))
126
+ .filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
127
+ .map((entry) => entry.name)
128
+ .sort();
129
+ }
130
+ catch {
131
+ continue;
132
+ }
133
+ for (const name of names) {
134
+ const relative = `${parent}/${name}`;
135
+ if (await readSafeInput(repoRoot, `${relative}/package.json`))
136
+ candidates.push(relative);
137
+ }
138
+ }
139
+ return [...new Set(candidates)].sort();
140
+ }
141
+ async function rootDotnetFiles(repoRoot) {
142
+ try {
143
+ return (await readdir(repoRoot, { withFileTypes: true }))
144
+ .filter((entry) => entry.isFile() && (/\.sln$/i.test(entry.name) || /\.csproj$/i.test(entry.name)))
145
+ .map((entry) => entry.name)
146
+ .sort();
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ }
152
+ export class ProjectCapabilityScanner {
153
+ async scan(repoRoot, options = {}) {
154
+ const root = path.resolve(repoRoot);
155
+ const evidence = new Map();
156
+ const add = async (relative) => {
157
+ if (evidence.has(relative))
158
+ return evidence.get(relative);
159
+ const value = await readSafeInput(root, relative);
160
+ if (value)
161
+ evidence.set(relative, value);
162
+ return value;
163
+ };
164
+ for (const name of [
165
+ "README.md", "package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
166
+ "pyproject.toml", "go.mod", "Cargo.toml", "pom.xml", "build.gradle", "build.gradle.kts",
167
+ "gradlew", "gradlew.bat", "Makefile",
168
+ ])
169
+ await add(name);
170
+ for (const name of await rootDotnetFiles(root))
171
+ await add(name);
172
+ const rootPkg = parseJson(evidence.get("package.json") ?? null);
173
+ const workspaceCandidates = rootPkg ? await discoverWorkspaceCandidates(root, workspacePatterns(rootPkg)) : [];
174
+ const requestedWorkspace = options.workspaceRoot?.replaceAll("\\", "/").replace(/^\.\//, "") ?? null;
175
+ const explicitRoot = requestedWorkspace === ".";
176
+ let selectedWorkspace = explicitRoot ? null : requestedWorkspace;
177
+ if (selectedWorkspace && !workspaceCandidates.includes(selectedWorkspace)) {
178
+ throw new Error(`workspaceRoot is not a discovered workspace: ${selectedWorkspace}`);
179
+ }
180
+ if (!explicitRoot && !selectedWorkspace && workspaceCandidates.length === 1)
181
+ selectedWorkspace = workspaceCandidates[0];
182
+ const ambiguousWorkspace = !explicitRoot && !selectedWorkspace && workspaceCandidates.length > 1;
183
+ if (selectedWorkspace)
184
+ await add(`${selectedWorkspace}/package.json`);
185
+ const commands = [];
186
+ const ecosystems = new Set();
187
+ const push = (ecosystem, id, command, evidencePaths, cwd = ".") => {
188
+ if (classifyVerificationCommand(command) !== "safe")
189
+ return;
190
+ ecosystems.add(ecosystem);
191
+ commands.push({ id, command, cwd, scope: cwd === "." ? ["**"] : [`${cwd}/**`], evidence: evidencePaths, confidence: "high", source: "scanner" });
192
+ };
193
+ const nodeManifestPath = selectedWorkspace ? `${selectedWorkspace}/package.json` : "package.json";
194
+ const nodePkg = parseJson(evidence.get(nodeManifestPath) ?? null);
195
+ if (nodePkg) {
196
+ ecosystems.add("node");
197
+ if (!ambiguousWorkspace) {
198
+ const pm = nodePackageManager(rootPkg ?? nodePkg, evidence);
199
+ for (const script of ["typecheck", "lint", "test", "build"]) {
200
+ const body = nodePkg.scripts?.[script];
201
+ if (typeof body !== "string" || !isSafeNodeScript(script, nodePkg.scripts ?? {}))
202
+ continue;
203
+ push("node", `node:${script}`, nodeCommand(pm, script), [nodeManifestPath], selectedWorkspace ?? ".");
204
+ }
205
+ }
206
+ }
207
+ if (evidence.has("pyproject.toml")) {
208
+ ecosystems.add("python");
209
+ if (/\[tool\.pytest\.ini_options\]/.test(evidence.get("pyproject.toml").content))
210
+ push("python", "python:test", "python -m pytest", ["pyproject.toml"]);
211
+ }
212
+ if (evidence.has("go.mod")) {
213
+ ecosystems.add("go");
214
+ push("go", "go:test", "go test ./...", ["go.mod"]);
215
+ }
216
+ if (evidence.has("Cargo.toml")) {
217
+ ecosystems.add("rust");
218
+ push("rust", "rust:test", "cargo test", ["Cargo.toml"]);
219
+ }
220
+ if (evidence.has("pom.xml")) {
221
+ ecosystems.add("maven");
222
+ push("maven", "maven:test", "mvn test", ["pom.xml"]);
223
+ }
224
+ const gradle = evidence.has("build.gradle.kts") ? "build.gradle.kts" : evidence.has("build.gradle") ? "build.gradle" : null;
225
+ if (gradle) {
226
+ ecosystems.add("gradle");
227
+ push("gradle", "gradle:test", process.platform === "win32" && evidence.has("gradlew.bat") ? "gradlew.bat test" : evidence.has("gradlew") ? "./gradlew test" : "gradle test", [gradle]);
228
+ }
229
+ if (evidence.has("Makefile")) {
230
+ ecosystems.add("make");
231
+ if (/^test\s*:/m.test(evidence.get("Makefile").content))
232
+ push("make", "make:test", "make test", ["Makefile"]);
233
+ }
234
+ const dotnet = [...evidence.keys()].filter((name) => /\.(?:sln|csproj)$/i.test(name));
235
+ if (dotnet.length > 0)
236
+ ecosystems.add("dotnet");
237
+ if (dotnet.length === 1)
238
+ push("dotnet", "dotnet:test", `dotnet test ${dotnet[0]}`, [dotnet[0]]);
239
+ const evidenceList = [...evidence.values()]
240
+ .map(({ content: _content, ...item }) => item)
241
+ .sort((a, b) => a.path.localeCompare(b.path));
242
+ const fingerprintHash = createHash("sha256");
243
+ fingerprintHash.update(`project-capability-scanner-v${PROJECT_CAPABILITY_SCANNER_VERSION}\n`);
244
+ for (const item of evidenceList)
245
+ fingerprintHash.update(`${item.path}\0${item.sha256}\n`);
246
+ fingerprintHash.update(`workspace\0${explicitRoot ? "." : selectedWorkspace ?? ""}\n`);
247
+ const status = ambiguousWorkspace ? "ambiguous" : commands.length > 0 ? "ready" : "unavailable";
248
+ return {
249
+ schemaVersion: 1,
250
+ scannerVersion: 1,
251
+ repoRoot: root,
252
+ workspace: { selected: explicitRoot ? "." : selectedWorkspace, candidates: workspaceCandidates },
253
+ ecosystems: [...ecosystems].sort(),
254
+ evidence: evidenceList,
255
+ fingerprint: fingerprintHash.digest("hex"),
256
+ verification: {
257
+ status,
258
+ commands: ambiguousWorkspace ? [] : commands,
259
+ requiredInput: ambiguousWorkspace ? ["workspaceRoot"] : commands.length === 0 ? ["verification"] : [],
260
+ },
261
+ };
262
+ }
263
+ }
@@ -0,0 +1,96 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { harnessManifestSchema, verificationCommandsSchema, workflowPolicySchema, workerPolicySchema, worktreeManifestConfigSchema, } from "./manifest-types.js";
5
+ function isSafeProjectPath(value) {
6
+ if (!value || path.isAbsolute(value))
7
+ return false;
8
+ return !value.replaceAll("\\", "/").split("/").some((part) => part === "" || part === "." || part === "..");
9
+ }
10
+ const nullablePathMapSchema = z.record(z.string(), z.string().refine(isSafeProjectPath, "path must be a safe project-relative path").nullable());
11
+ /** Shared, credential-free overrides that are safe to commit with the project. */
12
+ export const projectConfigSchema = z
13
+ .object({
14
+ $schema: z.string().optional(),
15
+ version: z.literal(1),
16
+ verification: verificationCommandsSchema.optional(),
17
+ entrypoints: nullablePathMapSchema.optional(),
18
+ artifacts: nullablePathMapSchema.optional(),
19
+ workflowPolicy: workflowPolicySchema.optional(),
20
+ workerPolicy: workerPolicySchema.optional(),
21
+ worktree: worktreeManifestConfigSchema.optional(),
22
+ })
23
+ .strict();
24
+ const EMPTY_VERIFICATION = { quick: [], standard: [], full: [] };
25
+ function appendUnique(left, right) {
26
+ return [...new Set([...left, ...right])];
27
+ }
28
+ function mergeVerification(tracked, local) {
29
+ const shared = tracked ?? EMPTY_VERIFICATION;
30
+ const machine = local ?? EMPTY_VERIFICATION;
31
+ const sources = [];
32
+ if ([...shared.quick, ...shared.standard, ...shared.full].length > 0)
33
+ sources.push("tracked");
34
+ if ([...machine.quick, ...machine.standard, ...machine.full].length > 0)
35
+ sources.push("local");
36
+ return {
37
+ quick: appendUnique(shared.quick, machine.quick),
38
+ standard: appendUnique(shared.standard, machine.standard),
39
+ full: appendUnique(shared.full, machine.full),
40
+ reproducibility: sources.includes("local") ? "local-only" : sources.includes("tracked") ? "tracked" : "inferred",
41
+ sources,
42
+ };
43
+ }
44
+ async function readOptionalJson(file) {
45
+ try {
46
+ return JSON.parse(await readFile(file, "utf8"));
47
+ }
48
+ catch (error) {
49
+ if (error.code === "ENOENT")
50
+ return undefined;
51
+ throw error;
52
+ }
53
+ }
54
+ /**
55
+ * Load package defaults, tracked project policy, then machine-local runtime
56
+ * settings. Local verification is additive by construction: it cannot remove
57
+ * or replace a command committed in project.json.
58
+ */
59
+ export async function loadNormalizedProjectConfig(repoRoot) {
60
+ const localPath = path.join(repoRoot, "harness.json");
61
+ const trackedPath = path.join(repoRoot, ".loop-agent", "project.json");
62
+ const [localRaw, trackedRaw] = await Promise.all([
63
+ readOptionalJson(localPath),
64
+ readOptionalJson(trackedPath),
65
+ ]);
66
+ if (localRaw === undefined)
67
+ throw Object.assign(new Error("harness.json not found"), { code: "HARNESS_NOT_FOUND" });
68
+ const local = harnessManifestSchema.parse(localRaw);
69
+ const tracked = trackedRaw === undefined ? undefined : projectConfigSchema.parse(trackedRaw);
70
+ const verification = mergeVerification(tracked?.verification, local.verification);
71
+ const localInput = localRaw;
72
+ const localFields = { ...local };
73
+ delete localFields.modelProfiles;
74
+ const manifest = harnessManifestSchema.parse({
75
+ ...localFields,
76
+ entrypoints: { ...(tracked?.entrypoints ?? {}), ...(local.entrypoints ?? {}) },
77
+ artifacts: { ...(tracked?.artifacts ?? {}), ...(local.artifacts ?? {}) },
78
+ workflowPolicy: Object.hasOwn(localInput, "workflowPolicy") ? local.workflowPolicy : tracked?.workflowPolicy ?? local.workflowPolicy,
79
+ workerPolicy: Object.hasOwn(localInput, "workerPolicy") ? local.workerPolicy : tracked?.workerPolicy ?? local.workerPolicy,
80
+ worktree: Object.hasOwn(localInput, "worktree") ? local.worktree : tracked?.worktree ?? local.worktree,
81
+ verification: {
82
+ quick: verification.quick,
83
+ standard: verification.standard,
84
+ full: verification.full,
85
+ },
86
+ });
87
+ return {
88
+ schemaVersion: 1,
89
+ manifest,
90
+ verification,
91
+ sources: {
92
+ tracked: tracked ? ".loop-agent/project.json" : null,
93
+ local: "harness.json",
94
+ },
95
+ };
96
+ }