supipowers 2.0.2 → 2.1.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 (76) hide show
  1. package/README.md +5 -6
  2. package/package.json +4 -2
  3. package/skills/harness/SKILL.md +1 -0
  4. package/src/bootstrap.ts +5 -133
  5. package/src/config/defaults.ts +5 -5
  6. package/src/config/loader.ts +1 -0
  7. package/src/config/schema.ts +2 -6
  8. package/src/context-mode/knowledge/store.ts +381 -43
  9. package/src/context-mode/tools.ts +41 -3
  10. package/src/deps/registry.ts +1 -12
  11. package/src/fix-pr/assessment.ts +1 -0
  12. package/src/fix-pr/prompt-builder.ts +1 -0
  13. package/src/git/commit.ts +76 -18
  14. package/src/harness/command.ts +103 -6
  15. package/src/harness/default-agents/docs.md +39 -0
  16. package/src/harness/docs/config.ts +29 -0
  17. package/src/harness/docs/glob-match.ts +27 -0
  18. package/src/harness/docs/index-renderer.ts +82 -0
  19. package/src/harness/docs/provenance.ts +125 -0
  20. package/src/harness/docs/regen-decision.ts +167 -0
  21. package/src/harness/docs/representative-files.ts +175 -0
  22. package/src/harness/docs/source-hash.ts +106 -0
  23. package/src/harness/docs/validator.ts +233 -0
  24. package/src/harness/hooks/layer-context-inject.ts +35 -1
  25. package/src/harness/hooks/register.ts +24 -3
  26. package/src/harness/pipeline.ts +20 -5
  27. package/src/harness/pr-comment/baseline.ts +105 -0
  28. package/src/harness/pr-comment/ci-env.ts +120 -0
  29. package/src/harness/pr-comment/gh-poster.ts +227 -0
  30. package/src/harness/pr-comment/handler.ts +198 -0
  31. package/src/harness/pr-comment/render.ts +297 -0
  32. package/src/harness/pr-comment/status.ts +95 -0
  33. package/src/harness/pr-comment/types.ts +73 -0
  34. package/src/harness/pr-comment/workflow-summary.ts +47 -0
  35. package/src/harness/project-paths.ts +95 -0
  36. package/src/harness/stages/design.ts +1 -0
  37. package/src/harness/stages/discover.ts +1 -13
  38. package/src/harness/stages/docs.ts +708 -0
  39. package/src/harness/stages/implement-apply.ts +877 -0
  40. package/src/harness/stages/implement.ts +64 -51
  41. package/src/harness/stages/plan.ts +25 -16
  42. package/src/harness/stages/validate.ts +370 -0
  43. package/src/harness/storage.ts +142 -0
  44. package/src/harness/tools.ts +130 -0
  45. package/src/mempalace/bridge.ts +207 -41
  46. package/src/mempalace/config.ts +10 -4
  47. package/src/mempalace/format.ts +122 -6
  48. package/src/mempalace/hooks.ts +204 -56
  49. package/src/mempalace/installer-helper.ts +18 -4
  50. package/src/mempalace/python/mempalace_bridge.py +128 -3
  51. package/src/mempalace/runtime.ts +53 -16
  52. package/src/mempalace/schema.ts +151 -30
  53. package/src/mempalace/session-summary.ts +5 -0
  54. package/src/mempalace/tool.ts +17 -4
  55. package/src/mempalace/upstream-limits.ts +69 -0
  56. package/src/planning/approval-flow.ts +25 -2
  57. package/src/planning/planning-ask-tool.ts +34 -4
  58. package/src/planning/system-prompt.ts +1 -1
  59. package/src/tool-catalog/active-tool-controller.ts +0 -22
  60. package/src/tool-catalog/active-tool-planner.ts +0 -26
  61. package/src/tool-catalog/tool-groups.ts +1 -9
  62. package/src/types.ts +87 -8
  63. package/src/ui-design/session.ts +114 -8
  64. package/src/utils/executable.ts +10 -1
  65. package/src/workspace/state-paths.ts +1 -1
  66. package/src/commands/mcp.ts +0 -814
  67. package/src/mcp/activation.ts +0 -77
  68. package/src/mcp/config.ts +0 -223
  69. package/src/mcp/docs.ts +0 -154
  70. package/src/mcp/gateway.ts +0 -103
  71. package/src/mcp/lifecycle.ts +0 -79
  72. package/src/mcp/manager-tool.ts +0 -104
  73. package/src/mcp/mcpc.ts +0 -113
  74. package/src/mcp/registry.ts +0 -98
  75. package/src/mcp/triggers.ts +0 -62
  76. package/src/mcp/types.ts +0 -95
