ultimate-pi 0.20.0 → 0.22.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 (149) hide show
  1. package/.agents/skills/harness-context/SKILL.md +3 -3
  2. package/.agents/skills/harness-debate-plan/SKILL.md +2 -2
  3. package/.agents/skills/harness-decisions/SKILL.md +68 -2
  4. package/.agents/skills/harness-eval/SKILL.md +1 -1
  5. package/.agents/skills/harness-git-commit/SKILL.md +72 -0
  6. package/.agents/skills/harness-governor/SKILL.md +6 -6
  7. package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
  8. package/.agents/skills/harness-orchestration/SKILL.md +4 -4
  9. package/.agents/skills/harness-plan/SKILL.md +14 -12
  10. package/.agents/skills/harness-review/SKILL.md +3 -3
  11. package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
  12. package/.agents/skills/harness-sentrux-setup/SKILL.md +2 -2
  13. package/.agents/skills/harness-spec/SKILL.md +1 -1
  14. package/.agents/skills/harness-steer/SKILL.md +2 -2
  15. package/.agents/skills/posthog-analyst/SKILL.md +1 -1
  16. package/.agents/skills/sentrux/SKILL.md +6 -4
  17. package/.agents/skills/web-retrieval/SKILL.md +1 -1
  18. package/.agents/skills/wiki-save/SKILL.md +1 -1
  19. package/.pi/PACKAGING.md +6 -0
  20. package/.pi/SYSTEM.md +21 -3
  21. package/.pi/agents/harness/ls-lint-steward.md +49 -0
  22. package/.pi/agents/harness/planning/decompose.md +5 -5
  23. package/.pi/agents/harness/planning/execution-plan-author.md +1 -1
  24. package/.pi/agents/harness/planning/hypothesis-validator.md +1 -1
  25. package/.pi/agents/harness/planning/hypothesis.md +1 -1
  26. package/.pi/agents/harness/planning/plan-adversary.md +1 -1
  27. package/.pi/agents/harness/planning/plan-evaluator.md +2 -2
  28. package/.pi/agents/harness/planning/plan-synthesizer.md +2 -2
  29. package/.pi/agents/harness/planning/review-integrator.md +1 -1
  30. package/.pi/agents/harness/planning/sprint-contract-auditor.md +5 -5
  31. package/.pi/agents/harness/reviewing/evaluator.md +1 -1
  32. package/.pi/agents/harness/running/executor.md +2 -2
  33. package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
  34. package/.pi/agents/harness/sentrux-steward.md +2 -2
  35. package/.pi/agents/pi-pi/prompt-expert.md +17 -2
  36. package/.pi/auto-commit.json +9 -2
  37. package/.pi/extensions/debate-orchestrator.ts +3 -0
  38. package/.pi/extensions/harness-anchored-edit.ts +7 -9
  39. package/.pi/extensions/harness-ask-user.ts +13 -34
  40. package/.pi/extensions/harness-debate-tools.ts +43 -4
  41. package/.pi/extensions/harness-live-widget.ts +28 -19
  42. package/.pi/extensions/harness-run-context.ts +278 -115
  43. package/.pi/extensions/harness-web-tools.ts +598 -471
  44. package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
  45. package/.pi/extensions/observation-bus.ts +4 -0
  46. package/.pi/extensions/policy-gate.ts +270 -229
  47. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  48. package/.pi/extensions/soundboard.ts +48 -48
  49. package/.pi/harness/README.md +4 -0
  50. package/.pi/harness/agents.manifest.json +24 -16
  51. package/.pi/harness/agents.policy.yaml +49 -82
  52. package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
  53. package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
  54. package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
  55. package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
  56. package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
  57. package/.pi/harness/docs/adrs/README.md +5 -0
  58. package/.pi/harness/docs/practice-map.md +10 -5
  59. package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
  60. package/.pi/harness/evolution/self-healing-rules.json +16 -0
  61. package/.pi/harness/ls-lint/naming.manifest.json +128 -0
  62. package/.pi/harness/sentrux/architecture.manifest.json +1 -1
  63. package/.pi/harness/specs/auto-commit.schema.json +63 -0
  64. package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
  65. package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
  66. package/.pi/harness/specs/naming-manifest.schema.json +54 -0
  67. package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
  68. package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
  69. package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
  70. package/.pi/harness/specs/sentrux-report.schema.json +119 -0
  71. package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
  72. package/.pi/lib/agents-policy.d.mts +26 -51
  73. package/.pi/lib/agents-policy.mjs +41 -28
  74. package/.pi/lib/agt/build-evaluation-context.ts +136 -64
  75. package/.pi/lib/ask-user/constants.mjs +3 -0
  76. package/.pi/lib/ask-user/constants.ts +4 -0
  77. package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
  78. package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
  79. package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
  80. package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
  81. package/.pi/lib/ask-user/dialog.ts +2 -314
  82. package/.pi/lib/ask-user/fallback.ts +2 -78
  83. package/.pi/lib/ask-user/format.ts +85 -0
  84. package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
  85. package/.pi/lib/ask-user/index.ts +114 -0
  86. package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
  87. package/.pi/lib/ask-user/policy.mjs +43 -0
  88. package/.pi/lib/ask-user/policy.ts +104 -0
  89. package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
  90. package/.pi/lib/ask-user/presenters/headless.ts +131 -0
  91. package/.pi/lib/ask-user/presenters/select.ts +60 -0
  92. package/.pi/lib/ask-user/presenters/tui.ts +373 -0
  93. package/.pi/lib/ask-user/presenters/types.ts +13 -0
  94. package/.pi/lib/ask-user/render.ts +40 -9
  95. package/.pi/lib/ask-user/schema.ts +66 -13
  96. package/.pi/lib/ask-user/types.ts +60 -3
  97. package/.pi/lib/ask-user/validate-core.mjs +193 -7
  98. package/.pi/lib/ask-user/validate.ts +53 -34
  99. package/.pi/lib/harness-anchored-edit/package.json +3 -0
  100. package/.pi/lib/harness-artifact-gate.ts +75 -21
  101. package/.pi/lib/harness-auto-commit-config.mjs +321 -0
  102. package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
  103. package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
  104. package/.pi/lib/harness-lens/index.ts +241 -108
  105. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
  106. package/.pi/lib/harness-repair-brief.ts +84 -25
  107. package/.pi/lib/harness-run-context.ts +42 -52
  108. package/.pi/lib/harness-sentrux-parse.mjs +272 -0
  109. package/.pi/lib/harness-sentrux-root.mjs +78 -0
  110. package/.pi/lib/harness-slash-completions.ts +116 -0
  111. package/.pi/lib/harness-spawn-topology.ts +121 -87
  112. package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
  113. package/.pi/lib/harness-subagents-bridge.ts +4 -1
  114. package/.pi/lib/harness-ui-state.ts +95 -48
  115. package/.pi/lib/plan-approval/dialog.ts +5 -0
  116. package/.pi/lib/plan-approval/validate.ts +1 -1
  117. package/.pi/lib/plan-approval-readiness.ts +32 -0
  118. package/.pi/lib/plan-debate-gate.ts +154 -114
  119. package/.pi/lib/plan-task-clarification.ts +158 -0
  120. package/.pi/prompts/harness-auto.md +2 -2
  121. package/.pi/prompts/harness-ls-lint-steward.md +43 -0
  122. package/.pi/prompts/harness-plan.md +63 -13
  123. package/.pi/prompts/harness-review.md +44 -10
  124. package/.pi/prompts/harness-run.md +35 -13
  125. package/.pi/prompts/harness-sentrux-steward.md +2 -2
  126. package/.pi/prompts/harness-setup.md +74 -5
  127. package/.pi/prompts/harness-steer.md +6 -5
  128. package/.pi/prompts/wiki-save.md +5 -4
  129. package/.pi/scripts/README.md +8 -0
  130. package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
  131. package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
  132. package/.pi/scripts/harness-cli-verify.sh +47 -0
  133. package/.pi/scripts/harness-git-churn.mjs +77 -0
  134. package/.pi/scripts/harness-git-commit.mjs +173 -0
  135. package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
  136. package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
  137. package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
  138. package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
  139. package/.pi/scripts/harness-sentrux-report.mjs +256 -0
  140. package/.pi/scripts/harness-verify.mjs +361 -125
  141. package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
  142. package/.pi/scripts/run-tests.mjs +1 -0
  143. package/.pi/settings.example.json +1 -0
  144. package/.sentrux/rules.toml +1 -1
  145. package/AGENTS.md +2 -0
  146. package/CHANGELOG.md +32 -0
  147. package/README.md +13 -4
  148. package/package.json +13 -6
  149. package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Optional git churn map (path → commit count) for Sentrux hotspot scoring.
