ultimate-pi 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.agents/skills/harness-context/SKILL.md +13 -6
  2. package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
  3. package/.agents/skills/harness-decisions/SKILL.md +1 -1
  4. package/.agents/skills/harness-eval/SKILL.md +6 -21
  5. package/.agents/skills/harness-governor/SKILL.md +4 -3
  6. package/.agents/skills/harness-orchestration/SKILL.md +41 -53
  7. package/.agents/skills/harness-plan/SKILL.md +23 -12
  8. package/.agents/skills/harness-review/SKILL.md +52 -0
  9. package/.agents/skills/harness-sentrux-setup/SKILL.md +16 -3
  10. package/.agents/skills/harness-steer/SKILL.md +14 -0
  11. package/.agents/skills/sentrux/SKILL.md +9 -9
  12. package/.pi/agents/harness/planning/decompose.md +7 -4
  13. package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
  14. package/.pi/agents/harness/planning/hypothesis.md +3 -1
  15. package/.pi/agents/harness/planning/plan-adversary.md +2 -0
  16. package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
  17. package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
  18. package/.pi/agents/harness/planning/planning-context.md +48 -0
  19. package/.pi/agents/harness/planning/review-integrator.md +2 -0
  20. package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
  21. package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +3 -10
  22. package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +3 -12
  23. package/.pi/agents/harness/running/executor.md +45 -0
  24. package/.pi/agents/harness/sentrux-steward.md +51 -0
  25. package/.pi/extensions/00-harness-project-control.ts +133 -0
  26. package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
  27. package/.pi/extensions/budget-guard.ts +2 -0
  28. package/.pi/extensions/debate-orchestrator.ts +2 -0
  29. package/.pi/extensions/harness-ask-user.ts +2 -2
  30. package/.pi/extensions/harness-debate-tools.ts +2 -2
  31. package/.pi/extensions/harness-live-widget.ts +60 -3
  32. package/.pi/extensions/harness-plan-approval.ts +64 -58
  33. package/.pi/extensions/harness-run-context.ts +715 -90
  34. package/.pi/extensions/harness-subagent-submit.ts +46 -12
  35. package/.pi/extensions/harness-subagents.ts +2 -2
  36. package/.pi/extensions/harness-telemetry.ts +2 -0
  37. package/.pi/extensions/harness-web-tools.ts +2 -2
  38. package/.pi/extensions/lib/extension-load-guard.ts +10 -0
  39. package/.pi/extensions/lib/harness-artifact-gate.ts +172 -0
  40. package/.pi/extensions/lib/harness-posthog.ts +9 -5
  41. package/.pi/extensions/lib/harness-spawn-topology.ts +165 -0
  42. package/.pi/extensions/lib/harness-subagent-auth.ts +1 -2
  43. package/.pi/extensions/lib/harness-subagent-policy.ts +28 -24
  44. package/.pi/extensions/lib/harness-subagent-precheck.ts +36 -10
  45. package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
  46. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +22 -22
  47. package/.pi/extensions/lib/harness-subagents-bridge.ts +7 -29
  48. package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
  49. package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
  50. package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
  51. package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
  52. package/.pi/extensions/lib/plan-approval/types.ts +1 -1
  53. package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
  54. package/.pi/extensions/lib/plan-approval-readiness.ts +192 -0
  55. package/.pi/extensions/lib/plan-debate-eligibility.ts +12 -5
  56. package/.pi/extensions/lib/plan-debate-gate.ts +22 -1
  57. package/.pi/extensions/lib/plan-debate-lanes.ts +32 -2
  58. package/.pi/extensions/lib/plan-review-gate.ts +8 -0
  59. package/.pi/extensions/lib/posthog-client.ts +76 -0
  60. package/.pi/extensions/lib/spawn-policy.ts +3 -3
  61. package/.pi/extensions/observation-bus.ts +2 -0
  62. package/.pi/extensions/policy-gate.ts +26 -19
  63. package/.pi/extensions/review-integrity.ts +91 -10
  64. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  65. package/.pi/extensions/test-diff-integrity.ts +1 -0
  66. package/.pi/extensions/trace-recorder.ts +2 -0
  67. package/.pi/harness/agents.manifest.json +37 -37
  68. package/.pi/harness/corpus/cron.example +8 -0
  69. package/.pi/harness/corpus/graphify-kb-updater.config.json +214 -0
  70. package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
  71. package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
  72. package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
  73. package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
  74. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +8 -6
  75. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
  76. package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
  77. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
  78. package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
  79. package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
  80. package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
  81. package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
  82. package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
  83. package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
  84. package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
  85. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +37 -0
  86. package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
  87. package/.pi/harness/docs/adrs/README.md +11 -0
  88. package/.pi/harness/docs/graphify-kb-updater-runbook.md +163 -0
  89. package/.pi/harness/docs/practice-map.md +110 -0
  90. package/.pi/harness/env.harness.template +5 -3
  91. package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
  92. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +5 -2
  93. package/.pi/harness/specs/README.md +1 -1
  94. package/.pi/harness/specs/harness-run-context.schema.json +11 -0
  95. package/.pi/harness/specs/harness-spawn-context.schema.json +15 -1
  96. package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
  97. package/.pi/harness/specs/plan-packet.schema.json +4 -0
  98. package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
  99. package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
  100. package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
  101. package/.pi/harness/specs/repair-brief.schema.json +45 -0
  102. package/.pi/harness/specs/review-outcome.schema.json +46 -0
  103. package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
  104. package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
  105. package/.pi/harness/specs/steer-state.schema.json +20 -0
  106. package/.pi/lib/harness-context-mode-policy.ts +256 -0
  107. package/.pi/lib/harness-project-config.ts +91 -0
  108. package/.pi/lib/harness-repair-brief.ts +145 -0
  109. package/.pi/lib/harness-run-context.ts +591 -32
  110. package/.pi/lib/harness-ui-state.ts +114 -21
  111. package/.pi/prompts/harness-auto.md +10 -10
  112. package/.pi/prompts/harness-critic.md +3 -30
  113. package/.pi/prompts/harness-eval.md +4 -37
  114. package/.pi/prompts/harness-plan.md +116 -54
  115. package/.pi/prompts/harness-review.md +150 -15
  116. package/.pi/prompts/harness-run.md +62 -10
  117. package/.pi/prompts/harness-sentrux-steward.md +55 -0
  118. package/.pi/prompts/harness-setup.md +5 -4
  119. package/.pi/prompts/harness-steer.md +30 -0
  120. package/.pi/scripts/README.md +1 -0
  121. package/.pi/scripts/graphify-kb-updater.mjs +398 -0
  122. package/.pi/scripts/harness-agents-manifest.mjs +1 -1
  123. package/.pi/scripts/harness-project-toggle.mjs +129 -0
  124. package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
  125. package/.pi/scripts/harness-verify.mjs +22 -6
  126. package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
  127. package/.pi/scripts/validate-plan-dag.mjs +3 -3
  128. package/AGENTS.md +1 -0
  129. package/CHANGELOG.md +23 -0
  130. package/README.md +94 -58
  131. package/package.json +5 -4
  132. package/.pi/agents/harness/executor.md +0 -47
  133. package/.pi/agents/harness/planning/scout-graphify.md +0 -37
  134. package/.pi/agents/harness/planning/scout-semantic.md +0 -39
  135. package/.pi/agents/harness/planning/scout-structure.md +0 -35
  136. package/.pi/prompts/git-sync.md +0 -124
  137. /package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Harness policy for context-mode execute tools (ctx_execute, ctx_batch_execute,
3
+ * ctx_execute_file). Mirrors bash/write phase rules so agents cannot bypass
4
+ * policy-gate via the MCP sandbox.
5
+ */
6
+
7
+ import type { HarnessPhase } from "./harness-run-context.js";
8
+
9
+ /** Union of policy-gate and harness-subagent bash mutation patterns. */
10
+ export const BASH_MUTATION_PATTERNS: RegExp[] = [
11
+ /\bgit\s+(add|commit|push|merge|rebase|reset|checkout|cherry-pick|apply)\b/i,
12
+ /\brm\s+(-rf?|--recursive|-)/i,
13
+ /\bmv\b/i,
14
+ /\bcp\b/i,
15
+ /\bmkdir\b/i,
16
+ /\btouch\b/i,
17
+ /\btee\b/i,
18
+ /\bchmod\b/i,
19
+ /\bchown\b/i,
20
+ /\bsed\s+-i\b/i,
21
+ /\bperl\s+-i\b/i,
22
+ /\bnpm\s+(install|uninstall|ci)\b/i,
23
+ /\bpnpm\s+(add|install|remove)\b/i,
24
+ /\byarn\s+(add|install|remove)\b/i,
25
+ ];
26
+
27
+ const CONTEXT_MODE_MUTATION_TOOLS = new Set([
28
+ "ctx_execute",
29
+ "ctx_batch_execute",
30
+ "ctx_execute_file",
31
+ ]);
32
+
33
+ const JS_TS_FS_MUTATION_PATTERNS: RegExp[] = [
34
+ /\bwriteFile(?:Sync)?\s*\(/,
35
+ /\bappendFile(?:Sync)?\s*\(/,
36
+ /\bunlink(?:Sync)?\s*\(/,
37
+ /\brename(?:Sync)?\s*\(/,
38
+ /\bcopyFile(?:Sync)?\s*\(/,
39
+ /\bmkdir(?:Sync)?\s*\(/,
40
+ /\brm(?:Sync)?\s*\(/,
41
+ /\brmdir(?:Sync)?\s*\(/,
42
+ /\btruncate(?:Sync)?\s*\(/,
43
+ /\bcreateWriteStream\s*\(/,
44
+ ];
45
+
46
+ const JS_TS_SHELL_ESCAPE_PATTERNS: RegExp[] = [
47
+ /\bexec(?:Sync)?\s*\(\s*[`'"]([^`'"]*)[`'"]/g,
48
+ /\bspawn(?:Sync)?\s*\(\s*[`'"]([^`'"]*)[`'"]/g,
49
+ ];
50
+
51
+ const PYTHON_SHELL_ESCAPE_PATTERNS: RegExp[] = [
52
+ /\bos\.system\s*\(\s*['"]([^'"]*)['"]\s*\)/g,
53
+ /\bsubprocess\.(?:run|call|Popen)\s*\(\s*['"]([^'"]*)['"]/g,
54
+ ];
55
+
56
+ export function normalizeContextModeToolName(toolName: string): string | null {
57
+ const raw = toolName.trim();
58
+ if (CONTEXT_MODE_MUTATION_TOOLS.has(raw)) return raw;
59
+ const prefixed = raw.replace(/^context_mode_/, "");
60
+ if (CONTEXT_MODE_MUTATION_TOOLS.has(prefixed)) return prefixed;
61
+ return null;
62
+ }
63
+
64
+ export function isMutatingBash(command: string): boolean {
65
+ return BASH_MUTATION_PATTERNS.some((pattern) => pattern.test(command));
66
+ }
67
+
68
+ /** Split shell on &&, ||, ;, | while respecting quotes (mirrors context-mode security). */
69
+ export function splitChainedCommands(command: string): string[] {
70
+ const parts: string[] = [];
71
+ let current = "";
72
+ let inSingle = false;
73
+ let inDouble = false;
74
+ let inBacktick = false;
75
+
76
+ for (let i = 0; i < command.length; i++) {
77
+ const ch = command[i];
78
+ const prev = i > 0 ? command[i - 1] : "";
79
+
80
+ if (ch === "'" && !inDouble && !inBacktick && prev !== "\\") {
81
+ inSingle = !inSingle;
82
+ current += ch;
83
+ } else if (ch === '"' && !inSingle && !inBacktick && prev !== "\\") {
84
+ inDouble = !inDouble;
85
+ current += ch;
86
+ } else if (ch === "`" && !inSingle && !inDouble && prev !== "\\") {
87
+ inBacktick = !inBacktick;
88
+ current += ch;
89
+ } else if (!inSingle && !inDouble && !inBacktick) {
90
+ if (ch === ";") {
91
+ parts.push(current.trim());
92
+ current = "";
93
+ } else if (ch === "|" && command[i + 1] === "|") {
94
+ parts.push(current.trim());
95
+ current = "";
96
+ i++;
97
+ } else if (ch === "&" && command[i + 1] === "&") {
98
+ parts.push(current.trim());
99
+ current = "";
100
+ i++;
101
+ } else if (ch === "|") {
102
+ parts.push(current.trim());
103
+ current = "";
104
+ } else {
105
+ current += ch;
106
+ }
107
+ } else {
108
+ current += ch;
109
+ }
110
+ }
111
+
112
+ if (current.trim()) parts.push(current.trim());
113
+ return parts.length > 0 ? parts : [command.trim()];
114
+ }
115
+
116
+ export function isMutatingShellScript(script: string): boolean {
117
+ return splitChainedCommands(script).some((segment) =>
118
+ isMutatingBash(segment),
119
+ );
120
+ }
121
+
122
+ function extractRegexCommands(code: string, patterns: RegExp[]): string[] {
123
+ const commands: string[] = [];
124
+ for (const pattern of patterns) {
125
+ pattern.lastIndex = 0;
126
+ let match = pattern.exec(code);
127
+ while (match !== null) {
128
+ const cmd = match[match.length - 1];
129
+ if (cmd) commands.push(cmd);
130
+ match = pattern.exec(code);
131
+ }
132
+ }
133
+ return commands;
134
+ }
135
+
136
+ export function hasJsTsFsMutation(code: string): boolean {
137
+ return JS_TS_FS_MUTATION_PATTERNS.some((pattern) => pattern.test(code));
138
+ }
139
+
140
+ function hasEmbeddedMutatingShell(code: string, language: string): boolean {
141
+ const lang = language.toLowerCase();
142
+ if (lang === "javascript" || lang === "typescript") {
143
+ const cmds = extractRegexCommands(code, JS_TS_SHELL_ESCAPE_PATTERNS);
144
+ return cmds.some((cmd) => isMutatingShellScript(cmd));
145
+ }
146
+ if (lang === "python") {
147
+ const cmds = extractRegexCommands(code, PYTHON_SHELL_ESCAPE_PATTERNS);
148
+ return cmds.some((cmd) => isMutatingShellScript(cmd));
149
+ }
150
+ return false;
151
+ }
152
+
153
+ function codeLooksMutating(language: string, code: string): boolean {
154
+ const lang = language.toLowerCase();
155
+ if (lang === "shell") {
156
+ return isMutatingShellScript(code);
157
+ }
158
+ if (lang === "javascript" || lang === "typescript") {
159
+ return hasJsTsFsMutation(code) || hasEmbeddedMutatingShell(code, lang);
160
+ }
161
+ if (lang === "python") {
162
+ return hasEmbeddedMutatingShell(code, lang);
163
+ }
164
+ return hasEmbeddedMutatingShell(code, lang);
165
+ }
166
+
167
+ function ctxExecuteLooksMutating(input: Record<string, unknown>): boolean {
168
+ const language = String(input.language ?? "javascript");
169
+ const code = String(input.code ?? "");
170
+ if (!code.trim()) return false;
171
+ return codeLooksMutating(language, code);
172
+ }
173
+
174
+ function ctxBatchExecuteLooksMutating(input: Record<string, unknown>): boolean {
175
+ const commands = input.commands;
176
+ if (!Array.isArray(commands)) return false;
177
+ for (const entry of commands) {
178
+ if (typeof entry === "string" && isMutatingShellScript(entry)) return true;
179
+ if (entry && typeof entry === "object") {
180
+ const cmd = String((entry as { command?: string }).command ?? "");
181
+ if (cmd && isMutatingShellScript(cmd)) return true;
182
+ }
183
+ }
184
+ return false;
185
+ }
186
+
187
+ function ctxExecuteFileLooksMutating(input: Record<string, unknown>): boolean {
188
+ const language = String(input.language ?? "javascript");
189
+ const code = String(input.code ?? "");
190
+ if (!code.trim()) return false;
191
+ return codeLooksMutating(language, code);
192
+ }
193
+
194
+ export function contextModeInputLooksMutating(
195
+ canonicalTool: string,
196
+ input: Record<string, unknown>,
197
+ ): boolean {
198
+ switch (canonicalTool) {
199
+ case "ctx_execute":
200
+ return ctxExecuteLooksMutating(input);
201
+ case "ctx_batch_execute":
202
+ return ctxBatchExecuteLooksMutating(input);
203
+ case "ctx_execute_file":
204
+ return ctxExecuteFileLooksMutating(input);
205
+ default:
206
+ return false;
207
+ }
208
+ }
209
+
210
+ export type ContextModePolicyDecision =
211
+ | { blocked: false }
212
+ | { blocked: true; reason: string };
213
+
214
+ export function evaluateContextModeMutation(
215
+ toolName: string,
216
+ input: Record<string, unknown>,
217
+ phase: HarnessPhase,
218
+ opts: {
219
+ aborted: boolean;
220
+ budgetBypass?: boolean;
221
+ readOnlyAgent?: boolean;
222
+ },
223
+ ): ContextModePolicyDecision {
224
+ const canonical = normalizeContextModeToolName(toolName);
225
+ if (!canonical) return { blocked: false };
226
+
227
+ if (opts.budgetBypass) return { blocked: false };
228
+
229
+ if (!contextModeInputLooksMutating(canonical, input)) {
230
+ return { blocked: false };
231
+ }
232
+
233
+ if (opts.aborted) {
234
+ return {
235
+ blocked: true,
236
+ reason:
237
+ "policy-gate: context-mode execute tool blocked because harness-abort lock is active. Attach a new approved plan first.",
238
+ };
239
+ }
240
+
241
+ if (opts.readOnlyAgent) {
242
+ return {
243
+ blocked: true,
244
+ reason: `policy-gate: ${canonical} mutating call blocked for read-only harness agent.`,
245
+ };
246
+ }
247
+
248
+ if (phase === "execute" || phase === "merge") {
249
+ return { blocked: false };
250
+ }
251
+
252
+ return {
253
+ blocked: true,
254
+ reason: `policy-gate: ${canonical} mutating call blocked in phase '${phase}'.`,
255
+ };
256
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Per-project harness enable/disable — `.pi/harness/project.json`.
3
+ * Default: enabled when the file is missing (backward compatible).
4
+ */
5
+
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+
9
+ export const HARNESS_PROJECT_CONFIG_BASENAME = "project.json";
10
+
11
+ export interface HarnessProjectConfig {
12
+ schema_version: "1.0.0";
13
+ enabled: boolean;
14
+ updated_at?: string;
15
+ }
16
+
17
+ export function harnessProjectConfigPath(projectRoot: string): string {
18
+ return join(projectRoot, ".pi", "harness", HARNESS_PROJECT_CONFIG_BASENAME);
19
+ }
20
+
21
+ function envOverrideEnabled(): boolean | null {
22
+ const raw = process.env.HARNESS_ENABLED?.trim().toLowerCase();
23
+ if (!raw) return null;
24
+ if (raw === "0" || raw === "false" || raw === "no") return false;
25
+ if (raw === "1" || raw === "true" || raw === "yes") return true;
26
+ return null;
27
+ }
28
+
29
+ export function readHarnessProjectConfig(
30
+ projectRoot: string = process.cwd(),
31
+ ): HarnessProjectConfig {
32
+ const fromEnv = envOverrideEnabled();
33
+ if (fromEnv !== null) {
34
+ return { schema_version: "1.0.0", enabled: fromEnv };
35
+ }
36
+
37
+ const path = harnessProjectConfigPath(projectRoot);
38
+ if (!existsSync(path)) {
39
+ return { schema_version: "1.0.0", enabled: true };
40
+ }
41
+
42
+ try {
43
+ const raw = JSON.parse(
44
+ readFileSync(path, "utf8"),
45
+ ) as Partial<HarnessProjectConfig>;
46
+ if (typeof raw.enabled === "boolean") {
47
+ return {
48
+ schema_version: "1.0.0",
49
+ enabled: raw.enabled,
50
+ updated_at: raw.updated_at,
51
+ };
52
+ }
53
+ } catch {
54
+ // corrupt file — treat as enabled so operators are not locked out
55
+ }
56
+
57
+ return { schema_version: "1.0.0", enabled: true };
58
+ }
59
+
60
+ export function isHarnessProjectEnabled(projectRoot?: string): boolean {
61
+ return readHarnessProjectConfig(projectRoot ?? process.cwd()).enabled;
62
+ }
63
+
64
+ export function writeHarnessProjectEnabled(
65
+ projectRoot: string,
66
+ enabled: boolean,
67
+ ): HarnessProjectConfig {
68
+ const path = harnessProjectConfigPath(projectRoot);
69
+ mkdirSync(dirname(path), { recursive: true });
70
+ const config: HarnessProjectConfig = {
71
+ schema_version: "1.0.0",
72
+ enabled,
73
+ updated_at: new Date().toISOString(),
74
+ };
75
+ writeFileSync(path, `${JSON.stringify(config, null, "\t")}\n`, "utf8");
76
+ return config;
77
+ }
78
+
79
+ /** Slash commands that stay available while governance is disabled. */
80
+ export const HARNESS_ALWAYS_ALLOWED_COMMANDS = new Set([
81
+ "harness-enable",
82
+ "harness-disable",
83
+ "harness-enabled-status",
84
+ "harness-setup",
85
+ ]);
86
+
87
+ export function isHarnessWorkflowCommand(command: string): boolean {
88
+ if (!command.startsWith("harness-")) return false;
89
+ if (HARNESS_ALWAYS_ALLOWED_COMMANDS.has(command)) return false;
90
+ return true;
91
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Build repair-brief.yaml from on-disk review artifacts (path-first, ADR 0043/0044).
3
+ */
4
+
5
+ import { join } from "node:path";
6
+ import { harnessRunsRoot } from "./harness-run-context.js";
7
+ import { readYamlFile } from "./harness-yaml.js";
8
+
9
+ const REPAIR_BRIEF_SCHEMA = "1.0.0";
10
+
11
+ function asRecord(v: unknown): Record<string, unknown> | null {
12
+ return v && typeof v === "object" && !Array.isArray(v)
13
+ ? (v as Record<string, unknown>)
14
+ : null;
15
+ }
16
+
17
+ function stringList(v: unknown): string[] {
18
+ if (!Array.isArray(v)) return [];
19
+ return v.map((x) => (typeof x === "string" ? x.trim() : "")).filter(Boolean);
20
+ }
21
+
22
+ async function readArtifactYaml(
23
+ runRoot: string,
24
+ rel: string,
25
+ label: string,
26
+ ): Promise<Record<string, unknown> | null> {
27
+ const trimmed = rel.replace(/\\/g, "/").replace(/^\.\//, "");
28
+ try {
29
+ return asRecord(await readYamlFile(join(runRoot, trimmed), label));
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export interface SynthesizeRepairBriefInput {
36
+ runId: string;
37
+ projectRoot: string;
38
+ steerAttempt: number;
39
+ reviewOutcomePath?: string;
40
+ evalVerdictPath?: string;
41
+ adversaryReportPath?: string;
42
+ planPacketPath?: string;
43
+ }
44
+
45
+ export async function synthesizeRepairBrief(
46
+ input: SynthesizeRepairBriefInput,
47
+ ): Promise<Record<string, unknown>> {
48
+ const runRoot = join(harnessRunsRoot(input.projectRoot), input.runId);
49
+ const review = await readArtifactYaml(
50
+ runRoot,
51
+ input.reviewOutcomePath ?? "artifacts/review-outcome.yaml",
52
+ "review-outcome",
53
+ );
54
+ const evalDoc = await readArtifactYaml(
55
+ runRoot,
56
+ input.evalVerdictPath ?? "artifacts/eval-verdict.yaml",
57
+ "eval-verdict",
58
+ );
59
+ const adversary = await readArtifactYaml(
60
+ runRoot,
61
+ input.adversaryReportPath ?? "artifacts/adversary-report.yaml",
62
+ "adversary-report",
63
+ );
64
+ const planRel =
65
+ input.planPacketPath?.replace(/\\/g, "/") ?? "plan-packet.yaml";
66
+ const plan = await readArtifactYaml(runRoot, planRel, "plan-packet");
67
+
68
+ const remediation =
69
+ (typeof review?.remediation_class === "string" &&
70
+ review.remediation_class) ||
71
+ "implementation_gap";
72
+
73
+ const sourceArtifacts: Record<string, string> = {
74
+ "review-outcome":
75
+ input.reviewOutcomePath ?? "artifacts/review-outcome.yaml",
76
+ };
77
+ if (evalDoc) {
78
+ sourceArtifacts["eval-verdict"] =
79
+ input.evalVerdictPath ?? "artifacts/eval-verdict.yaml";
80
+ }
81
+ if (adversary) {
82
+ sourceArtifacts["adversary-report"] =
83
+ input.adversaryReportPath ?? "artifacts/adversary-report.yaml";
84
+ }
85
+ if (plan) {
86
+ sourceArtifacts["plan-packet"] = planRel;
87
+ }
88
+
89
+ const failedIds = [
90
+ ...stringList(review?.failed_acceptance_check_ids),
91
+ ...stringList(evalDoc?.failed_acceptance_check_ids),
92
+ ...stringList(evalDoc?.failed_checks),
93
+ ];
94
+ const uniqueFailed = [...new Set(failedIds)];
95
+
96
+ const fixDirectives: string[] = [];
97
+ for (const key of [
98
+ "fix_directives",
99
+ "repair_directives",
100
+ "recommendations",
101
+ "required_fixes",
102
+ ]) {
103
+ fixDirectives.push(...stringList(review?.[key]));
104
+ fixDirectives.push(...stringList(adversary?.[key]));
105
+ fixDirectives.push(...stringList(evalDoc?.[key]));
106
+ }
107
+ if (typeof adversary?.summary === "string" && adversary.summary.trim()) {
108
+ fixDirectives.push(adversary.summary.trim());
109
+ }
110
+ if (typeof evalDoc?.summary === "string" && evalDoc.summary.trim()) {
111
+ fixDirectives.push(evalDoc.summary.trim());
112
+ }
113
+ const uniqueFixes = [...new Set(fixDirectives)];
114
+ if (uniqueFixes.length === 0) {
115
+ uniqueFixes.push(
116
+ "Address failures documented in review-outcome and eval-verdict; re-run acceptance checks.",
117
+ );
118
+ }
119
+
120
+ const execPlan = asRecord(plan?.execution_plan);
121
+ const priorityLakeIds = stringList(execPlan?.critical_path_lake_ids);
122
+ if (priorityLakeIds.length === 0) {
123
+ const lakes = Array.isArray(execPlan?.lakes) ? execPlan.lakes : [];
124
+ for (const lake of lakes) {
125
+ const l = asRecord(lake);
126
+ if (l && typeof l.id === "string") priorityLakeIds.push(l.id);
127
+ }
128
+ }
129
+
130
+ const brief: Record<string, unknown> = {
131
+ schema_version: REPAIR_BRIEF_SCHEMA,
132
+ run_id: input.runId,
133
+ steer_attempt: input.steerAttempt,
134
+ remediation_class: remediation,
135
+ source_artifacts: sourceArtifacts,
136
+ fix_directives: uniqueFixes,
137
+ };
138
+ if (uniqueFailed.length > 0) {
139
+ brief.failed_acceptance_check_ids = uniqueFailed;
140
+ }
141
+ if (priorityLakeIds.length > 0) {
142
+ brief.priority_lake_ids = [...new Set(priorityLakeIds)];
143
+ }
144
+ return brief;
145
+ }