ultimate-pi 0.18.1 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/harness-debate-plan/SKILL.md +1 -1
- package/.agents/skills/harness-decisions/SKILL.md +1 -2
- package/.agents/skills/harness-governor/SKILL.md +6 -5
- package/.agents/skills/web-retrieval/SKILL.md +163 -0
- package/.agents/skills/wiki-autoresearch/SKILL.md +6 -6
- package/.pi/PACKAGING.md +4 -4
- package/.pi/SYSTEM.md +75 -123
- package/.pi/agents/harness/incident-recorder.md +0 -1
- package/.pi/agents/harness/planning/decompose.md +0 -2
- 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 +1 -3
- 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 +5 -3
- package/.pi/agents/harness/reviewing/adversary.md +0 -2
- package/.pi/agents/harness/reviewing/evaluator.md +0 -2
- package/.pi/agents/harness/reviewing/tie-breaker.md +0 -2
- package/.pi/agents/harness/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/agents/harness/web-retrieval/web-answerer.md +35 -0
- package/.pi/agents/harness/web-retrieval/web-criteria-verifier.md +28 -0
- package/.pi/agents/harness/web-retrieval/web-gap-analyzer.md +31 -0
- package/.pi/agents/harness/web-retrieval/web-query-expander-fast.md +34 -0
- package/.pi/agents/harness/web-retrieval/web-query-expander.md +60 -0
- package/.pi/agents/harness/web-retrieval/web-summarizer.md +18 -0
- package/.pi/extensions/agt-kill-switch.ts +57 -0
- package/.pi/extensions/agt-prompt-guard.ts +32 -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 +6 -6
- package/.pi/extensions/harness-ask-user.ts +7 -7
- package/.pi/extensions/harness-debate-tools.ts +26 -42
- package/.pi/extensions/harness-lens.ts +94 -0
- package/.pi/extensions/harness-plan-approval.ts +11 -11
- package/.pi/extensions/harness-run-context.ts +1070 -876
- package/.pi/extensions/harness-subagent-governance.ts +8 -0
- package/.pi/extensions/harness-subagent-submit.ts +34 -163
- package/.pi/extensions/harness-subagents.ts +3 -3
- package/.pi/extensions/harness-telemetry.ts +2 -2
- package/.pi/extensions/harness-web-guard.ts +2 -1
- package/.pi/extensions/harness-web-tools.ts +691 -53
- package/.pi/extensions/policy-gate.ts +25 -5
- package/.pi/extensions/sentrux-rules-sync.ts +1 -1
- package/.pi/extensions/subagent-governance.ts +92 -0
- package/.pi/extensions/trace-recorder.ts +1 -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 +46 -25
- package/.pi/harness/agents.policy.yaml +309 -0
- 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/0045-harness-lens-minimal-contract.md +49 -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/0050-agentic-web-retrieval-stack.md +46 -0
- package/.pi/harness/docs/adrs/README.md +5 -0
- package/.pi/harness/docs/harness-web-search.md +97 -0
- package/.pi/harness/env.harness.template +9 -1
- 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/examples/web-heuristic-angles.project.yaml +22 -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/observation.schema.json +2 -1
- package/.pi/harness/web-heuristic-angles.json +278 -0
- package/.pi/harness/web-heuristic-angles.yaml +182 -0
- package/.pi/lib/agents-policy.d.mts +70 -0
- package/.pi/lib/agents-policy.mjs +331 -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 +13 -2
- package/.pi/lib/harness-agt-tool-guard.ts +5 -0
- package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +1 -1
- 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-run-context-responses.ts +9 -0
- package/.pi/lib/harness-run-context.ts +0 -2
- package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +1 -0
- package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +1 -1
- package/.pi/lib/harness-subagent-auth.ts +81 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +10 -7
- 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 -37
- package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +74 -14
- package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
- package/.pi/lib/harness-web/artifacts.ts +200 -0
- package/.pi/lib/harness-web/cache.ts +369 -0
- package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +42 -2
- 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-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-plan.md +2 -1
- package/.pi/prompts/harness-setup.md +40 -65
- package/.pi/scripts/README.md +2 -5
- package/.pi/scripts/gen-web-heuristic-angles-json.mjs +24 -0
- package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
- package/.pi/scripts/harness-agents-manifest.mjs +60 -3
- package/.pi/scripts/harness-agt-doctor.ts +36 -0
- package/.pi/scripts/harness-cli-verify.sh +14 -2
- package/.pi/scripts/harness-verify.mjs +191 -39
- package/.pi/scripts/harness-web-policy-guard.mjs +3 -3
- package/.pi/scripts/harness-web.py +218 -15
- package/.pi/scripts/harness_web/deep_search.py +55 -0
- package/.pi/scripts/harness_web/evidence_bundle.py +47 -0
- package/.pi/scripts/harness_web/find_similar.py +88 -0
- package/.pi/scripts/harness_web/heuristic_angles_shipped.py +85 -0
- package/.pi/scripts/harness_web/heuristic_config.py +251 -0
- package/.pi/scripts/harness_web/highlights.py +47 -0
- package/.pi/scripts/harness_web/multi_search.py +59 -0
- package/.pi/scripts/harness_web/output.py +24 -0
- package/.pi/scripts/harness_web/query_angles.py +116 -0
- package/.pi/scripts/harness_web/rank.py +163 -0
- package/.pi/scripts/harness_web/scrape.py +30 -0
- package/.pi/scripts/tests/test_harness_web_heuristic_config.py +132 -0
- package/.pi/scripts/tests/test_harness_web_query_angles.py +45 -0
- package/.pi/scripts/tests/test_harness_web_rank.py +56 -0
- 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 +7 -2
- package/CHANGELOG.md +20 -0
- package/README.md +3 -12
- package/THIRD_PARTY_NOTICES.md +12 -21
- package/package.json +17 -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/.agents/skills/scrapling-web/SKILL.md +0 -98
- package/.pi/agents/harness/meta-optimizer.md +0 -36
- package/.pi/extensions/00-posthog-network-bootstrap.ts +0 -11
- package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
- package/.pi/extensions/lib/harness-subagent-auth.ts +0 -207
- 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/harness_web/__pycache__/__init__.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/config.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/output.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/scrape.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search_ddg.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search_searxng.cpython-314.pyc +0 -0
- 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}/plan-approval/dialog.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval-readiness.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
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
import { basename, dirname, join } from "node:path";
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
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";
|
|
19
22
|
import {
|
|
20
23
|
canonicalPlanPath,
|
|
21
24
|
claimRunOwnership,
|
|
@@ -67,19 +70,16 @@ import {
|
|
|
67
70
|
validatePlanOverridePath,
|
|
68
71
|
validatePlanPacket,
|
|
69
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";
|
|
70
76
|
import {
|
|
71
77
|
normalizeHarnessYamlContent,
|
|
72
78
|
parseStructuredDocument,
|
|
73
79
|
writeYamlFile,
|
|
74
80
|
} from "../lib/harness-yaml.js";
|
|
75
|
-
import {
|
|
76
|
-
import {
|
|
77
|
-
evaluateHarnessSubagentToolCall,
|
|
78
|
-
isSubmitToolName,
|
|
79
|
-
} from "./lib/harness-subagent-policy.js";
|
|
80
|
-
import { bootstrapHarnessSubprocessFromEnv } from "./lib/harness-subprocess-bootstrap.js";
|
|
81
|
-
import { isReviewRoundArtifactPath } from "./lib/plan-debate-gate.js";
|
|
82
|
-
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";
|
|
83
83
|
|
|
84
84
|
// @ts-expect-error pi extensions run as ESM
|
|
85
85
|
const MODULE_URL = import.meta.url;
|
|
@@ -405,182 +405,154 @@ async function offerCrossSessionResume(
|
|
|
405
405
|
});
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
+
}
|
|
411
433
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
}
|
|
419
462
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
invoked_at: nowIso(),
|
|
434
|
-
});
|
|
435
|
-
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(),
|
|
436
476
|
});
|
|
477
|
+
}
|
|
437
478
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const turn = getLatestHarnessTurn(entries);
|
|
444
|
-
const parsed = turn
|
|
445
|
-
? { command: turn.command, args: turn.args }
|
|
446
|
-
: parseHarnessSlashInput(userPrompt);
|
|
447
|
-
const harnessTurn =
|
|
448
|
-
Boolean(turn) || Boolean(parsed) || needsClarificationFollowUp(activeCtx);
|
|
449
|
-
|
|
450
|
-
if (
|
|
451
|
-
userPrompt.toLowerCase().includes("/harness-abort") ||
|
|
452
|
-
userPrompt.toLowerCase().includes("harness-abort")
|
|
453
|
-
) {
|
|
454
|
-
if (!activeCtx) {
|
|
455
|
-
activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
|
|
456
|
-
}
|
|
457
|
-
if (activeCtx) {
|
|
458
|
-
activeCtx.status = "aborted";
|
|
459
|
-
activeCtx.plan_ready = false;
|
|
460
|
-
activeCtx.last_outcome = "aborted";
|
|
461
|
-
activeCtx.last_completed_step = "abort";
|
|
462
|
-
activeCtx.next_recommended_command = activeCtx.task_summary
|
|
463
|
-
? `/harness-plan "${activeCtx.task_summary}"`
|
|
464
|
-
: '/harness-plan "<task>"';
|
|
465
|
-
persistContext(pi, activeCtx);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (!harnessTurn) {
|
|
470
|
-
return undefined;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (!activeCtx) {
|
|
474
|
-
activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const policyPhase =
|
|
478
|
-
inferHarnessPhase(entries, userPrompt) ??
|
|
479
|
-
getLatestPolicyPhase(entries) ??
|
|
480
|
-
activeCtx?.phase ??
|
|
481
|
-
"plan";
|
|
482
|
-
const driftActive = driftGateActive(entries);
|
|
483
|
-
|
|
484
|
-
// Plain-language follow-up after needs_clarification
|
|
485
|
-
if (!parsed && needsClarificationFollowUp(activeCtx) && activeCtx) {
|
|
486
|
-
activeCtx.phase = "plan";
|
|
487
|
-
activeCtx.last_outcome = "needs_clarification";
|
|
488
|
-
const packet = activeCtx.plan_packet_path
|
|
489
|
-
? await readPlanPacketFromPath(activeCtx.plan_packet_path)
|
|
490
|
-
: null;
|
|
491
|
-
const planPath = activeCtx.plan_packet_path;
|
|
492
|
-
const summary =
|
|
493
|
-
packet && planPath
|
|
494
|
-
? planPacketSummary(packet, planPath, "needs_clarification")
|
|
495
|
-
: null;
|
|
496
|
-
syncPolicyFromPlan(
|
|
497
|
-
pi,
|
|
498
|
-
entries,
|
|
499
|
-
activeCtx.plan_id ?? "plan-pending",
|
|
500
|
-
"plan",
|
|
501
|
-
false,
|
|
502
|
-
);
|
|
503
|
-
persistContext(pi, activeCtx);
|
|
504
|
-
return {
|
|
505
|
-
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.`,
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (!parsed) return undefined;
|
|
510
|
-
|
|
511
|
-
const { command, args } = parsed;
|
|
512
|
-
|
|
513
|
-
if (
|
|
514
|
-
!isHarnessBootstrapPrompt(userPrompt) &&
|
|
515
|
-
!hasHarnessAbortSignal(userPrompt)
|
|
516
|
-
) {
|
|
517
|
-
const policyBlock = getPolicyTransitionBlock(userPrompt, entries);
|
|
518
|
-
if (policyBlock.blocked) {
|
|
519
|
-
return {
|
|
520
|
-
message: {
|
|
521
|
-
customType: "harness-run-context-block",
|
|
522
|
-
display: true,
|
|
523
|
-
content:
|
|
524
|
-
policyBlock.message ?? "Harness command blocked by policy phase.",
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
}
|
|
479
|
+
function contextPrompt(systemPrompt: string, activeCtx: HarnessRunContext) {
|
|
480
|
+
return {
|
|
481
|
+
systemPrompt: `${systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}`,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
529
484
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
+
}
|
|
544
514
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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({
|
|
584
556
|
phase: activeCtx.phase,
|
|
585
557
|
planStatus: activeCtx.plan_ready ? "ready" : null,
|
|
586
558
|
lastCompletedStep: activeCtx.last_completed_step,
|
|
@@ -590,470 +562,462 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
590
562
|
adversaryComplete: statuses.adversaryComplete,
|
|
591
563
|
aborted: activeCtx.status === "aborted",
|
|
592
564
|
});
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}`,
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (command === "harness-run-status") {
|
|
603
|
-
return undefined;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (
|
|
607
|
-
command === "harness-plan" &&
|
|
608
|
-
activeCtx &&
|
|
609
|
-
isNewTaskPlanBlocked(activeCtx, userPrompt) &&
|
|
610
|
-
!isAmendPlanAllowed(activeCtx, userPrompt, driftActive)
|
|
611
|
-
) {
|
|
612
|
-
return {
|
|
613
|
-
message: {
|
|
614
|
-
customType: "harness-run-context-block",
|
|
615
|
-
display: true,
|
|
616
|
-
content:
|
|
617
|
-
"Active harness run in progress. Use /harness-abort or /harness-new-run before starting a new task plan.",
|
|
618
|
-
},
|
|
619
|
-
};
|
|
620
|
-
}
|
|
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
|
+
}
|
|
621
570
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
571
|
+
type ActiveContextAccess = {
|
|
572
|
+
get(): HarnessRunContext | null;
|
|
573
|
+
set(ctx: HarnessRunContext | null): void;
|
|
574
|
+
};
|
|
627
575
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
)
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
if (turn) {
|
|
648
|
-
pi.appendEntry("harness-plan-attempt", {
|
|
649
|
-
run_id: activeCtx.run_id,
|
|
650
|
-
command,
|
|
651
|
-
started_at: turn.invoked_at,
|
|
652
|
-
});
|
|
653
|
-
} else {
|
|
654
|
-
pi.appendEntry("harness-plan-attempt", {
|
|
655
|
-
run_id: activeCtx.run_id,
|
|
656
|
-
command,
|
|
657
|
-
started_at: nowIso(),
|
|
658
|
-
});
|
|
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;
|
|
659
594
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (pointer) {
|
|
670
|
-
if (isStaleActiveRunPointer(pointer, projectRoot)) {
|
|
671
|
-
const crossSessionCmd = new Set([
|
|
672
|
-
"harness-eval",
|
|
673
|
-
"harness-review",
|
|
674
|
-
"harness-steer",
|
|
675
|
-
"harness-critic",
|
|
676
|
-
"harness-trace",
|
|
677
|
-
"harness-incident",
|
|
678
|
-
]);
|
|
679
|
-
if (crossSessionCmd.has(command)) {
|
|
680
|
-
return {
|
|
681
|
-
message: {
|
|
682
|
-
customType: "harness-run-context-block",
|
|
683
|
-
display: true,
|
|
684
|
-
content:
|
|
685
|
-
'Project active-run pointer is stale or from another workspace. Run /harness-plan "<task>" or /harness-use-run <run-id> for recovery.',
|
|
686
|
-
},
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
} else {
|
|
690
|
-
const disk = await loadRunContextFromDisk(
|
|
691
|
-
pointer.run_id,
|
|
692
|
-
projectRoot,
|
|
693
|
-
);
|
|
694
|
-
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;
|
|
695
604
|
}
|
|
696
605
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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,
|
|
703
624
|
display: true,
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
activeCtx.phase = policyPhase;
|
|
711
|
-
activeCtx.updated_at = new Date().toISOString();
|
|
712
|
-
activeCtx.pi_session_id = sessionId;
|
|
713
|
-
|
|
714
|
-
if (
|
|
715
|
-
shouldAutoClaimHarnessRun(command, args) &&
|
|
716
|
-
activeCtx.owner_pi_session_id !== sessionId
|
|
717
|
-
) {
|
|
718
|
-
activeCtx = claimRunOwnership(activeCtx, sessionId);
|
|
719
|
-
}
|
|
625
|
+
});
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}
|
|
720
629
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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);
|
|
644
|
+
}
|
|
645
|
+
const next = createFreshRunContext(
|
|
646
|
+
sessionId,
|
|
725
647
|
projectRoot,
|
|
648
|
+
args.trim() || null,
|
|
726
649
|
);
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
};
|
|
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",
|
|
656
|
+
);
|
|
735
657
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if (command === "harness-run" && !activeCtx.plan_ready) {
|
|
740
|
-
return {
|
|
741
|
-
message: {
|
|
742
|
-
customType: "harness-run-context-block",
|
|
743
|
-
display: true,
|
|
744
|
-
content: "Plan not ready. Run /harness-plan first.",
|
|
745
|
-
},
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
command === "harness-run" &&
|
|
751
|
-
activeCtx.plan_ready &&
|
|
752
|
-
activeCtx.last_completed_step === "execute" &&
|
|
753
|
-
activeCtx.last_outcome === "completed"
|
|
754
|
-
) {
|
|
755
|
-
return {
|
|
756
|
-
message: {
|
|
757
|
-
customType: "harness-run-context-block",
|
|
758
|
-
display: true,
|
|
759
|
-
content:
|
|
760
|
-
"Execute already completed for this run. Next: /harness-review (same session), or /harness-abort to replan.",
|
|
761
|
-
},
|
|
762
|
-
};
|
|
763
|
-
}
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
764
661
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
)
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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,
|
|
777
678
|
);
|
|
778
|
-
activeCtx.plan_id = planPacketForSpawn.plan_id ?? activeCtx.plan_id;
|
|
779
679
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
contextSpawnOpts = {
|
|
789
|
-
mode: "execute",
|
|
790
|
-
critical_path_work_item_ids: criticalIds,
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
let activePlanBlock = "";
|
|
795
|
-
let planMode: "create" | "revise" | null = null;
|
|
796
|
-
if (command === "harness-plan" || command === "harness-auto") {
|
|
797
|
-
planMode =
|
|
798
|
-
activeCtx.plan_id ||
|
|
799
|
-
activeCtx.plan_packet_path ||
|
|
800
|
-
activeCtx.status === "aborted"
|
|
801
|
-
? "revise"
|
|
802
|
-
: "create";
|
|
803
|
-
activePlanBlock = formatActivePlanBlock(activeCtx, planMode, planSummary);
|
|
804
|
-
} else if (command === "harness-run") {
|
|
805
|
-
activePlanBlock = formatActivePlanBlock(
|
|
806
|
-
activeCtx,
|
|
807
|
-
"execute",
|
|
808
|
-
planSummary,
|
|
809
|
-
);
|
|
810
|
-
} else if (command === "harness-steer") {
|
|
811
|
-
activePlanBlock = formatActivePlanBlock(
|
|
812
|
-
activeCtx,
|
|
813
|
-
"execute",
|
|
814
|
-
planSummary,
|
|
815
|
-
);
|
|
816
|
-
contextSpawnOpts = {
|
|
817
|
-
mode: "repair",
|
|
818
|
-
repair_brief_path: "artifacts/repair-brief.yaml",
|
|
819
|
-
};
|
|
820
|
-
} else if (
|
|
821
|
-
command === "harness-eval" ||
|
|
822
|
-
command === "harness-review" ||
|
|
823
|
-
command === "harness-critic"
|
|
824
|
-
) {
|
|
825
|
-
activePlanBlock = formatActivePlanBlock(activeCtx, "read", planSummary);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
if (command === "harness-plan" || command === "harness-auto") {
|
|
829
|
-
const reviewOutcome = await readReviewOutcomeFromRun(
|
|
830
|
-
activeCtx.run_id,
|
|
831
|
-
projectRoot,
|
|
832
|
-
);
|
|
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
|
+
}
|
|
833
688
|
if (
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
runCtx: activeCtx,
|
|
838
|
-
reviewOutcome,
|
|
839
|
-
userPrompt,
|
|
689
|
+
!hasPlanUserApproval(entries, {
|
|
690
|
+
sincePlanCommand: true,
|
|
691
|
+
planId: runCtx.plan_id,
|
|
840
692
|
})
|
|
841
693
|
) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
pi.appendEntry("harness-plan-revision-reset", {
|
|
849
|
-
run_id: activeCtx.run_id,
|
|
850
|
-
archive_dir: reset.archiveDir,
|
|
851
|
-
moved: reset.moved,
|
|
852
|
-
reason: "review_plan_gap_revise",
|
|
853
|
-
recorded_at: nowIso(),
|
|
854
|
-
});
|
|
855
|
-
}
|
|
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;
|
|
856
700
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return {
|
|
862
|
-
systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx, contextSpawnOpts)}${activePlanBlock ? `\n\n${activePlanBlock}` : ""}`,
|
|
863
|
-
};
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
pi.on("agent_end", async (_event, ctx) => {
|
|
867
|
-
const projectRoot = process.cwd();
|
|
868
|
-
const entries = getEntries(ctx);
|
|
869
|
-
if (!activeCtx) {
|
|
870
|
-
activeCtx = getLatestRunContext(entries);
|
|
871
|
-
}
|
|
872
|
-
if (!activeCtx) return;
|
|
873
|
-
|
|
874
|
-
const userEntries = entries.filter((e) => {
|
|
875
|
-
const entry = e as { type?: string; message?: { role?: string } };
|
|
876
|
-
return entry.type === "message" && entry.message?.role === "user";
|
|
877
|
-
});
|
|
878
|
-
const lastUser = userEntries[userEntries.length - 1] as
|
|
879
|
-
| { message?: { content?: string | unknown[] } }
|
|
880
|
-
| undefined;
|
|
881
|
-
let lastPrompt = "";
|
|
882
|
-
if (lastUser?.message?.content) {
|
|
883
|
-
lastPrompt =
|
|
884
|
-
typeof lastUser.message.content === "string"
|
|
885
|
-
? lastUser.message.content
|
|
886
|
-
: "";
|
|
887
|
-
}
|
|
888
|
-
const lastTurn = getLatestHarnessTurn(entries);
|
|
889
|
-
const parsed = lastTurn
|
|
890
|
-
? { command: lastTurn.command, args: lastTurn.args }
|
|
891
|
-
: parseHarnessSlashInput(userVisiblePromptSlice(lastPrompt));
|
|
892
|
-
if (!parsed && !needsClarificationFollowUp(activeCtx)) return;
|
|
893
|
-
|
|
894
|
-
if (parsed?.command === "harness-abort") {
|
|
895
|
-
activeCtx.status = "aborted";
|
|
896
|
-
activeCtx.plan_ready = false;
|
|
897
|
-
activeCtx.last_outcome = "aborted";
|
|
898
|
-
activeCtx.last_completed_step = "abort";
|
|
899
|
-
activeCtx.next_recommended_command = activeCtx.task_summary
|
|
900
|
-
? `/harness-plan "${activeCtx.task_summary}"`
|
|
901
|
-
: '/harness-plan "<task>"';
|
|
902
|
-
persistContext(pi, activeCtx);
|
|
903
|
-
const msg = `Harness aborted. Next: ${activeCtx.next_recommended_command}`;
|
|
904
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
905
|
-
else
|
|
906
|
-
pi.sendMessage({
|
|
907
|
-
customType: "harness-step-handoff",
|
|
908
|
-
content: msg,
|
|
909
|
-
display: true,
|
|
910
|
-
});
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
let planReady = activeCtx.plan_ready;
|
|
915
|
-
if (
|
|
916
|
-
(parsed?.command === "harness-plan" ||
|
|
917
|
-
parsed?.command === "harness-auto") &&
|
|
918
|
-
activeCtx.plan_packet_path
|
|
919
|
-
) {
|
|
920
|
-
const packet = await readPlanPacketFromPath(activeCtx.plan_packet_path);
|
|
701
|
+
const pathArg = args.trim();
|
|
702
|
+
const packetPath = pathArg || runCtx.plan_packet_path;
|
|
703
|
+
const packet = await readPlanPacketFromPath(packetPath);
|
|
921
704
|
const validation = validatePlanPacket(packet);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
activeCtx.last_outcome = "needs_clarification";
|
|
929
|
-
activeCtx.last_completed_step = "plan";
|
|
930
|
-
const msg =
|
|
931
|
-
"Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.yaml.";
|
|
932
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
933
|
-
else
|
|
934
|
-
pi.sendMessage({
|
|
935
|
-
customType: "harness-plan-packet",
|
|
936
|
-
content: msg,
|
|
937
|
-
display: true,
|
|
938
|
-
});
|
|
939
|
-
} else if (planReady && packet?.plan_id) {
|
|
940
|
-
activeCtx.plan_id = packet.plan_id;
|
|
941
|
-
syncPolicyFromPlan(pi, entries, packet.plan_id, "plan", true);
|
|
942
|
-
const summary = planPacketSummary(packet, activeCtx.plan_packet_path);
|
|
943
|
-
pi.appendEntry("harness-plan-packet", summary);
|
|
944
|
-
activeCtx.last_completed_step = "plan";
|
|
945
|
-
activeCtx.last_outcome = summary.plan_status;
|
|
946
|
-
} else if (!validation.valid) {
|
|
947
|
-
activeCtx.last_outcome = "needs_clarification";
|
|
948
|
-
activeCtx.last_completed_step = "plan";
|
|
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;
|
|
949
711
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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,
|
|
736
|
+
);
|
|
737
|
+
pi.appendEntry(
|
|
738
|
+
"harness-plan-packet",
|
|
739
|
+
planPacketSummary(packet, target, "ready"),
|
|
740
|
+
);
|
|
741
|
+
if (ctx.hasUI) ctx.ui.notify(`Plan committed: ${target}`, "info");
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
}
|
|
953
745
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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;
|
|
968
762
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
activeCtx.steer_max_attempts =
|
|
976
|
-
activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv();
|
|
977
|
-
activeCtx.phase = "execute";
|
|
978
|
-
syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
|
|
979
|
-
}
|
|
980
|
-
if (
|
|
981
|
-
parsed?.command === "harness-eval" ||
|
|
982
|
-
parsed?.command === "harness-review" ||
|
|
983
|
-
parsed?.command === "harness-critic"
|
|
984
|
-
) {
|
|
985
|
-
activeCtx.last_completed_step =
|
|
986
|
-
parsed.command === "harness-critic" ? "adversary" : "review";
|
|
987
|
-
if (statuses.evalStatus) {
|
|
988
|
-
activeCtx.last_outcome = statuses.evalStatus;
|
|
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;
|
|
989
769
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
activeCtx.
|
|
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);
|
|
793
|
+
syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
|
|
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
|
+
);
|
|
995
800
|
}
|
|
996
|
-
}
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
}
|
|
997
804
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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",
|
|
1001
819
|
);
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
phase: activeCtx.phase,
|
|
1007
|
-
planStatus: statuses.planStatus,
|
|
1008
|
-
lastCompletedStep: activeCtx.last_completed_step,
|
|
1009
|
-
lastOutcome: activeCtx.last_outcome,
|
|
1010
|
-
executionStatus: statuses.executionStatus,
|
|
1011
|
-
evalStatus: statuses.evalStatus,
|
|
1012
|
-
adversaryComplete: statuses.adversaryComplete,
|
|
1013
|
-
aborted: activeCtx.status === "aborted",
|
|
1014
|
-
remediationClass: reviewOutcome?.remediation_class ?? null,
|
|
1015
|
-
steerAttempt: activeCtx.steer_attempt ?? 0,
|
|
1016
|
-
steerMaxAttempts:
|
|
1017
|
-
activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv(),
|
|
1018
|
-
reviewComplete,
|
|
1019
|
-
});
|
|
1020
|
-
activeCtx.next_recommended_command = next;
|
|
1021
|
-
activeCtx.updated_at = new Date().toISOString();
|
|
820
|
+
activeCtx.plan_id = planPacketForSpawn.plan_id ?? activeCtx.plan_id;
|
|
821
|
+
}
|
|
822
|
+
return { planSummary, planPacketForSpawn };
|
|
823
|
+
}
|
|
1022
824
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
+
}
|
|
1029
926
|
|
|
1030
|
-
|
|
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
|
+
}
|
|
1031
944
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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,
|
|
1038
965
|
});
|
|
966
|
+
}
|
|
1039
967
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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,
|
|
1050
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
|
+
}
|
|
1051
1012
|
|
|
1013
|
+
function registerPlanApprovalCapture(
|
|
1014
|
+
pi: ExtensionAPI,
|
|
1015
|
+
active: ActiveContextAccess,
|
|
1016
|
+
): void {
|
|
1052
1017
|
pi.on("tool_result", async (event, ctx) => {
|
|
1053
1018
|
if (event.isError) return;
|
|
1054
|
-
if (event.toolName !== "ask_user" && event.toolName !== "approve_plan")
|
|
1019
|
+
if (event.toolName !== "ask_user" && event.toolName !== "approve_plan")
|
|
1055
1020
|
return;
|
|
1056
|
-
}
|
|
1057
1021
|
const approval = parsePlanApprovalFromMessage({
|
|
1058
1022
|
toolName: event.toolName,
|
|
1059
1023
|
details: event.details,
|
|
@@ -1061,7 +1025,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
1061
1025
|
});
|
|
1062
1026
|
if (!approval) return;
|
|
1063
1027
|
const entries = getEntries(ctx);
|
|
1064
|
-
const runCtx = getLatestRunContext(entries) ??
|
|
1028
|
+
const runCtx = getLatestRunContext(entries) ?? active.get();
|
|
1065
1029
|
if (!runCtx) return;
|
|
1066
1030
|
pi.appendEntry("harness-plan-approval", {
|
|
1067
1031
|
plan_id: approval.plan_id ?? runCtx.plan_id,
|
|
@@ -1069,226 +1033,514 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
1069
1033
|
source: approval.source,
|
|
1070
1034
|
});
|
|
1071
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
|
+
}
|
|
1072
1381
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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,
|
|
1079
1417
|
);
|
|
1080
|
-
|
|
1081
|
-
return { block: true, reason: decision.reason };
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
if (event.toolName === "write") {
|
|
1085
|
-
const entries = getEntries(ctx);
|
|
1086
|
-
const runCtx = getLatestRunContext(entries) ?? activeCtx;
|
|
1087
|
-
if (runCtx) {
|
|
1088
|
-
const blocked = await coerceScopedHarnessYamlWrite(
|
|
1089
|
-
event,
|
|
1090
|
-
runCtx,
|
|
1091
|
-
process.cwd(),
|
|
1092
|
-
);
|
|
1093
|
-
if (blocked) return blocked;
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
if (activeCtx?.plan_packet_path) {
|
|
1097
|
-
const entries = getEntries(ctx);
|
|
1098
|
-
if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
|
|
1099
|
-
if (event.toolName === "approve_plan") {
|
|
1100
|
-
return {
|
|
1101
|
-
block: true,
|
|
1102
|
-
reason:
|
|
1103
|
-
"harness-run-context: plan already approved via planner subagent; do not call approve_plan again in the parent session.",
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
if (event.toolName === "ask_user") {
|
|
1107
|
-
const input = event.input as {
|
|
1108
|
-
question?: string;
|
|
1109
|
-
options?: unknown[];
|
|
1110
|
-
};
|
|
1111
|
-
if (isPlanApprovalAskUser(input)) {
|
|
1112
|
-
return {
|
|
1113
|
-
block: true,
|
|
1114
|
-
reason:
|
|
1115
|
-
"harness-run-context: plan already approved via planner subagent; do not call ask_user for plan approval in the parent session.",
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
if (!activeCtx?.plan_packet_path) return undefined;
|
|
1122
|
-
const phase = activeCtx.phase;
|
|
1123
|
-
if (phase !== "evaluate" && phase !== "adversary") return undefined;
|
|
1124
|
-
if (event.toolName !== "write" && event.toolName !== "edit") {
|
|
1125
|
-
return undefined;
|
|
1126
|
-
}
|
|
1127
|
-
const target = String(
|
|
1128
|
-
(event.input as { path?: string; filePath?: string }).path ??
|
|
1129
|
-
(event.input as { filePath?: string }).filePath ??
|
|
1130
|
-
"",
|
|
1131
|
-
);
|
|
1132
|
-
if (target.includes("plan-packet.yaml")) {
|
|
1133
|
-
return {
|
|
1134
|
-
block: true,
|
|
1135
|
-
reason:
|
|
1136
|
-
"harness-run-context: plan-packet.yaml is read-only in evaluate/adversary phases.",
|
|
1137
|
-
};
|
|
1418
|
+
execStatus = handoff?.execution_status ?? null;
|
|
1138
1419
|
}
|
|
1139
|
-
|
|
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,
|
|
1140
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
|
+
}
|
|
1141
1492
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
let ctxState = getLatestRunContext(entries) ?? activeCtx;
|
|
1150
|
-
if (!ctxState) {
|
|
1151
|
-
ctxState = await hydrateFromDisk(sessionId, projectRoot, entries);
|
|
1152
|
-
}
|
|
1153
|
-
if (!ctxState) {
|
|
1154
|
-
const msg = 'No active harness run. Start with /harness-plan "<task>".';
|
|
1155
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
let summary: PlanPacketSummary | null = null;
|
|
1159
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1160
|
-
const entry = entries[i] as SessionEntryLike;
|
|
1161
|
-
if (
|
|
1162
|
-
entry.type !== "custom" ||
|
|
1163
|
-
entry.customType !== "harness-plan-packet"
|
|
1164
|
-
)
|
|
1165
|
-
continue;
|
|
1166
|
-
summary = entry.data as PlanPacketSummary;
|
|
1167
|
-
break;
|
|
1168
|
-
}
|
|
1169
|
-
const lines = [
|
|
1170
|
-
"Harness run status:",
|
|
1171
|
-
` phase: ${ctxState.phase}`,
|
|
1172
|
-
` status: ${ctxState.status}`,
|
|
1173
|
-
` plan_ready: ${ctxState.plan_ready}`,
|
|
1174
|
-
` plan_id: ${ctxState.plan_id ?? "(none)"}`,
|
|
1175
|
-
summary
|
|
1176
|
-
? ` scope: ${summary.scope_one_liner}`
|
|
1177
|
-
: " scope: (no plan summary yet)",
|
|
1178
|
-
` last_step: ${ctxState.last_completed_step ?? "(none)"}`,
|
|
1179
|
-
` last_outcome: ${ctxState.last_outcome ?? "(none)"}`,
|
|
1180
|
-
` next: ${ctxState.next_recommended_command ?? "/harness-run-status"}`,
|
|
1181
|
-
];
|
|
1182
|
-
const text = lines.join("\n");
|
|
1183
|
-
if (ctx.hasUI) ctx.ui.notify(text, "info");
|
|
1184
|
-
else
|
|
1185
|
-
pi.sendMessage({
|
|
1186
|
-
customType: "harness-run-status",
|
|
1187
|
-
content: text,
|
|
1188
|
-
display: true,
|
|
1189
|
-
});
|
|
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;
|
|
1190
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);
|
|
1191
1509
|
});
|
|
1192
1510
|
|
|
1193
|
-
pi.
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const msg =
|
|
1210
|
-
'New harness run allocated. Next: /harness-plan "<your task>"';
|
|
1211
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "info");
|
|
1212
|
-
},
|
|
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 };
|
|
1213
1527
|
});
|
|
1214
1528
|
|
|
1215
|
-
pi.
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
let runCtx = getLatestRunContext(entries) ?? activeCtx;
|
|
1222
|
-
if (!runCtx) {
|
|
1223
|
-
runCtx = await hydrateFromDisk(
|
|
1224
|
-
ctx.sessionManager.getSessionId(),
|
|
1225
|
-
projectRoot,
|
|
1226
|
-
entries,
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
if (!runCtx?.plan_packet_path) {
|
|
1230
|
-
const msg = "No active harness run. Run /harness-plan first.";
|
|
1231
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
if (
|
|
1235
|
-
!hasPlanUserApproval(entries, {
|
|
1236
|
-
sincePlanCommand: true,
|
|
1237
|
-
planId: runCtx.plan_id,
|
|
1238
|
-
})
|
|
1239
|
-
) {
|
|
1240
|
-
const msg =
|
|
1241
|
-
"Plan commit blocked: no user approval recorded. Approve via approve_plan in this session first.";
|
|
1242
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
const pathArg = args.trim();
|
|
1246
|
-
let packetPath = runCtx.plan_packet_path;
|
|
1247
|
-
if (pathArg) {
|
|
1248
|
-
packetPath = pathArg;
|
|
1249
|
-
}
|
|
1250
|
-
const packet = await readPlanPacketFromPath(packetPath);
|
|
1251
|
-
const validation = validatePlanPacket(packet);
|
|
1252
|
-
if (!validation.valid || !packet) {
|
|
1253
|
-
const msg = !packet
|
|
1254
|
-
? "Plan packet file missing or unreadable."
|
|
1255
|
-
: `Invalid plan packet: ${validation.errors.join("; ")}`;
|
|
1256
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "error");
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
const target = runCtx.plan_packet_path;
|
|
1260
|
-
if (!target) {
|
|
1261
|
-
if (ctx.hasUI)
|
|
1262
|
-
ctx.ui.notify("No plan_packet_path on active run.", "error");
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
if (pathArg && pathArg !== target) {
|
|
1266
|
-
const raw = await readFile(pathArg, "utf-8");
|
|
1267
|
-
await writeFile(target, raw, "utf-8");
|
|
1268
|
-
}
|
|
1269
|
-
runCtx.plan_id = packet.plan_id ?? runCtx.plan_id;
|
|
1270
|
-
runCtx.plan_ready = true;
|
|
1271
|
-
runCtx.phase = "plan";
|
|
1272
|
-
runCtx.last_completed_step = "plan";
|
|
1273
|
-
runCtx.last_outcome = "ready";
|
|
1274
|
-
runCtx.next_recommended_command = "/harness-run";
|
|
1275
|
-
runCtx.updated_at = nowIso();
|
|
1276
|
-
activeCtx = runCtx;
|
|
1277
|
-
persistContext(pi, runCtx);
|
|
1278
|
-
syncPolicyFromPlan(
|
|
1279
|
-
pi,
|
|
1280
|
-
entries,
|
|
1281
|
-
runCtx.plan_id ?? packet.plan_id ?? "plan-pending",
|
|
1282
|
-
"plan",
|
|
1283
|
-
true,
|
|
1284
|
-
);
|
|
1285
|
-
const summary = planPacketSummary(packet, target, "ready");
|
|
1286
|
-
pi.appendEntry("harness-plan-packet", summary);
|
|
1287
|
-
const msg = `Plan committed: ${target}`;
|
|
1288
|
-
if (ctx.hasUI) ctx.ui.notify(msg, "info");
|
|
1289
|
-
},
|
|
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 });
|
|
1290
1535
|
});
|
|
1291
1536
|
|
|
1537
|
+
registerPlanApprovalCapture(pi, activeAccess);
|
|
1538
|
+
registerHarnessToolCallGuards(pi, activeAccess);
|
|
1539
|
+
registerHarnessRunStatusCommand(pi, activeAccess);
|
|
1540
|
+
registerHarnessNewRunCommand(pi, activeAccess);
|
|
1541
|
+
|
|
1542
|
+
registerHarnessPlanCommitCommand(pi, activeAccess);
|
|
1543
|
+
|
|
1292
1544
|
pi.registerTool({
|
|
1293
1545
|
name: "write_harness_yaml",
|
|
1294
1546
|
label: "Write Harness YAML",
|
|
@@ -1702,7 +1954,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
1702
1954
|
);
|
|
1703
1955
|
const specsDir = join(projectRoot, ".pi", "harness", "specs");
|
|
1704
1956
|
const { validateHarnessArtifactPaths } = await import(
|
|
1705
|
-
"
|
|
1957
|
+
"../lib/harness-artifact-gate.js"
|
|
1706
1958
|
);
|
|
1707
1959
|
const gate = await validateHarnessArtifactPaths(runRoot, paths, specsDir);
|
|
1708
1960
|
const text = gate.ok
|
|
@@ -1729,63 +1981,5 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
1729
1981
|
},
|
|
1730
1982
|
});
|
|
1731
1983
|
|
|
1732
|
-
pi
|
|
1733
|
-
description:
|
|
1734
|
-
"Point this session at an existing run directory (recovery; --claim for write ownership)",
|
|
1735
|
-
handler: async (args, ctx) => {
|
|
1736
|
-
const parsed = parseHarnessUseRunArgs(args);
|
|
1737
|
-
if (!parsed.runId) {
|
|
1738
|
-
if (ctx.hasUI)
|
|
1739
|
-
ctx.ui.notify(
|
|
1740
|
-
"Usage: /harness-use-run <run-id> [--claim] [--readonly]",
|
|
1741
|
-
"warning",
|
|
1742
|
-
);
|
|
1743
|
-
return;
|
|
1744
|
-
}
|
|
1745
|
-
const projectRoot = process.cwd();
|
|
1746
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
1747
|
-
const disk = await loadRunContextFromDisk(parsed.runId, projectRoot);
|
|
1748
|
-
if (!disk) {
|
|
1749
|
-
if (ctx.hasUI) ctx.ui.notify(`Run not found: ${parsed.runId}`, "error");
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1752
|
-
activeCtx = {
|
|
1753
|
-
...disk,
|
|
1754
|
-
pi_session_id: sessionId,
|
|
1755
|
-
};
|
|
1756
|
-
if (parsed.claim) {
|
|
1757
|
-
activeCtx = claimRunOwnership(activeCtx, sessionId);
|
|
1758
|
-
}
|
|
1759
|
-
const statuses = await resolveCompletionStatuses(
|
|
1760
|
-
getEntries(ctx),
|
|
1761
|
-
activeCtx.run_id,
|
|
1762
|
-
projectRoot,
|
|
1763
|
-
);
|
|
1764
|
-
if (activeCtx.owner_pi_session_id !== sessionId && !parsed.claim) {
|
|
1765
|
-
activeCtx.next_recommended_command =
|
|
1766
|
-
"Read-only: use /harness-use-run <run-id> --claim to take ownership.";
|
|
1767
|
-
} else {
|
|
1768
|
-
activeCtx.next_recommended_command = nextStepAfterOutcome({
|
|
1769
|
-
phase: activeCtx.phase,
|
|
1770
|
-
planStatus: activeCtx.plan_ready ? "ready" : null,
|
|
1771
|
-
lastCompletedStep: activeCtx.last_completed_step,
|
|
1772
|
-
lastOutcome: activeCtx.last_outcome,
|
|
1773
|
-
executionStatus: statuses.executionStatus,
|
|
1774
|
-
evalStatus: statuses.evalStatus,
|
|
1775
|
-
adversaryComplete: statuses.adversaryComplete,
|
|
1776
|
-
aborted: activeCtx.status === "aborted",
|
|
1777
|
-
});
|
|
1778
|
-
}
|
|
1779
|
-
activeCtx.updated_at = nowIso();
|
|
1780
|
-
persistContext(pi, activeCtx);
|
|
1781
|
-
syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
|
|
1782
|
-
if (ctx.hasUI) {
|
|
1783
|
-
const mode = parsed.claim ? "claimed" : "bound (read-only)";
|
|
1784
|
-
ctx.ui.notify(
|
|
1785
|
-
`Session ${mode} to run ${parsed.runId}. See /harness-run-status.`,
|
|
1786
|
-
"info",
|
|
1787
|
-
);
|
|
1788
|
-
}
|
|
1789
|
-
},
|
|
1790
|
-
});
|
|
1984
|
+
registerHarnessUseRunCommand(pi, activeAccess);
|
|
1791
1985
|
}
|