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.
Files changed (130) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +68 -2
  2. package/.agents/skills/harness-git-commit/SKILL.md +72 -0
  3. package/.agents/skills/harness-governor/SKILL.md +2 -2
  4. package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
  5. package/.agents/skills/harness-plan/SKILL.md +13 -11
  6. package/.agents/skills/harness-review/SKILL.md +1 -1
  7. package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
  8. package/.agents/skills/sentrux/SKILL.md +4 -2
  9. package/.agents/skills/wiki-save/SKILL.md +1 -1
  10. package/.pi/PACKAGING.md +6 -0
  11. package/.pi/SYSTEM.md +21 -3
  12. package/.pi/agents/harness/ls-lint-steward.md +49 -0
  13. package/.pi/agents/harness/planning/decompose.md +4 -4
  14. package/.pi/agents/harness/reviewing/evaluator.md +1 -1
  15. package/.pi/agents/harness/running/executor.md +1 -1
  16. package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
  17. package/.pi/agents/pi-pi/prompt-expert.md +17 -2
  18. package/.pi/auto-commit.json +9 -2
  19. package/.pi/extensions/debate-orchestrator.ts +3 -0
  20. package/.pi/extensions/harness-anchored-edit.ts +7 -9
  21. package/.pi/extensions/harness-ask-user.ts +13 -34
  22. package/.pi/extensions/harness-debate-tools.ts +43 -4
  23. package/.pi/extensions/harness-live-widget.ts +28 -19
  24. package/.pi/extensions/harness-run-context.ts +278 -115
  25. package/.pi/extensions/harness-web-tools.ts +598 -471
  26. package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
  27. package/.pi/extensions/observation-bus.ts +4 -0
  28. package/.pi/extensions/policy-gate.ts +270 -229
  29. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  30. package/.pi/extensions/soundboard.ts +48 -48
  31. package/.pi/harness/README.md +4 -0
  32. package/.pi/harness/agents.manifest.json +15 -7
  33. package/.pi/harness/agents.policy.yaml +49 -82
  34. package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
  35. package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
  36. package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
  37. package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
  38. package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
  39. package/.pi/harness/docs/adrs/README.md +5 -0
  40. package/.pi/harness/docs/practice-map.md +10 -5
  41. package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
  42. package/.pi/harness/evolution/self-healing-rules.json +16 -0
  43. package/.pi/harness/ls-lint/naming.manifest.json +128 -0
  44. package/.pi/harness/sentrux/architecture.manifest.json +1 -1
  45. package/.pi/harness/specs/auto-commit.schema.json +63 -0
  46. package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
  47. package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
  48. package/.pi/harness/specs/naming-manifest.schema.json +54 -0
  49. package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
  50. package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
  51. package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
  52. package/.pi/harness/specs/sentrux-report.schema.json +119 -0
  53. package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
  54. package/.pi/lib/agents-policy.d.mts +26 -51
  55. package/.pi/lib/agents-policy.mjs +41 -28
  56. package/.pi/lib/agt/build-evaluation-context.ts +136 -64
  57. package/.pi/lib/ask-user/constants.mjs +3 -0
  58. package/.pi/lib/ask-user/constants.ts +4 -0
  59. package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
  60. package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
  61. package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
  62. package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
  63. package/.pi/lib/ask-user/dialog.ts +2 -314
  64. package/.pi/lib/ask-user/fallback.ts +2 -78
  65. package/.pi/lib/ask-user/format.ts +85 -0
  66. package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
  67. package/.pi/lib/ask-user/index.ts +114 -0
  68. package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
  69. package/.pi/lib/ask-user/policy.mjs +43 -0
  70. package/.pi/lib/ask-user/policy.ts +104 -0
  71. package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
  72. package/.pi/lib/ask-user/presenters/headless.ts +131 -0
  73. package/.pi/lib/ask-user/presenters/select.ts +60 -0
  74. package/.pi/lib/ask-user/presenters/tui.ts +373 -0
  75. package/.pi/lib/ask-user/presenters/types.ts +13 -0
  76. package/.pi/lib/ask-user/render.ts +40 -9
  77. package/.pi/lib/ask-user/schema.ts +66 -13
  78. package/.pi/lib/ask-user/types.ts +60 -3
  79. package/.pi/lib/ask-user/validate-core.mjs +193 -7
  80. package/.pi/lib/ask-user/validate.ts +53 -34
  81. package/.pi/lib/harness-anchored-edit/package.json +3 -0
  82. package/.pi/lib/harness-artifact-gate.ts +75 -21
  83. package/.pi/lib/harness-auto-commit-config.mjs +321 -0
  84. package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
  85. package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
  86. package/.pi/lib/harness-lens/index.ts +241 -108
  87. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
  88. package/.pi/lib/harness-repair-brief.ts +84 -25
  89. package/.pi/lib/harness-run-context.ts +42 -52
  90. package/.pi/lib/harness-sentrux-parse.mjs +272 -0
  91. package/.pi/lib/harness-sentrux-root.mjs +78 -0
  92. package/.pi/lib/harness-slash-completions.ts +116 -0
  93. package/.pi/lib/harness-spawn-topology.ts +121 -87
  94. package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
  95. package/.pi/lib/harness-subagents-bridge.ts +4 -1
  96. package/.pi/lib/harness-ui-state.ts +95 -48
  97. package/.pi/lib/plan-approval/dialog.ts +5 -0
  98. package/.pi/lib/plan-approval/validate.ts +1 -1
  99. package/.pi/lib/plan-approval-readiness.ts +32 -0
  100. package/.pi/lib/plan-debate-gate.ts +154 -114
  101. package/.pi/lib/plan-task-clarification.ts +158 -0
  102. package/.pi/prompts/harness-auto.md +2 -2
  103. package/.pi/prompts/harness-ls-lint-steward.md +43 -0
  104. package/.pi/prompts/harness-plan.md +58 -8
  105. package/.pi/prompts/harness-review.md +40 -6
  106. package/.pi/prompts/harness-run.md +33 -11
  107. package/.pi/prompts/harness-setup.md +72 -3
  108. package/.pi/prompts/harness-steer.md +2 -1
  109. package/.pi/prompts/wiki-save.md +5 -4
  110. package/.pi/scripts/README.md +8 -0
  111. package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
  112. package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
  113. package/.pi/scripts/harness-cli-verify.sh +47 -0
  114. package/.pi/scripts/harness-git-churn.mjs +77 -0
  115. package/.pi/scripts/harness-git-commit.mjs +173 -0
  116. package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
  117. package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
  118. package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
  119. package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
  120. package/.pi/scripts/harness-sentrux-report.mjs +256 -0
  121. package/.pi/scripts/harness-verify.mjs +288 -125
  122. package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
  123. package/.pi/scripts/run-tests.mjs +1 -0
  124. package/.pi/settings.example.json +1 -0
  125. package/.sentrux/rules.toml +1 -1
  126. package/AGENTS.md +1 -0
  127. package/CHANGELOG.md +25 -0
  128. package/README.md +13 -4
  129. package/package.json +5 -1
  130. 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