4
+ *
5
+ * Usage:
6
+ * node harness-git-churn.mjs [--root <PROJECT_ROOT>] [--days 14]
7
+ */
8
+
9
+ import { spawn } from "node:child_process";
10
+ import { resolveSentruxProjectRoot, takeRootArg } from "../lib/harness-sentrux-root.mjs";
11
+
12
+ function runGit(args, cwd) {
13
+ return new Promise((resolve, reject) => {
14
+ const child = spawn("git", args, { cwd, env: process.env });
15
+ let stdout = "";
16
+ let stderr = "";
17
+ child.stdout?.on("data", (c) => {
18
+ stdout += c.toString();
19
+ });
20
+ child.stderr?.on("data", (c) => {
21
+ stderr += c.toString();
22
+ });
23
+ child.on("error", reject);
24
+ child.on("close", (code) => {
25
+ if (code !== 0) {
26
+ reject(new Error(stderr.trim() || `git exit ${code}`));
27
+ return;
28
+ }
29
+ resolve(stdout);
30
+ });
31
+ });
32
+ }
33
+
34
+ /**
35
+ * @param {string} projectRoot
36
+ * @param {{ days?: number }} opts
37
+ * @returns {Promise<Record<string, number>>}
38
+ */
39
+ export async function loadGitChurn(projectRoot, opts = {}) {
40
+ const days = opts.days ?? 14;
41
+ const since = `${days} days ago`;
42
+ const stdout = await runGit(
43
+ [
44
+ "log",
45
+ `--since=${since}`,
46
+ "--name-only",
47
+ "--pretty=format:",
48
+ ],
49
+ projectRoot,
50
+ );
51
+ const counts = {};
52
+ for (const line of stdout.split(/\r?\n/)) {
53
+ const path = line.trim();
54
+ if (!path || path.startsWith(".")) continue;
55
+ counts[path] = (counts[path] || 0) + 1;
56
+ }
57
+ return counts;
58
+ }
59
+
60
+ async function main() {
61
+ const { args, explicitRoot } = takeRootArg(process.argv.slice(2));
62
+ let days = 14;
63
+ for (let i = 0; i < args.length; i++) {
64
+ if (args[i] === "--days") days = Number.parseInt(args[++i] || "14", 10);
65
+ }
66
+ const root = await resolveSentruxProjectRoot(explicitRoot);
67
+ const map = await loadGitChurn(root, { days });
68
+ process.stdout.write(`${JSON.stringify(map, null, 2)}\n`);
69
+ }
70
+
71
+ const isMain = process.argv[1]?.endsWith("harness-git-churn.mjs");
72
+ if (isMain) {
73
+ main().catch((err) => {
74
+ console.error(err.message || err);
75
+ process.exit(1);
76
+ });
77
+ }
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Git commit with merged .pi/auto-commit.json message format + co-author trailer.
4
+ *
5
+ * Usage:
6
+ * node "$UP_PKG/.pi/scripts/harness-git-commit.mjs" \
7
+ * --subject "..." [--type fix] [--scope app] [--body "..."] \
8
+ * [--message "full body"] [--amend] [--print-message] [--root DIR] \
9
+ * [--dry-run] [--no-verify] [--signoff]
10
+ *
11
+ * Does not run git add — stage files first.
12
+ */
13
+
14
+ import { writeFile, unlink, mkdtemp, access, constants } from "node:fs/promises";
15
+ import { tmpdir } from "node:os";
16
+ import { join, dirname } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { spawn } from "node:child_process";
19
+ import {
20
+ buildFullCommitMessage,
21
+ resolveAutoCommitConfig,
22
+ } from "../lib/harness-auto-commit-config.mjs";
23
+
24
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
25
+ const UP_PKG = join(SCRIPT_DIR, "..", "..");
26
+
27
+ function parseArgs(argv) {
28
+ const flags = new Set();
29
+ const opts = {};
30
+ const positional = [];
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const a = argv[i];
33
+ if (a === "--dry-run") flags.add("dry-run");
34
+ else if (a === "--print-message") flags.add("print-message");
35
+ else if (a === "--amend") flags.add("amend");
36
+ else if (a === "--no-verify") flags.add("no-verify");
37
+ else if (a === "--signoff") flags.add("signoff");
38
+ else if (a === "--subject" && argv[i + 1]) opts.subject = argv[++i];
39
+ else if (a === "--type" && argv[i + 1]) opts.type = argv[++i];
40
+ else if (a === "--scope" && argv[i + 1]) opts.scope = argv[++i];
41
+ else if (a === "--body" && argv[i + 1]) opts.body = argv[++i];
42
+ else if (a === "--message" && argv[i + 1]) opts.message = argv[++i];
43
+ else if (a === "--root" && argv[i + 1]) opts.root = argv[++i];
44
+ else if (a.startsWith("-")) {
45
+ console.error(`harness-git-commit: unknown flag ${a}`);
46
+ process.exit(1);
47
+ } else positional.push(a);
48
+ }
49
+ return { flags, opts, positional };
50
+ }
51
+
52
+ function runGit(args, cwd) {
53
+ return new Promise((resolve, reject) => {
54
+ const child = spawn("git", args, {
55
+ cwd,
56
+ env: process.env,
57
+ shell: false,
58
+ stdio: ["ignore", "pipe", "pipe"],
59
+ });
60
+ let stdout = "";
61
+ let stderr = "";
62
+ child.stdout?.on("data", (c) => {
63
+ stdout += c.toString();
64
+ });
65
+ child.stderr?.on("data", (c) => {
66
+ stderr += c.toString();
67
+ });
68
+ child.on("error", reject);
69
+ child.on("close", (code) => {
70
+ if (code !== 0) {
71
+ reject(new Error(stderr.trim() || stdout.trim() || `git exit ${code}`));
72
+ return;
73
+ }
74
+ resolve(stdout);
75
+ });
76
+ });
77
+ }
78
+
79
+ async function fileExists(path) {
80
+ try {
81
+ await access(path, constants.R_OK);
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ async function resolveUpPkg() {
89
+ const resolver = join(SCRIPT_DIR, "harness-resolve-up-pkg.mjs");
90
+ if (await fileExists(resolver)) {
91
+ const { spawnSync } = await import("node:child_process");
92
+ const r = spawnSync(process.execPath, [resolver], {
93
+ encoding: "utf-8",
94
+ shell: false,
95
+ });
96
+ if (r.status === 0 && r.stdout?.trim()) {
97
+ return r.stdout.trim();
98
+ }
99
+ }
100
+ return UP_PKG;
101
+ }
102
+
103
+ async function main() {
104
+ const { flags, opts, positional } = parseArgs(process.argv.slice(2));
105
+ const projectRoot = opts.root ?? positional[0] ?? process.cwd();
106
+ const upPkg = await resolveUpPkg();
107
+
108
+ const projectConfigPath = join(projectRoot, ".pi", "auto-commit.json");
109
+ if (!(await fileExists(projectConfigPath))) {
110
+ console.warn(
111
+ "harness-git-commit: hint — run node \"$UP_PKG/.pi/scripts/harness-auto-commit-bootstrap.mjs\" to seed .pi/auto-commit.json",
112
+ );
113
+ }
114
+
115
+ const config = await resolveAutoCommitConfig(projectRoot, upPkg);
116
+ if (config.dryRun === true && !flags.has("dry-run")) {
117
+ console.warn(
118
+ "harness-git-commit: warning — dryRun is true in config; pass --dry-run explicitly to preview without committing",
119
+ );
120
+ }
121
+
122
+ const dryRun =
123
+ flags.has("dry-run") ||
124
+ process.env.HARNESS_GIT_COMMIT_DRY_RUN === "1";
125
+
126
+ let input = {
127
+ type: opts.type,
128
+ scope: opts.scope,
129
+ subject: opts.subject,
130
+ body: opts.body,
131
+ message: opts.message,
132
+ };
133
+
134
+ if (flags.has("amend") && !input.message) {
135
+ const prior = await runGit(
136
+ ["log", "-1", "--format=%B"],
137
+ projectRoot,
138
+ ).catch(() => "");
139
+ if (prior.trim()) {
140
+ input = { message: prior.trim() };
141
+ }
142
+ }
143
+
144
+ const fullMessage = buildFullCommitMessage(config, input);
145
+
146
+ if (flags.has("print-message") || dryRun) {
147
+ process.stdout.write(`${fullMessage}\n`);
148
+ return;
149
+ }
150
+
151
+ await runGit(["rev-parse", "--git-dir"], projectRoot);
152
+
153
+ const tmpDir = await mkdtemp(join(tmpdir(), "harness-git-commit-"));
154
+ const msgFile = join(tmpDir, "COMMIT_EDITMSG");
155
+ try {
156
+ await writeFile(msgFile, `${fullMessage}\n`, "utf-8");
157
+ const gitArgs = ["commit", "-F", msgFile];
158
+ if (flags.has("amend")) gitArgs.push("--amend");
159
+ if (flags.has("no-verify")) gitArgs.push("--no-verify");
160
+ if (flags.has("signoff")) gitArgs.push("--signoff");
161
+
162
+ const out = await runGit(gitArgs, projectRoot);
163
+ if (out.trim()) process.stdout.write(out);
164
+ console.log("harness-git-commit: committed with co-author trailer");
165
+ } finally {
166
+ await unlink(msgFile).catch(() => {});
167
+ }
168
+ }
169
+
170
+ main().catch((err) => {
171
+ console.error(`harness-git-commit: ${err.message}`);
172
+ process.exit(1);
173
+ });
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Idempotent ls-lint naming bootstrap for harness projects.
4
+ *
5
+ * 1. Seeds `.pi/harness/ls-lint/naming.manifest.json` from the package template when missing
6
+ * 2. Personalizes `project` on first seed from target package.json / directory name
7
+ * 3. Runs `ls-lint-rules-sync.mjs` (merge-safe; preserves custom YAML outside managed markers)
8
+ *
9
+ * Usage:
10
+ * node "$UP_PKG/.pi/scripts/harness-ls-lint-bootstrap.mjs" [PROJECT_ROOT] [--force] [--check]
11
+ */
12
+
13
+ import { readFile, writeFile, mkdir, access, copyFile } from "node:fs/promises";
14
+ import { constants } from "node:fs";
15
+ import { join, dirname, basename } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { spawn } from "node:child_process";
18
+
19
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
20
+ const UP_PKG = join(SCRIPT_DIR, "..", "..");
21
+
22
+ const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
23
+ const flags = process.argv.slice(2).filter((a) => a.startsWith("-"));
24
+ const force = flags.includes("--force");
25
+ const checkOnly = flags.includes("--check");
26
+
27
+ const PROJECT_ROOT = args[0] || process.cwd();
28
+ const MANIFEST = join(
29
+ PROJECT_ROOT,
30
+ ".pi",
31
+ "harness",
32
+ "ls-lint",
33
+ "naming.manifest.json",
34
+ );
35
+ const MANIFEST_TEMPLATE = join(
36
+ UP_PKG,
37
+ ".pi",
38
+ "harness",
39
+ "ls-lint",
40
+ "naming.manifest.json",
41
+ );
42
+ const SYNC_SCRIPT = join(SCRIPT_DIR, "ls-lint-rules-sync.mjs");
43
+
44
+ async function fileExists(path) {
45
+ try {
46
+ await access(path, constants.R_OK);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ async function resolveProjectName(root) {
54
+ const pkgPath = join(root, "package.json");
55
+ if (await fileExists(pkgPath)) {
56
+ try {
57
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
58
+ if (typeof pkg.name === "string" && pkg.name.trim()) {
59
+ return pkg.name.trim();
60
+ }
61
+ } catch {
62
+ /* ignore */
63
+ }
64
+ }
65
+ return basename(root) || "project";
66
+ }
67
+
68
+ async function seedManifestIfMissing() {
69
+ if (await fileExists(MANIFEST)) {
70
+ return { seeded: false };
71
+ }
72
+ if (!(await fileExists(MANIFEST_TEMPLATE))) {
73
+ console.error(
74
+ "harness-ls-lint-bootstrap: missing package template",
75
+ MANIFEST_TEMPLATE,
76
+ );
77
+ process.exit(1);
78
+ }
79
+ await mkdir(dirname(MANIFEST), { recursive: true });
80
+ await copyFile(MANIFEST_TEMPLATE, MANIFEST);
81
+ const projectName = await resolveProjectName(PROJECT_ROOT);
82
+ const manifest = JSON.parse(await readFile(MANIFEST, "utf-8"));
83
+ manifest.project = projectName;
84
+ await writeFile(MANIFEST, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
85
+ console.log(
86
+ `harness-ls-lint-bootstrap: seeded manifest -> ${MANIFEST} (project: ${projectName})`,
87
+ );
88
+ return { seeded: true, projectName };
89
+ }
90
+
91
+ function runSync(extraArgs) {
92
+ return new Promise((resolve) => {
93
+ const child = spawn(
94
+ process.execPath,
95
+ [SYNC_SCRIPT, ...extraArgs, PROJECT_ROOT],
96
+ {
97
+ cwd: PROJECT_ROOT,
98
+ stdio: ["ignore", "pipe", "pipe"],
99
+ env: process.env,
100
+ },
101
+ );
102
+ let out = "";
103
+ child.stdout?.on("data", (d) => {
104
+ out += d.toString();
105
+ });
106
+ child.stderr?.on("data", (d) => {
107
+ out += d.toString();
108
+ });
109
+ child.on("close", (code) => resolve({ code: code ?? 1, out: out.trim() }));
110
+ child.on("error", (err) =>
111
+ resolve({ code: 1, out: String(err.message) }),
112
+ );
113
+ });
114
+ }
115
+
116
+ async function main() {
117
+ const { seeded } = await seedManifestIfMissing();
118
+ if (!seeded) {
119
+ console.log(
120
+ "harness-ls-lint-bootstrap: manifest present (edit naming rules there, then re-run with --force)",
121
+ );
122
+ }
123
+
124
+ const syncArgs = [];
125
+ if (checkOnly) syncArgs.push("--check");
126
+ else if (force) syncArgs.push("--force");
127
+
128
+ const { code, out } = await runSync(syncArgs);
129
+ if (out) console.log(out);
130
+ if (code !== 0) process.exit(code);
131
+
132
+ if (!checkOnly && !force) {
133
+ console.log(
134
+ 'harness-ls-lint-bootstrap: done (idempotent). After manifest edits: node "$UP_PKG/.pi/scripts/harness-ls-lint-bootstrap.mjs" --force',
135
+ );
136
+ }
137
+ }
138
+
139
+ main().catch((err) => {
140
+ console.error(err);
141
+ process.exit(1);
142
+ });
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run ls-lint against the harness project root.
4
+ *
5
+ * ls-lint reads `.ls-lint.yml` from the working directory. Harness commands must
6
+ * find the nearest ancestor with harness naming config and run from that root.
7
+ *
8
+ * Usage:
9
+ * node "$UP_PKG/.pi/scripts/harness-ls-lint-cli.mjs" [--root <PROJECT_ROOT>]
10
+ * node "$UP_PKG/.pi/scripts/harness-ls-lint-cli.mjs" --print-root
11
+ * node "$UP_PKG/.pi/scripts/harness-ls-lint-cli.mjs" --json
12
+ */
13
+
14
+ import { access } from "node:fs/promises";
15
+ import { constants } from "node:fs";
16
+ import { dirname, isAbsolute, join, resolve } from "node:path";
17
+ import { spawn, execSync } from "node:child_process";
18
+
19
+ const ROOT_MARKERS = [
20
+ ".ls-lint.yml",
21
+ join(".pi", "harness", "ls-lint", "naming.manifest.json"),
22
+ ];
23
+
24
+ async function fileExists(path) {
25
+ try {
26
+ await access(path, constants.R_OK);
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ async function hasRootMarker(dir) {
34
+ for (const marker of ROOT_MARKERS) {
35
+ if (await fileExists(join(dir, marker))) return true;
36
+ }
37
+ return false;
38
+ }
39
+
40
+ async function findProjectRoot(startDir) {
41
+ let dir = resolve(startDir || process.cwd());
42
+ while (true) {
43
+ if (await hasRootMarker(dir)) return dir;
44
+ const parent = dirname(dir);
45
+ if (parent === dir) return null;
46
+ dir = parent;
47
+ }
48
+ }
49
+
50
+ function takeRootArg(args) {
51
+ const next = [];
52
+ let explicitRoot = process.env.HARNESS_PROJECT_ROOT || "";
53
+ for (let i = 0; i < args.length; i++) {
54
+ const arg = args[i];
55
+ if (arg === "--root") {
56
+ explicitRoot = args[i + 1] || "";
57
+ i++;
58
+ continue;
59
+ }
60
+ if (arg.startsWith("--root=")) {
61
+ explicitRoot = arg.slice("--root=".length);
62
+ continue;
63
+ }
64
+ next.push(arg);
65
+ }
66
+ return { args: next, explicitRoot };
67
+ }
68
+
69
+ async function resolveProjectRoot(explicitRoot) {
70
+ if (explicitRoot) {
71
+ const root = isAbsolute(explicitRoot)
72
+ ? resolve(explicitRoot)
73
+ : resolve(process.cwd(), explicitRoot);
74
+ if (!(await hasRootMarker(root))) {
75
+ console.error(
76
+ `harness-ls-lint-cli: ${root} has no .ls-lint.yml or .pi/harness/ls-lint/naming.manifest.json`,
77
+ );
78
+ process.exit(1);
79
+ }
80
+ return root;
81
+ }
82
+
83
+ const root = await findProjectRoot(process.cwd());
84
+ if (!root) {
85
+ console.error(
86
+ "harness-ls-lint-cli: could not find a harness project root above the current directory",
87
+ );
88
+ process.exit(1);
89
+ }
90
+ return root;
91
+ }
92
+
93
+ function countViolations(output) {
94
+ const m = output.match(/(\d+)\s+violations?/i);
95
+ if (m) return Number.parseInt(m[1], 10);
96
+ if (/0\s+violations/i.test(output) || /no\s+violations/i.test(output)) {
97
+ return 0;
98
+ }
99
+ return output ? 1 : 0;
100
+ }
101
+
102
+ function lintPathEnv() {
103
+ const extra = [
104
+ process.env.PATH,
105
+ `${process.env.HOME || ""}/.local/bin`,
106
+ ].filter(Boolean);
107
+ try {
108
+ const npmBin = execSync("npm prefix -g", { encoding: "utf-8" }).trim();
109
+ extra.push(`${npmBin}/bin`);
110
+ } catch {
111
+ /* ignore */
112
+ }
113
+ return { ...process.env, PATH: extra.join(":") };
114
+ }
115
+
116
+ async function runLsLint(projectRoot) {
117
+ return new Promise((resolve) => {
118
+ const child = spawn("ls-lint", [], {
119
+ cwd: projectRoot,
120
+ stdio: ["ignore", "pipe", "pipe"],
121
+ env: lintPathEnv(),
122
+ });
123
+ let out = "";
124
+ child.stdout?.on("data", (d) => {
125
+ out += d.toString();
126
+ });
127
+ child.stderr?.on("data", (d) => {
128
+ out += d.toString();
129
+ });
130
+ child.on("close", (code) => {
131
+ resolve({ code: code ?? 1, out: out.trim() });
132
+ });
133
+ child.on("error", (err) => {
134
+ if (err?.code === "ENOENT") {
135
+ resolve({ code: 127, out: "ls-lint not installed" });
136
+ return;
137
+ }
138
+ resolve({ code: 1, out: String(err.message) });
139
+ });
140
+ });
141
+ }
142
+
143
+ async function main() {
144
+ const parsed = takeRootArg(process.argv.slice(2));
145
+ const printRoot = parsed.args.includes("--print-root");
146
+ const jsonOut = parsed.args.includes("--json");
147
+ const projectRoot = await resolveProjectRoot(parsed.explicitRoot);
148
+
149
+ if (printRoot) {
150
+ console.log(projectRoot);
151
+ return;
152
+ }
153
+
154
+ const { code, out } = await runLsLint(projectRoot);
155
+
156
+ if (jsonOut) {
157
+ const payload = {
158
+ lint_pass: code === 0,
159
+ violation_count: code === 0 ? 0 : countViolations(out),
160
+ status:
161
+ code === 127
162
+ ? "not_installed"
163
+ : code === 0
164
+ ? "pass"
165
+ : "fail",
166
+ quality_signal_summary: out.slice(0, 500) || (code === 0 ? "pass" : "fail"),
167
+ project_root: projectRoot,
168
+ };
169
+ console.log(JSON.stringify(payload));
170
+ process.exit(code === 127 ? 0 : code);
171
+ }
172
+
173
+ if (out) {
174
+ if (code === 0) console.log(out);
175
+ else console.error(out);
176
+ }
177
+
178
+ process.exit(code === 127 ? 127 : code);
179
+ }
180
+
181
+ main().catch((err) => {
182
+ console.error(err);
183
+ process.exit(1);
184
+ });
@@ -15,6 +15,7 @@ import { copyFile, mkdir, readdir, access } from "node:fs/promises";
15
15
  import { constants } from "node:fs";
16
16
  import { join, dirname } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
+ import { spawn } from "node:child_process";
18
19
 
19
20
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
20
21
  const UP_PKG = join(SCRIPT_DIR, "..", "..");
@@ -26,6 +27,13 @@ const SENTRUX_TEMPLATE = join(
26
27
  "sentrux",
27
28
  "architecture.manifest.json",
28
29
  );
30
+ const LS_LINT_TEMPLATE = join(
31
+ UP_PKG,
32
+ ".pi",
33
+ "harness",
34
+ "ls-lint",
35
+ "naming.manifest.json",
36
+ );
29
37
 
30
38
  const projectRoot = process.argv[2] || process.cwd();
31
39
  const specDest = join(projectRoot, ".pi", "harness", "specs");
@@ -36,6 +44,13 @@ const sentruxDest = join(
36
44
  "sentrux",
37
45
  "architecture.manifest.json",
38
46
  );
47
+ const lsLintDest = join(
48
+ projectRoot,
49
+ ".pi",
50
+ "harness",
51
+ "ls-lint",
52
+ "naming.manifest.json",
53
+ );
39
54
 
40
55
  async function fileExists(path) {
41
56
  try {
@@ -73,6 +88,38 @@ async function main() {
73
88
  `harness-seed-project-contracts: seeded Sentrux manifest -> ${sentruxDest} (run harness-sentrux-bootstrap.mjs to sync rules.toml)`,
74
89
  );
75
90
  }
91
+
92
+ if (!(await fileExists(lsLintDest)) && (await fileExists(LS_LINT_TEMPLATE))) {
93
+ await mkdir(dirname(lsLintDest), { recursive: true });
94
+ await copyFile(LS_LINT_TEMPLATE, lsLintDest);
95
+ console.log(
96
+ `harness-seed-project-contracts: seeded ls-lint manifest -> ${lsLintDest} (run harness-ls-lint-bootstrap.mjs to sync .ls-lint.yml)`,
97
+ );
98
+ }
99
+
100
+ const autoCommitBootstrap = join(
101
+ UP_PKG,
102
+ ".pi",
103
+ "scripts",
104
+ "harness-auto-commit-bootstrap.mjs",
105
+ );
106
+ const autoCommitDest = join(projectRoot, ".pi", "auto-commit.json");
107
+ if (
108
+ !(await fileExists(autoCommitDest)) &&
109
+ (await fileExists(autoCommitBootstrap))
110
+ ) {
111
+ await new Promise((resolve, reject) => {
112
+ const child = spawn(
113
+ process.execPath,
114
+ [autoCommitBootstrap, projectRoot],
115
+ { cwd: projectRoot, stdio: "inherit", env: process.env },
116
+ );
117
+ child.on("error", reject);
118
+ child.on("close", (code) =>
119
+ code === 0 ? resolve() : reject(new Error(`bootstrap exit ${code}`)),
120
+ );
121
+ });
122
+ }
76
123
  }
77
124
 
78
125
  main().catch((err) => {