@@ -0,0 +1,877 @@
1
+ /**
2
+ * IMPLEMENT stage — programmatic apply.
3
+ *
4
+ * Single deterministic pass that materializes every Tier 1 artifact from the design spec.
5
+ * Replaces the previous agent-handoff model: the harness now writes its own outputs the
6
+ * same way `/supi:checks` runs its gates, so a single `/supi:harness` invocation drives
7
+ * discover → research → design → plan → implement → docs → validate end-to-end.
8
+ *
9
+ * Every applier is idempotent: rerunning over an already-installed repo compares existing
10
+ * content to the desired bytes and reports `"skipped"`. Side-effecting installers (fallow,
11
+ * desloppify) inherit the apply/skip contract from `anti_slop/installer.ts`.
12
+ *
13
+ * Failures are aggregated and returned alongside the partial result so the stage runner
14
+ * can surface a structured blocker without losing track of what already landed.
15
+ */
16
+
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+
20
+ import type { Platform, PlatformPaths } from "../../platform/types.js";
21
+ import type {
22
+ HarnessAntiSlopBackend,
23
+ HarnessDesignSpec,
24
+ HarnessSlopQueueEntry,
25
+ } from "../../types.js";
26
+ import { renderAgentsMd } from "../artifacts/agents-md.js";
27
+ import {
28
+ renderArchitectureMd,
29
+ renderGoldenPrinciplesMd,
30
+ } from "../artifacts/docs-tree.js";
31
+ import { renderHarnessArchitectureReviewAgent } from "../artifacts/review-agents.js";
32
+ import {
33
+ renderEvalConfig,
34
+ renderLintConfig,
35
+ renderStructuralTestConfig,
36
+ } from "../artifacts/lint-configs.js";
37
+ import { buildChecksWiringPatch } from "../artifacts/checks-wiring.js";
38
+ import { writeMarker } from "../bare-entry.js";
39
+ import { computeScore } from "../anti_slop/score.js";
40
+ import {
41
+ ensureDesloppifyGitignore,
42
+ installFallow,
43
+ distributeAgentSkills,
44
+ } from "../anti_slop/installer.js";
45
+ import { readSlopQueue, saveHarnessRepoScore } from "../storage.js";
46
+ import {
47
+ getHarnessAgentsMdPath,
48
+ getHarnessArchitectureDocPath,
49
+ getHarnessGoldenPrinciplesPath,
50
+ getHarnessQueuePath,
51
+ getHarnessRepoLocalDir,
52
+ } from "../project-paths.js";
53
+ import { getLocalStatePath } from "../../workspace/state-paths.js";
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Public types.
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export interface ApplyHarnessPlanInput {
60
+ platform: Platform;
61
+ paths: PlatformPaths;
62
+ cwd: string;
63
+ spec: HarnessDesignSpec;
64
+ /** When false, every applier reports what *would* happen without touching disk. */
65
+ apply?: boolean;
66
+ }
67
+
68
+ export type ApplyAction = "wrote" | "skipped" | "patched" | "noop";
69
+
70
+ export interface ApplyResult {
71
+ step: string;
72
+ path: string;
73
+ action: ApplyAction;
74
+ detail?: string;
75
+ }
76
+
77
+ export interface ApplyError {
78
+ step: string;
79
+ message: string;
80
+ }
81
+
82
+ export interface ApplyOutcome {
83
+ applied: ApplyResult[];
84
+ warnings: string[];
85
+ errors: ApplyError[];
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Public entry point.
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * Apply every Tier 1 artifact described by the design spec. The set of artifacts mirrors
94
+ * `buildHarnessPlanTasks(spec)` exactly so the rendered plan and the actual file actions
95
+ * stay in lock-step.
96
+ *
97
+ * The function never throws on a single applier failure — failures are captured in
98
+ * `errors[]` and the rest of the pipeline continues so a broken anti-slop install does
99
+ * not stop the docs from landing. The caller (stage runner) decides whether to surface
100
+ * the aggregated `errors` as a blocker.
101
+ */
102
+ export async function applyHarnessPlan(
103
+ input: ApplyHarnessPlanInput,
104
+ ): Promise<ApplyOutcome> {
105
+ const apply = input.apply !== false;
106
+ const outcome: ApplyOutcome = { applied: [], warnings: [], errors: [] };
107
+
108
+ const steps: Array<() => Promise<void> | void> = [
109
+ () => applyArchitectureDoc(input, outcome, apply),
110
+ () => applyGoldenPrinciplesDoc(input, outcome, apply),
111
+ () => applyLintConfig(input, outcome, apply),
112
+ () => applyStructuralTestConfig(input, outcome, apply),
113
+ () => applyEvalConfig(input, outcome, apply),
114
+ () => applyPackageJsonScript(input, outcome, apply),
115
+ () => applyCiWorkflow(input, outcome, apply),
116
+ () => applyAntiSlopBackend(input, outcome, apply),
117
+ () => applyHarnessMarker(input, outcome, apply),
118
+ () => applySlopQueueInit(input, outcome, apply),
119
+ () => applyScorecardSkeleton(input, outcome, apply),
120
+ () => applyReviewAgent(input, outcome, apply),
121
+ () => applyChecksGateWiring(input, outcome, apply),
122
+ () => applyAgentsMd(input, outcome, apply),
123
+ ];
124
+
125
+ for (const step of steps) {
126
+ try {
127
+ await step();
128
+ } catch (error) {
129
+ outcome.errors.push({
130
+ step: "(unhandled)",
131
+ message: error instanceof Error ? error.message : String(error),
132
+ });
133
+ }
134
+ }
135
+
136
+ return outcome;
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Generic write helpers.
141
+ // ---------------------------------------------------------------------------
142
+
143
+ interface DesiredFile {
144
+ step: string;
145
+ absPath: string;
146
+ /** Repo-relative or canonical path used in the result + audit log. */
147
+ reportPath: string;
148
+ contents: string;
149
+ }
150
+
151
+ function writeIfChanged(
152
+ desired: DesiredFile,
153
+ apply: boolean,
154
+ outcome: ApplyOutcome,
155
+ ): void {
156
+ let existing: string | null = null;
157
+ if (fs.existsSync(desired.absPath)) {
158
+ try {
159
+ existing = fs.readFileSync(desired.absPath, "utf8");
160
+ } catch (error) {
161
+ outcome.warnings.push(
162
+ `${desired.step}: unable to read existing ${desired.reportPath}: ${describe(error)}`,
163
+ );
164
+ }
165
+ }
166
+
167
+ if (existing === desired.contents) {
168
+ outcome.applied.push({
169
+ step: desired.step,
170
+ path: desired.reportPath,
171
+ action: "skipped",
172
+ detail: "up-to-date",
173
+ });
174
+ return;
175
+ }
176
+
177
+ if (!apply) {
178
+ outcome.applied.push({
179
+ step: desired.step,
180
+ path: desired.reportPath,
181
+ action: "noop",
182
+ detail: existing === null ? "would create" : "would update",
183
+ });
184
+ return;
185
+ }
186
+
187
+ try {
188
+ fs.mkdirSync(path.dirname(desired.absPath), { recursive: true });
189
+ fs.writeFileSync(desired.absPath, desired.contents);
190
+ outcome.applied.push({
191
+ step: desired.step,
192
+ path: desired.reportPath,
193
+ action: "wrote",
194
+ detail: existing === null ? "created" : "updated",
195
+ });
196
+ } catch (error) {
197
+ outcome.errors.push({
198
+ step: desired.step,
199
+ message: `failed to write ${desired.reportPath}: ${describe(error)}`,
200
+ });
201
+ }
202
+ }
203
+
204
+ function describe(error: unknown): string {
205
+ return error instanceof Error ? error.message : String(error);
206
+ }
207
+
208
+ function repoRelative(cwd: string, absPath: string): string {
209
+ const rel = path.relative(cwd, absPath);
210
+ return rel.split(path.sep).join("/");
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Per-task appliers.
215
+ // ---------------------------------------------------------------------------
216
+
217
+ function applyArchitectureDoc(
218
+ input: ApplyHarnessPlanInput,
219
+ outcome: ApplyOutcome,
220
+ apply: boolean,
221
+ ): void {
222
+ const absPath = getHarnessArchitectureDocPath(input.paths, input.cwd);
223
+ writeIfChanged(
224
+ {
225
+ step: "Write docs/architecture.md",
226
+ absPath,
227
+ reportPath: repoRelative(input.cwd, absPath),
228
+ contents: renderArchitectureMd({ spec: input.spec }),
229
+ },
230
+ apply,
231
+ outcome,
232
+ );
233
+ }
234
+
235
+ function applyGoldenPrinciplesDoc(
236
+ input: ApplyHarnessPlanInput,
237
+ outcome: ApplyOutcome,
238
+ apply: boolean,
239
+ ): void {
240
+ const absPath = getHarnessGoldenPrinciplesPath(input.paths, input.cwd);
241
+ writeIfChanged(
242
+ {
243
+ step: "Write docs/golden-principles.md",
244
+ absPath,
245
+ reportPath: repoRelative(input.cwd, absPath),
246
+ contents: renderGoldenPrinciplesMd({ spec: input.spec }),
247
+ },
248
+ apply,
249
+ outcome,
250
+ );
251
+ }
252
+
253
+ function applyLintConfig(
254
+ input: ApplyHarnessPlanInput,
255
+ outcome: ApplyOutcome,
256
+ apply: boolean,
257
+ ): void {
258
+ const tool = input.spec.tooling.lint;
259
+ if (!tool) return;
260
+ const rendered = renderLintConfig({ tool, languages: [] });
261
+ if (!rendered) {
262
+ outcome.warnings.push(
263
+ `lint tool "${tool}" has no canonical template; skipping config emit. Run /supi:harness design to pick a supported tool.`,
264
+ );
265
+ return;
266
+ }
267
+ const absPath = path.join(input.cwd, rendered.filename);
268
+ writeIfChanged(
269
+ {
270
+ step: `Wire lint tool (${tool})`,
271
+ absPath,
272
+ reportPath: rendered.filename,
273
+ contents: rendered.content,
274
+ },
275
+ apply,
276
+ outcome,
277
+ );
278
+ }
279
+
280
+ function applyStructuralTestConfig(
281
+ input: ApplyHarnessPlanInput,
282
+ outcome: ApplyOutcome,
283
+ apply: boolean,
284
+ ): void {
285
+ const tool = input.spec.tooling.structuralTest;
286
+ if (!tool) return;
287
+ const rendered = renderStructuralTestConfig({ tool });
288
+ if (!rendered) {
289
+ outcome.warnings.push(
290
+ `structural-test tool "${tool}" has no canonical template; skipping config emit.`,
291
+ );
292
+ return;
293
+ }
294
+ const absPath = path.join(input.cwd, rendered.filename);
295
+ writeIfChanged(
296
+ {
297
+ step: `Wire structural test tool (${tool})`,
298
+ absPath,
299
+ reportPath: rendered.filename,
300
+ contents: rendered.content,
301
+ },
302
+ apply,
303
+ outcome,
304
+ );
305
+ }
306
+
307
+ function applyEvalConfig(
308
+ input: ApplyHarnessPlanInput,
309
+ outcome: ApplyOutcome,
310
+ apply: boolean,
311
+ ): void {
312
+ const tool = input.spec.tooling.eval;
313
+ if (!tool) return;
314
+ const rendered = renderEvalConfig({ tool });
315
+ if (!rendered) {
316
+ outcome.warnings.push(
317
+ `eval tool "${tool}" has no canonical template; skipping config emit.`,
318
+ );
319
+ return;
320
+ }
321
+ const absPath = path.join(input.cwd, rendered.filename);
322
+ writeIfChanged(
323
+ {
324
+ step: `Wire eval framework (${tool})`,
325
+ absPath,
326
+ reportPath: rendered.filename,
327
+ contents: rendered.content,
328
+ },
329
+ apply,
330
+ outcome,
331
+ );
332
+ }
333
+
334
+ /**
335
+ * Extract the npm-script name from a `bun run X` / `npm run X` / `pnpm X` / `yarn X`
336
+ * command. Mirrors the parser in validate.ts so the wiring + the validator agree on what
337
+ * counts as a wired script.
338
+ */
339
+ function scriptNameFromLocalCommand(command: string): string | null {
340
+ const trimmed = command.trim();
341
+ const runMatch = /^(?:bun|npm|pnpm|yarn)\s+run\s+([^\s]+)$/.exec(trimmed);
342
+ if (runMatch) return runMatch[1];
343
+ const pnpmOrYarnMatch = /^(?:pnpm|yarn)\s+([^\s]+)$/.exec(trimmed);
344
+ if (pnpmOrYarnMatch) return pnpmOrYarnMatch[1];
345
+ return null;
346
+ }
347
+
348
+ function applyPackageJsonScript(
349
+ input: ApplyHarnessPlanInput,
350
+ outcome: ApplyOutcome,
351
+ apply: boolean,
352
+ ): void {
353
+ const step = "Wire local harness quality command";
354
+ const scriptName = scriptNameFromLocalCommand(input.spec.ci.localCommand);
355
+ if (!scriptName) {
356
+ outcome.warnings.push(
357
+ `${step}: local command "${input.spec.ci.localCommand}" is not a package-script invocation; skipping package.json wiring.`,
358
+ );
359
+ return;
360
+ }
361
+ const packageJsonPath = path.join(input.cwd, "package.json");
362
+ if (!fs.existsSync(packageJsonPath)) {
363
+ outcome.warnings.push(
364
+ `${step}: package.json not found at repo root; cannot wire script "${scriptName}".`,
365
+ );
366
+ return;
367
+ }
368
+
369
+ let raw: string;
370
+ try {
371
+ raw = fs.readFileSync(packageJsonPath, "utf8");
372
+ } catch (error) {
373
+ outcome.errors.push({ step, message: `unable to read package.json: ${describe(error)}` });
374
+ return;
375
+ }
376
+ let pkg: Record<string, unknown>;
377
+ try {
378
+ pkg = JSON.parse(raw) as Record<string, unknown>;
379
+ } catch (error) {
380
+ outcome.errors.push({ step, message: `package.json is invalid JSON: ${describe(error)}` });
381
+ return;
382
+ }
383
+
384
+ const scripts = (pkg.scripts && typeof pkg.scripts === "object"
385
+ ? { ...(pkg.scripts as Record<string, unknown>) }
386
+ : {}) as Record<string, unknown>;
387
+ const desired = buildHarnessQualityScript(input.spec);
388
+ if (scripts[scriptName] === desired) {
389
+ outcome.applied.push({
390
+ step,
391
+ path: "package.json",
392
+ action: "skipped",
393
+ detail: `scripts.${scriptName} already wired`,
394
+ });
395
+ return;
396
+ }
397
+ if (!apply) {
398
+ outcome.applied.push({
399
+ step,
400
+ path: "package.json",
401
+ action: "noop",
402
+ detail: `would set scripts.${scriptName}`,
403
+ });
404
+ return;
405
+ }
406
+ scripts[scriptName] = desired;
407
+ const next = { ...pkg, scripts };
408
+ const indent = detectJsonIndent(raw);
409
+ const serialized = `${JSON.stringify(next, null, indent)}\n`;
410
+ try {
411
+ fs.writeFileSync(packageJsonPath, serialized);
412
+ outcome.applied.push({
413
+ step,
414
+ path: "package.json",
415
+ action: "patched",
416
+ detail: `set scripts.${scriptName}`,
417
+ });
418
+ } catch (error) {
419
+ outcome.errors.push({ step, message: `failed to write package.json: ${describe(error)}` });
420
+ }
421
+ }
422
+
423
+ /**
424
+ * The harness quality command runs lint + structural-test + eval gates if configured.
425
+ * Each one is best-effort: a missing tool produces a single-line warning, not a hard fail.
426
+ */
427
+ function buildHarnessQualityScript(spec: HarnessDesignSpec): string {
428
+ const parts: string[] = [];
429
+ if (spec.tooling.lint) parts.push(`${spec.tooling.lint} .`);
430
+ if (spec.tooling.structuralTest) parts.push(spec.tooling.structuralTest);
431
+ if (spec.tooling.eval) parts.push(spec.tooling.eval);
432
+ if (parts.length === 0) {
433
+ return "echo 'harness:quality has no gates configured; edit design-spec.json to add tooling.'";
434
+ }
435
+ return parts.join(" && ");
436
+ }
437
+
438
+ function detectJsonIndent(raw: string): number {
439
+ const match = /^\{\s*\n([ \t]+)/.exec(raw);
440
+ if (!match) return 2;
441
+ const ws = match[1];
442
+ if (ws.startsWith("\t")) return 2; // bun's JSON.stringify uses spaces; keep that.
443
+ return ws.length || 2;
444
+ }
445
+
446
+ function applyCiWorkflow(
447
+ input: ApplyHarnessPlanInput,
448
+ outcome: ApplyOutcome,
449
+ apply: boolean,
450
+ ): void {
451
+ const step = "Wire CI harness quality workflow";
452
+ const workflowRel = input.spec.ci.workflowPath;
453
+ if (input.spec.ci.provider !== "github-actions") {
454
+ outcome.warnings.push(
455
+ `${step}: provider "${input.spec.ci.provider}" has no canonical workflow template; skipping CI workflow emit.`,
456
+ );
457
+ return;
458
+ }
459
+ const absPath = path.join(input.cwd, workflowRel);
460
+ const content = renderGithubActionsWorkflow(input.spec);
461
+ writeIfChanged(
462
+ {
463
+ step,
464
+ absPath,
465
+ reportPath: workflowRel,
466
+ contents: content,
467
+ },
468
+ apply,
469
+ outcome,
470
+ );
471
+ }
472
+
473
+ function renderGithubActionsWorkflow(spec: HarnessDesignSpec): string {
474
+ const trigger = spec.ci.trigger;
475
+ const onBlock = trigger.mode === "all-prs"
476
+ ? ["on:", " pull_request:", " branches: ['**']"]
477
+ : ["on:", " pull_request:", ` branches: [${trigger.branches.map((b) => `'${b}'`).join(", ")}]`];
478
+ const setupNode = [
479
+ " - uses: actions/checkout@v4",
480
+ " - uses: oven-sh/setup-bun@v2",
481
+ " with:",
482
+ " bun-version: latest",
483
+ ];
484
+ const installStep = " - run: bun install";
485
+ const runStep = ` - run: ${spec.ci.localCommand}`;
486
+ return [
487
+ "# Generated by /supi:harness.",
488
+ "name: Harness Quality",
489
+ ...onBlock,
490
+ "permissions:",
491
+ " contents: read",
492
+ " pull-requests: write",
493
+ "jobs:",
494
+ " harness-quality:",
495
+ " runs-on: ubuntu-latest",
496
+ " steps:",
497
+ ...setupNode,
498
+ installStep,
499
+ runStep,
500
+ "",
501
+ ].join("\n");
502
+ }
503
+
504
+ async function applyAntiSlopBackend(
505
+ input: ApplyHarnessPlanInput,
506
+ outcome: ApplyOutcome,
507
+ apply: boolean,
508
+ ): Promise<void> {
509
+ const backend = input.spec.antiSlop.backend;
510
+ const entryPoints = collectEntryPoints(input.cwd);
511
+
512
+ if (backend === "fallow" || backend === "hybrid") {
513
+ const step = "Install fallow + write .fallowrc.json";
514
+ const result = await installFallow(input.paths, {
515
+ cwd: input.cwd,
516
+ backend,
517
+ layerRules: input.spec.layerRules,
518
+ entryPoints,
519
+ skillTargets: input.spec.antiSlop.skillTargets,
520
+ apply,
521
+ });
522
+ foldInstallResult(step, result, outcome);
523
+ }
524
+
525
+ if (backend === "desloppify" || backend === "hybrid") {
526
+ const gitignoreStep = "Install desloppify (gitignore)";
527
+ const gitignoreResult = await ensureDesloppifyGitignore({ cwd: input.cwd, apply });
528
+ foldInstallResult(gitignoreStep, gitignoreResult, outcome);
529
+
530
+ if (input.spec.antiSlop.skillTargets.length > 0) {
531
+ const skillStep = "Distribute agent-skills";
532
+ const skillResult = await distributeAgentSkills(input.platform, {
533
+ cwd: input.cwd,
534
+ targets: input.spec.antiSlop.skillTargets,
535
+ apply,
536
+ });
537
+ foldInstallResult(skillStep, skillResult, outcome);
538
+ }
539
+ }
540
+
541
+ if (backend === "supi-native") {
542
+ outcome.applied.push({
543
+ step: "Install anti-slop backend",
544
+ path: "(none)",
545
+ action: "noop",
546
+ detail: "supi-native backend has no external installer",
547
+ });
548
+ }
549
+ }
550
+
551
+ function foldInstallResult(
552
+ step: string,
553
+ result: { ok: boolean; actions: string[]; warnings: string[] },
554
+ outcome: ApplyOutcome,
555
+ ): void {
556
+ for (const action of result.actions) {
557
+ outcome.applied.push({
558
+ step,
559
+ path: "(installer)",
560
+ action: action.startsWith("wrote") || action.startsWith("appended")
561
+ ? "wrote"
562
+ : action.includes("already")
563
+ ? "skipped"
564
+ : "noop",
565
+ detail: action,
566
+ });
567
+ }
568
+ for (const warning of result.warnings) {
569
+ outcome.warnings.push(`${step}: ${warning}`);
570
+ }
571
+ if (!result.ok) {
572
+ outcome.errors.push({
573
+ step,
574
+ message: `installer reported failure: ${result.warnings.join("; ") || "see warnings"}`,
575
+ });
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Walk the repo's top-level package manifest to pick reasonable entry points. The fallow
581
+ * installer accepts any non-empty array; we err on the side of "every top-level src dir"
582
+ * so the audit covers user code without needing manual edits.
583
+ */
584
+ function collectEntryPoints(cwd: string): readonly string[] {
585
+ const candidates = ["src", "lib", "app", "packages"];
586
+ const found = candidates.filter((dir) => {
587
+ try {
588
+ return fs.statSync(path.join(cwd, dir)).isDirectory();
589
+ } catch {
590
+ return false;
591
+ }
592
+ });
593
+ return found.length > 0 ? found : ["."];
594
+ }
595
+
596
+ function applyHarnessMarker(
597
+ input: ApplyHarnessPlanInput,
598
+ outcome: ApplyOutcome,
599
+ apply: boolean,
600
+ ): void {
601
+ const step = "Enable repo-local anti-slop hooks";
602
+ const reportPath = ".omp/supipowers/harness/marker.json";
603
+ const desired = {
604
+ installedAt: new Date(0).toISOString(), // overridden below; only used for diff identity
605
+ backend: input.spec.antiSlop.backend,
606
+ notes: [
607
+ `Generated by /supi:harness session ${input.spec.sessionId}.`,
608
+ `Run /supi:harness status to inspect; /supi:harness validate to verify.`,
609
+ ],
610
+ } satisfies { installedAt: string; backend: HarnessAntiSlopBackend; notes: string[] };
611
+
612
+ // Idempotency: if a marker already exists with the same backend + notes, skip the
613
+ // write entirely so commit history stays clean across rebuilds.
614
+ const markerPath = path.join(getHarnessRepoLocalDir(input.paths, input.cwd), "marker.json");
615
+ let existingBackend: string | null = null;
616
+ if (fs.existsSync(markerPath)) {
617
+ try {
618
+ const parsed = JSON.parse(fs.readFileSync(markerPath, "utf8")) as { backend?: string };
619
+ existingBackend = parsed.backend ?? null;
620
+ } catch {
621
+ existingBackend = null;
622
+ }
623
+ }
624
+ if (existingBackend === desired.backend) {
625
+ outcome.applied.push({
626
+ step,
627
+ path: reportPath,
628
+ action: "skipped",
629
+ detail: `marker.json already records backend=${desired.backend}`,
630
+ });
631
+ return;
632
+ }
633
+ if (!apply) {
634
+ outcome.applied.push({
635
+ step,
636
+ path: reportPath,
637
+ action: "noop",
638
+ detail: existingBackend === null ? "would create marker.json" : "would update marker.json",
639
+ });
640
+ return;
641
+ }
642
+
643
+ const result = writeMarker(input.paths, input.cwd, {
644
+ installedAt: new Date().toISOString(),
645
+ backend: desired.backend,
646
+ notes: desired.notes,
647
+ });
648
+ if (!result.ok) {
649
+ outcome.errors.push({ step, message: result.message });
650
+ return;
651
+ }
652
+ outcome.applied.push({
653
+ step,
654
+ path: reportPath,
655
+ action: existingBackend === null ? "wrote" : "patched",
656
+ detail: `backend=${desired.backend}`,
657
+ });
658
+ }
659
+
660
+ function applySlopQueueInit(
661
+ input: ApplyHarnessPlanInput,
662
+ outcome: ApplyOutcome,
663
+ apply: boolean,
664
+ ): void {
665
+ const step = "Initialize slop queue";
666
+ const queuePath = getHarnessQueuePath(input.paths, input.cwd);
667
+ if (fs.existsSync(queuePath)) {
668
+ outcome.applied.push({
669
+ step,
670
+ path: "queue.jsonl",
671
+ action: "skipped",
672
+ detail: "queue already exists",
673
+ });
674
+ return;
675
+ }
676
+ if (!apply) {
677
+ outcome.applied.push({
678
+ step,
679
+ path: "queue.jsonl",
680
+ action: "noop",
681
+ detail: "would create empty queue.jsonl",
682
+ });
683
+ return;
684
+ }
685
+ try {
686
+ fs.mkdirSync(path.dirname(queuePath), { recursive: true });
687
+ fs.writeFileSync(queuePath, "");
688
+ outcome.applied.push({
689
+ step,
690
+ path: "queue.jsonl",
691
+ action: "wrote",
692
+ detail: "created empty queue",
693
+ });
694
+ } catch (error) {
695
+ outcome.errors.push({ step, message: `failed to create queue.jsonl: ${describe(error)}` });
696
+ }
697
+ }
698
+
699
+ function applyScorecardSkeleton(
700
+ input: ApplyHarnessPlanInput,
701
+ outcome: ApplyOutcome,
702
+ apply: boolean,
703
+ ): void {
704
+ const step = "Generate scorecard skeleton + README badge";
705
+ const queueResult = readSlopQueue(input.paths, input.cwd);
706
+ const entries: HarnessSlopQueueEntry[] = queueResult.ok ? queueResult.value : [];
707
+ const score = computeScore({
708
+ computedAt: new Date().toISOString(),
709
+ entries,
710
+ });
711
+ const reportPath = ".omp/supipowers/harness/score.json";
712
+
713
+ // Idempotency uses the score *shape* (everything but computedAt), so re-runs against
714
+ // an unchanged queue produce no diff.
715
+ const existing = readExistingScore(input);
716
+ if (existing && scoresEquivalent(existing, score)) {
717
+ outcome.applied.push({
718
+ step,
719
+ path: reportPath,
720
+ action: "skipped",
721
+ detail: `score lenient=${score.lenient} strict=${score.strict} unchanged`,
722
+ });
723
+ return;
724
+ }
725
+ if (!apply) {
726
+ outcome.applied.push({
727
+ step,
728
+ path: reportPath,
729
+ action: "noop",
730
+ detail: existing === null ? "would create score.json" : "would refresh score.json",
731
+ });
732
+ return;
733
+ }
734
+ const saved = saveHarnessRepoScore(input.paths, input.cwd, score);
735
+ if (!saved.ok) {
736
+ outcome.errors.push({ step, message: saved.error.message });
737
+ return;
738
+ }
739
+ outcome.applied.push({
740
+ step,
741
+ path: reportPath,
742
+ action: existing === null ? "wrote" : "patched",
743
+ detail: `lenient=${score.lenient} strict=${score.strict}`,
744
+ });
745
+ }
746
+
747
+ interface ScoreShape {
748
+ lenient: number;
749
+ strict: number;
750
+ dimensions: unknown[];
751
+ }
752
+
753
+ function readExistingScore(input: ApplyHarnessPlanInput): ScoreShape | null {
754
+ const scorePath = path.join(
755
+ getHarnessRepoLocalDir(input.paths, input.cwd),
756
+ "score.json",
757
+ );
758
+ if (!fs.existsSync(scorePath)) return null;
759
+ try {
760
+ const parsed = JSON.parse(fs.readFileSync(scorePath, "utf8")) as Partial<ScoreShape>;
761
+ if (typeof parsed.lenient !== "number" || typeof parsed.strict !== "number") return null;
762
+ return {
763
+ lenient: parsed.lenient,
764
+ strict: parsed.strict,
765
+ dimensions: Array.isArray(parsed.dimensions) ? parsed.dimensions : [],
766
+ };
767
+ } catch {
768
+ return null;
769
+ }
770
+ }
771
+
772
+ function scoresEquivalent(a: ScoreShape, b: { lenient: number; strict: number; dimensions: unknown[] }): boolean {
773
+ if (a.lenient !== b.lenient || a.strict !== b.strict) return false;
774
+ return JSON.stringify(a.dimensions) === JSON.stringify(b.dimensions);
775
+ }
776
+
777
+ function applyReviewAgent(
778
+ input: ApplyHarnessPlanInput,
779
+ outcome: ApplyOutcome,
780
+ apply: boolean,
781
+ ): void {
782
+ if (!input.spec.supipowersWiring.addReviewAgent) return;
783
+ const absPath = getLocalStatePath(
784
+ input.paths,
785
+ input.cwd,
786
+ "review-agents",
787
+ "harness-architecture.md",
788
+ );
789
+ writeIfChanged(
790
+ {
791
+ step: "Add architecture-aware review agent",
792
+ absPath,
793
+ reportPath: ".omp/supipowers/review-agents/harness-architecture.md",
794
+ contents: renderHarnessArchitectureReviewAgent({ spec: input.spec }),
795
+ },
796
+ apply,
797
+ outcome,
798
+ );
799
+ }
800
+
801
+ /**
802
+ * The supipowers config schema does not yet model `harness.*` fields, so we cannot edit
803
+ * `.omp/supipowers/config.json` without breaking validation. Persist the wiring intent
804
+ * to a sidecar `checks-wiring.json` next to the marker; a future config-schema extension
805
+ * can promote it into the canonical config file.
806
+ */
807
+ function applyChecksGateWiring(
808
+ input: ApplyHarnessPlanInput,
809
+ outcome: ApplyOutcome,
810
+ apply: boolean,
811
+ ): void {
812
+ if (!input.spec.supipowersWiring.wireChecksGate) return;
813
+ const step = "Wire `/supi:checks` gate";
814
+ const reportPath = ".omp/supipowers/harness/checks-wiring.json";
815
+ const absPath = path.join(getHarnessRepoLocalDir(input.paths, input.cwd), "checks-wiring.json");
816
+ const patch = buildChecksWiringPatch({
817
+ backend: input.spec.antiSlop.backend,
818
+ strictFloor: input.spec.antiSlop.hooks.score_floor.strict,
819
+ releaseBlocking: input.spec.antiSlop.hooks.score_floor.release_blocking,
820
+ });
821
+ const contents = `${JSON.stringify(patch, null, 2)}\n`;
822
+ writeIfChanged({ step, absPath, reportPath, contents }, apply, outcome);
823
+ }
824
+
825
+ function applyAgentsMd(
826
+ input: ApplyHarnessPlanInput,
827
+ outcome: ApplyOutcome,
828
+ apply: boolean,
829
+ ): void {
830
+ const absPath = getHarnessAgentsMdPath(input.paths, input.cwd);
831
+ const backendLabel = backendDisplayLabel(input.spec.antiSlop.backend);
832
+ const contents = renderAgentsMd({
833
+ projectName: path.basename(input.cwd) || "project",
834
+ spec: input.spec,
835
+ backendLabel,
836
+ bootstrapHint: detectBootstrapHint(input.cwd),
837
+ });
838
+ writeIfChanged(
839
+ {
840
+ step: "Generate AGENTS.md",
841
+ absPath,
842
+ reportPath: "AGENTS.md",
843
+ contents,
844
+ },
845
+ apply,
846
+ outcome,
847
+ );
848
+ }
849
+
850
+ function backendDisplayLabel(backend: HarnessAntiSlopBackend): string {
851
+ switch (backend) {
852
+ case "fallow":
853
+ return "fallow";
854
+ case "desloppify":
855
+ return "desloppify";
856
+ case "hybrid":
857
+ return "fallow + desloppify (hybrid)";
858
+ case "supi-native":
859
+ return "supi-native";
860
+ }
861
+ }
862
+
863
+ function detectBootstrapHint(cwd: string): string | undefined {
864
+ if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) {
865
+ return "bun install && bun test";
866
+ }
867
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
868
+ return "pnpm install && pnpm test";
869
+ }
870
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
871
+ return "yarn && yarn test";
872
+ }
873
+ if (fs.existsSync(path.join(cwd, "package-lock.json"))) {
874
+ return "npm install && npm test";
875
+ }
876
+ return undefined;
877
+ }