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
@@ -1,7 +1,12 @@
1
- /** Keep in sync with validate.ts — test entrypoint for node --test */
1
+ /** Keep in sync with validate.ts + format.ts — test entrypoint for node --test */
2
+
3
+ import { createRequire } from "node:module";
4
+ import { MAX_QUESTIONNAIRE_QUESTIONS } from "./constants.mjs";
5
+
6
+ const require = createRequire(import.meta.url);
2
7
 
3
8
  /**
4
- * @param {string | { title: string, description?: string }} raw
9
+ * @param {string | { title: string, description?: string, recommended?: boolean }} raw
5
10
  */
6
11
  export function normalizeOption(raw) {
7
12
  if (typeof raw === "string") {
@@ -10,6 +15,26 @@ export function normalizeOption(raw) {
10
15
  return {
11
16
  title: raw.title.trim(),
12
17
  description: raw.description?.trim() || undefined,
18
+ recommended: raw.recommended === true ? true : undefined,
19
+ };
20
+ }
21
+
22
+ function normalizeQuestion(raw) {
23
+ const title = raw.title?.trim();
24
+ if (!title) return "ask_user: each questions[] item requires title";
25
+
26
+ const options = (raw.options ?? [])
27
+ .map(normalizeOption)
28
+ .filter((o) => o.title);
29
+ if (options.length > 0 && options.length < 2) {
30
+ return `ask_user: question "${title}" needs at least 2 options or omit options for freeform`;
31
+ }
32
+
33
+ return {
34
+ title,
35
+ description: raw.description?.trim() || undefined,
36
+ options,
37
+ allowMultiple: raw.allowMultiple === true,
13
38
  };
14
39
  }
15
40
 
@@ -20,6 +45,22 @@ export function validateAskParams(params) {
20
45
  return "ask_user: question is required";
21
46
  }
22
47
 
48
+ const rawQuestions = params.questions ?? [];
49
+ if (rawQuestions.length > MAX_QUESTIONNAIRE_QUESTIONS) {
50
+ return `ask_user: at most ${MAX_QUESTIONNAIRE_QUESTIONS} questions in questionnaire mode`;
51
+ }
52
+
53
+ if (rawQuestions.length > 0 && (params.options?.length ?? 0) > 0) {
54
+ return "ask_user: use either options or questions[], not both";
55
+ }
56
+
57
+ const questions = [];
58
+ for (const q of rawQuestions) {
59
+ const normalized = normalizeQuestion(q);
60
+ if (typeof normalized === "string") return normalized;
61
+ questions.push(normalized);
62
+ }
63
+
23
64
  const options = (params.options ?? [])
24
65
  .map(normalizeOption)
25
66
  .filter((o) => o.title);
@@ -28,7 +69,9 @@ export function validateAskParams(params) {
28
69
  }
29
70
 
30
71
  const allowFreeform = params.allowFreeform !== false;
31
- if (options.length === 0 && !allowFreeform) {
72
+ const mode = questions.length > 0 ? "questionnaire" : "flat";
73
+
74
+ if (mode === "flat" && options.length === 0 && !allowFreeform) {
32
75
  return "ask_user: options required when allowFreeform is false";
33
76
  }
34
77
 
@@ -42,9 +85,15 @@ export function validateAskParams(params) {
42
85
  return {
43
86
  question,
44
87
  context: params.context?.trim() || undefined,
88
+ contextFormat:
89
+ params.contextFormat === "html" ? "html" : "markdown",
45
90
  options,
91
+ questions,
92
+ mode,
46
93
  allowMultiple: params.allowMultiple === true,
47
94
  allowFreeform,
95
+ allowComment: params.allowComment === true,
96
+ allowSkip: params.allowSkip === true,
48
97
  displayMode,
49
98
  timeout:
50
99
  typeof params.timeout === "number" && params.timeout > 0
@@ -53,14 +102,30 @@ export function validateAskParams(params) {
53
102
  };
54
103
  }
55
104
 
56
- /** @param {import('./types.js').AskResponse | null} response @param {boolean} cancelled */
57
- export function formatResultText(response, cancelled) {
105
+ /** @param {import('./types.js').AskResponse | null} response @param {boolean} cancelled @param {{ ui_degraded?: boolean }} [opts] */
106
+ export function formatResultText(response, cancelled, opts) {
58
107
  if (cancelled || !response) {
59
108
  return "User cancelled (no answer)";
60
109
  }
110
+ if (opts?.ui_degraded) {
111
+ return `Rich ask UI unavailable; using terminal prompt.\n\n${formatResponseBody(response)}`;
112
+ }
113
+ return formatResponseBody(response);
114
+ }
115
+
116
+ /** @param {import('./types.js').AskResponse} response */
117
+ function formatResponseBody(response) {
61
118
  if (response.kind === "freeform") {
62
119
  return `User wrote: ${response.text}`;
63
120
  }
121
+ if (response.kind === "questionnaire") {
122
+ const lines = response.questionnaireDetails.map((d) => {
123
+ let line = `- ${d.question}: ${d.answer}`;
124
+ if (d.comment) line += ` (comment: ${d.comment})`;
125
+ return line;
126
+ });
127
+ return `User answered questionnaire:\n${lines.join("\n")}`;
128
+ }
64
129
  if (response.selections.length === 1) {
65
130
  return `User selected: ${response.selections[0]}`;
66
131
  }
@@ -68,12 +133,133 @@ export function formatResultText(response, cancelled) {
68
133
  }
69
134
 
70
135
  /** @param {import('./types.js').ValidatedAskParams} validated */
71
- export function toToolDetails(validated, response, cancelled) {
136
+ export function toToolDetails(validated, response, cancelled, ui_backend = "headless", opts = {}) {
137
+ const options =
138
+ validated.mode === "questionnaire"
139
+ ? validated.questions.map((q) => q.title)
140
+ : validated.options.map((o) => o.title);
72
141
  return {
73
142
  question: validated.question,
74
143
  context: validated.context,
75
- options: validated.options.map((o) => o.title),
144
+ contextFormat: validated.contextFormat,
145
+ options,
76
146
  response,
77
147
  cancelled,
148
+ ui_backend,
149
+ ui_degraded: opts.ui_degraded,
150
+ };
151
+ }
152
+
153
+ export { isPlanApprovalAskUser } from "./policy.mjs";
154
+
155
+ /** @param {import('./types.js').ValidatedAskParams} validated */
156
+ export function buildGlimpsePayload(validated, sessionName) {
157
+ const hasQuestions = validated.mode === "questionnaire";
158
+ const hasOptions = validated.options.length > 0;
159
+ let payloadType;
160
+ if (hasQuestions) payloadType = "questionnaire";
161
+ else if (!hasOptions) payloadType = "freeform";
162
+ else if (validated.allowMultiple) payloadType = "multi-select";
163
+ else payloadType = "single-select";
164
+
165
+ let question = validated.question;
166
+ let context = validated.context;
167
+ if (!context && validated.question.length > 120) {
168
+ const match = validated.question.match(/^(.+?[.?!])(\s+|$)/);
169
+ if (match && match[0].length < validated.question.length) {
170
+ question = match[1].trim();
171
+ context = validated.question.slice(match[0].length).trim();
172
+ }
173
+ }
174
+
175
+ return {
176
+ type: payloadType,
177
+ question,
178
+ context,
179
+ contextFormat: validated.contextFormat,
180
+ options: validated.options.map((o) => ({
181
+ title: o.title,
182
+ description: o.description,
183
+ recommended: o.recommended,
184
+ })),
185
+ questions: validated.questions.map((q) => ({
186
+ title: q.title,
187
+ description: q.description,
188
+ allowMultiple: q.allowMultiple,
189
+ options: q.options.map((o) => ({
190
+ title: o.title,
191
+ description: o.description,
192
+ recommended: o.recommended,
193
+ })),
194
+ })),
195
+ allowMultiple: validated.allowMultiple,
196
+ allowFreeform: validated.allowFreeform,
197
+ allowComment: validated.allowComment,
198
+ allowSkip: validated.allowSkip,
199
+ sessionName,
200
+ };
201
+ }
202
+
203
+ function isGlimpseAvailable() {
204
+ try {
205
+ require.resolve("@alexleekt/pi-ask-user-glimpse/package.json");
206
+ require.resolve("glimpseui");
207
+ return true;
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+
213
+ /** @param {import('./types.js').ValidatedAskParams} validated @param {boolean} hasUI */
214
+ export function resolvePresenterChoice(validated, hasUI) {
215
+ if (validated.displayMode === "inline") return "tui";
216
+
217
+ const forced = process.env.HARNESS_ASK_USER_UI?.toLowerCase();
218
+ if (forced === "tui") return "tui";
219
+ if (forced === "glimpse") return "glimpse";
220
+ if (forced === "headless") return "headless";
221
+ if (hasUI && isGlimpseAvailable()) return "glimpse";
222
+ if (hasUI) return "tui";
223
+ return "headless";
224
+ }
225
+
226
+ /** @param {Record<string, unknown>} doc @param {import('./types.js').AskToolDetails} details */
227
+ export function applyAskUserToTaskClarification(doc, details) {
228
+ const next = { ...doc };
229
+ const assumptions = Array.isArray(next.assumptions) ? [...next.assumptions] : [];
230
+ if (!details.response || details.cancelled) return next;
231
+
232
+ const applyPair = (question, answer) => {
233
+ const n = question.toLowerCase();
234
+ if (n.includes("done") || n.includes("success")) {
235
+ next.success_definition = answer;
236
+ return;
237
+ }
238
+ if (n.includes("risk")) {
239
+ const a = answer.toLowerCase();
240
+ if (/\blow\b/.test(a)) next.risk_level = "low";
241
+ else if (/\bhigh\b/.test(a)) next.risk_level = "high";
242
+ else if (/\bmed/.test(a) || /\bmedium\b/.test(a)) next.risk_level = "med";
243
+ return;
244
+ }
245
+ if (n.includes("scope") || n.includes("in scope")) {
246
+ const items = answer.split(/[,;\n]/).map((s) => s.trim()).filter(Boolean);
247
+ if (items.length) next.in_scope = items;
248
+ return;
249
+ }
250
+ assumptions.push(`${question}: ${answer}`);
78
251
  };
252
+
253
+ const r = details.response;
254
+ if (r.kind === "questionnaire") {
255
+ for (const d of r.questionnaireDetails) applyPair(d.question, d.answer);
256
+ } else if (r.kind === "freeform") {
257
+ applyPair(details.question, r.text);
258
+ } else if (r.kind === "selection") {
259
+ applyPair(details.question, r.selections.join(", "));
260
+ }
261
+
262
+ if (assumptions.length) next.assumptions = assumptions;
263
+ if (Array.isArray(next.unresolved_questions)) next.unresolved_questions = [];
264
+ return next;
79
265
  }
@@ -1,15 +1,15 @@
1
+ import { MAX_QUESTIONNAIRE_QUESTIONS } from "./constants.js";
1
2
  import type {
2
- AskResponse,
3
- AskToolDetails,
4
3
  AskUserParams,
5
4
  NormalizedOption,
5
+ NormalizedQuestion,
6
6
  ValidatedAskParams,
7
7
  } from "./types.js";
8
8
 
9
9
  export type { ValidatedAskParams };
10
10
 
11
11
  export function normalizeOption(
12
- raw: string | { title: string; description?: string },
12
+ raw: string | { title: string; description?: string; recommended?: boolean },
13
13
  ): NormalizedOption {
14
14
  if (typeof raw === "string") {
15
15
  return { title: raw.trim() };
@@ -17,6 +17,28 @@ export function normalizeOption(
17
17
  return {
18
18
  title: raw.title.trim(),
19
19
  description: raw.description?.trim() || undefined,
20
+ recommended: raw.recommended === true ? true : undefined,
21
+ };
22
+ }
23
+
24
+ function normalizeQuestion(
25
+ raw: NonNullable<AskUserParams["questions"]>[number],
26
+ ): NormalizedQuestion | string {
27
+ const title = raw.title?.trim();
28
+ if (!title) return "ask_user: each questions[] item requires title";
29
+
30
+ const options = (raw.options ?? [])
31
+ .map(normalizeOption)
32
+ .filter((o) => o.title);
33
+ if (options.length > 0 && options.length < 2) {
34
+ return `ask_user: question "${title}" needs at least 2 options or omit options for freeform`;
35
+ }
36
+
37
+ return {
38
+ title,
39
+ description: raw.description?.trim() || undefined,
40
+ options,
41
+ allowMultiple: raw.allowMultiple === true,
20
42
  };
21
43
  }
22
44
 
@@ -28,6 +50,22 @@ export function validateAskParams(
28
50
  return "ask_user: question is required";
29
51
  }
30
52
 
53
+ const rawQuestions = params.questions ?? [];
54
+ if (rawQuestions.length > MAX_QUESTIONNAIRE_QUESTIONS) {
55
+ return `ask_user: at most ${MAX_QUESTIONNAIRE_QUESTIONS} questions in questionnaire mode`;
56
+ }
57
+
58
+ if (rawQuestions.length > 0 && (params.options?.length ?? 0) > 0) {
59
+ return "ask_user: use either options or questions[], not both";
60
+ }
61
+
62
+ const questions: NormalizedQuestion[] = [];
63
+ for (const q of rawQuestions) {
64
+ const normalized = normalizeQuestion(q);
65
+ if (typeof normalized === "string") return normalized;
66
+ questions.push(normalized);
67
+ }
68
+
31
69
  const options = (params.options ?? [])
32
70
  .map(normalizeOption)
33
71
  .filter((o) => o.title);
@@ -36,10 +74,16 @@ export function validateAskParams(
36
74
  }
37
75
 
38
76
  const allowFreeform = params.allowFreeform !== false;
39
- if (options.length === 0 && !allowFreeform) {
77
+ const mode = questions.length > 0 ? "questionnaire" : "flat";
78
+
79
+ if (mode === "flat" && options.length === 0 && !allowFreeform) {
40
80
  return "ask_user: options required when allowFreeform is false";
41
81
  }
42
82
 
83
+ if (mode === "questionnaire" && questions.length === 0) {
84
+ return "ask_user: questions[] must not be empty";
85
+ }
86
+
43
87
  const displayMode =
44
88
  process.env.HARNESS_ASK_USER_DISPLAY_MODE === "inline"
45
89
  ? "inline"
@@ -50,9 +94,14 @@ export function validateAskParams(
50
94
  return {
51
95
  question,
52
96
  context: params.context?.trim() || undefined,
97
+ contextFormat: params.contextFormat === "html" ? "html" : "markdown",
53
98
  options,
99
+ questions,
100
+ mode,
54
101
  allowMultiple: params.allowMultiple === true,
55
102
  allowFreeform,
103
+ allowComment: params.allowComment === true,
104
+ allowSkip: params.allowSkip === true,
56
105
  displayMode,
57
106
  timeout:
58
107
  typeof params.timeout === "number" && params.timeout > 0
@@ -60,33 +109,3 @@ export function validateAskParams(
60
109
  : undefined,
61
110
  };
62
111
  }
63
-
64
- export function formatResultText(
65
- response: AskResponse | null,
66
- cancelled: boolean,
67
- ): string {
68
- if (cancelled || !response) {
69
- return "User cancelled (no answer)";
70
- }
71
- if (response.kind === "freeform") {
72
- return `User wrote: ${response.text}`;
73
- }
74
- if (response.selections.length === 1) {
75
- return `User selected: ${response.selections[0]}`;
76
- }
77
- return `User selected: ${response.selections.join(", ")}`;
78
- }
79
-
80
- export function toToolDetails(
81
- validated: ValidatedAskParams,
82
- response: AskResponse | null,
83
- cancelled: boolean,
84
- ): AskToolDetails {
85
- return {
86
- question: validated.question,
87
- context: validated.context,
88
- options: validated.options.map((o) => o.title),
89
- response,
90
- cancelled,
91
- };
92
- }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -7,6 +7,10 @@ import { access, readFile, stat } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
  import { parse as parseYaml } from "yaml";
9
9
  import { validateAgainstHarnessSchema } from "./harness-schema-validate.js";
10
+ import {
11
+ TASK_CLARIFICATION_ARTIFACT,
12
+ validateTaskClarificationDoc,
13
+ } from "./plan-task-clarification.js";
10
14
 
11
15
  export interface ArtifactGateResult {
12
16
  ok: boolean;
@@ -19,12 +23,15 @@ const ARTIFACT_SCHEMA: Record<string, string> = {
19
23
  "artifacts/implementation-research.yaml":
20
24
  "plan-implementation-research-brief.schema.json",
21
25
  "artifacts/stack.yaml": "plan-stack-brief.schema.json",
26
+ "artifacts/task-clarification.yaml": "plan-task-clarification.schema.json",
22
27
  "artifacts/planning-context.yaml": "plan-planning-context.schema.json",
23
28
  "artifacts/eval-verdict.yaml": "eval-verdict.schema.json",
24
29
  "artifacts/adversary-report.yaml": "adversary-report.schema.json",
30
+ "artifacts/sentrux-repair-plan.yaml": "sentrux-repair-plan.schema.json",
25
31
  };
26
32
 
27
33
  const PREREQUISITE_ORDER: Record<string, string[]> = {
34
+ "artifacts/planning-context.yaml": [TASK_CLARIFICATION_ARTIFACT],
28
35
  "artifacts/hypothesis.yaml": ["artifacts/decomposition.yaml"],
29
36
  "artifacts/implementation-research.yaml": [
30
37
  "artifacts/decomposition.yaml",
@@ -53,6 +60,63 @@ function artifactStatusBad(doc: Record<string, unknown>): string | null {
53
60
  return null;
54
61
  }
55
62
 
63
+ async function validatePlanningContextArtifact(
64
+ normalized: string,
65
+ doc: Record<string, unknown>,
66
+ ): Promise<string[]> {
67
+ const errors: string[] = [];
68
+ if (normalized !== "artifacts/planning-context.yaml") return errors;
69
+ const statusErr = artifactStatusBad(doc);
70
+ if (statusErr) errors.push(`${normalized}: ${statusErr}`);
71
+ const coverage = doc.coverage as Record<string, unknown> | undefined;
72
+ if (!coverage || typeof coverage !== "object") return errors;
73
+ for (const lane of ["architecture", "structure"] as const) {
74
+ const laneDoc = coverage[lane] as Record<string, unknown> | undefined;
75
+ const laneStatus = String(laneDoc?.status ?? "").toLowerCase();
76
+ if (laneStatus !== "ok" && laneStatus !== "partial") {
77
+ errors.push(
78
+ `${normalized}: coverage.${lane}.status must be ok or partial (got "${laneStatus || "missing"}")`,
79
+ );
80
+ }
81
+ }
82
+ return errors;
83
+ }
84
+
85
+ async function validateArtifactPrerequisites(
86
+ runRoot: string,
87
+ normalized: string,
88
+ prereqs: string[],
89
+ ): Promise<string[]> {
90
+ const errors: string[] = [];
91
+ for (const prereq of prereqs) {
92
+ const prereqPath = join(runRoot, prereq);
93
+ if (!(await fileExists(prereqPath))) {
94
+ errors.push(`${normalized}: prerequisite missing (${prereq})`);
95
+ continue;
96
+ }
97
+ if (prereq !== TASK_CLARIFICATION_ARTIFACT) continue;
98
+ try {
99
+ const raw = await readFile(prereqPath, "utf-8");
100
+ const prereqDoc = parseYaml(raw) as Record<string, unknown>;
101
+ const clar = validateTaskClarificationDoc(prereqDoc, {
102
+ requireReady: true,
103
+ });
104
+ if (!clar.ok) {
105
+ errors.push(
106
+ ...clar.errors.map(
107
+ (e) => `${normalized}: prerequisite ${prereq} — ${e}`,
108
+ ),
109
+ );
110
+ }
111
+ } catch {
112
+ errors.push(
113
+ `${normalized}: prerequisite ${prereq} invalid or unreadable`,
114
+ );
115
+ }
116
+ }
117
+ return errors;
118
+ }
119
+
56
120
  export async function validateHarnessArtifactFile(
57
121
  runRoot: string,
58
122
  relPath: string,
@@ -102,32 +166,22 @@ export async function validateHarnessArtifactFile(
102
166
  }
103
167
  }
104
168
 
105
- if (doc && normalized === "artifacts/planning-context.yaml") {
106
- const statusErr = artifactStatusBad(doc);
107
- if (statusErr) {
108
- errors.push(`${normalized}: ${statusErr}`);
109
- }
110
- const coverage = doc.coverage as Record<string, unknown> | undefined;
111
- if (coverage && typeof coverage === "object") {
112
- for (const lane of ["architecture", "structure"] as const) {
113
- const laneDoc = coverage[lane] as Record<string, unknown> | undefined;
114
- const laneStatus = String(laneDoc?.status ?? "").toLowerCase();
115
- if (laneStatus !== "ok" && laneStatus !== "partial") {
116
- errors.push(
117
- `${normalized}: coverage.${lane}.status must be ok or partial (got "${laneStatus || "missing"}")`,
118
- );
119
- }
120
- }
169
+ if (doc && normalized === TASK_CLARIFICATION_ARTIFACT) {
170
+ const clar = validateTaskClarificationDoc(doc, { requireReady: true });
171
+ if (!clar.ok) {
172
+ errors.push(...clar.errors.map((e) => `${normalized}: ${e}`));
121
173
  }
122
174
  }
123
175
 
124
- const prereqs = PREREQUISITE_ORDER[normalized] ?? [];
125
- for (const prereq of prereqs) {
126
- if (!(await fileExists(join(runRoot, prereq)))) {
127
- errors.push(`${normalized}: prerequisite missing (${prereq})`);
128
- }
176
+ if (doc) {
177
+ errors.push(...(await validatePlanningContextArtifact(normalized, doc)));
129
178
  }
130
179
 
180
+ const prereqs = PREREQUISITE_ORDER[normalized] ?? [];
181
+ errors.push(
182
+ ...(await validateArtifactPrerequisites(runRoot, normalized, prereqs)),
183
+ );
184
+
131
185
  return { ok: errors.length === 0, errors };
132
186
  }
133
187