ultimate-pi 0.18.0 → 0.19.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.
- package/.agents/skills/harness-debate-plan/SKILL.md +1 -1
- package/.agents/skills/harness-decisions/SKILL.md +2 -3
- package/.agents/skills/harness-governor/SKILL.md +6 -5
- package/.agents/skills/harness-orchestration/SKILL.md +4 -4
- package/.agents/skills/harness-review/SKILL.md +7 -7
- package/.agents/skills/harness-sentrux-setup/SKILL.md +4 -3
- package/.agents/skills/harness-steer/SKILL.md +1 -1
- package/.agents/skills/sentrux/SKILL.md +9 -9
- package/.pi/PACKAGING.md +4 -4
- package/.pi/SYSTEM.md +54 -120
- package/.pi/agents/harness/incident-recorder.md +0 -1
- package/.pi/agents/harness/planning/decompose.md +1 -3
- package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
- package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
- package/.pi/agents/harness/planning/hypothesis.md +0 -2
- package/.pi/agents/harness/planning/implementation-researcher.md +0 -2
- package/.pi/agents/harness/planning/plan-adversary.md +0 -2
- package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
- package/.pi/agents/harness/planning/planning-context.md +0 -2
- package/.pi/agents/harness/planning/review-integrator.md +0 -2
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
- package/.pi/agents/harness/planning/stack-researcher.md +0 -2
- package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +0 -2
- package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +0 -2
- package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -2
- package/.pi/agents/harness/{executor.md → running/executor.md} +0 -2
- package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
- package/.pi/agents/harness/sentrux-steward.md +0 -2
- package/.pi/agents/harness/trace-librarian.md +0 -1
- package/.pi/extensions/00-harness-project-control.ts +133 -0
- package/.pi/extensions/00-posthog-network-bootstrap.ts +1 -1
- package/.pi/extensions/agt-kill-switch.ts +57 -0
- package/.pi/extensions/agt-prompt-guard.ts +32 -0
- package/.pi/extensions/budget-guard.ts +2 -0
- package/.pi/extensions/custom-footer.ts +46 -145
- package/.pi/extensions/custom-header.ts +1 -1
- package/.pi/extensions/custom-system-prompt.ts +1 -1
- package/.pi/extensions/debate-orchestrator.ts +7 -5
- package/.pi/extensions/harness-ask-user.ts +8 -8
- package/.pi/extensions/harness-debate-tools.ts +27 -43
- package/.pi/extensions/harness-lens.ts +94 -0
- package/.pi/extensions/harness-live-widget.ts +33 -2
- package/.pi/extensions/harness-plan-approval.ts +12 -12
- package/.pi/extensions/harness-run-context.ts +1214 -852
- package/.pi/extensions/harness-subagent-governance.ts +8 -0
- package/.pi/extensions/harness-subagent-submit.ts +36 -164
- package/.pi/extensions/harness-subagents.ts +4 -4
- package/.pi/extensions/harness-telemetry.ts +3 -1
- package/.pi/extensions/harness-web-tools.ts +3 -3
- package/.pi/extensions/observation-bus.ts +2 -0
- package/.pi/extensions/policy-gate.ts +27 -5
- package/.pi/extensions/review-integrity.ts +91 -10
- package/.pi/extensions/sentrux-rules-sync.ts +3 -1
- package/.pi/extensions/subagent-governance.ts +92 -0
- package/.pi/extensions/test-diff-integrity.ts +1 -0
- package/.pi/extensions/trace-recorder.ts +3 -1
- package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
- package/.pi/harness/README.md +6 -2
- package/.pi/harness/agents.manifest.json +38 -49
- package/.pi/harness/agents.policy.yaml +275 -0
- package/.pi/harness/corpus/graphify-kb-updater.config.json +55 -0
- package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +2 -1
- package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
- package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +3 -2
- package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
- package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
- package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
- package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
- package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
- package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
- package/.pi/harness/docs/adrs/README.md +6 -0
- package/.pi/harness/docs/graphify-kb-updater-runbook.md +11 -5
- package/.pi/harness/docs/practice-map.md +2 -2
- package/.pi/harness/evolution/README.md +1 -2
- package/.pi/harness/examples/agents.policy.project.yaml +19 -0
- package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
- package/.pi/harness/policies/bash-denylists.yaml +5 -0
- package/.pi/harness/policies/defaults.yaml +51 -0
- package/.pi/harness/policies/orchestrator.yaml +18 -0
- package/.pi/harness/policies/phases.yaml +10 -0
- package/.pi/harness/policies/roles.yaml +5 -0
- package/.pi/harness/policies/web-guard.yaml +5 -0
- package/.pi/harness/policies/workflow-sequences.yaml +9 -0
- package/.pi/harness/sentrux/architecture.manifest.json +26 -4
- package/.pi/harness/specs/harness-spawn-context.schema.json +1 -1
- package/.pi/harness/specs/observation.schema.json +2 -1
- package/.pi/lib/agents-policy.d.mts +70 -0
- package/.pi/lib/agents-policy.mjs +325 -0
- package/.pi/lib/agents-policy.ts +19 -0
- package/.pi/lib/agt/audit-run-sink.ts +52 -0
- package/.pi/lib/agt/build-evaluation-context.ts +285 -0
- package/.pi/lib/agt/config.ts +28 -0
- package/.pi/lib/agt/delegation.ts +69 -0
- package/.pi/lib/agt/evaluate-policy.ts +56 -0
- package/.pi/lib/agt/identity-registry.ts +41 -0
- package/.pi/lib/agt/index.ts +55 -0
- package/.pi/lib/agt/kill-switch-state.ts +11 -0
- package/.pi/lib/agt/legacy-evaluate.ts +101 -0
- package/.pi/lib/agt/policy-engine.ts +154 -0
- package/.pi/lib/agt/rings.ts +21 -0
- package/.pi/lib/agt/sre-hooks.ts +45 -0
- package/.pi/lib/agt/trust-run-store.ts +26 -0
- package/.pi/lib/agt/workflow-history.ts +29 -0
- package/.pi/lib/agt-governance-active.ts +14 -0
- package/.pi/lib/agt-tool-guard.ts +78 -0
- package/.pi/lib/ask-user/dialog.ts +314 -0
- package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
- package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
- package/.pi/{extensions/lib → lib}/extension-load-guard.ts +21 -0
- package/.pi/lib/harness-agt-tool-guard.ts +5 -0
- package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +6 -16
- package/.pi/lib/harness-debate-core-deps.ts +14 -0
- package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
- package/.pi/lib/harness-lens/.gitattributes +1 -0
- package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
- package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
- package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
- package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
- package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
- package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
- package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
- package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
- package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
- package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
- package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
- package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
- package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
- package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
- package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
- package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
- package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
- package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
- package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
- package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
- package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
- package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
- package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
- package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
- package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
- package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
- package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
- package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
- package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
- package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
- package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
- package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
- package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
- package/.pi/lib/harness-lens/clients/types.ts +59 -0
- package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
- package/.pi/lib/harness-lens/index.ts +532 -0
- package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
- package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
- package/.pi/lib/harness-project-config.ts +91 -0
- package/.pi/lib/harness-run-context-responses.ts +9 -0
- package/.pi/lib/harness-run-context.ts +1 -3
- package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +4 -3
- package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +5 -28
- package/.pi/lib/harness-subagent-auth.ts +51 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +13 -10
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
- package/.pi/lib/harness-subagent-submit-register.ts +163 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -55
- package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +53 -14
- package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
- package/.pi/lib/harness-ui-state.ts +27 -12
- package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
- package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
- package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +3 -52
- package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
- package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-plan.md +4 -6
- package/.pi/prompts/harness-review.md +9 -9
- package/.pi/prompts/harness-run.md +7 -7
- package/.pi/prompts/harness-setup.md +42 -68
- package/.pi/prompts/harness-steer.md +2 -2
- package/.pi/scripts/README.md +3 -5
- package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
- package/.pi/scripts/graphify-kb-updater.mjs +48 -8
- package/.pi/scripts/harness-agents-manifest.mjs +61 -4
- package/.pi/scripts/harness-agt-doctor.ts +36 -0
- package/.pi/scripts/harness-cli-verify.sh +9 -2
- package/.pi/scripts/harness-project-toggle.mjs +129 -0
- package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
- package/.pi/scripts/harness-verify.mjs +113 -39
- package/.pi/scripts/harness-web-policy-guard.mjs +2 -2
- package/.pi/scripts/validate-plan-dag.mjs +65 -74
- package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
- package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
- package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
- package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
- package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
- package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
- package/.pi/skills/architecture/layered/SKILL.md +68 -0
- package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
- package/.pi/skills/architecture/microservices/SKILL.md +64 -0
- package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
- package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
- package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
- package/.pi/skills/architecture/service-based/SKILL.md +64 -0
- package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
- package/.pi/skills/architecture/space-based/SKILL.md +60 -0
- package/.pi/skills/ast-grep/SKILL.md +40 -321
- package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
- package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
- package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
- package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
- package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
- package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
- package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
- package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
- package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
- package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
- package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
- package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
- package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
- package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
- package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
- package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
- package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
- package/.pi/skills/lsp-navigation/SKILL.md +89 -0
- package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
- package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
- package/.pi/skills/quality/security-review/SKILL.md +34 -0
- package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
- package/.pi/skills/quality/testability-design/SKILL.md +33 -0
- package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
- package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
- package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
- package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
- package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
- package/.sentrux/rules.toml +20 -4
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +26 -0
- package/README.md +85 -58
- package/THIRD_PARTY_NOTICES.md +12 -21
- package/package.json +15 -7
- package/vendor/pi-subagents/src/agents.ts +45 -1
- package/vendor/pi-subagents/src/subagents.ts +866 -811
- package/vendor/pi-vcc/src/core/brief.ts +68 -99
- package/vendor/pi-vcc/src/core/settings.ts +2 -2
- package/.agents/skills/caveman/SKILL.md +0 -67
- package/.pi/agents/harness/meta-optimizer.md +0 -36
- package/.pi/agents/harness/planning/scout-graphify.md +0 -39
- package/.pi/agents/harness/planning/scout-semantic.md +0 -41
- package/.pi/agents/harness/planning/scout-structure.md +0 -37
- package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
- package/.pi/extensions/lib/harness-subagent-auth.ts +0 -209
- package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
- package/.pi/extensions/pi-model-router-harness.ts +0 -42
- package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
- package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
- package/.pi/model-router.example.json +0 -36
- package/.pi/prompts/harness-critic.md +0 -10
- package/.pi/prompts/harness-eval.md +0 -10
- package/.pi/prompts/harness-router-tune.md +0 -52
- package/.pi/scripts/harness-generate-model-router.mjs +0 -327
- package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
- package/.pi/scripts/harness-sync-model-router.mjs +0 -97
- package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
- package/vendor/pi-model-router/.prettierignore +0 -4
- package/vendor/pi-model-router/.prettierrc +0 -5
- package/vendor/pi-model-router/AGENTS.md +0 -39
- package/vendor/pi-model-router/LICENSE +0 -21
- package/vendor/pi-model-router/README.md +0 -99
- package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
- package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
- package/vendor/pi-model-router/extensions/commands.ts +0 -720
- package/vendor/pi-model-router/extensions/config.ts +0 -348
- package/vendor/pi-model-router/extensions/constants.ts +0 -1
- package/vendor/pi-model-router/extensions/index.ts +0 -478
- package/vendor/pi-model-router/extensions/provider.ts +0 -580
- package/vendor/pi-model-router/extensions/routing.ts +0 -564
- package/vendor/pi-model-router/extensions/state.ts +0 -52
- package/vendor/pi-model-router/extensions/types.ts +0 -95
- package/vendor/pi-model-router/extensions/ui.ts +0 -144
- package/vendor/pi-model-router/model-router.example.json +0 -48
- package/vendor/pi-model-router/package.json +0 -48
- package/vendor/pi-model-router/tsconfig.json +0 -16
- /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
|
@@ -5,10 +5,20 @@
|
|
|
5
5
|
* in before_agent_start so trace-recorder reuses it on agent_start.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
mkdir,
|
|
10
|
+
readdir,
|
|
11
|
+
readFile,
|
|
12
|
+
rename,
|
|
13
|
+
stat,
|
|
14
|
+
writeFile,
|
|
15
|
+
} from "node:fs/promises";
|
|
16
|
+
import { basename, dirname, join } from "node:path";
|
|
10
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
18
|
import { Type } from "@sinclair/typebox";
|
|
19
|
+
import { allowsAgentTool } from "../lib/agents-policy.mjs";
|
|
20
|
+
import { claimHarnessGovernanceLoad } from "../lib/extension-load-guard.js";
|
|
21
|
+
import { getHarnessPackageRoot } from "../lib/harness-paths.js";
|
|
12
22
|
import {
|
|
13
23
|
canonicalPlanPath,
|
|
14
24
|
claimRunOwnership,
|
|
@@ -60,19 +70,16 @@ import {
|
|
|
60
70
|
validatePlanOverridePath,
|
|
61
71
|
validatePlanPacket,
|
|
62
72
|
} from "../lib/harness-run-context.js";
|
|
73
|
+
import { blockRunContextMessage } from "../lib/harness-run-context-responses.js";
|
|
74
|
+
import { isSubmitToolName } from "../lib/harness-subagent-submit-registry.js";
|
|
75
|
+
import { bootstrapHarnessSubprocessFromEnv } from "../lib/harness-subprocess-bootstrap.js";
|
|
63
76
|
import {
|
|
64
77
|
normalizeHarnessYamlContent,
|
|
65
78
|
parseStructuredDocument,
|
|
66
79
|
writeYamlFile,
|
|
67
80
|
} from "../lib/harness-yaml.js";
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
evaluateHarnessSubagentToolCall,
|
|
71
|
-
isSubmitToolName,
|
|
72
|
-
} from "./lib/harness-subagent-policy.js";
|
|
73
|
-
import { bootstrapHarnessSubprocessFromEnv } from "./lib/harness-subprocess-bootstrap.js";
|
|
74
|
-
import { isReviewRoundArtifactPath } from "./lib/plan-debate-gate.js";
|
|
75
|
-
import { isReviewRoundYamlWriteAllowed } from "./lib/plan-debate-write-guard.js";
|
|
81
|
+
import { isReviewRoundArtifactPath } from "../lib/plan-debate-gate.js";
|
|
82
|
+
import { isReviewRoundYamlWriteAllowed } from "../lib/plan-debate-write-guard.js";
|
|
76
83
|
|
|
77
84
|
// @ts-expect-error pi extensions run as ESM
|
|
78
85
|
const MODULE_URL = import.meta.url;
|
|
@@ -96,6 +103,136 @@ function persistContext(pi: ExtensionAPI, ctx: HarnessRunContext): void {
|
|
|
96
103
|
pi.events.emit("harness-run-context:updated", { run_id: ctx.run_id });
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
const PLAN_REVISION_ARTIFACT_FILES = new Set([
|
|
107
|
+
"planning-context.yaml",
|
|
108
|
+
"decomposition.yaml",
|
|
109
|
+
"hypothesis.yaml",
|
|
110
|
+
"implementation-research.yaml",
|
|
111
|
+
"stack.yaml",
|
|
112
|
+
"execution-plan-draft.yaml",
|
|
113
|
+
"plan-phase-status.yaml",
|
|
114
|
+
"plan-phase-waiver.yaml",
|
|
115
|
+
"sentrux-manifest-proposal.yaml",
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const PLAN_REVISION_ARTIFACT_PREFIXES = [
|
|
119
|
+
"hypothesis-validation-r",
|
|
120
|
+
"review-round-r",
|
|
121
|
+
"plan-evaluator-r",
|
|
122
|
+
"plan-adversary-r",
|
|
123
|
+
"sprint-contract-audit-r",
|
|
124
|
+
"adversary-brief-r",
|
|
125
|
+
] as const;
|
|
126
|
+
|
|
127
|
+
async function moveIfExists(from: string, to: string): Promise<boolean> {
|
|
128
|
+
try {
|
|
129
|
+
await stat(from);
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
await mkdir(dirname(to), { recursive: true });
|
|
134
|
+
await rename(from, to);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isPlanRevisionArtifactFile(name: string): boolean {
|
|
139
|
+
if (PLAN_REVISION_ARTIFACT_FILES.has(name)) return true;
|
|
140
|
+
if (name === "review-round-consolidated.yaml") return true;
|
|
141
|
+
return PLAN_REVISION_ARTIFACT_PREFIXES.some((prefix) =>
|
|
142
|
+
name.startsWith(prefix),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function archivePlanRevisionArtifacts(input: {
|
|
147
|
+
projectRoot: string;
|
|
148
|
+
runId: string;
|
|
149
|
+
reason: string;
|
|
150
|
+
recordedAt?: string;
|
|
151
|
+
}): Promise<{ archiveDir: string; moved: string[] }> {
|
|
152
|
+
const recordedAt = input.recordedAt ?? nowIso();
|
|
153
|
+
const revisionId = recordedAt.replace(/[:.]/g, "-");
|
|
154
|
+
const runDir = join(input.projectRoot, ".pi", "harness", "runs", input.runId);
|
|
155
|
+
const artifactsDir = join(runDir, "artifacts");
|
|
156
|
+
const archiveDir = join(artifactsDir, "revisions", revisionId);
|
|
157
|
+
const moved: string[] = [];
|
|
158
|
+
|
|
159
|
+
async function archiveRel(rel: string): Promise<void> {
|
|
160
|
+
const ok = await moveIfExists(join(runDir, rel), join(archiveDir, rel));
|
|
161
|
+
if (ok) moved.push(rel);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await archiveRel("plan-packet.yaml");
|
|
165
|
+
await archiveRel("plan-review.md");
|
|
166
|
+
await archiveRel("research-brief.yaml");
|
|
167
|
+
await archiveRel("debate-messenger");
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const names = await readdir(artifactsDir);
|
|
171
|
+
for (const name of names) {
|
|
172
|
+
if (!isPlanRevisionArtifactFile(name)) continue;
|
|
173
|
+
await archiveRel(join("artifacts", name));
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// No artifacts yet.
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const debateRel = join(
|
|
180
|
+
".pi",
|
|
181
|
+
"harness",
|
|
182
|
+
"debates",
|
|
183
|
+
`plan-${input.runId}.jsonl`,
|
|
184
|
+
);
|
|
185
|
+
const debateArchived = await moveIfExists(
|
|
186
|
+
join(input.projectRoot, debateRel),
|
|
187
|
+
join(archiveDir, "debates", basename(debateRel)),
|
|
188
|
+
);
|
|
189
|
+
if (debateArchived) moved.push(debateRel);
|
|
190
|
+
|
|
191
|
+
if (moved.length > 0) {
|
|
192
|
+
await mkdir(archiveDir, { recursive: true });
|
|
193
|
+
await writeFile(
|
|
194
|
+
join(archiveDir, "revision-reset.json"),
|
|
195
|
+
`${JSON.stringify(
|
|
196
|
+
{
|
|
197
|
+
schema_version: "1.0.0",
|
|
198
|
+
run_id: input.runId,
|
|
199
|
+
reason: input.reason,
|
|
200
|
+
recorded_at: recordedAt,
|
|
201
|
+
moved,
|
|
202
|
+
},
|
|
203
|
+
null,
|
|
204
|
+
2,
|
|
205
|
+
)}\n`,
|
|
206
|
+
"utf-8",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { archiveDir, moved };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function shouldArchiveForPlanRevise(input: {
|
|
214
|
+
command: string;
|
|
215
|
+
mode: "create" | "revise" | null;
|
|
216
|
+
runCtx: HarnessRunContext;
|
|
217
|
+
reviewOutcome: Awaited<ReturnType<typeof readReviewOutcomeFromRun>>;
|
|
218
|
+
userPrompt: string;
|
|
219
|
+
}): boolean {
|
|
220
|
+
if (input.command !== "harness-plan" && input.command !== "harness-auto") {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
if (input.mode !== "revise") return false;
|
|
224
|
+
const next = (input.runCtx.next_recommended_command ?? "").toLowerCase();
|
|
225
|
+
const prompt = input.userPrompt.toLowerCase();
|
|
226
|
+
return (
|
|
227
|
+
input.reviewOutcome?.remediation_class === "plan_gap" ||
|
|
228
|
+
next.includes("/harness-plan") ||
|
|
229
|
+
next.includes("revise") ||
|
|
230
|
+
prompt.includes("--mode revise") ||
|
|
231
|
+
prompt.includes("--mode=revise") ||
|
|
232
|
+
prompt.includes("mode: revise")
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
99
236
|
function syncPolicyFromRunContext(
|
|
100
237
|
pi: ExtensionAPI,
|
|
101
238
|
entries: unknown[],
|
|
@@ -236,10 +373,7 @@ async function offerCrossSessionResume(
|
|
|
236
373
|
hasUI: boolean;
|
|
237
374
|
sessionManager: { getEntries(): unknown[] };
|
|
238
375
|
ui: {
|
|
239
|
-
notify(
|
|
240
|
-
message: string,
|
|
241
|
-
type?: "info" | "warning" | "error",
|
|
242
|
-
): void;
|
|
376
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
243
377
|
};
|
|
244
378
|
},
|
|
245
379
|
): Promise<void> {
|
|
@@ -271,182 +405,154 @@ async function offerCrossSessionResume(
|
|
|
271
405
|
});
|
|
272
406
|
}
|
|
273
407
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
408
|
+
async function applyAbortSignal(input: {
|
|
409
|
+
pi: ExtensionAPI;
|
|
410
|
+
activeCtx: HarnessRunContext | null;
|
|
411
|
+
sessionId: string;
|
|
412
|
+
projectRoot: string;
|
|
413
|
+
entries: unknown[];
|
|
414
|
+
userPrompt: string;
|
|
415
|
+
}): Promise<HarnessRunContext | null> {
|
|
416
|
+
if (!input.userPrompt.toLowerCase().includes("harness-abort")) {
|
|
417
|
+
return input.activeCtx;
|
|
418
|
+
}
|
|
419
|
+
const nextCtx =
|
|
420
|
+
input.activeCtx ??
|
|
421
|
+
(await hydrateFromDisk(input.sessionId, input.projectRoot, input.entries));
|
|
422
|
+
if (!nextCtx) return nextCtx;
|
|
423
|
+
nextCtx.status = "aborted";
|
|
424
|
+
nextCtx.plan_ready = false;
|
|
425
|
+
nextCtx.last_outcome = "aborted";
|
|
426
|
+
nextCtx.last_completed_step = "abort";
|
|
427
|
+
nextCtx.next_recommended_command = nextCtx.task_summary
|
|
428
|
+
? `/harness-plan "${nextCtx.task_summary}"`
|
|
429
|
+
: '/harness-plan "<task>"';
|
|
430
|
+
persistContext(input.pi, nextCtx);
|
|
431
|
+
return nextCtx;
|
|
432
|
+
}
|
|
277
433
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
434
|
+
async function maybeHandleClarificationFollowUp(input: {
|
|
435
|
+
pi: ExtensionAPI;
|
|
436
|
+
activeCtx: HarnessRunContext;
|
|
437
|
+
entries: unknown[];
|
|
438
|
+
systemPrompt: string;
|
|
439
|
+
}) {
|
|
440
|
+
input.activeCtx.phase = "plan";
|
|
441
|
+
input.activeCtx.last_outcome = "needs_clarification";
|
|
442
|
+
const packet = input.activeCtx.plan_packet_path
|
|
443
|
+
? await readPlanPacketFromPath(input.activeCtx.plan_packet_path)
|
|
444
|
+
: null;
|
|
445
|
+
const planPath = input.activeCtx.plan_packet_path;
|
|
446
|
+
const summary =
|
|
447
|
+
packet && planPath
|
|
448
|
+
? planPacketSummary(packet, planPath, "needs_clarification")
|
|
449
|
+
: null;
|
|
450
|
+
syncPolicyFromPlan(
|
|
451
|
+
input.pi,
|
|
452
|
+
input.entries,
|
|
453
|
+
input.activeCtx.plan_id ?? "plan-pending",
|
|
454
|
+
"plan",
|
|
455
|
+
false,
|
|
456
|
+
);
|
|
457
|
+
persistContext(input.pi, input.activeCtx);
|
|
458
|
+
return {
|
|
459
|
+
systemPrompt: `${input.systemPrompt}\n\n${formatPlanContextBlock(input.activeCtx)}\n\n${formatActivePlanBlock(input.activeCtx, "revise", summary)}\n\nReply with clarification answers; the harness will treat this as plan amend.`,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
285
462
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
invoked_at: nowIso(),
|
|
300
|
-
});
|
|
301
|
-
return { action: "continue" as const };
|
|
463
|
+
function startFreshPlanAttempt(input: {
|
|
464
|
+
pi: ExtensionAPI;
|
|
465
|
+
activeCtx: HarnessRunContext;
|
|
466
|
+
command: string;
|
|
467
|
+
turn: HarnessTurnEntry | null;
|
|
468
|
+
}): void {
|
|
469
|
+
input.activeCtx.plan_ready = false;
|
|
470
|
+
input.activeCtx.phase = "plan";
|
|
471
|
+
input.activeCtx.status = "active";
|
|
472
|
+
input.pi.appendEntry("harness-plan-attempt", {
|
|
473
|
+
run_id: input.activeCtx.run_id,
|
|
474
|
+
command: input.command,
|
|
475
|
+
started_at: input.turn?.invoked_at ?? nowIso(),
|
|
302
476
|
});
|
|
477
|
+
}
|
|
303
478
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const turn = getLatestHarnessTurn(entries);
|
|
310
|
-
const parsed = turn
|
|
311
|
-
? { command: turn.command, args: turn.args }
|
|
312
|
-
: parseHarnessSlashInput(userPrompt);
|
|
313
|
-
const harnessTurn =
|
|
314
|
-
Boolean(turn) || Boolean(parsed) || needsClarificationFollowUp(activeCtx);
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
userPrompt.toLowerCase().includes("/harness-abort") ||
|
|
318
|
-
userPrompt.toLowerCase().includes("harness-abort")
|
|
319
|
-
) {
|
|
320
|
-
if (!activeCtx) {
|
|
321
|
-
activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
|
|
322
|
-
}
|
|
323
|
-
if (activeCtx) {
|
|
324
|
-
activeCtx.status = "aborted";
|
|
325
|
-
activeCtx.plan_ready = false;
|
|
326
|
-
activeCtx.last_outcome = "aborted";
|
|
327
|
-
activeCtx.last_completed_step = "abort";
|
|
328
|
-
activeCtx.next_recommended_command = activeCtx.task_summary
|
|
329
|
-
? `/harness-plan "${activeCtx.task_summary}"`
|
|
330
|
-
: '/harness-plan "<task>"';
|
|
331
|
-
persistContext(pi, activeCtx);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (!harnessTurn) {
|
|
336
|
-
return undefined;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (!activeCtx) {
|
|
340
|
-
activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const policyPhase =
|
|
344
|
-
inferHarnessPhase(entries, userPrompt) ??
|
|
345
|
-
getLatestPolicyPhase(entries) ??
|
|
346
|
-
activeCtx?.phase ??
|
|
347
|
-
"plan";
|
|
348
|
-
const driftActive = driftGateActive(entries);
|
|
349
|
-
|
|
350
|
-
// Plain-language follow-up after needs_clarification
|
|
351
|
-
if (!parsed && needsClarificationFollowUp(activeCtx) && activeCtx) {
|
|
352
|
-
activeCtx.phase = "plan";
|
|
353
|
-
activeCtx.last_outcome = "needs_clarification";
|
|
354
|
-
const packet = activeCtx.plan_packet_path
|
|
355
|
-
? await readPlanPacketFromPath(activeCtx.plan_packet_path)
|
|
356
|
-
: null;
|
|
357
|
-
const planPath = activeCtx.plan_packet_path;
|
|
358
|
-
const summary =
|
|
359
|
-
packet && planPath
|
|
360
|
-
? planPacketSummary(packet, planPath, "needs_clarification")
|
|
361
|
-
: null;
|
|
362
|
-
syncPolicyFromPlan(
|
|
363
|
-
pi,
|
|
364
|
-
entries,
|
|
365
|
-
activeCtx.plan_id ?? "plan-pending",
|
|
366
|
-
"plan",
|
|
367
|
-
false,
|
|
368
|
-
);
|
|
369
|
-
persistContext(pi, activeCtx);
|
|
370
|
-
return {
|
|
371
|
-
systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}\n\n${formatActivePlanBlock(activeCtx, "revise", summary)}\n\nReply with clarification answers; the harness will treat this as plan amend.`,
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (!parsed) return undefined;
|
|
376
|
-
|
|
377
|
-
const { command, args } = parsed;
|
|
378
|
-
|
|
379
|
-
if (
|
|
380
|
-
!isHarnessBootstrapPrompt(userPrompt) &&
|
|
381
|
-
!hasHarnessAbortSignal(userPrompt)
|
|
382
|
-
) {
|
|
383
|
-
const policyBlock = getPolicyTransitionBlock(userPrompt, entries);
|
|
384
|
-
if (policyBlock.blocked) {
|
|
385
|
-
return {
|
|
386
|
-
message: {
|
|
387
|
-
customType: "harness-run-context-block",
|
|
388
|
-
display: true,
|
|
389
|
-
content:
|
|
390
|
-
policyBlock.message ?? "Harness command blocked by policy phase.",
|
|
391
|
-
},
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
}
|
|
479
|
+
function contextPrompt(systemPrompt: string, activeCtx: HarnessRunContext) {
|
|
480
|
+
return {
|
|
481
|
+
systemPrompt: `${systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}`,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
395
484
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
485
|
+
function createNewRunContextForCommand(input: {
|
|
486
|
+
pi: ExtensionAPI;
|
|
487
|
+
activeCtx: HarnessRunContext | null;
|
|
488
|
+
sessionId: string;
|
|
489
|
+
projectRoot: string;
|
|
490
|
+
args: string;
|
|
491
|
+
userPrompt: string;
|
|
492
|
+
systemPrompt: string;
|
|
493
|
+
}) {
|
|
494
|
+
if (input.activeCtx?.status === "active") {
|
|
495
|
+
input.activeCtx.status = "aborted";
|
|
496
|
+
input.activeCtx.plan_ready = false;
|
|
497
|
+
input.activeCtx.last_outcome = "abandoned";
|
|
498
|
+
persistContext(input.pi, input.activeCtx);
|
|
499
|
+
}
|
|
500
|
+
const task = extractTaskSummary(input.args, input.userPrompt);
|
|
501
|
+
const activeCtx = createFreshRunContext(
|
|
502
|
+
input.sessionId,
|
|
503
|
+
input.projectRoot,
|
|
504
|
+
task,
|
|
505
|
+
);
|
|
506
|
+
persistContext(input.pi, activeCtx);
|
|
507
|
+
return {
|
|
508
|
+
activeCtx,
|
|
509
|
+
response: {
|
|
510
|
+
systemPrompt: `${input.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}\n\n${formatActivePlanBlock(activeCtx, "create")}`,
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
410
514
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
515
|
+
async function bindExistingRunForCommand(input: {
|
|
516
|
+
pi: ExtensionAPI;
|
|
517
|
+
sessionId: string;
|
|
518
|
+
projectRoot: string;
|
|
519
|
+
entries: unknown[];
|
|
520
|
+
args: string;
|
|
521
|
+
systemPrompt: string;
|
|
522
|
+
}) {
|
|
523
|
+
const parsed = parseHarnessUseRunArgs(input.args);
|
|
524
|
+
if (!parsed.runId) {
|
|
525
|
+
return {
|
|
526
|
+
activeCtx: null,
|
|
527
|
+
response: blockRunContextMessage(
|
|
528
|
+
"Usage: /harness-use-run <run-id> [--claim] [--readonly]",
|
|
529
|
+
),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const disk = await loadRunContextFromDisk(parsed.runId, input.projectRoot);
|
|
533
|
+
if (!disk) {
|
|
534
|
+
return {
|
|
535
|
+
activeCtx: null,
|
|
536
|
+
response: blockRunContextMessage(
|
|
537
|
+
`No run directory for ${parsed.runId}. Check .pi/harness/runs/.`,
|
|
538
|
+
),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
let activeCtx: HarnessRunContext = {
|
|
542
|
+
...disk,
|
|
543
|
+
pi_session_id: input.sessionId,
|
|
544
|
+
turn_override_run_id: parsed.runId,
|
|
545
|
+
};
|
|
546
|
+
if (parsed.claim) activeCtx = claimRunOwnership(activeCtx, input.sessionId);
|
|
547
|
+
const statuses = await resolveCompletionStatuses(
|
|
548
|
+
input.entries,
|
|
549
|
+
activeCtx.run_id,
|
|
550
|
+
input.projectRoot,
|
|
551
|
+
);
|
|
552
|
+
activeCtx.next_recommended_command =
|
|
553
|
+
activeCtx.owner_pi_session_id !== input.sessionId && !parsed.claim
|
|
554
|
+
? "Read-only: use /harness-use-run <run-id> --claim to take ownership, or /harness-new-run."
|
|
555
|
+
: nextStepAfterOutcome({
|
|
450
556
|
phase: activeCtx.phase,
|
|
451
557
|
planStatus: activeCtx.plan_ready ? "ready" : null,
|
|
452
558
|
lastCompletedStep: activeCtx.last_completed_step,
|
|
@@ -456,436 +562,462 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
456
562
|
adversaryComplete: statuses.adversaryComplete,
|
|
457
563
|
aborted: activeCtx.status === "aborted",
|
|
458
564
|
});
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}`,
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (command === "harness-run-status") {
|
|
469
|
-
return undefined;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (
|
|
473
|
-
command === "harness-plan" &&
|
|
474
|
-
activeCtx &&
|
|
475
|
-
isNewTaskPlanBlocked(activeCtx, userPrompt) &&
|
|
476
|
-
!isAmendPlanAllowed(activeCtx, userPrompt, driftActive)
|
|
477
|
-
) {
|
|
478
|
-
return {
|
|
479
|
-
message: {
|
|
480
|
-
customType: "harness-run-context-block",
|
|
481
|
-
display: true,
|
|
482
|
-
content:
|
|
483
|
-
"Active harness run in progress. Use /harness-abort or /harness-new-run before starting a new task plan.",
|
|
484
|
-
},
|
|
485
|
-
};
|
|
486
|
-
}
|
|
565
|
+
activeCtx.updated_at = nowIso();
|
|
566
|
+
persistContext(input.pi, activeCtx);
|
|
567
|
+
syncPolicyFromRunContext(input.pi, input.entries, activeCtx);
|
|
568
|
+
return { activeCtx, response: contextPrompt(input.systemPrompt, activeCtx) };
|
|
569
|
+
}
|
|
487
570
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
571
|
+
type ActiveContextAccess = {
|
|
572
|
+
get(): HarnessRunContext | null;
|
|
573
|
+
set(ctx: HarnessRunContext | null): void;
|
|
574
|
+
};
|
|
493
575
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
if (turn) {
|
|
514
|
-
pi.appendEntry("harness-plan-attempt", {
|
|
515
|
-
run_id: activeCtx.run_id,
|
|
516
|
-
command,
|
|
517
|
-
started_at: turn.invoked_at,
|
|
518
|
-
});
|
|
519
|
-
} else {
|
|
520
|
-
pi.appendEntry("harness-plan-attempt", {
|
|
521
|
-
run_id: activeCtx.run_id,
|
|
522
|
-
command,
|
|
523
|
-
started_at: nowIso(),
|
|
524
|
-
});
|
|
576
|
+
function registerHarnessRunStatusCommand(
|
|
577
|
+
pi: ExtensionAPI,
|
|
578
|
+
active: ActiveContextAccess,
|
|
579
|
+
): void {
|
|
580
|
+
pi.registerCommand("harness-run-status", {
|
|
581
|
+
description:
|
|
582
|
+
"Show harness phase, plan readiness, and next command (no run id)",
|
|
583
|
+
handler: async (_args, ctx) => {
|
|
584
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
585
|
+
const projectRoot = process.cwd();
|
|
586
|
+
const entries = getEntries(ctx);
|
|
587
|
+
let ctxState = getLatestRunContext(entries) ?? active.get();
|
|
588
|
+
if (!ctxState)
|
|
589
|
+
ctxState = await hydrateFromDisk(sessionId, projectRoot, entries);
|
|
590
|
+
if (!ctxState) {
|
|
591
|
+
const msg = 'No active harness run. Start with /harness-plan "<task>".';
|
|
592
|
+
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
593
|
+
return;
|
|
525
594
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (pointer) {
|
|
536
|
-
if (isStaleActiveRunPointer(pointer, projectRoot)) {
|
|
537
|
-
const crossSessionCmd = new Set([
|
|
538
|
-
"harness-eval",
|
|
539
|
-
"harness-review",
|
|
540
|
-
"harness-steer",
|
|
541
|
-
"harness-critic",
|
|
542
|
-
"harness-trace",
|
|
543
|
-
"harness-incident",
|
|
544
|
-
]);
|
|
545
|
-
if (crossSessionCmd.has(command)) {
|
|
546
|
-
return {
|
|
547
|
-
message: {
|
|
548
|
-
customType: "harness-run-context-block",
|
|
549
|
-
display: true,
|
|
550
|
-
content:
|
|
551
|
-
'Project active-run pointer is stale or from another workspace. Run /harness-plan "<task>" or /harness-use-run <run-id> for recovery.',
|
|
552
|
-
},
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
} else {
|
|
556
|
-
const disk = await loadRunContextFromDisk(
|
|
557
|
-
pointer.run_id,
|
|
558
|
-
projectRoot,
|
|
559
|
-
);
|
|
560
|
-
if (disk) activeCtx = disk;
|
|
595
|
+
let summary: PlanPacketSummary | null = null;
|
|
596
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
597
|
+
const entry = entries[i] as SessionEntryLike;
|
|
598
|
+
if (
|
|
599
|
+
entry.type === "custom" &&
|
|
600
|
+
entry.customType === "harness-plan-packet"
|
|
601
|
+
) {
|
|
602
|
+
summary = entry.data as PlanPacketSummary;
|
|
603
|
+
break;
|
|
561
604
|
}
|
|
562
605
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
606
|
+
const text = [
|
|
607
|
+
"Harness run status:",
|
|
608
|
+
` phase: ${ctxState.phase}`,
|
|
609
|
+
` status: ${ctxState.status}`,
|
|
610
|
+
` plan_ready: ${ctxState.plan_ready}`,
|
|
611
|
+
` plan_id: ${ctxState.plan_id ?? "(none)"}`,
|
|
612
|
+
summary
|
|
613
|
+
? ` scope: ${summary.scope_one_liner}`
|
|
614
|
+
: " scope: (no plan summary yet)",
|
|
615
|
+
` last_step: ${ctxState.last_completed_step ?? "(none)"}`,
|
|
616
|
+
` last_outcome: ${ctxState.last_outcome ?? "(none)"}`,
|
|
617
|
+
` next: ${ctxState.next_recommended_command ?? "/harness-run-status"}`,
|
|
618
|
+
].join("\n");
|
|
619
|
+
if (ctx.hasUI) ctx.ui.notify(text, "info");
|
|
620
|
+
else
|
|
621
|
+
pi.sendMessage({
|
|
622
|
+
customType: "harness-run-status",
|
|
623
|
+
content: text,
|
|
569
624
|
display: true,
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
activeCtx.phase = policyPhase;
|
|
577
|
-
activeCtx.updated_at = new Date().toISOString();
|
|
578
|
-
activeCtx.pi_session_id = sessionId;
|
|
579
|
-
|
|
580
|
-
if (
|
|
581
|
-
shouldAutoClaimHarnessRun(command, args) &&
|
|
582
|
-
activeCtx.owner_pi_session_id !== sessionId
|
|
583
|
-
) {
|
|
584
|
-
activeCtx = claimRunOwnership(activeCtx, sessionId);
|
|
585
|
-
}
|
|
625
|
+
});
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}
|
|
586
629
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
630
|
+
function registerHarnessNewRunCommand(
|
|
631
|
+
pi: ExtensionAPI,
|
|
632
|
+
active: ActiveContextAccess,
|
|
633
|
+
): void {
|
|
634
|
+
pi.registerCommand("harness-new-run", {
|
|
635
|
+
description: "Abandon current active run and start a fresh harness run",
|
|
636
|
+
handler: async (args, ctx) => {
|
|
637
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
638
|
+
const projectRoot = process.cwd();
|
|
639
|
+
const current = active.get();
|
|
640
|
+
if (current?.status === "active") {
|
|
641
|
+
current.status = "aborted";
|
|
642
|
+
current.plan_ready = false;
|
|
643
|
+
persistContext(pi, current);
|
|
601
644
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return {
|
|
607
|
-
message: {
|
|
608
|
-
customType: "harness-run-context-block",
|
|
609
|
-
display: true,
|
|
610
|
-
content: "Plan not ready. Run /harness-plan first.",
|
|
611
|
-
},
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
command === "harness-run" &&
|
|
617
|
-
activeCtx.plan_ready &&
|
|
618
|
-
activeCtx.last_completed_step === "execute" &&
|
|
619
|
-
activeCtx.last_outcome === "completed"
|
|
620
|
-
) {
|
|
621
|
-
return {
|
|
622
|
-
message: {
|
|
623
|
-
customType: "harness-run-context-block",
|
|
624
|
-
display: true,
|
|
625
|
-
content:
|
|
626
|
-
"Execute already completed for this run. Next: /harness-review (same session), or /harness-abort to replan.",
|
|
627
|
-
},
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
let planSummary: PlanPacketSummary | null = null;
|
|
632
|
-
let planPacketForSpawn: Awaited<ReturnType<typeof readPlanPacketFromPath>> =
|
|
633
|
-
null;
|
|
634
|
-
if (activeCtx.plan_packet_path) {
|
|
635
|
-
planPacketForSpawn = await readPlanPacketFromPath(
|
|
636
|
-
activeCtx.plan_packet_path,
|
|
645
|
+
const next = createFreshRunContext(
|
|
646
|
+
sessionId,
|
|
647
|
+
projectRoot,
|
|
648
|
+
args.trim() || null,
|
|
637
649
|
);
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
650
|
+
active.set(next);
|
|
651
|
+
persistContext(pi, next);
|
|
652
|
+
if (ctx.hasUI) {
|
|
653
|
+
ctx.ui.notify(
|
|
654
|
+
'New harness run allocated. Next: /harness-plan "<your task>"',
|
|
655
|
+
"info",
|
|
643
656
|
);
|
|
644
|
-
activeCtx.plan_id = planPacketForSpawn.plan_id ?? activeCtx.plan_id;
|
|
645
657
|
}
|
|
646
|
-
}
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
647
661
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
662
|
+
function registerHarnessPlanCommitCommand(
|
|
663
|
+
pi: ExtensionAPI,
|
|
664
|
+
active: ActiveContextAccess,
|
|
665
|
+
): void {
|
|
666
|
+
pi.registerCommand("harness-plan-commit", {
|
|
667
|
+
description:
|
|
668
|
+
"Write approved plan-packet.yaml to the active run (requires harness-plan-approval)",
|
|
669
|
+
handler: async (args, ctx) => {
|
|
670
|
+
const projectRoot = process.cwd();
|
|
671
|
+
const entries = getEntries(ctx);
|
|
672
|
+
let runCtx = getLatestRunContext(entries) ?? active.get();
|
|
673
|
+
if (!runCtx) {
|
|
674
|
+
runCtx = await hydrateFromDisk(
|
|
675
|
+
ctx.sessionManager.getSessionId(),
|
|
676
|
+
projectRoot,
|
|
677
|
+
entries,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (!runCtx?.plan_packet_path) {
|
|
681
|
+
if (ctx.hasUI)
|
|
682
|
+
ctx.ui.notify(
|
|
683
|
+
"No active harness run. Run /harness-plan first.",
|
|
684
|
+
"warning",
|
|
685
|
+
);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (
|
|
689
|
+
!hasPlanUserApproval(entries, {
|
|
690
|
+
sincePlanCommand: true,
|
|
691
|
+
planId: runCtx.plan_id,
|
|
692
|
+
})
|
|
693
|
+
) {
|
|
694
|
+
if (ctx.hasUI)
|
|
695
|
+
ctx.ui.notify(
|
|
696
|
+
"Plan commit blocked: no user approval recorded. Approve via approve_plan in this session first.",
|
|
697
|
+
"warning",
|
|
698
|
+
);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const pathArg = args.trim();
|
|
702
|
+
const packetPath = pathArg || runCtx.plan_packet_path;
|
|
703
|
+
const packet = await readPlanPacketFromPath(packetPath);
|
|
704
|
+
const validation = validatePlanPacket(packet);
|
|
705
|
+
if (!validation.valid || !packet) {
|
|
706
|
+
const msg = !packet
|
|
707
|
+
? "Plan packet file missing or unreadable."
|
|
708
|
+
: `Invalid plan packet: ${validation.errors.join("; ")}`;
|
|
709
|
+
if (ctx.hasUI) ctx.ui.notify(msg, "error");
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const target = runCtx.plan_packet_path;
|
|
713
|
+
if (!target) {
|
|
714
|
+
if (ctx.hasUI)
|
|
715
|
+
ctx.ui.notify("No plan_packet_path on active run.", "error");
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (pathArg && pathArg !== target) {
|
|
719
|
+
await writeFile(target, await readFile(pathArg, "utf-8"), "utf-8");
|
|
720
|
+
}
|
|
721
|
+
runCtx.plan_id = packet.plan_id ?? runCtx.plan_id;
|
|
722
|
+
runCtx.plan_ready = true;
|
|
723
|
+
runCtx.phase = "plan";
|
|
724
|
+
runCtx.last_completed_step = "plan";
|
|
725
|
+
runCtx.last_outcome = "ready";
|
|
726
|
+
runCtx.next_recommended_command = "/harness-run";
|
|
727
|
+
runCtx.updated_at = nowIso();
|
|
728
|
+
active.set(runCtx);
|
|
729
|
+
persistContext(pi, runCtx);
|
|
730
|
+
syncPolicyFromPlan(
|
|
731
|
+
pi,
|
|
732
|
+
entries,
|
|
733
|
+
runCtx.plan_id ?? packet.plan_id ?? "plan-pending",
|
|
734
|
+
"plan",
|
|
735
|
+
true,
|
|
672
736
|
);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
"execute",
|
|
677
|
-
planSummary,
|
|
737
|
+
pi.appendEntry(
|
|
738
|
+
"harness-plan-packet",
|
|
739
|
+
planPacketSummary(packet, target, "ready"),
|
|
678
740
|
);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
repair_brief_path: "artifacts/repair-brief.yaml",
|
|
682
|
-
};
|
|
683
|
-
} else if (
|
|
684
|
-
command === "harness-eval" ||
|
|
685
|
-
command === "harness-review" ||
|
|
686
|
-
command === "harness-critic"
|
|
687
|
-
) {
|
|
688
|
-
activePlanBlock = formatActivePlanBlock(activeCtx, "read", planSummary);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
persistContext(pi, activeCtx);
|
|
692
|
-
|
|
693
|
-
return {
|
|
694
|
-
systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx, contextSpawnOpts)}${activePlanBlock ? `\n\n${activePlanBlock}` : ""}`,
|
|
695
|
-
};
|
|
741
|
+
if (ctx.hasUI) ctx.ui.notify(`Plan committed: ${target}`, "info");
|
|
742
|
+
},
|
|
696
743
|
});
|
|
744
|
+
}
|
|
697
745
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
if (lastUser?.message?.content) {
|
|
715
|
-
lastPrompt =
|
|
716
|
-
typeof lastUser.message.content === "string"
|
|
717
|
-
? lastUser.message.content
|
|
718
|
-
: "";
|
|
719
|
-
}
|
|
720
|
-
const lastTurn = getLatestHarnessTurn(entries);
|
|
721
|
-
const parsed = lastTurn
|
|
722
|
-
? { command: lastTurn.command, args: lastTurn.args }
|
|
723
|
-
: parseHarnessSlashInput(userVisiblePromptSlice(lastPrompt));
|
|
724
|
-
if (!parsed && !needsClarificationFollowUp(activeCtx)) return;
|
|
725
|
-
|
|
726
|
-
if (parsed?.command === "harness-abort") {
|
|
727
|
-
activeCtx.status = "aborted";
|
|
728
|
-
activeCtx.plan_ready = false;
|
|
729
|
-
activeCtx.last_outcome = "aborted";
|
|
730
|
-
activeCtx.last_completed_step = "abort";
|
|
731
|
-
activeCtx.next_recommended_command = activeCtx.task_summary
|
|
732
|
-
? `/harness-plan "${activeCtx.task_summary}"`
|
|
733
|
-
: '/harness-plan "<task>"';
|
|
734
|
-
persistContext(pi, activeCtx);
|
|
735
|
-
const msg = `Harness aborted. Next: ${activeCtx.next_recommended_command}`;
|
|
736
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
737
|
-
else
|
|
738
|
-
pi.sendMessage({
|
|
739
|
-
customType: "harness-step-handoff",
|
|
740
|
-
content: msg,
|
|
741
|
-
display: true,
|
|
742
|
-
});
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
let planReady = activeCtx.plan_ready;
|
|
747
|
-
if (
|
|
748
|
-
(parsed?.command === "harness-plan" ||
|
|
749
|
-
parsed?.command === "harness-auto") &&
|
|
750
|
-
activeCtx.plan_packet_path
|
|
751
|
-
) {
|
|
752
|
-
const packet = await readPlanPacketFromPath(activeCtx.plan_packet_path);
|
|
753
|
-
const validation = validatePlanPacket(packet);
|
|
754
|
-
const approved = hasPlanUserApproval(entries, {
|
|
755
|
-
sincePlanCommand: true,
|
|
756
|
-
planId: packet?.plan_id ?? null,
|
|
757
|
-
});
|
|
758
|
-
planReady = validation.valid && approved;
|
|
759
|
-
if (validation.valid && !approved) {
|
|
760
|
-
activeCtx.last_outcome = "needs_clarification";
|
|
761
|
-
activeCtx.last_completed_step = "plan";
|
|
762
|
-
const msg =
|
|
763
|
-
"Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.yaml.";
|
|
764
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
765
|
-
else
|
|
766
|
-
pi.sendMessage({
|
|
767
|
-
customType: "harness-plan-packet",
|
|
768
|
-
content: msg,
|
|
769
|
-
display: true,
|
|
770
|
-
});
|
|
771
|
-
} else if (planReady && packet?.plan_id) {
|
|
772
|
-
activeCtx.plan_id = packet.plan_id;
|
|
773
|
-
syncPolicyFromPlan(pi, entries, packet.plan_id, "plan", true);
|
|
774
|
-
const summary = planPacketSummary(packet, activeCtx.plan_packet_path);
|
|
775
|
-
pi.appendEntry("harness-plan-packet", summary);
|
|
776
|
-
activeCtx.last_completed_step = "plan";
|
|
777
|
-
activeCtx.last_outcome = summary.plan_status;
|
|
778
|
-
} else if (!validation.valid) {
|
|
779
|
-
activeCtx.last_outcome = "needs_clarification";
|
|
780
|
-
activeCtx.last_completed_step = "plan";
|
|
746
|
+
function registerHarnessUseRunCommand(
|
|
747
|
+
pi: ExtensionAPI,
|
|
748
|
+
active: ActiveContextAccess,
|
|
749
|
+
): void {
|
|
750
|
+
pi.registerCommand("harness-use-run", {
|
|
751
|
+
description:
|
|
752
|
+
"Point this session at an existing run directory (recovery; --claim for write ownership)",
|
|
753
|
+
handler: async (args, ctx) => {
|
|
754
|
+
const parsed = parseHarnessUseRunArgs(args);
|
|
755
|
+
if (!parsed.runId) {
|
|
756
|
+
if (ctx.hasUI)
|
|
757
|
+
ctx.ui.notify(
|
|
758
|
+
"Usage: /harness-use-run <run-id> [--claim] [--readonly]",
|
|
759
|
+
"warning",
|
|
760
|
+
);
|
|
761
|
+
return;
|
|
781
762
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
activeCtx.run_id,
|
|
789
|
-
projectRoot,
|
|
790
|
-
);
|
|
791
|
-
if (parsed?.command === "harness-run") {
|
|
792
|
-
activeCtx.last_completed_step = "execute";
|
|
793
|
-
let execStatus = statuses.executionStatus;
|
|
794
|
-
if (!execStatus) {
|
|
795
|
-
const handoff = await readExecutorHandoffFromRun(
|
|
796
|
-
activeCtx.run_id,
|
|
797
|
-
projectRoot,
|
|
798
|
-
);
|
|
799
|
-
execStatus = handoff?.execution_status ?? null;
|
|
763
|
+
const projectRoot = process.cwd();
|
|
764
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
765
|
+
const disk = await loadRunContextFromDisk(parsed.runId, projectRoot);
|
|
766
|
+
if (!disk) {
|
|
767
|
+
if (ctx.hasUI) ctx.ui.notify(`Run not found: ${parsed.runId}`, "error");
|
|
768
|
+
return;
|
|
800
769
|
}
|
|
801
|
-
activeCtx
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
770
|
+
let activeCtx: HarnessRunContext = { ...disk, pi_session_id: sessionId };
|
|
771
|
+
if (parsed.claim) activeCtx = claimRunOwnership(activeCtx, sessionId);
|
|
772
|
+
const statuses = await resolveCompletionStatuses(
|
|
773
|
+
getEntries(ctx),
|
|
774
|
+
activeCtx.run_id,
|
|
775
|
+
projectRoot,
|
|
776
|
+
);
|
|
777
|
+
activeCtx.next_recommended_command =
|
|
778
|
+
activeCtx.owner_pi_session_id !== sessionId && !parsed.claim
|
|
779
|
+
? "Read-only: use /harness-use-run <run-id> --claim to take ownership."
|
|
780
|
+
: nextStepAfterOutcome({
|
|
781
|
+
phase: activeCtx.phase,
|
|
782
|
+
planStatus: activeCtx.plan_ready ? "ready" : null,
|
|
783
|
+
lastCompletedStep: activeCtx.last_completed_step,
|
|
784
|
+
lastOutcome: activeCtx.last_outcome,
|
|
785
|
+
executionStatus: statuses.executionStatus,
|
|
786
|
+
evalStatus: statuses.evalStatus,
|
|
787
|
+
adversaryComplete: statuses.adversaryComplete,
|
|
788
|
+
aborted: activeCtx.status === "aborted",
|
|
789
|
+
});
|
|
790
|
+
activeCtx.updated_at = nowIso();
|
|
791
|
+
active.set(activeCtx);
|
|
792
|
+
persistContext(pi, activeCtx);
|
|
810
793
|
syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
activeCtx.last_completed_step =
|
|
818
|
-
parsed.command === "harness-critic" ? "adversary" : "review";
|
|
819
|
-
if (statuses.evalStatus) {
|
|
820
|
-
activeCtx.last_outcome = statuses.evalStatus;
|
|
821
|
-
}
|
|
822
|
-
if (statuses.adversaryComplete) {
|
|
823
|
-
activeCtx.phase = "adversary";
|
|
824
|
-
activeCtx.last_completed_step = "adversary";
|
|
825
|
-
} else if (statuses.evalStatus) {
|
|
826
|
-
activeCtx.phase = "evaluate";
|
|
794
|
+
if (ctx.hasUI) {
|
|
795
|
+
const mode = parsed.claim ? "claimed" : "bound (read-only)";
|
|
796
|
+
ctx.ui.notify(
|
|
797
|
+
`Session ${mode} to run ${parsed.runId}. See /harness-run-status.`,
|
|
798
|
+
"info",
|
|
799
|
+
);
|
|
827
800
|
}
|
|
828
|
-
}
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
}
|
|
829
804
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
805
|
+
async function readPlanSpawnState(activeCtx: HarnessRunContext): Promise<{
|
|
806
|
+
planSummary: PlanPacketSummary | null;
|
|
807
|
+
planPacketForSpawn: Awaited<ReturnType<typeof readPlanPacketFromPath>>;
|
|
808
|
+
}> {
|
|
809
|
+
let planSummary: PlanPacketSummary | null = null;
|
|
810
|
+
let planPacketForSpawn: Awaited<ReturnType<typeof readPlanPacketFromPath>> =
|
|
811
|
+
null;
|
|
812
|
+
if (!activeCtx.plan_packet_path) return { planSummary, planPacketForSpawn };
|
|
813
|
+
planPacketForSpawn = await readPlanPacketFromPath(activeCtx.plan_packet_path);
|
|
814
|
+
if (planPacketForSpawn) {
|
|
815
|
+
planSummary = planPacketSummary(
|
|
816
|
+
planPacketForSpawn,
|
|
817
|
+
activeCtx.plan_packet_path,
|
|
818
|
+
activeCtx.plan_ready ? "ready" : "draft",
|
|
833
819
|
);
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
phase: activeCtx.phase,
|
|
839
|
-
planStatus: statuses.planStatus,
|
|
840
|
-
lastCompletedStep: activeCtx.last_completed_step,
|
|
841
|
-
lastOutcome: activeCtx.last_outcome,
|
|
842
|
-
executionStatus: statuses.executionStatus,
|
|
843
|
-
evalStatus: statuses.evalStatus,
|
|
844
|
-
adversaryComplete: statuses.adversaryComplete,
|
|
845
|
-
aborted: activeCtx.status === "aborted",
|
|
846
|
-
remediationClass: reviewOutcome?.remediation_class ?? null,
|
|
847
|
-
steerAttempt: activeCtx.steer_attempt ?? 0,
|
|
848
|
-
steerMaxAttempts:
|
|
849
|
-
activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv(),
|
|
850
|
-
reviewComplete,
|
|
851
|
-
});
|
|
852
|
-
activeCtx.next_recommended_command = next;
|
|
853
|
-
activeCtx.updated_at = new Date().toISOString();
|
|
820
|
+
activeCtx.plan_id = planPacketForSpawn.plan_id ?? activeCtx.plan_id;
|
|
821
|
+
}
|
|
822
|
+
return { planSummary, planPacketForSpawn };
|
|
823
|
+
}
|
|
854
824
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
825
|
+
function buildSpawnPromptBlocks(input: {
|
|
826
|
+
command: string;
|
|
827
|
+
activeCtx: HarnessRunContext;
|
|
828
|
+
planSummary: PlanPacketSummary | null;
|
|
829
|
+
planPacketForSpawn: Awaited<ReturnType<typeof readPlanPacketFromPath>>;
|
|
830
|
+
}): {
|
|
831
|
+
activePlanBlock: string;
|
|
832
|
+
planMode: "create" | "revise" | null;
|
|
833
|
+
contextSpawnOpts: Parameters<typeof formatPlanContextBlock>[1] | undefined;
|
|
834
|
+
} {
|
|
835
|
+
let activePlanBlock = "";
|
|
836
|
+
let planMode: "create" | "revise" | null = null;
|
|
837
|
+
let contextSpawnOpts:
|
|
838
|
+
| Parameters<typeof formatPlanContextBlock>[1]
|
|
839
|
+
| undefined;
|
|
840
|
+
if (input.command === "harness-run" && input.planPacketForSpawn) {
|
|
841
|
+
contextSpawnOpts = {
|
|
842
|
+
mode: "execute",
|
|
843
|
+
critical_path_work_item_ids: criticalPathWorkItemIdsFromPlanPacket(
|
|
844
|
+
input.planPacketForSpawn,
|
|
845
|
+
),
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
if (input.command === "harness-plan" || input.command === "harness-auto") {
|
|
849
|
+
planMode =
|
|
850
|
+
input.activeCtx.plan_id ||
|
|
851
|
+
input.activeCtx.plan_packet_path ||
|
|
852
|
+
input.activeCtx.status === "aborted"
|
|
853
|
+
? "revise"
|
|
854
|
+
: "create";
|
|
855
|
+
activePlanBlock = formatActivePlanBlock(
|
|
856
|
+
input.activeCtx,
|
|
857
|
+
planMode,
|
|
858
|
+
input.planSummary,
|
|
859
|
+
);
|
|
860
|
+
} else if (input.command === "harness-run") {
|
|
861
|
+
activePlanBlock = formatActivePlanBlock(
|
|
862
|
+
input.activeCtx,
|
|
863
|
+
"execute",
|
|
864
|
+
input.planSummary,
|
|
865
|
+
);
|
|
866
|
+
} else if (input.command === "harness-steer") {
|
|
867
|
+
activePlanBlock = formatActivePlanBlock(
|
|
868
|
+
input.activeCtx,
|
|
869
|
+
"execute",
|
|
870
|
+
input.planSummary,
|
|
871
|
+
);
|
|
872
|
+
contextSpawnOpts = {
|
|
873
|
+
mode: "repair",
|
|
874
|
+
repair_brief_path: "artifacts/repair-brief.yaml",
|
|
875
|
+
};
|
|
876
|
+
} else if (
|
|
877
|
+
["harness-eval", "harness-review", "harness-critic"].includes(input.command)
|
|
878
|
+
) {
|
|
879
|
+
activePlanBlock = formatActivePlanBlock(
|
|
880
|
+
input.activeCtx,
|
|
881
|
+
"read",
|
|
882
|
+
input.planSummary,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
return { activePlanBlock, planMode, contextSpawnOpts };
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async function archivePlanRevisionIfNeeded(input: {
|
|
889
|
+
pi: ExtensionAPI;
|
|
890
|
+
command: string;
|
|
891
|
+
planMode: "create" | "revise" | null;
|
|
892
|
+
activeCtx: HarnessRunContext;
|
|
893
|
+
projectRoot: string;
|
|
894
|
+
userPrompt: string;
|
|
895
|
+
}): Promise<void> {
|
|
896
|
+
if (input.command !== "harness-plan" && input.command !== "harness-auto")
|
|
897
|
+
return;
|
|
898
|
+
const reviewOutcome = await readReviewOutcomeFromRun(
|
|
899
|
+
input.activeCtx.run_id,
|
|
900
|
+
input.projectRoot,
|
|
901
|
+
);
|
|
902
|
+
if (
|
|
903
|
+
!shouldArchiveForPlanRevise({
|
|
904
|
+
command: input.command,
|
|
905
|
+
mode: input.planMode,
|
|
906
|
+
runCtx: input.activeCtx,
|
|
907
|
+
reviewOutcome,
|
|
908
|
+
userPrompt: input.userPrompt,
|
|
909
|
+
})
|
|
910
|
+
)
|
|
911
|
+
return;
|
|
912
|
+
const reset = await archivePlanRevisionArtifacts({
|
|
913
|
+
projectRoot: input.projectRoot,
|
|
914
|
+
runId: input.activeCtx.run_id,
|
|
915
|
+
reason: "review_plan_gap_revise",
|
|
916
|
+
});
|
|
917
|
+
if (reset.moved.length === 0) return;
|
|
918
|
+
input.pi.appendEntry("harness-plan-revision-reset", {
|
|
919
|
+
run_id: input.activeCtx.run_id,
|
|
920
|
+
archive_dir: reset.archiveDir,
|
|
921
|
+
moved: reset.moved,
|
|
922
|
+
reason: "review_plan_gap_revise",
|
|
923
|
+
recorded_at: nowIso(),
|
|
924
|
+
});
|
|
925
|
+
}
|
|
861
926
|
|
|
862
|
-
|
|
927
|
+
function latestParsedHarnessCommand(entries: unknown[]) {
|
|
928
|
+
const userEntries = entries.filter((e) => {
|
|
929
|
+
const entry = e as { type?: string; message?: { role?: string } };
|
|
930
|
+
return entry.type === "message" && entry.message?.role === "user";
|
|
931
|
+
});
|
|
932
|
+
const lastUser = userEntries[userEntries.length - 1] as
|
|
933
|
+
| { message?: { content?: string | unknown[] } }
|
|
934
|
+
| undefined;
|
|
935
|
+
const lastPrompt =
|
|
936
|
+
typeof lastUser?.message?.content === "string"
|
|
937
|
+
? lastUser.message.content
|
|
938
|
+
: "";
|
|
939
|
+
const lastTurn = getLatestHarnessTurn(entries);
|
|
940
|
+
return lastTurn
|
|
941
|
+
? { command: lastTurn.command, args: lastTurn.args }
|
|
942
|
+
: parseHarnessSlashInput(userVisiblePromptSlice(lastPrompt));
|
|
943
|
+
}
|
|
863
944
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
945
|
+
function handleAgentEndAbort(input: {
|
|
946
|
+
pi: ExtensionAPI;
|
|
947
|
+
ctx: { hasUI: boolean; ui: { notify(message: string, type?: string): void } };
|
|
948
|
+
activeCtx: HarnessRunContext;
|
|
949
|
+
}): void {
|
|
950
|
+
input.activeCtx.status = "aborted";
|
|
951
|
+
input.activeCtx.plan_ready = false;
|
|
952
|
+
input.activeCtx.last_outcome = "aborted";
|
|
953
|
+
input.activeCtx.last_completed_step = "abort";
|
|
954
|
+
input.activeCtx.next_recommended_command = input.activeCtx.task_summary
|
|
955
|
+
? `/harness-plan "${input.activeCtx.task_summary}"`
|
|
956
|
+
: '/harness-plan "<task>"';
|
|
957
|
+
persistContext(input.pi, input.activeCtx);
|
|
958
|
+
const msg = `Harness aborted. Next: ${input.activeCtx.next_recommended_command}`;
|
|
959
|
+
if (input.ctx.hasUI) input.ctx.ui.notify(msg, "warning");
|
|
960
|
+
else
|
|
961
|
+
input.pi.sendMessage({
|
|
962
|
+
customType: "harness-step-handoff",
|
|
963
|
+
content: msg,
|
|
964
|
+
display: true,
|
|
870
965
|
});
|
|
966
|
+
}
|
|
871
967
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
968
|
+
async function updatePlanReadinessAfterAgent(input: {
|
|
969
|
+
pi: ExtensionAPI;
|
|
970
|
+
ctx: { hasUI: boolean; ui: { notify(message: string, type?: string): void } };
|
|
971
|
+
entries: unknown[];
|
|
972
|
+
parsed: { command: string; args: string } | null;
|
|
973
|
+
activeCtx: HarnessRunContext;
|
|
974
|
+
}): Promise<void> {
|
|
975
|
+
if (
|
|
976
|
+
input.parsed?.command !== "harness-plan" &&
|
|
977
|
+
input.parsed?.command !== "harness-auto"
|
|
978
|
+
)
|
|
979
|
+
return;
|
|
980
|
+
if (!input.activeCtx.plan_packet_path) return;
|
|
981
|
+
const packet = await readPlanPacketFromPath(input.activeCtx.plan_packet_path);
|
|
982
|
+
const validation = validatePlanPacket(packet);
|
|
983
|
+
const approved = hasPlanUserApproval(input.entries, {
|
|
984
|
+
sincePlanCommand: true,
|
|
985
|
+
planId: packet?.plan_id ?? null,
|
|
882
986
|
});
|
|
987
|
+
input.activeCtx.plan_ready = validation.valid && approved;
|
|
988
|
+
if (validation.valid && !approved) {
|
|
989
|
+
input.activeCtx.last_outcome = "needs_clarification";
|
|
990
|
+
input.activeCtx.last_completed_step = "plan";
|
|
991
|
+
const msg =
|
|
992
|
+
"Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.yaml.";
|
|
993
|
+
if (input.ctx.hasUI) input.ctx.ui.notify(msg, "warning");
|
|
994
|
+
else
|
|
995
|
+
input.pi.sendMessage({
|
|
996
|
+
customType: "harness-plan-packet",
|
|
997
|
+
content: msg,
|
|
998
|
+
display: true,
|
|
999
|
+
});
|
|
1000
|
+
} else if (input.activeCtx.plan_ready && packet?.plan_id) {
|
|
1001
|
+
input.activeCtx.plan_id = packet.plan_id;
|
|
1002
|
+
syncPolicyFromPlan(input.pi, input.entries, packet.plan_id, "plan", true);
|
|
1003
|
+
const summary = planPacketSummary(packet, input.activeCtx.plan_packet_path);
|
|
1004
|
+
input.pi.appendEntry("harness-plan-packet", summary);
|
|
1005
|
+
input.activeCtx.last_completed_step = "plan";
|
|
1006
|
+
input.activeCtx.last_outcome = summary.plan_status;
|
|
1007
|
+
} else if (!validation.valid) {
|
|
1008
|
+
input.activeCtx.last_outcome = "needs_clarification";
|
|
1009
|
+
input.activeCtx.last_completed_step = "plan";
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
883
1012
|
|
|
1013
|
+
function registerPlanApprovalCapture(
|
|
1014
|
+
pi: ExtensionAPI,
|
|
1015
|
+
active: ActiveContextAccess,
|
|
1016
|
+
): void {
|
|
884
1017
|
pi.on("tool_result", async (event, ctx) => {
|
|
885
1018
|
if (event.isError) return;
|
|
886
|
-
if (event.toolName !== "ask_user" && event.toolName !== "approve_plan")
|
|
1019
|
+
if (event.toolName !== "ask_user" && event.toolName !== "approve_plan")
|
|
887
1020
|
return;
|
|
888
|
-
}
|
|
889
1021
|
const approval = parsePlanApprovalFromMessage({
|
|
890
1022
|
toolName: event.toolName,
|
|
891
1023
|
details: event.details,
|
|
@@ -893,7 +1025,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
893
1025
|
});
|
|
894
1026
|
if (!approval) return;
|
|
895
1027
|
const entries = getEntries(ctx);
|
|
896
|
-
const runCtx = getLatestRunContext(entries) ??
|
|
1028
|
+
const runCtx = getLatestRunContext(entries) ?? active.get();
|
|
897
1029
|
if (!runCtx) return;
|
|
898
1030
|
pi.appendEntry("harness-plan-approval", {
|
|
899
1031
|
plan_id: approval.plan_id ?? runCtx.plan_id,
|
|
@@ -901,226 +1033,514 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
901
1033
|
source: approval.source,
|
|
902
1034
|
});
|
|
903
1035
|
});
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async function guardToolCall(input: {
|
|
1039
|
+
event: { toolName: string; input: unknown };
|
|
1040
|
+
ctx: { sessionManager: { getEntries(): unknown[] } };
|
|
1041
|
+
activeCtx: HarnessRunContext | null;
|
|
1042
|
+
}) {
|
|
1043
|
+
const { isHarnessAgtPolicyEnabled } = await import("../lib/agt/config.js");
|
|
1044
|
+
if (!isHarnessAgtPolicyEnabled()) {
|
|
1045
|
+
if (isSubmitToolName(input.event.toolName)) {
|
|
1046
|
+
const packageRoot = getHarnessPackageRoot(MODULE_URL);
|
|
1047
|
+
const allowed = allowsAgentTool({
|
|
1048
|
+
packageRoot,
|
|
1049
|
+
projectRoot: process.cwd(),
|
|
1050
|
+
agentId: "parent-orchestrator",
|
|
1051
|
+
toolName: input.event.toolName,
|
|
1052
|
+
toolInput: input.event.input as Record<string, unknown>,
|
|
1053
|
+
isSubprocess: false,
|
|
1054
|
+
isParentOrchestrator: true,
|
|
1055
|
+
});
|
|
1056
|
+
if (!allowed) {
|
|
1057
|
+
return {
|
|
1058
|
+
block: true,
|
|
1059
|
+
reason: `agents-policy: ${input.event.toolName} blocked for parent-orchestrator`,
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (input.event.toolName === "write") {
|
|
1065
|
+
const entries = getEntries(input.ctx);
|
|
1066
|
+
const runCtx = getLatestRunContext(entries) ?? input.activeCtx;
|
|
1067
|
+
if (runCtx) {
|
|
1068
|
+
const blocked = await coerceScopedHarnessYamlWrite(
|
|
1069
|
+
input.event as { toolName: string; input: Record<string, unknown> },
|
|
1070
|
+
runCtx,
|
|
1071
|
+
process.cwd(),
|
|
1072
|
+
);
|
|
1073
|
+
if (blocked) return blocked;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const activeCtx = input.activeCtx;
|
|
1077
|
+
if (activeCtx?.plan_packet_path) {
|
|
1078
|
+
const entries = getEntries(input.ctx);
|
|
1079
|
+
if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
|
|
1080
|
+
if (input.event.toolName === "approve_plan") {
|
|
1081
|
+
return {
|
|
1082
|
+
block: true,
|
|
1083
|
+
reason:
|
|
1084
|
+
"harness-run-context: plan already approved via planner subagent; do not call approve_plan again in the parent session.",
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
if (input.event.toolName === "ask_user") {
|
|
1088
|
+
const askInput = input.event.input as {
|
|
1089
|
+
question?: string;
|
|
1090
|
+
options?: unknown[];
|
|
1091
|
+
};
|
|
1092
|
+
if (isPlanApprovalAskUser(askInput)) {
|
|
1093
|
+
return {
|
|
1094
|
+
block: true,
|
|
1095
|
+
reason:
|
|
1096
|
+
"harness-run-context: plan already approved via planner subagent; do not call ask_user for plan approval in the parent session.",
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (!isHarnessAgtPolicyEnabled()) {
|
|
1103
|
+
if (!activeCtx?.plan_packet_path) return undefined;
|
|
1104
|
+
const phase = activeCtx.phase;
|
|
1105
|
+
if (phase !== "evaluate" && phase !== "adversary") return undefined;
|
|
1106
|
+
if (input.event.toolName !== "write" && input.event.toolName !== "edit")
|
|
1107
|
+
return undefined;
|
|
1108
|
+
const target = String(
|
|
1109
|
+
(input.event.input as { path?: string; filePath?: string }).path ??
|
|
1110
|
+
(input.event.input as { filePath?: string }).filePath ??
|
|
1111
|
+
"",
|
|
1112
|
+
);
|
|
1113
|
+
if (target.includes("plan-packet.yaml")) {
|
|
1114
|
+
return {
|
|
1115
|
+
block: true,
|
|
1116
|
+
reason:
|
|
1117
|
+
"harness-run-context: plan-packet.yaml is read-only in evaluate/adversary phases.",
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return undefined;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function registerHarnessToolCallGuards(
|
|
1125
|
+
pi: ExtensionAPI,
|
|
1126
|
+
active: ActiveContextAccess,
|
|
1127
|
+
): void {
|
|
1128
|
+
pi.on("tool_call", async (event, ctx) =>
|
|
1129
|
+
guardToolCall({ event, ctx, activeCtx: active.get() }),
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async function resolveCommandRunContext(input: {
|
|
1134
|
+
pi: ExtensionAPI;
|
|
1135
|
+
activeCtx: HarnessRunContext | null;
|
|
1136
|
+
command: string;
|
|
1137
|
+
args: string;
|
|
1138
|
+
userPrompt: string;
|
|
1139
|
+
sessionId: string;
|
|
1140
|
+
projectRoot: string;
|
|
1141
|
+
turn: HarnessTurnEntry | null;
|
|
1142
|
+
}) {
|
|
1143
|
+
let activeCtx = input.activeCtx;
|
|
1144
|
+
const resolved = resolveArgsForCommand(input.command, input.args, activeCtx);
|
|
1145
|
+
if (resolved.overrideRun && resolved.runId) {
|
|
1146
|
+
const disk = await loadRunContextFromDisk(
|
|
1147
|
+
resolved.runId,
|
|
1148
|
+
input.projectRoot,
|
|
1149
|
+
);
|
|
1150
|
+
if (disk) activeCtx = { ...disk, turn_override_run_id: resolved.runId };
|
|
1151
|
+
}
|
|
1152
|
+
if (
|
|
1153
|
+
input.command === "harness-plan" ||
|
|
1154
|
+
input.command === "harness-auto" ||
|
|
1155
|
+
(!activeCtx && input.command !== "harness-abort")
|
|
1156
|
+
) {
|
|
1157
|
+
if (
|
|
1158
|
+
!activeCtx ||
|
|
1159
|
+
!shouldReuseHarnessRunId(input.userPrompt, activeCtx, input.command)
|
|
1160
|
+
) {
|
|
1161
|
+
activeCtx = createFreshRunContext(
|
|
1162
|
+
input.sessionId,
|
|
1163
|
+
input.projectRoot,
|
|
1164
|
+
extractTaskSummary(input.args, input.userPrompt),
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
if (input.command === "harness-plan") {
|
|
1168
|
+
const task = extractTaskSummary(input.args, input.userPrompt);
|
|
1169
|
+
if (task) activeCtx.task_summary = task;
|
|
1170
|
+
}
|
|
1171
|
+
startFreshPlanAttempt({
|
|
1172
|
+
pi: input.pi,
|
|
1173
|
+
activeCtx,
|
|
1174
|
+
command: input.command,
|
|
1175
|
+
turn: input.turn,
|
|
1176
|
+
});
|
|
1177
|
+
} else if (
|
|
1178
|
+
activeCtx &&
|
|
1179
|
+
shouldReuseHarnessRunId(input.userPrompt, activeCtx, input.command)
|
|
1180
|
+
) {
|
|
1181
|
+
activeCtx.turn_override_run_id = resolved.overrideRun
|
|
1182
|
+
? resolved.runId
|
|
1183
|
+
: null;
|
|
1184
|
+
} else if (!activeCtx) {
|
|
1185
|
+
const pointer = await loadProjectActiveRun(input.projectRoot);
|
|
1186
|
+
if (pointer && isStaleActiveRunPointer(pointer, input.projectRoot)) {
|
|
1187
|
+
const crossSessionCmd = new Set([
|
|
1188
|
+
"harness-eval",
|
|
1189
|
+
"harness-review",
|
|
1190
|
+
"harness-steer",
|
|
1191
|
+
"harness-critic",
|
|
1192
|
+
"harness-trace",
|
|
1193
|
+
"harness-incident",
|
|
1194
|
+
]);
|
|
1195
|
+
if (crossSessionCmd.has(input.command)) {
|
|
1196
|
+
return {
|
|
1197
|
+
activeCtx,
|
|
1198
|
+
resolved,
|
|
1199
|
+
response: blockRunContextMessage(
|
|
1200
|
+
'Project active-run pointer is stale or from another workspace. Run /harness-plan "<task>" or /harness-use-run <run-id> for recovery.',
|
|
1201
|
+
),
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
} else if (pointer) {
|
|
1205
|
+
const disk = await loadRunContextFromDisk(
|
|
1206
|
+
pointer.run_id,
|
|
1207
|
+
input.projectRoot,
|
|
1208
|
+
);
|
|
1209
|
+
if (disk) activeCtx = disk;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return { activeCtx, resolved, response: null };
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
async function handleBeforeAgentStart(input: {
|
|
1216
|
+
pi: ExtensionAPI;
|
|
1217
|
+
event: any;
|
|
1218
|
+
ctx: any;
|
|
1219
|
+
active: ActiveContextAccess;
|
|
1220
|
+
}) {
|
|
1221
|
+
const sessionId = input.ctx.sessionManager.getSessionId();
|
|
1222
|
+
const projectRoot = process.cwd();
|
|
1223
|
+
const entries = getEntries(input.ctx);
|
|
1224
|
+
const userPrompt = userVisiblePromptSlice(input.event.prompt);
|
|
1225
|
+
const turn = getLatestHarnessTurn(entries);
|
|
1226
|
+
const parsed = turn
|
|
1227
|
+
? { command: turn.command, args: turn.args }
|
|
1228
|
+
: parseHarnessSlashInput(userPrompt);
|
|
1229
|
+
const harnessTurn =
|
|
1230
|
+
Boolean(turn) ||
|
|
1231
|
+
Boolean(parsed) ||
|
|
1232
|
+
needsClarificationFollowUp(input.active.get());
|
|
1233
|
+
let activeCtx = await applyAbortSignal({
|
|
1234
|
+
pi: input.pi,
|
|
1235
|
+
activeCtx: input.active.get(),
|
|
1236
|
+
sessionId,
|
|
1237
|
+
projectRoot,
|
|
1238
|
+
entries,
|
|
1239
|
+
userPrompt,
|
|
1240
|
+
});
|
|
1241
|
+
input.active.set(activeCtx);
|
|
1242
|
+
if (!harnessTurn) return undefined;
|
|
1243
|
+
if (!activeCtx) {
|
|
1244
|
+
activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
|
|
1245
|
+
input.active.set(activeCtx);
|
|
1246
|
+
}
|
|
1247
|
+
const policyPhase =
|
|
1248
|
+
inferHarnessPhase(entries, userPrompt) ??
|
|
1249
|
+
getLatestPolicyPhase(entries) ??
|
|
1250
|
+
activeCtx?.phase ??
|
|
1251
|
+
"plan";
|
|
1252
|
+
const driftActive = driftGateActive(entries);
|
|
1253
|
+
if (!parsed && needsClarificationFollowUp(activeCtx) && activeCtx) {
|
|
1254
|
+
return maybeHandleClarificationFollowUp({
|
|
1255
|
+
pi: input.pi,
|
|
1256
|
+
activeCtx,
|
|
1257
|
+
entries,
|
|
1258
|
+
systemPrompt: input.event.systemPrompt,
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
if (!parsed) return undefined;
|
|
1262
|
+
const { command, args } = parsed;
|
|
1263
|
+
if (
|
|
1264
|
+
!isHarnessBootstrapPrompt(userPrompt) &&
|
|
1265
|
+
!hasHarnessAbortSignal(userPrompt)
|
|
1266
|
+
) {
|
|
1267
|
+
const policyBlock = getPolicyTransitionBlock(userPrompt, entries);
|
|
1268
|
+
if (policyBlock.blocked) {
|
|
1269
|
+
return blockRunContextMessage(
|
|
1270
|
+
policyBlock.message ?? "Harness command blocked by policy phase.",
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (command === "harness-new-run") {
|
|
1275
|
+
const next = createNewRunContextForCommand({
|
|
1276
|
+
pi: input.pi,
|
|
1277
|
+
activeCtx,
|
|
1278
|
+
sessionId,
|
|
1279
|
+
projectRoot,
|
|
1280
|
+
args,
|
|
1281
|
+
userPrompt,
|
|
1282
|
+
systemPrompt: input.event.systemPrompt,
|
|
1283
|
+
});
|
|
1284
|
+
input.active.set(next.activeCtx);
|
|
1285
|
+
return next.response;
|
|
1286
|
+
}
|
|
1287
|
+
if (command === "harness-use-run") {
|
|
1288
|
+
const next = await bindExistingRunForCommand({
|
|
1289
|
+
pi: input.pi,
|
|
1290
|
+
sessionId,
|
|
1291
|
+
projectRoot,
|
|
1292
|
+
entries,
|
|
1293
|
+
args,
|
|
1294
|
+
systemPrompt: input.event.systemPrompt,
|
|
1295
|
+
});
|
|
1296
|
+
if (next.activeCtx) input.active.set(next.activeCtx);
|
|
1297
|
+
return next.response;
|
|
1298
|
+
}
|
|
1299
|
+
if (command === "harness-run-status") return undefined;
|
|
1300
|
+
if (
|
|
1301
|
+
command === "harness-plan" &&
|
|
1302
|
+
activeCtx &&
|
|
1303
|
+
isNewTaskPlanBlocked(activeCtx, userPrompt) &&
|
|
1304
|
+
!isAmendPlanAllowed(activeCtx, userPrompt, driftActive)
|
|
1305
|
+
) {
|
|
1306
|
+
return blockRunContextMessage(
|
|
1307
|
+
"Active harness run in progress. Use /harness-abort or /harness-new-run before starting a new task plan.",
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
const prepared = await resolveCommandRunContext({
|
|
1311
|
+
pi: input.pi,
|
|
1312
|
+
activeCtx,
|
|
1313
|
+
command,
|
|
1314
|
+
args,
|
|
1315
|
+
userPrompt,
|
|
1316
|
+
sessionId,
|
|
1317
|
+
projectRoot,
|
|
1318
|
+
turn,
|
|
1319
|
+
});
|
|
1320
|
+
activeCtx = prepared.activeCtx;
|
|
1321
|
+
const { resolved } = prepared;
|
|
1322
|
+
if (prepared.response) return prepared.response;
|
|
1323
|
+
if (!activeCtx)
|
|
1324
|
+
return blockRunContextMessage(
|
|
1325
|
+
'No active harness run. Run /harness-plan "<task>" first, or /harness-use-run <run-id> for recovery.',
|
|
1326
|
+
);
|
|
1327
|
+
activeCtx.phase = policyPhase;
|
|
1328
|
+
activeCtx.updated_at = new Date().toISOString();
|
|
1329
|
+
activeCtx.pi_session_id = sessionId;
|
|
1330
|
+
if (
|
|
1331
|
+
shouldAutoClaimHarnessRun(command, args) &&
|
|
1332
|
+
activeCtx.owner_pi_session_id !== sessionId
|
|
1333
|
+
) {
|
|
1334
|
+
activeCtx = claimRunOwnership(activeCtx, sessionId);
|
|
1335
|
+
}
|
|
1336
|
+
if (resolved.planPath && resolved.runId) {
|
|
1337
|
+
const check = validatePlanOverridePath(
|
|
1338
|
+
resolved.planPath,
|
|
1339
|
+
resolved.runId,
|
|
1340
|
+
projectRoot,
|
|
1341
|
+
);
|
|
1342
|
+
if (!check.ok)
|
|
1343
|
+
return blockRunContextMessage(check.reason ?? "Invalid --plan override");
|
|
1344
|
+
activeCtx.plan_packet_path = resolved.planPath;
|
|
1345
|
+
}
|
|
1346
|
+
if (command === "harness-run" && !activeCtx.plan_ready)
|
|
1347
|
+
return blockRunContextMessage("Plan not ready. Run /harness-plan first.");
|
|
1348
|
+
if (
|
|
1349
|
+
command === "harness-run" &&
|
|
1350
|
+
activeCtx.plan_ready &&
|
|
1351
|
+
activeCtx.last_completed_step === "execute" &&
|
|
1352
|
+
activeCtx.last_outcome === "completed"
|
|
1353
|
+
) {
|
|
1354
|
+
return blockRunContextMessage(
|
|
1355
|
+
"Execute already completed for this run. Next: /harness-review (same session), or /harness-abort to replan.",
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
const { planSummary, planPacketForSpawn } =
|
|
1359
|
+
await readPlanSpawnState(activeCtx);
|
|
1360
|
+
const { activePlanBlock, planMode, contextSpawnOpts } =
|
|
1361
|
+
buildSpawnPromptBlocks({
|
|
1362
|
+
command,
|
|
1363
|
+
activeCtx,
|
|
1364
|
+
planSummary,
|
|
1365
|
+
planPacketForSpawn,
|
|
1366
|
+
});
|
|
1367
|
+
await archivePlanRevisionIfNeeded({
|
|
1368
|
+
pi: input.pi,
|
|
1369
|
+
command,
|
|
1370
|
+
planMode,
|
|
1371
|
+
activeCtx,
|
|
1372
|
+
projectRoot,
|
|
1373
|
+
userPrompt,
|
|
1374
|
+
});
|
|
1375
|
+
input.active.set(activeCtx);
|
|
1376
|
+
persistContext(input.pi, activeCtx);
|
|
1377
|
+
return {
|
|
1378
|
+
systemPrompt: `${input.event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx, contextSpawnOpts)}${activePlanBlock ? `\n\n${activePlanBlock}` : ""}`,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
904
1381
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1382
|
+
async function handleAgentEnd(input: {
|
|
1383
|
+
pi: ExtensionAPI;
|
|
1384
|
+
ctx: any;
|
|
1385
|
+
active: ActiveContextAccess;
|
|
1386
|
+
}): Promise<void> {
|
|
1387
|
+
const projectRoot = process.cwd();
|
|
1388
|
+
const entries = getEntries(input.ctx);
|
|
1389
|
+
const activeCtx = input.active.get() ?? getLatestRunContext(entries);
|
|
1390
|
+
if (!activeCtx) return;
|
|
1391
|
+
input.active.set(activeCtx);
|
|
1392
|
+
const parsed = latestParsedHarnessCommand(entries);
|
|
1393
|
+
if (!parsed && !needsClarificationFollowUp(activeCtx)) return;
|
|
1394
|
+
if (parsed?.command === "harness-abort") {
|
|
1395
|
+
handleAgentEndAbort({ pi: input.pi, ctx: input.ctx, activeCtx });
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
await updatePlanReadinessAfterAgent({
|
|
1399
|
+
pi: input.pi,
|
|
1400
|
+
ctx: input.ctx,
|
|
1401
|
+
entries,
|
|
1402
|
+
parsed,
|
|
1403
|
+
activeCtx,
|
|
1404
|
+
});
|
|
1405
|
+
const statuses = await resolveCompletionStatuses(
|
|
1406
|
+
entries,
|
|
1407
|
+
activeCtx.run_id,
|
|
1408
|
+
projectRoot,
|
|
1409
|
+
);
|
|
1410
|
+
if (parsed?.command === "harness-run") {
|
|
1411
|
+
activeCtx.last_completed_step = "execute";
|
|
1412
|
+
let execStatus = statuses.executionStatus;
|
|
1413
|
+
if (!execStatus) {
|
|
1414
|
+
const handoff = await readExecutorHandoffFromRun(
|
|
1415
|
+
activeCtx.run_id,
|
|
1416
|
+
projectRoot,
|
|
911
1417
|
);
|
|
912
|
-
|
|
913
|
-
return { block: true, reason: decision.reason };
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
if (event.toolName === "write") {
|
|
917
|
-
const entries = getEntries(ctx);
|
|
918
|
-
const runCtx = getLatestRunContext(entries) ?? activeCtx;
|
|
919
|
-
if (runCtx) {
|
|
920
|
-
const blocked = await coerceScopedHarnessYamlWrite(
|
|
921
|
-
event,
|
|
922
|
-
runCtx,
|
|
923
|
-
process.cwd(),
|
|
924
|
-
);
|
|
925
|
-
if (blocked) return blocked;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
if (activeCtx?.plan_packet_path) {
|
|
929
|
-
const entries = getEntries(ctx);
|
|
930
|
-
if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
|
|
931
|
-
if (event.toolName === "approve_plan") {
|
|
932
|
-
return {
|
|
933
|
-
block: true,
|
|
934
|
-
reason:
|
|
935
|
-
"harness-run-context: plan already approved via planner subagent; do not call approve_plan again in the parent session.",
|
|
936
|
-
};
|
|
937
|
-
}
|
|
938
|
-
if (event.toolName === "ask_user") {
|
|
939
|
-
const input = event.input as {
|
|
940
|
-
question?: string;
|
|
941
|
-
options?: unknown[];
|
|
942
|
-
};
|
|
943
|
-
if (isPlanApprovalAskUser(input)) {
|
|
944
|
-
return {
|
|
945
|
-
block: true,
|
|
946
|
-
reason:
|
|
947
|
-
"harness-run-context: plan already approved via planner subagent; do not call ask_user for plan approval in the parent session.",
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
if (!activeCtx?.plan_packet_path) return undefined;
|
|
954
|
-
const phase = activeCtx.phase;
|
|
955
|
-
if (phase !== "evaluate" && phase !== "adversary") return undefined;
|
|
956
|
-
if (event.toolName !== "write" && event.toolName !== "edit") {
|
|
957
|
-
return undefined;
|
|
958
|
-
}
|
|
959
|
-
const target = String(
|
|
960
|
-
(event.input as { path?: string; filePath?: string }).path ??
|
|
961
|
-
(event.input as { filePath?: string }).filePath ??
|
|
962
|
-
"",
|
|
963
|
-
);
|
|
964
|
-
if (target.includes("plan-packet.yaml")) {
|
|
965
|
-
return {
|
|
966
|
-
block: true,
|
|
967
|
-
reason:
|
|
968
|
-
"harness-run-context: plan-packet.yaml is read-only in evaluate/adversary phases.",
|
|
969
|
-
};
|
|
1418
|
+
execStatus = handoff?.execution_status ?? null;
|
|
970
1419
|
}
|
|
971
|
-
|
|
1420
|
+
activeCtx.last_outcome = execStatus ?? "completed";
|
|
1421
|
+
activeCtx.phase = "evaluate";
|
|
1422
|
+
}
|
|
1423
|
+
if (parsed?.command === "harness-steer") {
|
|
1424
|
+
activeCtx.last_completed_step = "steer";
|
|
1425
|
+
activeCtx.steer_attempt = (activeCtx.steer_attempt ?? 0) + 1;
|
|
1426
|
+
activeCtx.steer_max_attempts =
|
|
1427
|
+
activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv();
|
|
1428
|
+
activeCtx.phase = "execute";
|
|
1429
|
+
syncPolicyFromRunContext(input.pi, entries, activeCtx);
|
|
1430
|
+
}
|
|
1431
|
+
if (
|
|
1432
|
+
["harness-eval", "harness-review", "harness-critic"].includes(
|
|
1433
|
+
parsed?.command ?? "",
|
|
1434
|
+
)
|
|
1435
|
+
) {
|
|
1436
|
+
activeCtx.last_completed_step =
|
|
1437
|
+
parsed?.command === "harness-critic" ? "adversary" : "review";
|
|
1438
|
+
if (statuses.evalStatus) activeCtx.last_outcome = statuses.evalStatus;
|
|
1439
|
+
if (statuses.adversaryComplete) {
|
|
1440
|
+
activeCtx.phase = "adversary";
|
|
1441
|
+
activeCtx.last_completed_step = "adversary";
|
|
1442
|
+
} else if (statuses.evalStatus) activeCtx.phase = "evaluate";
|
|
1443
|
+
}
|
|
1444
|
+
const reviewOutcome = await readReviewOutcomeFromRun(
|
|
1445
|
+
activeCtx.run_id,
|
|
1446
|
+
projectRoot,
|
|
1447
|
+
);
|
|
1448
|
+
const reviewComplete =
|
|
1449
|
+
activeCtx.last_completed_step === "review" ||
|
|
1450
|
+
activeCtx.last_completed_step === "adversary";
|
|
1451
|
+
const next = nextStepAfterOutcome({
|
|
1452
|
+
phase: activeCtx.phase,
|
|
1453
|
+
planStatus: statuses.planStatus,
|
|
1454
|
+
lastCompletedStep: activeCtx.last_completed_step,
|
|
1455
|
+
lastOutcome: activeCtx.last_outcome,
|
|
1456
|
+
executionStatus: statuses.executionStatus,
|
|
1457
|
+
evalStatus: statuses.evalStatus,
|
|
1458
|
+
adversaryComplete: statuses.adversaryComplete,
|
|
1459
|
+
aborted: activeCtx.status === "aborted",
|
|
1460
|
+
remediationClass: reviewOutcome?.remediation_class ?? null,
|
|
1461
|
+
steerAttempt: activeCtx.steer_attempt ?? 0,
|
|
1462
|
+
steerMaxAttempts: activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv(),
|
|
1463
|
+
reviewComplete,
|
|
1464
|
+
});
|
|
1465
|
+
activeCtx.next_recommended_command = next;
|
|
1466
|
+
activeCtx.updated_at = new Date().toISOString();
|
|
1467
|
+
if (
|
|
1468
|
+
parsed?.command === "harness-run" &&
|
|
1469
|
+
activeCtx.last_outcome === "completed"
|
|
1470
|
+
) {
|
|
1471
|
+
syncPolicyFromRunContext(input.pi, entries, activeCtx);
|
|
1472
|
+
}
|
|
1473
|
+
persistContext(input.pi, activeCtx);
|
|
1474
|
+
input.pi.appendEntry("harness-step-handoff", {
|
|
1475
|
+
next_command: next,
|
|
1476
|
+
plan_status: statuses.planStatus,
|
|
1477
|
+
execution_status: statuses.executionStatus,
|
|
1478
|
+
eval_status: statuses.evalStatus,
|
|
1479
|
+
phase: activeCtx.phase,
|
|
972
1480
|
});
|
|
1481
|
+
if (next && parsed) {
|
|
1482
|
+
const notify = `Next: ${next}`;
|
|
1483
|
+
if (input.ctx.hasUI) input.ctx.ui.notify(notify, "info");
|
|
1484
|
+
else
|
|
1485
|
+
input.pi.sendMessage({
|
|
1486
|
+
customType: "harness-step-handoff",
|
|
1487
|
+
content: notify,
|
|
1488
|
+
display: true,
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
973
1492
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
let ctxState = getLatestRunContext(entries) ?? activeCtx;
|
|
982
|
-
if (!ctxState) {
|
|
983
|
-
ctxState = await hydrateFromDisk(sessionId, projectRoot, entries);
|
|
984
|
-
}
|
|
985
|
-
if (!ctxState) {
|
|
986
|
-
const msg = 'No active harness run. Start with /harness-plan "<task>".';
|
|
987
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
let summary: PlanPacketSummary | null = null;
|
|
991
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
992
|
-
const entry = entries[i] as SessionEntryLike;
|
|
993
|
-
if (
|
|
994
|
-
entry.type !== "custom" ||
|
|
995
|
-
entry.customType !== "harness-plan-packet"
|
|
996
|
-
)
|
|
997
|
-
continue;
|
|
998
|
-
summary = entry.data as PlanPacketSummary;
|
|
999
|
-
break;
|
|
1000
|
-
}
|
|
1001
|
-
const lines = [
|
|
1002
|
-
"Harness run status:",
|
|
1003
|
-
` phase: ${ctxState.phase}`,
|
|
1004
|
-
` status: ${ctxState.status}`,
|
|
1005
|
-
` plan_ready: ${ctxState.plan_ready}`,
|
|
1006
|
-
` plan_id: ${ctxState.plan_id ?? "(none)"}`,
|
|
1007
|
-
summary
|
|
1008
|
-
? ` scope: ${summary.scope_one_liner}`
|
|
1009
|
-
: " scope: (no plan summary yet)",
|
|
1010
|
-
` last_step: ${ctxState.last_completed_step ?? "(none)"}`,
|
|
1011
|
-
` last_outcome: ${ctxState.last_outcome ?? "(none)"}`,
|
|
1012
|
-
` next: ${ctxState.next_recommended_command ?? "/harness-run-status"}`,
|
|
1013
|
-
];
|
|
1014
|
-
const text = lines.join("\n");
|
|
1015
|
-
if (ctx.hasUI) ctx.ui.notify(text, "info");
|
|
1016
|
-
else
|
|
1017
|
-
pi.sendMessage({
|
|
1018
|
-
customType: "harness-run-status",
|
|
1019
|
-
content: text,
|
|
1020
|
-
display: true,
|
|
1021
|
-
});
|
|
1493
|
+
export default function harnessRunContext(pi: ExtensionAPI) {
|
|
1494
|
+
if (!claimHarnessGovernanceLoad("harness-run-context", MODULE_URL)) return;
|
|
1495
|
+
let activeCtx: HarnessRunContext | null = null;
|
|
1496
|
+
const activeAccess: ActiveContextAccess = {
|
|
1497
|
+
get: () => activeCtx,
|
|
1498
|
+
set: (ctx) => {
|
|
1499
|
+
activeCtx = ctx;
|
|
1022
1500
|
},
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1504
|
+
const entries = getEntries(ctx);
|
|
1505
|
+
activeCtx = hydrateFromSession(entries);
|
|
1506
|
+
const booted = await bootstrapHarnessSubprocessFromEnv(pi, ctx);
|
|
1507
|
+
if (booted) activeCtx = booted;
|
|
1508
|
+
if (!booted) await offerCrossSessionResume(pi, ctx);
|
|
1023
1509
|
});
|
|
1024
1510
|
|
|
1025
|
-
pi.
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
const msg =
|
|
1042
|
-
'New harness run allocated. Next: /harness-plan "<your task>"';
|
|
1043
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "info");
|
|
1044
|
-
},
|
|
1511
|
+
pi.on("input", async (event) => {
|
|
1512
|
+
if (event.source === "extension") {
|
|
1513
|
+
return { action: "continue" as const };
|
|
1514
|
+
}
|
|
1515
|
+
const parsed = parseHarnessSlashInput(event.text);
|
|
1516
|
+
if (!parsed) {
|
|
1517
|
+
return { action: "continue" as const };
|
|
1518
|
+
}
|
|
1519
|
+
appendHarnessTurn(pi, {
|
|
1520
|
+
schema_version: "1.0.0",
|
|
1521
|
+
command: parsed.command,
|
|
1522
|
+
args: parsed.args,
|
|
1523
|
+
source: "slash",
|
|
1524
|
+
invoked_at: nowIso(),
|
|
1525
|
+
});
|
|
1526
|
+
return { action: "continue" as const };
|
|
1045
1527
|
});
|
|
1046
1528
|
|
|
1047
|
-
pi.
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
let runCtx = getLatestRunContext(entries) ?? activeCtx;
|
|
1054
|
-
if (!runCtx) {
|
|
1055
|
-
runCtx = await hydrateFromDisk(
|
|
1056
|
-
ctx.sessionManager.getSessionId(),
|
|
1057
|
-
projectRoot,
|
|
1058
|
-
entries,
|
|
1059
|
-
);
|
|
1060
|
-
}
|
|
1061
|
-
if (!runCtx?.plan_packet_path) {
|
|
1062
|
-
const msg = "No active harness run. Run /harness-plan first.";
|
|
1063
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
if (
|
|
1067
|
-
!hasPlanUserApproval(entries, {
|
|
1068
|
-
sincePlanCommand: true,
|
|
1069
|
-
planId: runCtx.plan_id,
|
|
1070
|
-
})
|
|
1071
|
-
) {
|
|
1072
|
-
const msg =
|
|
1073
|
-
"Plan commit blocked: no user approval recorded. Approve via approve_plan in this session first.";
|
|
1074
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
const pathArg = args.trim();
|
|
1078
|
-
let packetPath = runCtx.plan_packet_path;
|
|
1079
|
-
if (pathArg) {
|
|
1080
|
-
packetPath = pathArg;
|
|
1081
|
-
}
|
|
1082
|
-
const packet = await readPlanPacketFromPath(packetPath);
|
|
1083
|
-
const validation = validatePlanPacket(packet);
|
|
1084
|
-
if (!validation.valid || !packet) {
|
|
1085
|
-
const msg = !packet
|
|
1086
|
-
? "Plan packet file missing or unreadable."
|
|
1087
|
-
: `Invalid plan packet: ${validation.errors.join("; ")}`;
|
|
1088
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "error");
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
const target = runCtx.plan_packet_path;
|
|
1092
|
-
if (!target) {
|
|
1093
|
-
if (ctx.hasUI)
|
|
1094
|
-
ctx.ui.notify("No plan_packet_path on active run.", "error");
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
if (pathArg && pathArg !== target) {
|
|
1098
|
-
const raw = await readFile(pathArg, "utf-8");
|
|
1099
|
-
await writeFile(target, raw, "utf-8");
|
|
1100
|
-
}
|
|
1101
|
-
runCtx.plan_id = packet.plan_id ?? runCtx.plan_id;
|
|
1102
|
-
runCtx.plan_ready = true;
|
|
1103
|
-
runCtx.phase = "plan";
|
|
1104
|
-
runCtx.last_completed_step = "plan";
|
|
1105
|
-
runCtx.last_outcome = "ready";
|
|
1106
|
-
runCtx.next_recommended_command = "/harness-run";
|
|
1107
|
-
runCtx.updated_at = nowIso();
|
|
1108
|
-
activeCtx = runCtx;
|
|
1109
|
-
persistContext(pi, runCtx);
|
|
1110
|
-
syncPolicyFromPlan(
|
|
1111
|
-
pi,
|
|
1112
|
-
entries,
|
|
1113
|
-
runCtx.plan_id ?? packet.plan_id ?? "plan-pending",
|
|
1114
|
-
"plan",
|
|
1115
|
-
true,
|
|
1116
|
-
);
|
|
1117
|
-
const summary = planPacketSummary(packet, target, "ready");
|
|
1118
|
-
pi.appendEntry("harness-plan-packet", summary);
|
|
1119
|
-
const msg = `Plan committed: ${target}`;
|
|
1120
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "info");
|
|
1121
|
-
},
|
|
1529
|
+
pi.on("before_agent_start", async (event, ctx) =>
|
|
1530
|
+
handleBeforeAgentStart({ pi, event, ctx, active: activeAccess }),
|
|
1531
|
+
);
|
|
1532
|
+
|
|
1533
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
1534
|
+
await handleAgentEnd({ pi, ctx, active: activeAccess });
|
|
1122
1535
|
});
|
|
1123
1536
|
|
|
1537
|
+
registerPlanApprovalCapture(pi, activeAccess);
|
|
1538
|
+
registerHarnessToolCallGuards(pi, activeAccess);
|
|
1539
|
+
registerHarnessRunStatusCommand(pi, activeAccess);
|
|
1540
|
+
registerHarnessNewRunCommand(pi, activeAccess);
|
|
1541
|
+
|
|
1542
|
+
registerHarnessPlanCommitCommand(pi, activeAccess);
|
|
1543
|
+
|
|
1124
1544
|
pi.registerTool({
|
|
1125
1545
|
name: "write_harness_yaml",
|
|
1126
1546
|
label: "Write Harness YAML",
|
|
@@ -1211,7 +1631,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
1211
1631
|
content: [
|
|
1212
1632
|
{
|
|
1213
1633
|
type: "text",
|
|
1214
|
-
text: `Path not allowed: ${pathArg}. Post-run verdicts must be written via submit_* in harness/evaluator or harness/adversary subagents; parent gates with harness_artifact_ready only.`,
|
|
1634
|
+
text: `Path not allowed: ${pathArg}. Post-run verdicts must be written via submit_* in harness/reviewing/evaluator or harness/reviewing/adversary subagents; parent gates with harness_artifact_ready only.`,
|
|
1215
1635
|
},
|
|
1216
1636
|
],
|
|
1217
1637
|
details: { path: pathArg },
|
|
@@ -1534,7 +1954,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
1534
1954
|
);
|
|
1535
1955
|
const specsDir = join(projectRoot, ".pi", "harness", "specs");
|
|
1536
1956
|
const { validateHarnessArtifactPaths } = await import(
|
|
1537
|
-
"
|
|
1957
|
+
"../lib/harness-artifact-gate.js"
|
|
1538
1958
|
);
|
|
1539
1959
|
const gate = await validateHarnessArtifactPaths(runRoot, paths, specsDir);
|
|
1540
1960
|
const text = gate.ok
|
|
@@ -1561,63 +1981,5 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
1561
1981
|
},
|
|
1562
1982
|
});
|
|
1563
1983
|
|
|
1564
|
-
pi
|
|
1565
|
-
description:
|
|
1566
|
-
"Point this session at an existing run directory (recovery; --claim for write ownership)",
|
|
1567
|
-
handler: async (args, ctx) => {
|
|
1568
|
-
const parsed = parseHarnessUseRunArgs(args);
|
|
1569
|
-
if (!parsed.runId) {
|
|
1570
|
-
if (ctx.hasUI)
|
|
1571
|
-
ctx.ui.notify(
|
|
1572
|
-
"Usage: /harness-use-run <run-id> [--claim] [--readonly]",
|
|
1573
|
-
"warning",
|
|
1574
|
-
);
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
const projectRoot = process.cwd();
|
|
1578
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
1579
|
-
const disk = await loadRunContextFromDisk(parsed.runId, projectRoot);
|
|
1580
|
-
if (!disk) {
|
|
1581
|
-
if (ctx.hasUI) ctx.ui.notify(`Run not found: ${parsed.runId}`, "error");
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
|
-
activeCtx = {
|
|
1585
|
-
...disk,
|
|
1586
|
-
pi_session_id: sessionId,
|
|
1587
|
-
};
|
|
1588
|
-
if (parsed.claim) {
|
|
1589
|
-
activeCtx = claimRunOwnership(activeCtx, sessionId);
|
|
1590
|
-
}
|
|
1591
|
-
const statuses = await resolveCompletionStatuses(
|
|
1592
|
-
getEntries(ctx),
|
|
1593
|
-
activeCtx.run_id,
|
|
1594
|
-
projectRoot,
|
|
1595
|
-
);
|
|
1596
|
-
if (activeCtx.owner_pi_session_id !== sessionId && !parsed.claim) {
|
|
1597
|
-
activeCtx.next_recommended_command =
|
|
1598
|
-
"Read-only: use /harness-use-run <run-id> --claim to take ownership.";
|
|
1599
|
-
} else {
|
|
1600
|
-
activeCtx.next_recommended_command = nextStepAfterOutcome({
|
|
1601
|
-
phase: activeCtx.phase,
|
|
1602
|
-
planStatus: activeCtx.plan_ready ? "ready" : null,
|
|
1603
|
-
lastCompletedStep: activeCtx.last_completed_step,
|
|
1604
|
-
lastOutcome: activeCtx.last_outcome,
|
|
1605
|
-
executionStatus: statuses.executionStatus,
|
|
1606
|
-
evalStatus: statuses.evalStatus,
|
|
1607
|
-
adversaryComplete: statuses.adversaryComplete,
|
|
1608
|
-
aborted: activeCtx.status === "aborted",
|
|
1609
|
-
});
|
|
1610
|
-
}
|
|
1611
|
-
activeCtx.updated_at = nowIso();
|
|
1612
|
-
persistContext(pi, activeCtx);
|
|
1613
|
-
syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
|
|
1614
|
-
if (ctx.hasUI) {
|
|
1615
|
-
const mode = parsed.claim ? "claimed" : "bound (read-only)";
|
|
1616
|
-
ctx.ui.notify(
|
|
1617
|
-
`Session ${mode} to run ${parsed.runId}. See /harness-run-status.`,
|
|
1618
|
-
"info",
|
|
1619
|
-
);
|
|
1620
|
-
}
|
|
1621
|
-
},
|
|
1622
|
-
});
|
|
1984
|
+
registerHarnessUseRunCommand(pi, activeAccess);
|
|
1623
1985
|
}
|