ultimate-pi 0.20.0 → 0.22.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-decisions/SKILL.md +68 -2
- package/.agents/skills/harness-git-commit/SKILL.md +72 -0
- package/.agents/skills/harness-governor/SKILL.md +2 -2
- package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
- package/.agents/skills/harness-plan/SKILL.md +13 -11
- package/.agents/skills/harness-review/SKILL.md +1 -1
- package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
- package/.agents/skills/sentrux/SKILL.md +4 -2
- package/.agents/skills/wiki-save/SKILL.md +1 -1
- package/.pi/PACKAGING.md +6 -0
- package/.pi/SYSTEM.md +21 -3
- package/.pi/agents/harness/ls-lint-steward.md +49 -0
- package/.pi/agents/harness/planning/decompose.md +4 -4
- package/.pi/agents/harness/reviewing/evaluator.md +1 -1
- package/.pi/agents/harness/running/executor.md +1 -1
- package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
- package/.pi/agents/pi-pi/prompt-expert.md +17 -2
- package/.pi/auto-commit.json +9 -2
- package/.pi/extensions/debate-orchestrator.ts +3 -0
- package/.pi/extensions/harness-anchored-edit.ts +7 -9
- package/.pi/extensions/harness-ask-user.ts +13 -34
- package/.pi/extensions/harness-debate-tools.ts +43 -4
- package/.pi/extensions/harness-live-widget.ts +28 -19
- package/.pi/extensions/harness-run-context.ts +278 -115
- package/.pi/extensions/harness-web-tools.ts +598 -471
- package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
- package/.pi/extensions/observation-bus.ts +4 -0
- package/.pi/extensions/policy-gate.ts +270 -229
- package/.pi/extensions/sentrux-rules-sync.ts +2 -0
- package/.pi/extensions/soundboard.ts +48 -48
- package/.pi/harness/README.md +4 -0
- package/.pi/harness/agents.manifest.json +15 -7
- package/.pi/harness/agents.policy.yaml +49 -82
- package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
- package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
- package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
- package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
- package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
- package/.pi/harness/docs/adrs/README.md +5 -0
- package/.pi/harness/docs/practice-map.md +10 -5
- package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
- package/.pi/harness/evolution/self-healing-rules.json +16 -0
- package/.pi/harness/ls-lint/naming.manifest.json +128 -0
- package/.pi/harness/sentrux/architecture.manifest.json +1 -1
- package/.pi/harness/specs/auto-commit.schema.json +63 -0
- package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
- package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
- package/.pi/harness/specs/naming-manifest.schema.json +54 -0
- package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
- package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
- package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
- package/.pi/harness/specs/sentrux-report.schema.json +119 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
- package/.pi/lib/agents-policy.d.mts +26 -51
- package/.pi/lib/agents-policy.mjs +41 -28
- package/.pi/lib/agt/build-evaluation-context.ts +136 -64
- package/.pi/lib/ask-user/constants.mjs +3 -0
- package/.pi/lib/ask-user/constants.ts +4 -0
- package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
- package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
- package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
- package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
- package/.pi/lib/ask-user/dialog.ts +2 -314
- package/.pi/lib/ask-user/fallback.ts +2 -78
- package/.pi/lib/ask-user/format.ts +85 -0
- package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
- package/.pi/lib/ask-user/index.ts +114 -0
- package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
- package/.pi/lib/ask-user/policy.mjs +43 -0
- package/.pi/lib/ask-user/policy.ts +104 -0
- package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
- package/.pi/lib/ask-user/presenters/headless.ts +131 -0
- package/.pi/lib/ask-user/presenters/select.ts +60 -0
- package/.pi/lib/ask-user/presenters/tui.ts +373 -0
- package/.pi/lib/ask-user/presenters/types.ts +13 -0
- package/.pi/lib/ask-user/render.ts +40 -9
- package/.pi/lib/ask-user/schema.ts +66 -13
- package/.pi/lib/ask-user/types.ts +60 -3
- package/.pi/lib/ask-user/validate-core.mjs +193 -7
- package/.pi/lib/ask-user/validate.ts +53 -34
- package/.pi/lib/harness-anchored-edit/package.json +3 -0
- package/.pi/lib/harness-artifact-gate.ts +75 -21
- package/.pi/lib/harness-auto-commit-config.mjs +321 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
- package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
- package/.pi/lib/harness-lens/index.ts +241 -108
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
- package/.pi/lib/harness-repair-brief.ts +84 -25
- package/.pi/lib/harness-run-context.ts +42 -52
- package/.pi/lib/harness-sentrux-parse.mjs +272 -0
- package/.pi/lib/harness-sentrux-root.mjs +78 -0
- package/.pi/lib/harness-slash-completions.ts +116 -0
- package/.pi/lib/harness-spawn-topology.ts +121 -87
- package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
- package/.pi/lib/harness-subagents-bridge.ts +4 -1
- package/.pi/lib/harness-ui-state.ts +95 -48
- package/.pi/lib/plan-approval/dialog.ts +5 -0
- package/.pi/lib/plan-approval/validate.ts +1 -1
- package/.pi/lib/plan-approval-readiness.ts +32 -0
- package/.pi/lib/plan-debate-gate.ts +154 -114
- package/.pi/lib/plan-task-clarification.ts +158 -0
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-ls-lint-steward.md +43 -0
- package/.pi/prompts/harness-plan.md +58 -8
- package/.pi/prompts/harness-review.md +40 -6
- package/.pi/prompts/harness-run.md +33 -11
- package/.pi/prompts/harness-setup.md +72 -3
- package/.pi/prompts/harness-steer.md +2 -1
- package/.pi/prompts/wiki-save.md +5 -4
- package/.pi/scripts/README.md +8 -0
- package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
- package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
- package/.pi/scripts/harness-cli-verify.sh +47 -0
- package/.pi/scripts/harness-git-churn.mjs +77 -0
- package/.pi/scripts/harness-git-commit.mjs +173 -0
- package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
- package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
- package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
- package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
- package/.pi/scripts/harness-sentrux-report.mjs +256 -0
- package/.pi/scripts/harness-verify.mjs +288 -125
- package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
- package/.pi/scripts/run-tests.mjs +1 -0
- package/.pi/settings.example.json +1 -0
- package/.sentrux/rules.toml +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +25 -0
- package/README.md +13 -4
- package/package.json +5 -1
- package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared slash-command argument completions for harness extension commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
8
|
+
import {
|
|
9
|
+
harnessRunsRoot,
|
|
10
|
+
RUN_CONTEXT_BASENAME,
|
|
11
|
+
} from "./harness-run-context.js";
|
|
12
|
+
|
|
13
|
+
const MAX_RUN_SUGGESTIONS = 20;
|
|
14
|
+
|
|
15
|
+
export function filterPrefix(
|
|
16
|
+
items: AutocompleteItem[],
|
|
17
|
+
prefix: string,
|
|
18
|
+
): AutocompleteItem[] | null {
|
|
19
|
+
const p = prefix.trim();
|
|
20
|
+
if (!p) return items.length > 0 ? items : null;
|
|
21
|
+
const filtered = items.filter(
|
|
22
|
+
(item) =>
|
|
23
|
+
item.value.startsWith(p) ||
|
|
24
|
+
(item.label !== undefined && item.label.startsWith(p)),
|
|
25
|
+
);
|
|
26
|
+
return filtered.length > 0 ? filtered : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function completeFlags(
|
|
30
|
+
prefix: string,
|
|
31
|
+
flags: string[],
|
|
32
|
+
): AutocompleteItem[] | null {
|
|
33
|
+
const items = flags.map((flag) => ({ value: flag, label: flag }));
|
|
34
|
+
return filterPrefix(items, prefix);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function completeEnum(
|
|
38
|
+
prefix: string,
|
|
39
|
+
values: string[],
|
|
40
|
+
): AutocompleteItem[] | null {
|
|
41
|
+
return completeFlags(prefix, values);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function completeRunIds(
|
|
45
|
+
prefix: string,
|
|
46
|
+
projectRoot: string,
|
|
47
|
+
): Promise<AutocompleteItem[] | null> {
|
|
48
|
+
const runsDir = harnessRunsRoot(projectRoot);
|
|
49
|
+
let names: string[];
|
|
50
|
+
try {
|
|
51
|
+
names = await readdir(runsDir);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const entries: { id: string; mtime: number; phase?: string }[] = [];
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
if (name.startsWith(".")) continue;
|
|
59
|
+
const dir = join(runsDir, name);
|
|
60
|
+
try {
|
|
61
|
+
const st = await stat(dir);
|
|
62
|
+
if (!st.isDirectory()) continue;
|
|
63
|
+
let phase: string | undefined;
|
|
64
|
+
try {
|
|
65
|
+
const raw = await readFile(join(dir, RUN_CONTEXT_BASENAME), "utf-8");
|
|
66
|
+
const match = raw.match(/^phase:\s*(\S+)/m);
|
|
67
|
+
if (match) phase = match[1];
|
|
68
|
+
} catch {
|
|
69
|
+
/* optional */
|
|
70
|
+
}
|
|
71
|
+
entries.push({ id: name, mtime: st.mtimeMs, phase });
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
entries.sort((a, b) => b.mtime - a.mtime);
|
|
76
|
+
const items: AutocompleteItem[] = entries
|
|
77
|
+
.slice(0, MAX_RUN_SUGGESTIONS)
|
|
78
|
+
.map((entry) => ({
|
|
79
|
+
value: entry.id,
|
|
80
|
+
label: entry.id,
|
|
81
|
+
description: entry.phase ? `phase ${entry.phase}` : undefined,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
return filterPrefix(items, prefix);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function completeHarnessUseRun(
|
|
88
|
+
prefix: string,
|
|
89
|
+
projectRoot: string,
|
|
90
|
+
): Promise<AutocompleteItem[] | null> {
|
|
91
|
+
const p = prefix.trim();
|
|
92
|
+
if (p.startsWith("-")) {
|
|
93
|
+
return completeFlags(p, ["--claim", "--readonly"]);
|
|
94
|
+
}
|
|
95
|
+
return completeRunIds(p, projectRoot);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function completeStrictFlag(prefix: string): AutocompleteItem[] | null {
|
|
99
|
+
return completeFlags(prefix, ["--strict"]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function completeDebateOpen(
|
|
103
|
+
prefix: string,
|
|
104
|
+
projectRoot: string,
|
|
105
|
+
): Promise<AutocompleteItem[] | null> {
|
|
106
|
+
const runs = await completeRunIds("", projectRoot);
|
|
107
|
+
if (!runs?.length) {
|
|
108
|
+
return filterPrefix([{ value: "plan-", label: "plan-<run-id>" }], prefix);
|
|
109
|
+
}
|
|
110
|
+
const items = runs.map((run) => ({
|
|
111
|
+
value: `plan-${run.value}`,
|
|
112
|
+
label: `plan-${run.value}`,
|
|
113
|
+
description: run.description,
|
|
114
|
+
}));
|
|
115
|
+
return filterPrefix(items, prefix);
|
|
116
|
+
}
|
|
@@ -6,6 +6,7 @@ import { constants } from "node:fs";
|
|
|
6
6
|
import { access } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import type { HarnessPhase } from "./harness-run-context.js";
|
|
9
|
+
import { isTaskClarificationReady } from "./plan-task-clarification.js";
|
|
9
10
|
|
|
10
11
|
export interface SpawnTopologyResult {
|
|
11
12
|
ok: boolean;
|
|
@@ -30,6 +31,18 @@ const PARALLEL_RESEARCH_AGENTS = new Set([
|
|
|
30
31
|
"harness/planning/stack-researcher",
|
|
31
32
|
]);
|
|
32
33
|
|
|
34
|
+
const CLARIFICATION_GATED_AGENTS = new Set([
|
|
35
|
+
PLANNING_CONTEXT_AGENT,
|
|
36
|
+
DECOMPOSE_AGENT,
|
|
37
|
+
HYPOTHESIS_AGENT,
|
|
38
|
+
...PARALLEL_RESEARCH_AGENTS,
|
|
39
|
+
...DEBATE_LANE_AGENTS,
|
|
40
|
+
"harness/planning/plan-synthesizer",
|
|
41
|
+
"harness/planning/execution-plan-author",
|
|
42
|
+
"harness/sentrux-steward",
|
|
43
|
+
"harness/ls-lint-steward",
|
|
44
|
+
]);
|
|
45
|
+
|
|
33
46
|
function countInSet(names: string[], allowed: Set<string>): number {
|
|
34
47
|
return names.filter((n) => allowed.has(n)).length;
|
|
35
48
|
}
|
|
@@ -58,6 +71,105 @@ async function decompositionReady(
|
|
|
58
71
|
return false;
|
|
59
72
|
}
|
|
60
73
|
}
|
|
74
|
+
function validateParallelBatch(
|
|
75
|
+
names: string[],
|
|
76
|
+
taskCount: number,
|
|
77
|
+
): string | null {
|
|
78
|
+
if (taskCount <= 1) return null;
|
|
79
|
+
const hasDecompose = names.includes(DECOMPOSE_AGENT);
|
|
80
|
+
const hasHypothesis = names.includes(HYPOTHESIS_AGENT);
|
|
81
|
+
if (hasDecompose && hasHypothesis) {
|
|
82
|
+
return (
|
|
83
|
+
"Cannot spawn decompose and hypothesis in the same parallel batch. " +
|
|
84
|
+
"Gate artifacts/decomposition.yaml, then spawn hypothesis sequentially."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const debateCount = countInSet(names, DEBATE_LANE_AGENTS);
|
|
89
|
+
const debateNames = names.filter((n) => DEBATE_LANE_AGENTS.has(n));
|
|
90
|
+
const parallelProbePair =
|
|
91
|
+
debateCount === 2 &&
|
|
92
|
+
debateNames.includes("harness/planning/plan-evaluator") &&
|
|
93
|
+
debateNames.includes("harness/planning/plan-adversary");
|
|
94
|
+
if (debateCount > 1 && !parallelProbePair) {
|
|
95
|
+
return `Review Gate: spawn one debate lane agent per subagent call (got ${debateCount}: ${debateNames.join(", ")}). Exception: plan-evaluator ∥ plan-adversary for parallel_probes.`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const planningContext = names.filter(
|
|
99
|
+
(n) => n === PLANNING_CONTEXT_AGENT,
|
|
100
|
+
).length;
|
|
101
|
+
const research = countInSet(names, PARALLEL_RESEARCH_AGENTS);
|
|
102
|
+
const recon = planningContext;
|
|
103
|
+
if (planningContext > 1) {
|
|
104
|
+
return "At most one planning-context subagent per parallel batch.";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const otherHarness = names.filter(
|
|
108
|
+
(n) =>
|
|
109
|
+
n.startsWith("harness/") &&
|
|
110
|
+
!isReconnaissanceAgent(n) &&
|
|
111
|
+
!PARALLEL_RESEARCH_AGENTS.has(n) &&
|
|
112
|
+
!DEBATE_LANE_AGENTS.has(n) &&
|
|
113
|
+
n !== DECOMPOSE_AGENT &&
|
|
114
|
+
n !== HYPOTHESIS_AGENT,
|
|
115
|
+
);
|
|
116
|
+
if (
|
|
117
|
+
(recon > 0 && (research > 0 || otherHarness.length > 0)) ||
|
|
118
|
+
(research > 0 && otherHarness.length > 0)
|
|
119
|
+
) {
|
|
120
|
+
return (
|
|
121
|
+
"Parallel batches may include only one independent group: " +
|
|
122
|
+
"research (≤2 lanes), optional single planning-context, " +
|
|
123
|
+
"or a single sequential lane agent."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (research > 2) {
|
|
127
|
+
return "At most 2 research lanes (implementation-researcher, stack-researcher) per parallel batch.";
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function validateClarificationGate(
|
|
133
|
+
names: string[],
|
|
134
|
+
phase: HarnessPhase,
|
|
135
|
+
opts?: { projectRoot?: string; runId?: string | null },
|
|
136
|
+
): Promise<string | null> {
|
|
137
|
+
if (!(phase === "plan" && opts?.projectRoot && opts?.runId)) return null;
|
|
138
|
+
const needsClar = names.some((n) => CLARIFICATION_GATED_AGENTS.has(n));
|
|
139
|
+
if (!needsClar) return null;
|
|
140
|
+
const runDir = join(opts.projectRoot, ".pi", "harness", "runs", opts.runId);
|
|
141
|
+
const clar = await isTaskClarificationReady(runDir);
|
|
142
|
+
if (clar.ok) return null;
|
|
143
|
+
return (
|
|
144
|
+
"Cannot spawn planning subagents before task clarification is ready. " +
|
|
145
|
+
`Complete Phase 0 and harness_artifact_ready on artifacts/task-clarification.yaml. ${clar.errors.join("; ")}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function validateHypothesisDependency(
|
|
150
|
+
names: string[],
|
|
151
|
+
opts?: { projectRoot?: string; runId?: string | null },
|
|
152
|
+
): Promise<string | null> {
|
|
153
|
+
if (!(names.includes(HYPOTHESIS_AGENT) && opts?.projectRoot && opts?.runId)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const ready = await decompositionReady(opts.projectRoot, opts.runId);
|
|
157
|
+
if (ready) return null;
|
|
158
|
+
return (
|
|
159
|
+
"Cannot spawn hypothesis before artifacts/decomposition.yaml exists. " +
|
|
160
|
+
"Complete decompose and harness_artifact_ready on decomposition first."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validatePlanPhaseMutations(
|
|
165
|
+
names: string[],
|
|
166
|
+
phase: HarnessPhase,
|
|
167
|
+
): string | null {
|
|
168
|
+
if (phase !== "plan") return null;
|
|
169
|
+
const mutating = names.filter((n) => n.startsWith("harness/running/"));
|
|
170
|
+
if (mutating.length === 0) return null;
|
|
171
|
+
return `Plan phase: cannot spawn mutating subagents (${mutating.join(", ")}).`;
|
|
172
|
+
}
|
|
61
173
|
|
|
62
174
|
export async function validateHarnessSpawnTopology(
|
|
63
175
|
names: string[],
|
|
@@ -71,95 +183,17 @@ export async function validateHarnessSpawnTopology(
|
|
|
71
183
|
const taskCount =
|
|
72
184
|
opts?.parallelTaskCount ?? (names.length > 1 ? names.length : 1);
|
|
73
185
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const hasHypothesis = names.includes(HYPOTHESIS_AGENT);
|
|
77
|
-
if (hasDecompose && hasHypothesis) {
|
|
78
|
-
return {
|
|
79
|
-
ok: false,
|
|
80
|
-
message:
|
|
81
|
-
"Cannot spawn decompose and hypothesis in the same parallel batch. " +
|
|
82
|
-
"Gate artifacts/decomposition.yaml, then spawn hypothesis sequentially.",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const debateCount = countInSet(names, DEBATE_LANE_AGENTS);
|
|
87
|
-
const debateNames = names.filter((n) => DEBATE_LANE_AGENTS.has(n));
|
|
88
|
-
const parallelProbePair =
|
|
89
|
-
debateCount === 2 &&
|
|
90
|
-
debateNames.includes("harness/planning/plan-evaluator") &&
|
|
91
|
-
debateNames.includes("harness/planning/plan-adversary");
|
|
92
|
-
if (debateCount > 1 && !parallelProbePair) {
|
|
93
|
-
return {
|
|
94
|
-
ok: false,
|
|
95
|
-
message: `Review Gate: spawn one debate lane agent per subagent call (got ${debateCount}: ${debateNames.join(", ")}). Exception: plan-evaluator ∥ plan-adversary for parallel_probes.`,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const planningContext = names.filter(
|
|
100
|
-
(n) => n === PLANNING_CONTEXT_AGENT,
|
|
101
|
-
).length;
|
|
102
|
-
const research = countInSet(names, PARALLEL_RESEARCH_AGENTS);
|
|
103
|
-
const recon = planningContext;
|
|
104
|
-
|
|
105
|
-
if (planningContext > 1) {
|
|
106
|
-
return {
|
|
107
|
-
ok: false,
|
|
108
|
-
message: "At most one planning-context subagent per parallel batch.",
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const otherHarness = names.filter(
|
|
113
|
-
(n) =>
|
|
114
|
-
n.startsWith("harness/") &&
|
|
115
|
-
!isReconnaissanceAgent(n) &&
|
|
116
|
-
!PARALLEL_RESEARCH_AGENTS.has(n) &&
|
|
117
|
-
!DEBATE_LANE_AGENTS.has(n) &&
|
|
118
|
-
n !== DECOMPOSE_AGENT &&
|
|
119
|
-
n !== HYPOTHESIS_AGENT,
|
|
120
|
-
);
|
|
121
|
-
if (
|
|
122
|
-
(recon > 0 && (research > 0 || otherHarness.length > 0)) ||
|
|
123
|
-
(research > 0 && otherHarness.length > 0)
|
|
124
|
-
) {
|
|
125
|
-
return {
|
|
126
|
-
ok: false,
|
|
127
|
-
message:
|
|
128
|
-
"Parallel batches may include only one independent group: " +
|
|
129
|
-
"research (≤2 lanes), optional single planning-context, " +
|
|
130
|
-
"or a single sequential lane agent.",
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
if (research > 2) {
|
|
134
|
-
return {
|
|
135
|
-
ok: false,
|
|
136
|
-
message:
|
|
137
|
-
"At most 2 research lanes (implementation-researcher, stack-researcher) per parallel batch.",
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
}
|
|
186
|
+
const parallelError = validateParallelBatch(names, taskCount);
|
|
187
|
+
if (parallelError) return { ok: false, message: parallelError };
|
|
141
188
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!ready) {
|
|
145
|
-
return {
|
|
146
|
-
ok: false,
|
|
147
|
-
message:
|
|
148
|
-
"Cannot spawn hypothesis before artifacts/decomposition.yaml exists. " +
|
|
149
|
-
"Complete decompose and harness_artifact_ready on decomposition first.",
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
}
|
|
189
|
+
const clarError = await validateClarificationGate(names, phase, opts);
|
|
190
|
+
if (clarError) return { ok: false, message: clarError };
|
|
153
191
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
message: `Plan phase: cannot spawn mutating subagents (${mutating.join(", ")}).`,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
}
|
|
192
|
+
const hypothesisError = await validateHypothesisDependency(names, opts);
|
|
193
|
+
if (hypothesisError) return { ok: false, message: hypothesisError };
|
|
194
|
+
|
|
195
|
+
const mutationError = validatePlanPhaseMutations(names, phase);
|
|
196
|
+
if (mutationError) return { ok: false, message: mutationError };
|
|
163
197
|
|
|
164
198
|
return { ok: true };
|
|
165
199
|
}
|
|
@@ -106,6 +106,16 @@ export const SUBMIT_TOOL_SPECS: readonly SubmitToolSpec[] = [
|
|
|
106
106
|
schemaFile: "sentrux-manifest-proposal.schema.json",
|
|
107
107
|
artifactPath: "artifacts/sentrux-manifest-proposal.yaml",
|
|
108
108
|
},
|
|
109
|
+
{
|
|
110
|
+
toolName: "submit_sentrux_repair_plan",
|
|
111
|
+
schemaFile: "sentrux-repair-plan.schema.json",
|
|
112
|
+
artifactPath: "artifacts/sentrux-repair-plan.yaml",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
toolName: "submit_ls_lint_manifest_proposal",
|
|
116
|
+
schemaFile: "ls-lint-manifest-proposal.schema.json",
|
|
117
|
+
artifactPath: "artifacts/ls-lint-manifest-proposal.yaml",
|
|
118
|
+
},
|
|
109
119
|
] as const;
|
|
110
120
|
|
|
111
121
|
export function specForSubmitTool(
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
type HarnessSubagentsOptions,
|
|
14
14
|
type SpawnAuthForward,
|
|
15
15
|
} from "../../vendor/pi-subagents/src/subagents.js";
|
|
16
|
-
import { subagentGovernanceExtensionPath } from "../extensions/subagent-governance.js";
|
|
17
16
|
import { getAgentKind, resolveExtensionBundlePaths } from "./agents-policy.mjs";
|
|
18
17
|
import {
|
|
19
18
|
delegationEnvFromBundle,
|
|
@@ -61,6 +60,10 @@ type PendingSpawnTelemetry = {
|
|
|
61
60
|
};
|
|
62
61
|
let pendingSpawnTelemetry: PendingSpawnTelemetry | null = null;
|
|
63
62
|
|
|
63
|
+
function subagentGovernanceExtensionPath(packageRoot: string): string {
|
|
64
|
+
return join(packageRoot, ".pi", "extensions", "subagent-governance.ts");
|
|
65
|
+
}
|
|
66
|
+
|
|
64
67
|
function collectHarnessAgentIds(params: Record<string, unknown>): string[] {
|
|
65
68
|
const out = new Set<string>();
|
|
66
69
|
const maybe = params as {
|
|
@@ -201,26 +201,33 @@ function deriveFlowSubstate(state: HarnessUiState): HarnessFlowSubstate {
|
|
|
201
201
|
return "idle";
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
severity: { ...DEFAULT_STATE.severity },
|
|
209
|
-
};
|
|
210
|
-
|
|
204
|
+
function applyPolicyState(
|
|
205
|
+
state: HarnessUiState,
|
|
206
|
+
latest: Map<string, unknown>,
|
|
207
|
+
): void {
|
|
211
208
|
const policy = latest.get("harness-policy-state") as
|
|
212
209
|
| PolicyStateLike
|
|
213
210
|
| undefined;
|
|
214
211
|
if (policy?.phase) state.phase = policy.phase;
|
|
215
212
|
state.planApproved = Boolean(policy?.approvedPlan);
|
|
216
213
|
state.planId = typeof policy?.planId === "string" ? policy.planId : null;
|
|
214
|
+
}
|
|
217
215
|
|
|
216
|
+
function applyReviewState(
|
|
217
|
+
state: HarnessUiState,
|
|
218
|
+
latest: Map<string, unknown>,
|
|
219
|
+
): void {
|
|
218
220
|
const review = latest.get("harness-review-integrity") as
|
|
219
221
|
| ReviewIntegrityStateLike
|
|
220
222
|
| undefined;
|
|
221
223
|
state.reviewViolationActive = Boolean(review?.violationActive);
|
|
222
224
|
state.reviewIsolationOk = !state.reviewViolationActive;
|
|
225
|
+
}
|
|
223
226
|
|
|
227
|
+
function applyBudgetState(
|
|
228
|
+
state: HarnessUiState,
|
|
229
|
+
latest: Map<string, unknown>,
|
|
230
|
+
): void {
|
|
224
231
|
const budget = latest.get("harness-budget-exhausted") as
|
|
225
232
|
| BudgetExhaustedLike
|
|
226
233
|
| undefined;
|
|
@@ -234,30 +241,41 @@ export function createStateFromEntries(entries: unknown[]): HarnessUiState {
|
|
|
234
241
|
if (budgetUsed != null) state.debateBudgetUsed = budgetUsed;
|
|
235
242
|
const cap = asNumber(budget.caps?.debate_global_cap);
|
|
236
243
|
if (cap != null) state.debateBudgetCap = cap;
|
|
244
|
+
return;
|
|
237
245
|
}
|
|
238
246
|
const telemetry = latest.get("harness-budget-telemetry") as
|
|
239
247
|
| BudgetExhaustedLike
|
|
240
248
|
| undefined;
|
|
241
|
-
if (telemetry
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
if (!telemetry) return;
|
|
250
|
+
const budgetUsed = asNumber(telemetry.budget_used);
|
|
251
|
+
if (budgetUsed != null) state.debateBudgetUsed = budgetUsed;
|
|
252
|
+
const cap = asNumber(telemetry.caps?.debate_global_cap);
|
|
253
|
+
if (cap != null) state.debateBudgetCap = cap;
|
|
254
|
+
}
|
|
247
255
|
|
|
256
|
+
function applyTestIntegrityState(
|
|
257
|
+
state: HarnessUiState,
|
|
258
|
+
latest: Map<string, unknown>,
|
|
259
|
+
): void {
|
|
248
260
|
const testIntegrity = latest.get("harness-test-integrity-flag") as
|
|
249
261
|
| TestIntegrityLike
|
|
250
262
|
| undefined;
|
|
251
263
|
if (
|
|
252
|
-
testIntegrity?.severity
|
|
253
|
-
testIntegrity?.severity
|
|
264
|
+
testIntegrity?.severity !== "high" &&
|
|
265
|
+
testIntegrity?.severity !== "medium"
|
|
254
266
|
) {
|
|
255
|
-
|
|
256
|
-
state.testIntegrityReasons = Array.isArray(testIntegrity.reasons)
|
|
257
|
-
? testIntegrity.reasons.filter((r): r is string => typeof r === "string")
|
|
258
|
-
: [];
|
|
267
|
+
return;
|
|
259
268
|
}
|
|
269
|
+
state.testIntegritySeverity = testIntegrity.severity;
|
|
270
|
+
state.testIntegrityReasons = Array.isArray(testIntegrity.reasons)
|
|
271
|
+
? testIntegrity.reasons.filter((r): r is string => typeof r === "string")
|
|
272
|
+
: [];
|
|
273
|
+
}
|
|
260
274
|
|
|
275
|
+
function applyDebateState(
|
|
276
|
+
state: HarnessUiState,
|
|
277
|
+
latest: Map<string, unknown>,
|
|
278
|
+
): void {
|
|
261
279
|
const debate = latest.get("harness-debate-state") as
|
|
262
280
|
| DebateStateLike
|
|
263
281
|
| undefined;
|
|
@@ -269,7 +287,12 @@ export function createStateFromEntries(entries: unknown[]): HarnessUiState {
|
|
|
269
287
|
if (debateBudgetUsed != null) state.debateBudgetUsed = debateBudgetUsed;
|
|
270
288
|
const debateBudgetCap = asNumber(debate?.debate_global_cap);
|
|
271
289
|
if (debateBudgetCap != null) state.debateBudgetCap = debateBudgetCap;
|
|
290
|
+
}
|
|
272
291
|
|
|
292
|
+
function applyRoundAndConsensusState(
|
|
293
|
+
state: HarnessUiState,
|
|
294
|
+
latest: Map<string, unknown>,
|
|
295
|
+
): void {
|
|
273
296
|
const roundResult = latest.get("harness-round-result") as
|
|
274
297
|
| RoundLike
|
|
275
298
|
| undefined;
|
|
@@ -291,14 +314,19 @@ export function createStateFromEntries(entries: unknown[]): HarnessUiState {
|
|
|
291
314
|
state.policyDecision = consensus.policy_decision;
|
|
292
315
|
}
|
|
293
316
|
const correctness = asNumber(consensus?.severity_scores?.correctness);
|
|
294
|
-
const security = asNumber(consensus?.severity_scores?.security);
|
|
295
|
-
const architecture = asNumber(consensus?.severity_scores?.architecture);
|
|
296
|
-
const test = asNumber(consensus?.severity_scores?.test_integrity);
|
|
297
317
|
if (correctness != null) state.severity.correctness = correctness;
|
|
318
|
+
const security = asNumber(consensus?.severity_scores?.security);
|
|
298
319
|
if (security != null) state.severity.security = security;
|
|
320
|
+
const architecture = asNumber(consensus?.severity_scores?.architecture);
|
|
299
321
|
if (architecture != null) state.severity.architecture = architecture;
|
|
322
|
+
const test = asNumber(consensus?.severity_scores?.test_integrity);
|
|
300
323
|
if (test != null) state.severity.testIntegrity = test;
|
|
324
|
+
}
|
|
301
325
|
|
|
326
|
+
function applyTraceState(
|
|
327
|
+
state: HarnessUiState,
|
|
328
|
+
latest: Map<string, unknown>,
|
|
329
|
+
): void {
|
|
302
330
|
const runTrace = latest.get("harness-run-trace") as TraceLike | undefined;
|
|
303
331
|
const traceState = latest.get("harness-trace-state") as TraceLike | undefined;
|
|
304
332
|
state.traceRunId =
|
|
@@ -307,7 +335,13 @@ export function createStateFromEntries(entries: unknown[]): HarnessUiState {
|
|
|
307
335
|
: typeof traceState?.run_id === "string"
|
|
308
336
|
? traceState.run_id
|
|
309
337
|
: null;
|
|
338
|
+
}
|
|
310
339
|
|
|
340
|
+
function applyRunContextState(
|
|
341
|
+
state: HarnessUiState,
|
|
342
|
+
latest: Map<string, unknown>,
|
|
343
|
+
entries: unknown[],
|
|
344
|
+
): void {
|
|
311
345
|
const runCtx = latest.get("harness-run-context") as
|
|
312
346
|
| {
|
|
313
347
|
phase?: HarnessPhase;
|
|
@@ -320,36 +354,49 @@ export function createStateFromEntries(entries: unknown[]): HarnessUiState {
|
|
|
320
354
|
status?: string;
|
|
321
355
|
}
|
|
322
356
|
| undefined;
|
|
323
|
-
if (runCtx
|
|
357
|
+
if (!runCtx) {
|
|
358
|
+
state.nextRecommendedCommand = null;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (runCtx.plan_ready) {
|
|
324
362
|
state.planApproved = true;
|
|
325
363
|
if (typeof runCtx.plan_id === "string") state.planId = runCtx.plan_id;
|
|
326
364
|
}
|
|
327
|
-
if (runCtx
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
365
|
+
if (runCtx.phase) state.phase = runCtx.phase;
|
|
366
|
+
if (typeof runCtx.run_id === "string") state.traceRunId = runCtx.run_id;
|
|
367
|
+
|
|
368
|
+
const persisted = runCtx.next_recommended_command;
|
|
369
|
+
if (typeof persisted === "string" && persisted.startsWith("/")) {
|
|
370
|
+
state.nextRecommendedCommand = persisted;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const statuses = extractCompletionStatuses(entries);
|
|
374
|
+
state.nextRecommendedCommand = nextStepAfterOutcome({
|
|
375
|
+
phase: state.phase,
|
|
376
|
+
planStatus: runCtx.plan_ready ? "ready" : null,
|
|
377
|
+
lastCompletedStep: runCtx.last_completed_step,
|
|
378
|
+
lastOutcome: runCtx.last_outcome,
|
|
379
|
+
executionStatus: statuses.executionStatus,
|
|
380
|
+
evalStatus: statuses.evalStatus,
|
|
381
|
+
aborted: runCtx.status === "aborted",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function createStateFromEntries(entries: unknown[]): HarnessUiState {
|
|
386
|
+
const latest = pickLatestCustomEntries(entries);
|
|
387
|
+
const state: HarnessUiState = {
|
|
388
|
+
...DEFAULT_STATE,
|
|
389
|
+
severity: { ...DEFAULT_STATE.severity },
|
|
390
|
+
};
|
|
352
391
|
|
|
392
|
+
applyPolicyState(state, latest);
|
|
393
|
+
applyReviewState(state, latest);
|
|
394
|
+
applyBudgetState(state, latest);
|
|
395
|
+
applyTestIntegrityState(state, latest);
|
|
396
|
+
applyDebateState(state, latest);
|
|
397
|
+
applyRoundAndConsensusState(state, latest);
|
|
398
|
+
applyTraceState(state, latest);
|
|
399
|
+
applyRunContextState(state, latest, entries);
|
|
353
400
|
state.flowSubstate = deriveFlowSubstate(state);
|
|
354
401
|
return state;
|
|
355
402
|
}
|
|
@@ -30,9 +30,14 @@ function toAskParams(
|
|
|
30
30
|
return {
|
|
31
31
|
question: "How would you like to proceed with this harness plan?",
|
|
32
32
|
context: buildPlanApprovalMarkdown(validated),
|
|
33
|
+
contextFormat: "markdown",
|
|
33
34
|
options: validated.options,
|
|
35
|
+
questions: [],
|
|
36
|
+
mode: "flat",
|
|
34
37
|
allowMultiple: false,
|
|
35
38
|
allowFreeform: false,
|
|
39
|
+
allowComment: false,
|
|
40
|
+
allowSkip: false,
|
|
36
41
|
// Inline prompt below the plan — no full-screen overlay.
|
|
37
42
|
displayMode: "inline",
|
|
38
43
|
};
|
|
@@ -6,6 +6,10 @@ import { constants } from "node:fs";
|
|
|
6
6
|
import { access, readFile } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { parse as parseYaml } from "yaml";
|
|
9
|
+
import {
|
|
10
|
+
isTaskClarificationReady,
|
|
11
|
+
TASK_CLARIFICATION_ARTIFACT,
|
|
12
|
+
} from "./plan-task-clarification.js";
|
|
9
13
|
|
|
10
14
|
export interface PlanApprovalReadiness {
|
|
11
15
|
ok: boolean;
|
|
@@ -140,6 +144,14 @@ export async function validatePlanApprovalReadiness(
|
|
|
140
144
|
const risk = String(opts?.risk_level ?? "med").toLowerCase();
|
|
141
145
|
const quick = opts?.quick === true;
|
|
142
146
|
|
|
147
|
+
const clarReady = await isTaskClarificationReady(runDir);
|
|
148
|
+
if (!clarReady.ok) {
|
|
149
|
+
const waived = await hasPhaseWaiver(runDir, "missing:task-clarification");
|
|
150
|
+
if (!waived) {
|
|
151
|
+
errors.push(...clarReady.errors);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
143
155
|
const statusPath = join(runDir, "artifacts", "plan-phase-status.yaml");
|
|
144
156
|
const statusDoc = await readYamlObject(statusPath);
|
|
145
157
|
if (statusDoc) {
|
|
@@ -160,6 +172,26 @@ export async function validatePlanApprovalReadiness(
|
|
|
160
172
|
errors,
|
|
161
173
|
);
|
|
162
174
|
|
|
175
|
+
if (hasPlanningContext) {
|
|
176
|
+
const ctxDoc = await readYamlObject(
|
|
177
|
+
join(runDir, PLANNING_CONTEXT_ARTIFACT),
|
|
178
|
+
);
|
|
179
|
+
const taskRef = String(ctxDoc?.task_ref ?? "").trim();
|
|
180
|
+
if (
|
|
181
|
+
taskRef &&
|
|
182
|
+
taskRef !== TASK_CLARIFICATION_ARTIFACT &&
|
|
183
|
+
!taskRef.endsWith("task-clarification.yaml")
|
|
184
|
+
) {
|
|
185
|
+
warnings.push(
|
|
186
|
+
`${PLANNING_CONTEXT_ARTIFACT}: task_ref should point at ${TASK_CLARIFICATION_ARTIFACT}`,
|
|
187
|
+
);
|
|
188
|
+
} else if (!taskRef) {
|
|
189
|
+
warnings.push(
|
|
190
|
+
`${PLANNING_CONTEXT_ARTIFACT}: set task_ref to ${TASK_CLARIFICATION_ARTIFACT}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
163
195
|
if (!hasPlanningContext) {
|
|
164
196
|
const waived = await hasPhaseWaiver(
|
|
165
197
|
runDir,
|