ultimate-pi 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/harness-debate-plan/SKILL.md +1 -1
- package/.agents/skills/harness-decisions/SKILL.md +1 -2
- package/.agents/skills/harness-governor/SKILL.md +6 -5
- package/.pi/PACKAGING.md +4 -4
- package/.pi/SYSTEM.md +54 -120
- package/.pi/agents/harness/incident-recorder.md +0 -1
- package/.pi/agents/harness/planning/decompose.md +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 +0 -2
- package/.pi/agents/harness/planning/plan-adversary.md +0 -2
- package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
- package/.pi/agents/harness/planning/planning-context.md +0 -2
- package/.pi/agents/harness/planning/review-integrator.md +0 -2
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
- package/.pi/agents/harness/planning/stack-researcher.md +0 -2
- package/.pi/agents/harness/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/extensions/00-posthog-network-bootstrap.ts +1 -1
- package/.pi/extensions/agt-kill-switch.ts +57 -0
- package/.pi/extensions/agt-prompt-guard.ts +32 -0
- package/.pi/extensions/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-tools.ts +2 -2
- 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 +22 -25
- package/.pi/harness/agents.policy.yaml +275 -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/README.md +5 -0
- package/.pi/harness/evolution/README.md +1 -2
- package/.pi/harness/examples/agents.policy.project.yaml +19 -0
- package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
- package/.pi/harness/policies/bash-denylists.yaml +5 -0
- package/.pi/harness/policies/defaults.yaml +51 -0
- package/.pi/harness/policies/orchestrator.yaml +18 -0
- package/.pi/harness/policies/phases.yaml +10 -0
- package/.pi/harness/policies/roles.yaml +5 -0
- package/.pi/harness/policies/web-guard.yaml +5 -0
- package/.pi/harness/policies/workflow-sequences.yaml +9 -0
- package/.pi/harness/sentrux/architecture.manifest.json +26 -4
- package/.pi/harness/specs/observation.schema.json +2 -1
- package/.pi/lib/agents-policy.d.mts +70 -0
- package/.pi/lib/agents-policy.mjs +325 -0
- package/.pi/lib/agents-policy.ts +19 -0
- package/.pi/lib/agt/audit-run-sink.ts +52 -0
- package/.pi/lib/agt/build-evaluation-context.ts +285 -0
- package/.pi/lib/agt/config.ts +28 -0
- package/.pi/lib/agt/delegation.ts +69 -0
- package/.pi/lib/agt/evaluate-policy.ts +56 -0
- package/.pi/lib/agt/identity-registry.ts +41 -0
- package/.pi/lib/agt/index.ts +55 -0
- package/.pi/lib/agt/kill-switch-state.ts +11 -0
- package/.pi/lib/agt/legacy-evaluate.ts +101 -0
- package/.pi/lib/agt/policy-engine.ts +154 -0
- package/.pi/lib/agt/rings.ts +21 -0
- package/.pi/lib/agt/sre-hooks.ts +45 -0
- package/.pi/lib/agt/trust-run-store.ts +26 -0
- package/.pi/lib/agt/workflow-history.ts +29 -0
- package/.pi/lib/agt-governance-active.ts +14 -0
- package/.pi/lib/agt-tool-guard.ts +78 -0
- package/.pi/lib/ask-user/dialog.ts +314 -0
- package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
- package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
- package/.pi/{extensions/lib → lib}/extension-load-guard.ts +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 +51 -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 +53 -14
- package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
- 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 +1 -1
- package/.pi/prompts/harness-setup.md +37 -64
- package/.pi/scripts/README.md +2 -5
- 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 +9 -2
- package/.pi/scripts/harness-verify.mjs +113 -39
- package/.pi/scripts/harness-web-policy-guard.mjs +2 -2
- package/.pi/scripts/validate-plan-dag.mjs +65 -74
- package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
- package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
- package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
- package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
- package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
- package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
- package/.pi/skills/architecture/layered/SKILL.md +68 -0
- package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
- package/.pi/skills/architecture/microservices/SKILL.md +64 -0
- package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
- package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
- package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
- package/.pi/skills/architecture/service-based/SKILL.md +64 -0
- package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
- package/.pi/skills/architecture/space-based/SKILL.md +60 -0
- package/.pi/skills/ast-grep/SKILL.md +40 -321
- package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
- package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
- package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
- package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
- package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
- package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
- package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
- package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
- package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
- package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
- package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
- package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
- package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
- package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
- package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
- package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
- package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
- package/.pi/skills/lsp-navigation/SKILL.md +89 -0
- package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
- package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
- package/.pi/skills/quality/security-review/SKILL.md +34 -0
- package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
- package/.pi/skills/quality/testability-design/SKILL.md +33 -0
- package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
- package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
- package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
- package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
- package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
- package/.sentrux/rules.toml +20 -4
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +14 -0
- package/README.md +3 -12
- package/THIRD_PARTY_NOTICES.md +12 -21
- package/package.json +15 -7
- package/vendor/pi-subagents/src/agents.ts +45 -1
- package/vendor/pi-subagents/src/subagents.ts +866 -811
- package/vendor/pi-vcc/src/core/brief.ts +68 -99
- package/vendor/pi-vcc/src/core/settings.ts +2 -2
- package/.agents/skills/caveman/SKILL.md +0 -67
- package/.pi/agents/harness/meta-optimizer.md +0 -36
- package/.pi/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/vendor-sync-pi-model-router.sh +0 -47
- package/vendor/pi-model-router/.prettierignore +0 -4
- package/vendor/pi-model-router/.prettierrc +0 -5
- package/vendor/pi-model-router/AGENTS.md +0 -39
- package/vendor/pi-model-router/LICENSE +0 -21
- package/vendor/pi-model-router/README.md +0 -99
- package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
- package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
- package/vendor/pi-model-router/extensions/commands.ts +0 -720
- package/vendor/pi-model-router/extensions/config.ts +0 -348
- package/vendor/pi-model-router/extensions/constants.ts +0 -1
- package/vendor/pi-model-router/extensions/index.ts +0 -478
- package/vendor/pi-model-router/extensions/provider.ts +0 -580
- package/vendor/pi-model-router/extensions/routing.ts +0 -564
- package/vendor/pi-model-router/extensions/state.ts +0 -52
- package/vendor/pi-model-router/extensions/types.ts +0 -95
- package/vendor/pi-model-router/extensions/ui.ts +0 -144
- package/vendor/pi-model-router/model-router.example.json +0 -48
- package/vendor/pi-model-router/package.json +0 -48
- package/vendor/pi-model-router/tsconfig.json +0 -16
- /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
ConflictResolutionStrategy,
|
|
5
|
+
PolicyEngine,
|
|
6
|
+
} from "@microsoft/agent-governance-sdk";
|
|
7
|
+
import {
|
|
8
|
+
resolveHarnessPoliciesDir,
|
|
9
|
+
resolveProjectPoliciesDir,
|
|
10
|
+
} from "./config.js";
|
|
11
|
+
|
|
12
|
+
const PACKAGE_POLICY_FILES = [
|
|
13
|
+
"defaults.yaml",
|
|
14
|
+
"phases.yaml",
|
|
15
|
+
"roles.yaml",
|
|
16
|
+
"orchestrator.yaml",
|
|
17
|
+
"bash-denylists.yaml",
|
|
18
|
+
"web-guard.yaml",
|
|
19
|
+
"workflow-sequences.yaml",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
let cachedEngine: PolicyEngine | null = null;
|
|
23
|
+
let cachedKey: string | null = null;
|
|
24
|
+
|
|
25
|
+
export class HarnessPolicyLoadError extends Error {
|
|
26
|
+
constructor(message: string) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "HarnessPolicyLoadError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadYamlFile(engine: PolicyEngine, path: string): void {
|
|
33
|
+
let raw: string;
|
|
34
|
+
try {
|
|
35
|
+
raw = readFileSync(path, "utf-8");
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new HarnessPolicyLoadError(
|
|
38
|
+
`Missing or unreadable policy file: ${path} (${String(err)})`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
engine.loadYaml(raw);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadProjectPolicyDir(
|
|
45
|
+
engine: PolicyEngine,
|
|
46
|
+
projectRoot: string,
|
|
47
|
+
): string[] {
|
|
48
|
+
const dir = resolveProjectPoliciesDir(projectRoot);
|
|
49
|
+
const loaded: string[] = [];
|
|
50
|
+
if (!existsSync(dir)) return loaded;
|
|
51
|
+
const names = readdirSync(dir)
|
|
52
|
+
.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"))
|
|
53
|
+
.sort();
|
|
54
|
+
for (const name of names) {
|
|
55
|
+
loadYamlFile(engine, join(dir, name));
|
|
56
|
+
loaded.push(name);
|
|
57
|
+
}
|
|
58
|
+
return loaded;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CreateAgtPolicyEngineInput {
|
|
62
|
+
packageRoot: string;
|
|
63
|
+
projectRoot: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createAgtPolicyEngine(
|
|
67
|
+
input: CreateAgtPolicyEngineInput,
|
|
68
|
+
): PolicyEngine {
|
|
69
|
+
const engine = new PolicyEngine([], ConflictResolutionStrategy.DenyOverrides);
|
|
70
|
+
const dir = resolveHarnessPoliciesDir(input.packageRoot);
|
|
71
|
+
for (const file of PACKAGE_POLICY_FILES) {
|
|
72
|
+
loadYamlFile(engine, join(dir, file));
|
|
73
|
+
}
|
|
74
|
+
loadProjectPolicyDir(engine, input.projectRoot);
|
|
75
|
+
return engine;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @deprecated Use createAgtPolicyEngine */
|
|
79
|
+
export function createHarnessPolicyEngine(packageRoot: string): PolicyEngine {
|
|
80
|
+
return createAgtPolicyEngine({
|
|
81
|
+
packageRoot,
|
|
82
|
+
projectRoot: process.cwd(),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getAgtPolicyEngine(
|
|
87
|
+
packageRoot: string,
|
|
88
|
+
projectRoot: string,
|
|
89
|
+
): PolicyEngine {
|
|
90
|
+
const key = `${packageRoot}\0${projectRoot}`;
|
|
91
|
+
if (cachedEngine && cachedKey === key) return cachedEngine;
|
|
92
|
+
cachedEngine = createAgtPolicyEngine({ packageRoot, projectRoot });
|
|
93
|
+
cachedKey = key;
|
|
94
|
+
return cachedEngine;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getHarnessPolicyEngine(packageRoot: string): PolicyEngine {
|
|
98
|
+
return getAgtPolicyEngine(packageRoot, process.cwd());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resetHarnessPolicyEngineCache(): void {
|
|
102
|
+
cachedEngine = null;
|
|
103
|
+
cachedKey = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Doctor: policies dir exists and all expected YAML files present. */
|
|
107
|
+
export function doctorHarnessPolicies(
|
|
108
|
+
packageRoot: string,
|
|
109
|
+
projectRoot = process.cwd(),
|
|
110
|
+
): {
|
|
111
|
+
ok: boolean;
|
|
112
|
+
errors: string[];
|
|
113
|
+
policyDir: string;
|
|
114
|
+
loaded: string[];
|
|
115
|
+
projectLoaded: string[];
|
|
116
|
+
} {
|
|
117
|
+
const errors: string[] = [];
|
|
118
|
+
const policyDir = resolveHarnessPoliciesDir(packageRoot);
|
|
119
|
+
if (!existsSync(policyDir)) {
|
|
120
|
+
errors.push(`policy directory missing: ${policyDir}`);
|
|
121
|
+
}
|
|
122
|
+
const loaded: string[] = [];
|
|
123
|
+
for (const file of PACKAGE_POLICY_FILES) {
|
|
124
|
+
const path = join(policyDir, file);
|
|
125
|
+
if (!existsSync(path)) {
|
|
126
|
+
errors.push(`missing policy file: ${path}`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
loaded.push(file);
|
|
130
|
+
}
|
|
131
|
+
let projectLoaded: string[] = [];
|
|
132
|
+
try {
|
|
133
|
+
const names = readdirSync(policyDir).filter((f) => f.endsWith(".yaml"));
|
|
134
|
+
if (names.length === 0) {
|
|
135
|
+
errors.push(`no YAML policies in ${policyDir}`);
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
errors.push(`cannot read policy dir: ${String(err)}`);
|
|
139
|
+
}
|
|
140
|
+
if (errors.length === 0) {
|
|
141
|
+
try {
|
|
142
|
+
createAgtPolicyEngine({ packageRoot, projectRoot });
|
|
143
|
+
const projDir = resolveProjectPoliciesDir(projectRoot);
|
|
144
|
+
if (existsSync(projDir)) {
|
|
145
|
+
projectLoaded = readdirSync(projDir).filter(
|
|
146
|
+
(f) => f.endsWith(".yaml") || f.endsWith(".yml"),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
errors.push(`PolicyEngine load failed: ${String(err)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { ok: errors.length === 0, errors, policyDir, loaded, projectLoaded };
|
|
154
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ExecutionRing, RingEnforcer } from "@microsoft/agent-governance-sdk";
|
|
2
|
+
|
|
3
|
+
const KIND_RING: Record<string, ExecutionRing> = {
|
|
4
|
+
planner: ExecutionRing.Ring0,
|
|
5
|
+
executor: ExecutionRing.Ring2,
|
|
6
|
+
evaluator: ExecutionRing.Ring0,
|
|
7
|
+
adversary: ExecutionRing.Ring3,
|
|
8
|
+
tie_breaker: ExecutionRing.Ring0,
|
|
9
|
+
meta: ExecutionRing.Ring0,
|
|
10
|
+
trace: ExecutionRing.Ring0,
|
|
11
|
+
incident: ExecutionRing.Ring0,
|
|
12
|
+
other: ExecutionRing.Ring3,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function ringForHarnessAgentKind(kind: string): ExecutionRing {
|
|
16
|
+
return KIND_RING[kind] ?? ExecutionRing.Ring3;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createRingEnforcer(): RingEnforcer {
|
|
20
|
+
return new RingEnforcer();
|
|
21
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircuitBreaker,
|
|
3
|
+
GovernanceMetrics,
|
|
4
|
+
} from "@microsoft/agent-governance-sdk";
|
|
5
|
+
|
|
6
|
+
let metrics: GovernanceMetrics | null = null;
|
|
7
|
+
const breakers = new Map<string, CircuitBreaker>();
|
|
8
|
+
|
|
9
|
+
export function getHarnessGovernanceMetrics(): GovernanceMetrics {
|
|
10
|
+
if (!metrics) {
|
|
11
|
+
metrics = new GovernanceMetrics();
|
|
12
|
+
}
|
|
13
|
+
return metrics;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getSpawnCircuitBreaker(runId: string): CircuitBreaker {
|
|
17
|
+
let cb = breakers.get(runId);
|
|
18
|
+
if (!cb) {
|
|
19
|
+
cb = new CircuitBreaker(5, 60_000);
|
|
20
|
+
breakers.set(runId, cb);
|
|
21
|
+
}
|
|
22
|
+
return cb;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isSreEnforceEnabled(): boolean {
|
|
26
|
+
return process.env.HARNESS_AGT_SRE_ENFORCE === "1";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function recordSpawnAttempt(runId: string, ok: boolean): void {
|
|
30
|
+
const cb = getSpawnCircuitBreaker(runId);
|
|
31
|
+
if (ok) {
|
|
32
|
+
cb.onSuccess();
|
|
33
|
+
} else {
|
|
34
|
+
cb.onFailure();
|
|
35
|
+
}
|
|
36
|
+
getHarnessGovernanceMetrics().recordPolicyDecision(ok ? "allow" : "deny", 0, {
|
|
37
|
+
run_id: runId,
|
|
38
|
+
kind: "harness_spawn",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function spawnCircuitOpen(runId: string): boolean {
|
|
43
|
+
if (!isSreEnforceEnabled()) return false;
|
|
44
|
+
return !getSpawnCircuitBreaker(runId).canExecute();
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { TrustManager } from "@microsoft/agent-governance-sdk";
|
|
2
|
+
|
|
3
|
+
const stores = new Map<string, TrustManager>();
|
|
4
|
+
|
|
5
|
+
export function getTrustManagerForRun(runId: string): TrustManager {
|
|
6
|
+
let tm = stores.get(runId);
|
|
7
|
+
if (!tm) {
|
|
8
|
+
tm = new TrustManager({ initialScore: 0.85, decayFactor: 0.98 });
|
|
9
|
+
stores.set(runId, tm);
|
|
10
|
+
}
|
|
11
|
+
return tm;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function recordPolicyDeny(runId: string, agentDid: string): void {
|
|
15
|
+
const tm = getTrustManagerForRun(runId);
|
|
16
|
+
tm.recordFailure(agentDid, 0.08);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function recordPolicyAllow(runId: string, agentDid: string): void {
|
|
20
|
+
const tm = getTrustManagerForRun(runId);
|
|
21
|
+
tm.recordSuccess(agentDid, 0.02);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function trustScoreForAgent(runId: string, agentDid: string): number {
|
|
25
|
+
return getTrustManagerForRun(runId).getTrustScore(agentDid).overall;
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Lightweight workflow flags from session custom entries (observation-bus parity). */
|
|
2
|
+
|
|
3
|
+
type EntryLike = {
|
|
4
|
+
type?: string;
|
|
5
|
+
customType?: string;
|
|
6
|
+
data?: { kind?: string; flags?: string[] };
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function workflowFlagsFromEntries(entries: unknown[]): Set<string> {
|
|
10
|
+
const flags = new Set<string>();
|
|
11
|
+
for (const raw of entries) {
|
|
12
|
+
const entry = raw as EntryLike;
|
|
13
|
+
if (entry.type !== "custom") continue;
|
|
14
|
+
if (entry.customType !== "harness-observation") continue;
|
|
15
|
+
const data = entry.data;
|
|
16
|
+
if (!data?.flags) continue;
|
|
17
|
+
for (const f of data.flags) {
|
|
18
|
+
if (typeof f === "string") flags.add(f);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return flags;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function workflowBlocked(
|
|
25
|
+
flags: Set<string>,
|
|
26
|
+
requiredPrior: string,
|
|
27
|
+
): boolean {
|
|
28
|
+
return !flags.has(requiredPrior);
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { isAgtGovernanceActive as hasProjectAgtFiles } from "./agents-policy.mjs";
|
|
3
|
+
import { isHarnessProjectEnabled } from "./harness-project-config.js";
|
|
4
|
+
|
|
5
|
+
/** Subprocess AGT + merged policies when harness is on or project declares policy files. */
|
|
6
|
+
export function isAgtGovernanceActive(projectRoot?: string): boolean {
|
|
7
|
+
const root = projectRoot ?? process.cwd();
|
|
8
|
+
if (isHarnessProjectEnabled(root)) return true;
|
|
9
|
+
return hasProjectAgtFiles(root);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function projectHasPolicyDir(projectRoot: string): boolean {
|
|
13
|
+
return hasProjectAgtFiles(projectRoot);
|
|
14
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { appendPolicyAuditEvent } from "./agt/audit-run-sink.js";
|
|
3
|
+
import type { BuildEvaluationContextInput } from "./agt/build-evaluation-context.js";
|
|
4
|
+
import { evaluateHarnessToolPolicy } from "./agt/evaluate-policy.js";
|
|
5
|
+
import { recordHarnessPolicyDeny } from "./agt/kill-switch-state.js";
|
|
6
|
+
import { recordPolicyAllow, recordPolicyDeny } from "./agt/trust-run-store.js";
|
|
7
|
+
import { getHarnessPackageRoot } from "./harness-paths.js";
|
|
8
|
+
|
|
9
|
+
type HarnessPhase = "plan" | "execute" | "evaluate" | "adversary" | "merge";
|
|
10
|
+
|
|
11
|
+
export interface PolicyStateSlice {
|
|
12
|
+
phase: HarnessPhase;
|
|
13
|
+
approvedPlan: boolean;
|
|
14
|
+
planId: string | null;
|
|
15
|
+
aborted: boolean;
|
|
16
|
+
budgetBypass: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function evaluateAgtToolCall(input: {
|
|
20
|
+
moduleUrl: string;
|
|
21
|
+
toolName: string;
|
|
22
|
+
toolInput: Record<string, unknown>;
|
|
23
|
+
policyState: PolicyStateSlice;
|
|
24
|
+
entries: unknown[];
|
|
25
|
+
sessionId: string;
|
|
26
|
+
projectRoot: string;
|
|
27
|
+
}): Promise<{ block: boolean; reason?: string } | undefined> {
|
|
28
|
+
const packageRoot = getHarnessPackageRoot(input.moduleUrl);
|
|
29
|
+
const evalInput: BuildEvaluationContextInput = {
|
|
30
|
+
toolName: input.toolName,
|
|
31
|
+
toolInput: input.toolInput,
|
|
32
|
+
packageRoot,
|
|
33
|
+
projectRoot: input.projectRoot,
|
|
34
|
+
sessionId: input.sessionId,
|
|
35
|
+
entries: input.entries,
|
|
36
|
+
policyState: input.policyState,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = await evaluateHarnessToolPolicy(packageRoot, evalInput);
|
|
40
|
+
const runId = process.env.HARNESS_RUN_ID?.trim();
|
|
41
|
+
const runDir =
|
|
42
|
+
process.env.HARNESS_RUN_DIR?.trim() ??
|
|
43
|
+
(runId ? join(packageRoot, ".pi", "harness", "runs", runId) : "");
|
|
44
|
+
|
|
45
|
+
if (runId && runDir) {
|
|
46
|
+
const agentDid =
|
|
47
|
+
process.env.HARNESS_AGENT_DID?.trim() ??
|
|
48
|
+
(process.env.HARNESS_AGENT_ID?.trim() || "parent-orchestrator");
|
|
49
|
+
appendPolicyAuditEvent({
|
|
50
|
+
runDir,
|
|
51
|
+
runId,
|
|
52
|
+
toolName: input.toolName,
|
|
53
|
+
allowed: result.allowed,
|
|
54
|
+
reason: result.reason,
|
|
55
|
+
agentDid,
|
|
56
|
+
phase: input.policyState.phase,
|
|
57
|
+
});
|
|
58
|
+
if (result.allowed) {
|
|
59
|
+
recordPolicyAllow(runId, agentDid);
|
|
60
|
+
} else {
|
|
61
|
+
recordPolicyDeny(runId, agentDid);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!result.allowed) {
|
|
66
|
+
recordHarnessPolicyDeny(input.sessionId);
|
|
67
|
+
return {
|
|
68
|
+
block: true,
|
|
69
|
+
reason: result.reason.startsWith("agt-policy")
|
|
70
|
+
? result.reason
|
|
71
|
+
: `agt-policy: ${result.reason}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @deprecated Use evaluateAgtToolCall */
|
|
78
|
+
export const evaluateAgtHarnessToolCall = evaluateAgtToolCall;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
Editor,
|
|
4
|
+
type EditorTheme,
|
|
5
|
+
Key,
|
|
6
|
+
matchesKey,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
} from "@earendil-works/pi-tui";
|
|
9
|
+
import type { AskResponse, DialogResult, ValidatedAskParams } from "./types.js";
|
|
10
|
+
|
|
11
|
+
type DisplayOption = {
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
isFreeform?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
interface CustomAnswer {
|
|
18
|
+
response: AskResponse;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ThemeLike = { fg(color: string, text: string): string };
|
|
22
|
+
type TuiLike = ConstructorParameters<typeof Editor>[0] & {
|
|
23
|
+
requestRender(): void;
|
|
24
|
+
};
|
|
25
|
+
type Done = (answer: CustomAnswer | null) => void;
|
|
26
|
+
|
|
27
|
+
function withTimeout<T>(
|
|
28
|
+
promise: Promise<T | null>,
|
|
29
|
+
ms: number | undefined,
|
|
30
|
+
): Promise<T | null> {
|
|
31
|
+
if (!ms) return promise;
|
|
32
|
+
return Promise.race([
|
|
33
|
+
promise,
|
|
34
|
+
new Promise<null>((resolve) => {
|
|
35
|
+
setTimeout(() => resolve(null), ms);
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function displayOptionsFor(validated: ValidatedAskParams): DisplayOption[] {
|
|
41
|
+
const displayOptions: DisplayOption[] = [...validated.options];
|
|
42
|
+
if (validated.allowFreeform) {
|
|
43
|
+
displayOptions.push({ title: "Type something…", isFreeform: true });
|
|
44
|
+
}
|
|
45
|
+
return displayOptions;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runFreeformOnly(
|
|
49
|
+
ui: ExtensionUIContext,
|
|
50
|
+
question: string,
|
|
51
|
+
): Promise<DialogResult> {
|
|
52
|
+
const text = await ui.input(question, "");
|
|
53
|
+
if (!text?.trim()) return { response: null, cancelled: true };
|
|
54
|
+
return {
|
|
55
|
+
response: { kind: "freeform", text: text.trim() },
|
|
56
|
+
cancelled: false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function editorThemeFor(theme: ThemeLike): EditorTheme {
|
|
61
|
+
return {
|
|
62
|
+
borderColor: (s) => theme.fg("accent", s),
|
|
63
|
+
selectList: {
|
|
64
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
65
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
66
|
+
description: (t) => theme.fg("muted", t),
|
|
67
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
68
|
+
noMatch: (t) => theme.fg("warning", t),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class AskDialogController {
|
|
74
|
+
private optionIndex = 0;
|
|
75
|
+
private editMode = false;
|
|
76
|
+
private readonly selected = new Set<number>();
|
|
77
|
+
private cachedLines: string[] | undefined;
|
|
78
|
+
private readonly editor: Editor;
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
private readonly validated: ValidatedAskParams,
|
|
82
|
+
private readonly displayOptions: DisplayOption[],
|
|
83
|
+
private readonly tui: TuiLike,
|
|
84
|
+
private readonly theme: ThemeLike,
|
|
85
|
+
private readonly done: Done,
|
|
86
|
+
) {
|
|
87
|
+
this.editor = new Editor(tui, editorThemeFor(theme));
|
|
88
|
+
this.editor.onSubmit = (value) => this.submitFreeform(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
invalidate(): void {
|
|
92
|
+
this.cachedLines = undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
handleInput(data: string): void {
|
|
96
|
+
if (this.editMode) {
|
|
97
|
+
this.handleEditInput(data);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (this.handleNavigationInput(data)) return;
|
|
101
|
+
if (this.validated.allowMultiple && matchesKey(data, Key.space)) {
|
|
102
|
+
this.toggleMultiSelect();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (matchesKey(data, Key.enter)) this.submitSelection();
|
|
106
|
+
if (matchesKey(data, Key.escape)) this.done(null);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
render(width: number): string[] {
|
|
110
|
+
if (this.cachedLines) return this.cachedLines;
|
|
111
|
+
const lines: string[] = [];
|
|
112
|
+
const add = (s: string) => lines.push(truncateToWidth(s, width));
|
|
113
|
+
const useOverlay = this.validated.displayMode !== "inline";
|
|
114
|
+
this.renderHeader(lines, add, width, useOverlay);
|
|
115
|
+
this.renderOptions(add);
|
|
116
|
+
this.renderEditor(lines, add, width);
|
|
117
|
+
this.renderFooter(add, width, useOverlay);
|
|
118
|
+
this.cachedLines = lines;
|
|
119
|
+
return lines;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private refresh(): void {
|
|
123
|
+
this.invalidate();
|
|
124
|
+
this.tui.requestRender();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private submitFreeform(value: string): void {
|
|
128
|
+
const trimmed = value.trim();
|
|
129
|
+
if (trimmed) {
|
|
130
|
+
this.done({ response: { kind: "freeform", text: trimmed } });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.editMode = false;
|
|
134
|
+
this.editor.setText("");
|
|
135
|
+
this.refresh();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private submitSelection(): void {
|
|
139
|
+
if (this.validated.allowMultiple) {
|
|
140
|
+
const titles = [...this.selected]
|
|
141
|
+
.sort((a, b) => a - b)
|
|
142
|
+
.map((i) => this.displayOptions[i].title)
|
|
143
|
+
.filter((t) => t !== "Type something…");
|
|
144
|
+
if (titles.length) {
|
|
145
|
+
this.done({ response: { kind: "selection", selections: titles } });
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const opt = this.displayOptions[this.optionIndex];
|
|
150
|
+
if (opt.isFreeform) {
|
|
151
|
+
this.editMode = true;
|
|
152
|
+
this.refresh();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.done({ response: { kind: "selection", selections: [opt.title] } });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private handleEditInput(data: string): void {
|
|
159
|
+
if (matchesKey(data, Key.escape)) {
|
|
160
|
+
this.editMode = false;
|
|
161
|
+
this.editor.setText("");
|
|
162
|
+
} else {
|
|
163
|
+
this.editor.handleInput(data);
|
|
164
|
+
}
|
|
165
|
+
this.refresh();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private handleNavigationInput(data: string): boolean {
|
|
169
|
+
if (matchesKey(data, Key.up)) {
|
|
170
|
+
this.optionIndex = Math.max(0, this.optionIndex - 1);
|
|
171
|
+
this.refresh();
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (matchesKey(data, Key.down)) {
|
|
175
|
+
this.optionIndex = Math.min(
|
|
176
|
+
this.displayOptions.length - 1,
|
|
177
|
+
this.optionIndex + 1,
|
|
178
|
+
);
|
|
179
|
+
this.refresh();
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private toggleMultiSelect(): void {
|
|
186
|
+
const opt = this.displayOptions[this.optionIndex];
|
|
187
|
+
if (opt.isFreeform) return;
|
|
188
|
+
if (this.selected.has(this.optionIndex)) {
|
|
189
|
+
this.selected.delete(this.optionIndex);
|
|
190
|
+
} else {
|
|
191
|
+
this.selected.add(this.optionIndex);
|
|
192
|
+
}
|
|
193
|
+
this.refresh();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private renderHeader(
|
|
197
|
+
lines: string[],
|
|
198
|
+
add: (s: string) => void,
|
|
199
|
+
width: number,
|
|
200
|
+
useOverlay: boolean,
|
|
201
|
+
): void {
|
|
202
|
+
if (useOverlay) add(this.theme.fg("accent", "─".repeat(width)));
|
|
203
|
+
if (this.validated.context) {
|
|
204
|
+
for (const line of this.validated.context.split("\n")) {
|
|
205
|
+
add(this.theme.fg("muted", ` ${line}`));
|
|
206
|
+
}
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
209
|
+
add(this.theme.fg("text", ` ${this.validated.question}`));
|
|
210
|
+
lines.push("");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private renderOptions(add: (s: string) => void): void {
|
|
214
|
+
for (let i = 0; i < this.displayOptions.length; i++) {
|
|
215
|
+
const opt = this.displayOptions[i];
|
|
216
|
+
const prefix = this.optionPrefix(i, opt.isFreeform === true);
|
|
217
|
+
const label = this.optionLabel(i, opt);
|
|
218
|
+
add(`${prefix}${label}`);
|
|
219
|
+
if (opt.description)
|
|
220
|
+
add(` ${this.theme.fg("muted", opt.description)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private optionPrefix(index: number, isFreeform: boolean): string {
|
|
225
|
+
if (!this.validated.allowMultiple) {
|
|
226
|
+
return index === this.optionIndex ? this.theme.fg("accent", "> ") : " ";
|
|
227
|
+
}
|
|
228
|
+
if (isFreeform) return " ";
|
|
229
|
+
return this.selected.has(index) ? this.theme.fg("accent", "[x] ") : "[ ] ";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private optionLabel(index: number, opt: DisplayOption): string {
|
|
233
|
+
const raw = `${index + 1}. ${opt.title}`;
|
|
234
|
+
const focused = index === this.optionIndex;
|
|
235
|
+
if (opt.isFreeform && this.editMode && focused) {
|
|
236
|
+
return this.theme.fg("accent", `${raw} ✎`);
|
|
237
|
+
}
|
|
238
|
+
if (focused && !this.validated.allowMultiple) {
|
|
239
|
+
return this.theme.fg("accent", raw);
|
|
240
|
+
}
|
|
241
|
+
return this.theme.fg("text", raw);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private renderEditor(
|
|
245
|
+
lines: string[],
|
|
246
|
+
add: (s: string) => void,
|
|
247
|
+
width: number,
|
|
248
|
+
): void {
|
|
249
|
+
if (!this.editMode) return;
|
|
250
|
+
lines.push("");
|
|
251
|
+
add(this.theme.fg("muted", " Your answer:"));
|
|
252
|
+
for (const line of this.editor.render(width - 2)) add(` ${line}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private renderFooter(
|
|
256
|
+
add: (s: string) => void,
|
|
257
|
+
width: number,
|
|
258
|
+
useOverlay: boolean,
|
|
259
|
+
): void {
|
|
260
|
+
add("");
|
|
261
|
+
if (this.editMode) {
|
|
262
|
+
add(this.theme.fg("dim", " Enter to submit • Esc to go back"));
|
|
263
|
+
} else if (this.validated.allowMultiple) {
|
|
264
|
+
add(
|
|
265
|
+
this.theme.fg(
|
|
266
|
+
"dim",
|
|
267
|
+
" ↑↓ navigate • Space toggle • Enter confirm • Esc cancel",
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
} else {
|
|
271
|
+
add(
|
|
272
|
+
this.theme.fg("dim", " ↑↓ navigate • Enter to select • Esc to cancel"),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (useOverlay) add(this.theme.fg("accent", "─".repeat(width)));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function runOptionDialog(
|
|
280
|
+
ui: ExtensionUIContext,
|
|
281
|
+
validated: ValidatedAskParams,
|
|
282
|
+
displayOptions: DisplayOption[],
|
|
283
|
+
): Promise<CustomAnswer | null> {
|
|
284
|
+
return withTimeout(
|
|
285
|
+
ui.custom<CustomAnswer | null>((tui, theme, _kb, done) => {
|
|
286
|
+
const controller = new AskDialogController(
|
|
287
|
+
validated,
|
|
288
|
+
displayOptions,
|
|
289
|
+
tui as TuiLike,
|
|
290
|
+
theme,
|
|
291
|
+
done,
|
|
292
|
+
);
|
|
293
|
+
return {
|
|
294
|
+
render: (width: number) => controller.render(width),
|
|
295
|
+
invalidate: () => controller.invalidate(),
|
|
296
|
+
handleInput: (data: string) => controller.handleInput(data),
|
|
297
|
+
};
|
|
298
|
+
}),
|
|
299
|
+
validated.timeout,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function runAskDialog(
|
|
304
|
+
ui: ExtensionUIContext,
|
|
305
|
+
validated: ValidatedAskParams,
|
|
306
|
+
): Promise<DialogResult> {
|
|
307
|
+
const displayOptions = displayOptionsFor(validated);
|
|
308
|
+
if (displayOptions.length === 0) {
|
|
309
|
+
return runFreeformOnly(ui, validated.question);
|
|
310
|
+
}
|
|
311
|
+
const result = await runOptionDialog(ui, validated, displayOptions);
|
|
312
|
+
if (!result) return { response: null, cancelled: true };
|
|
313
|
+
return { response: result.response, cancelled: false };
|
|
314
|
+
}
|