- if (taskCount > 1) {
75
- const hasDecompose = names.includes(DECOMPOSE_AGENT);
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
- if (names.includes(HYPOTHESIS_AGENT) && opts?.projectRoot && opts?.runId) {
143
- const ready = await decompositionReady(opts.projectRoot, opts.runId);
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
- if (phase === "plan") {
155
- const mutating = names.filter((n) => n.startsWith("harness/running/"));
156
- if (mutating.length > 0) {
157
- return {
158
- ok: false,
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
- export function createStateFromEntries(entries: unknown[]): HarnessUiState {
205
- const latest = pickLatestCustomEntries(entries);
206
- const state: HarnessUiState = {
207
- ...DEFAULT_STATE,
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 && !state.budgetExhausted) {
242
- const budgetUsed = asNumber(telemetry.budget_used);
243
- if (budgetUsed != null) state.debateBudgetUsed = budgetUsed;
244
- const cap = asNumber(telemetry.caps?.debate_global_cap);
245
- if (cap != null) state.debateBudgetCap = cap;
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 === "high" ||
253
- testIntegrity?.severity === "medium"
264
+ testIntegrity?.severity !== "high" &&
265
+ testIntegrity?.severity !== "medium"
254
266
  ) {
255
- state.testIntegritySeverity = testIntegrity.severity;
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?.plan_ready) {
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?.phase) {
328
- state.phase = runCtx.phase;
329
- }
330
- if (typeof runCtx?.run_id === "string") {
331
- state.traceRunId = runCtx.run_id;
332
- }
333
- if (runCtx) {
334
- const persisted = runCtx.next_recommended_command;
335
- if (typeof persisted === "string" && persisted.startsWith("/")) {
336
- state.nextRecommendedCommand = persisted;
337
- } else {
338
- const statuses = extractCompletionStatuses(entries);
339
- state.nextRecommendedCommand = nextStepAfterOutcome({
340
- phase: state.phase,
341
- planStatus: runCtx.plan_ready ? "ready" : null,
342
- lastCompletedStep: runCtx.last_completed_step,
343
- lastOutcome: runCtx.last_outcome,
344
- executionStatus: statuses.executionStatus,
345
- evalStatus: statuses.evalStatus,
346
- aborted: runCtx.status === "aborted",
347
- });
348
- }
349
- } else {
350
- state.nextRecommendedCommand = null;
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
  };
@@ -1,5 +1,5 @@
1
+ import { formatResultText } from "../ask-user/format.js";
1
2
  import type { AskResponse } from "../ask-user/types.js";
2
- import { formatResultText } from "../ask-user/validate.js";
3
3
  import {
4
4
  type PlanPacketLike,
5
5
  validatePlanPacket,
@@ -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,