total-recall-brain 3.40.0 → 3.40.2

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 (187) hide show
  1. package/README.md +152 -41
  2. package/bin/total-recall.mjs +16 -76
  3. package/docs/reference/cli-reference.md +10 -10
  4. package/extension/background.js +6 -2
  5. package/extension/config.js +8 -0
  6. package/extension/lib/brain-client.js +29 -4
  7. package/extension/options/options.html +1 -0
  8. package/extension/sidepanel/sidepanel.html +1 -0
  9. package/frontend/dist/assets/{index-CvQRdT2j.css → index-AGUes-VG.css} +1 -1
  10. package/frontend/dist/assets/index-Bt5iVPZj.js +5618 -0
  11. package/frontend/dist/index.html +2 -2
  12. package/frontend/src/api/keys.ts +9 -2
  13. package/frontend/src/api/plugins.ts +4 -0
  14. package/frontend/src/components/plugins/card/CardEngine.tsx +2 -1
  15. package/frontend/src/components/plugins/card/card.css +1 -1
  16. package/frontend/src/pages/IntegrationsPage.tsx +315 -8
  17. package/frontend/src/pages/PluginsPage.tsx +532 -87
  18. package/package.json +6 -5
  19. package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
  20. package/scaffold/.agent/skills/meta-harness/references/optimized/section-09.md +1 -1
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +10 -10
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -6
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +0 -1
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/src/browser-session.mjs +309 -4
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +44 -34
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/src/google-oauth.mjs +6 -4
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +1 -1
  28. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-rotation-recipes.mjs +118 -4
  29. package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/auth.mjs +2 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +3 -3
  31. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +24 -6
  33. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +164 -123
  34. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +65 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +76 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +19 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +290 -0
  38. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +446 -0
  39. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +152 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +71 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +36 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +35 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +175 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +18 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/code-quality/LICENSE +21 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/code-quality/README.md +84 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/code-quality/cli.mjs +79 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/check.mjs +6 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/report.mjs +6 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +27 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/snapshot.mjs +163 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +757 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/config.schema.json +86 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/detect.mjs +89 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +145 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +210 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/state.mjs +20 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +2869 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +28 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +186 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +18 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +272 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/code-quality-report.mjs +129 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/LICENSE +21 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/README.md +73 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/cli.mjs +375 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/echo-tool.mjs +23 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/group.mjs +262 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/project-id.mjs +74 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +2869 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +31 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +221 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/schemas/cli_command.schema.json +107 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/skills/composable-cli/SKILL.md +63 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/command.mjs +313 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compile.mjs +19 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compose.mjs +144 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/contract.mjs +110 -0
  79. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/library.mjs +23 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/registry.mjs +192 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/resolve.mjs +221 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/risk.mjs +126 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/ssss-documents.mjs +29 -0
  84. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +18 -0
  85. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +273 -0
  86. package/scaffold/.agent/skills/total-recall/plugins/embeddings/LICENSE +21 -0
  87. package/scaffold/.agent/skills/total-recall/plugins/embeddings/README.md +20 -0
  88. package/scaffold/.agent/skills/total-recall/plugins/embeddings/cli.mjs +310 -0
  89. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +2826 -0
  90. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +11 -0
  91. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +62 -0
  92. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +297 -0
  93. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/LICENSE +21 -0
  94. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +43 -0
  95. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/cli.mjs +258 -0
  96. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4005 -0
  97. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +20 -0
  98. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +125 -0
  99. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +18 -0
  100. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +278 -0
  101. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +9 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +6 -0
  103. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4721 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -21
  105. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +14 -1
  106. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +10 -0
  107. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/headscale-client.mjs +5 -1
  108. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +4 -13
  109. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-access-verify.mjs +22 -0
  110. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-files.mjs +18 -0
  111. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +2 -0
  112. package/scaffold/.agent/skills/total-recall/plugins/mesh/test-support/memory-root.mjs +14 -0
  113. package/scaffold/.agent/skills/total-recall/plugins/openwiki/LICENSE +21 -0
  114. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +35 -0
  115. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +124 -0
  116. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4097 -0
  117. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +12 -0
  118. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +165 -0
  119. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/ingest.mjs +127 -0
  120. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +18 -0
  121. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +279 -0
  122. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +539 -0
  123. package/scaffold/.agent/skills/total-recall/plugins/security/LICENSE +21 -0
  124. package/scaffold/.agent/skills/total-recall/plugins/security/README.md +67 -0
  125. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +932 -0
  126. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3256 -0
  127. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +25 -0
  128. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +297 -0
  129. package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +4 -0
  130. package/scaffold/.agent/skills/total-recall/plugins/security/skills/security/SKILL.md +37 -0
  131. package/scaffold/.agent/skills/total-recall/plugins/security/src/android.mjs +21 -0
  132. package/scaffold/.agent/skills/total-recall/plugins/security/src/devices.mjs +94 -0
  133. package/scaffold/.agent/skills/total-recall/plugins/security/src/machines.mjs +164 -0
  134. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +41 -0
  135. package/scaffold/.agent/skills/total-recall/plugins/security/src/windows.mjs +42 -0
  136. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +18 -0
  137. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +95 -0
  138. package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +98 -0
  139. package/scaffold/.agent/skills/total-recall/references/auth.md +1 -1
  140. package/src/cli/backup.mjs +71 -3
  141. package/src/cli/brief.mjs +33 -9
  142. package/src/cli/command.mjs +2 -7
  143. package/src/cli/config.mjs +1 -0
  144. package/src/cli/connect.mjs +5 -0
  145. package/src/cli/context.mjs +2 -1
  146. package/src/cli/daemon.mjs +36 -1
  147. package/src/cli/doctor.mjs +6 -0
  148. package/src/cli/harness.mjs +3 -0
  149. package/src/cli/help.mjs +26 -0
  150. package/src/cli/init.mjs +4 -4
  151. package/src/cli/memory-entry.mjs +1 -0
  152. package/src/cli/memory-init.mjs +2 -0
  153. package/src/cli/plugin/index.mjs +11 -0
  154. package/src/cli/plugin/install.mjs +2 -1
  155. package/src/cli/rebuild.mjs +3 -1
  156. package/src/cli/startup.mjs +2 -1
  157. package/src/cli/update.mjs +19 -0
  158. package/src/core/automatic-brain-updates.mjs +67 -0
  159. package/src/core/brain-updates.mjs +21 -2
  160. package/src/core/cli-commands.mjs +71 -0
  161. package/src/core/cli-maintenance.mjs +45 -0
  162. package/src/core/cli-quick-reference.mjs +20 -0
  163. package/src/core/cli-semantic-index.mjs +722 -0
  164. package/src/core/context-compiler.mjs +27 -1
  165. package/src/core/crons.mjs +2 -13
  166. package/src/core/daemon-control.mjs +6 -4
  167. package/src/core/daemon-loop.mjs +12 -8
  168. package/src/core/default-plugins.mjs +5 -0
  169. package/src/core/global-brain-updates.mjs +39 -0
  170. package/src/core/mesh-brain-updates.mjs +30 -0
  171. package/src/core/meta-harness.mjs +108 -0
  172. package/src/core/plugin-dependencies.mjs +2 -1
  173. package/src/core/plugin-loader.mjs +2 -1
  174. package/src/core/plugin-runner.mjs +12 -4
  175. package/src/core/plugin-setup.mjs +20 -0
  176. package/src/core/plugin-store.mjs +22 -4
  177. package/src/core/plugin-tasks.mjs +1 -1
  178. package/src/core/project-brain.mjs +2 -0
  179. package/src/core/project-status.mjs +64 -0
  180. package/src/core/required-plugins.mjs +46 -0
  181. package/src/core/scaffold-plugins.mjs +17 -3
  182. package/src/core/source-brain-updates.mjs +47 -0
  183. package/src/core/startup-health.mjs +5 -3
  184. package/src/core/surface.mjs +44 -10
  185. package/src/server/routes/extension.mjs +53 -8
  186. package/templates/onboarding-interview.md +1 -1
  187. package/frontend/dist/assets/index-SOVUlZqm.js +0 -5596
@@ -0,0 +1,86 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "code-quality repo configuration",
4
+ "description": "Repo-layer contract for the code-quality skill. Maintained with `total-recall skill config code-quality …`; config.json is its projection.",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["version", "toolchain", "checks"],
8
+ "properties": {
9
+ "version": { "const": 3 },
10
+ "toolchain": { "type": "string", "minLength": 1, "description": "Primary toolchain label shown in reports (javascript, typescript, python, …)" },
11
+ "repoDescription": { "type": "string", "description": "What quality means in this repo" },
12
+ "repoRoot": { "type": "string", "description": "Override the repo root (default: three levels above the skill)" },
13
+ "remoteNode": { "type": "string", "minLength": 1, "description": "Mesh node that runs the gates for the pre-push hook" },
14
+ "remoteRepo": { "type": "string", "minLength": 1, "description": "Absolute path of the source snapshot on that node (see `code-quality snapshot`)" },
15
+ "remotePath": { "type": "string", "description": "PATH prefix to use on that node, e.g. /opt/homebrew/bin" },
16
+ "globalLock": { "type": "boolean", "default": true, "description": "Take the machine-wide lock so only one check runs per host" },
17
+ "timeoutSeconds": { "type": "integer", "minimum": 1, "description": "Default per-check timeout" },
18
+ "memory": {
19
+ "type": "object",
20
+ "additionalProperties": false,
21
+ "properties": { "maxOldSpaceMb": { "type": "integer", "minimum": 256 } }
22
+ },
23
+ "lock": {
24
+ "type": "object",
25
+ "additionalProperties": false,
26
+ "properties": { "staleAfterMinutes": { "type": "integer", "minimum": 1 } }
27
+ },
28
+ "env": {
29
+ "type": "object",
30
+ "description": "Environment for every command check; ${REPO_ROOT} and ${env:NAME} expand",
31
+ "additionalProperties": { "type": "string" }
32
+ },
33
+ "sourceExtensions": {
34
+ "type": "array",
35
+ "items": { "type": "string", "pattern": "^\\.[A-Za-z0-9]+$" }
36
+ },
37
+ "checks": {
38
+ "type": "array",
39
+ "minItems": 1,
40
+ "x-collection": "gate",
41
+ "x-key": "id",
42
+ "items": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["id"],
46
+ "properties": {
47
+ "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]*$" },
48
+ "tier": { "type": "string", "enum": ["fast", "full", "remote"], "default": "fast" },
49
+ "kind": { "type": "string", "enum": ["command", "grep-forbid"] },
50
+ "description": { "type": "string" },
51
+ "cmd": {
52
+ "type": "array",
53
+ "minItems": 1,
54
+ "items": { "type": "string", "minLength": 1 },
55
+ "x-command": true
56
+ },
57
+ "cwd": { "type": "string" },
58
+ "env": { "type": "object", "additionalProperties": { "type": "string" } },
59
+ "parser": { "type": "string", "enum": ["tsc", "eslint-stylish", "eslint-compact", "biome", "flake8", "mypy", "black", "generic"] },
60
+ "timeoutSeconds": { "type": "integer", "minimum": 1 },
61
+ "remoteHint": { "type": "string" },
62
+ "extensions": { "type": "array", "items": { "type": "string", "pattern": "^\\.[A-Za-z0-9]+$" } },
63
+ "paths": { "type": "array", "items": { "type": "string" } },
64
+ "ignorePaths": { "type": "array", "items": { "type": "string" } },
65
+ "patterns": {
66
+ "type": "array",
67
+ "items": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "required": ["pattern"],
71
+ "properties": {
72
+ "pattern": { "type": "string", "minLength": 1 },
73
+ "flags": { "type": "string", "pattern": "^[dgimsuvy]*$" },
74
+ "code": { "type": "string" },
75
+ "message": { "type": "string" }
76
+ }
77
+ }
78
+ },
79
+ "scope": { "type": "string", "enum": ["all", "changed"] },
80
+ "baseline": { "type": "string" },
81
+ "fallbackViolationBaseline": { "type": "string" }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * code-quality / core/detect.mjs — probe a repo and propose a repo config.
4
+ *
5
+ * A bootstrap, not the source of truth. It reports what the repo actually has;
6
+ * the repo's real gate list (conformance suites, contract tests, grep gates for
7
+ * its own invariants) is repo knowledge added afterwards through the CLI:
8
+ *
9
+ * total-recall skill config code-quality detect # show the proposal
10
+ * total-recall skill config code-quality detect --apply # record it
11
+ * total-recall skill config code-quality gate add id=… tier=fast cmd='["npm","test"]'
12
+ *
13
+ * Run directly (`node detect.mjs`) it only prints facts and the proposal.
14
+ */
15
+
16
+ import { readFileSync, existsSync } from 'node:fs';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ export function detectFacts(repoRoot) {
21
+ const has = (p) => existsSync(path.join(repoRoot, p));
22
+ const readText = (p) => { try { return readFileSync(path.join(repoRoot, p), 'utf8'); } catch { return ''; } };
23
+ const readJson = (p) => { try { return JSON.parse(readText(p)); } catch { return null; } };
24
+ const pkg = readJson('package.json');
25
+ const scripts = pkg?.scripts || {};
26
+ const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
27
+ return {
28
+ scripts,
29
+ packageJson: !!pkg,
30
+ packageManager: has('pnpm-lock.yaml') ? 'pnpm' : has('yarn.lock') ? 'yarn' : pkg ? 'npm' : null,
31
+ workspace: has('pnpm-workspace.yaml') || !!pkg?.workspaces,
32
+ typescript: !!deps.typescript || has('tsconfig.json'),
33
+ tsProjectRefs: readJson('tsconfig.json')?.references?.length || 0,
34
+ eslint: !!deps.eslint || ['.eslintrc', '.eslintrc.json', '.eslintrc.cjs', 'eslint.config.js', 'eslint.config.mjs'].some(has),
35
+ biome: !!deps['@biomejs/biome'] || has('biome.json') || has('biome.jsonc'),
36
+ python: has('requirements.txt') || has('pyproject.toml') || has('setup.py'),
37
+ venvPython: has('.venv/bin/python'),
38
+ flake8: has('.flake8') || has('setup.cfg') || has('tox.ini'),
39
+ mypy: has('mypy.ini') || has('.mypy_cache'),
40
+ black: /\[tool\.black\]/.test(readText('pyproject.toml')),
41
+ ssssScripts: Object.keys(scripts).filter((k) => k.startsWith('ssss')),
42
+ };
43
+ }
44
+
45
+ /** Propose a config satisfying core/config.schema.json. */
46
+ export function detect({ repoRoot }) {
47
+ const facts = detectFacts(repoRoot);
48
+ const { scripts, packageManager: pm } = facts;
49
+ const run = (script) => (pm === 'pnpm' ? ['pnpm', 'run', script] : pm === 'yarn' ? ['yarn', script] : ['npm', 'run', script]);
50
+ const python = facts.venvPython ? '.venv/bin/python' : 'python3';
51
+ const checks = [];
52
+
53
+ if (facts.typescript && scripts.typecheck) {
54
+ checks.push({ id: 'types', tier: 'fast', cmd: run('typecheck'), parser: 'tsc' });
55
+ } else if (facts.typescript) {
56
+ checks.push({ id: 'types', tier: 'fast', cmd: ['npx', 'tsc', facts.tsProjectRefs > 0 ? '--build' : '--noEmit', '--pretty', 'false'], parser: 'tsc' });
57
+ }
58
+ if (facts.biome && scripts.lint) checks.push({ id: 'lint', tier: 'fast', cmd: run('lint'), parser: 'generic' });
59
+ else if (facts.eslint && scripts.lint) checks.push({ id: 'lint', tier: 'fast', cmd: run('lint'), parser: 'eslint-stylish' });
60
+ if (facts.flake8) checks.push({ id: 'flake8', tier: 'fast', cmd: [python, '-m', 'flake8', '.'], parser: 'flake8' });
61
+ if (facts.black) checks.push({ id: 'black-check', tier: 'fast', cmd: [python, '-m', 'black', '--check', '.'], parser: 'black' });
62
+ if (facts.mypy) checks.push({ id: 'mypy', tier: 'fast', cmd: [python, '-m', 'mypy', '.'], parser: 'mypy' });
63
+ for (const s of facts.ssssScripts) {
64
+ checks.push({ id: s.replace(/^ssss:/, 'ssss-').replace(/[^a-z0-9-]/g, '-'), tier: s.includes('validate') ? 'fast' : 'full', cmd: run(s), parser: 'generic' });
65
+ }
66
+ if (scripts.test) checks.push({ id: 'test', tier: 'full', cmd: run('test'), parser: 'generic' });
67
+
68
+ return {
69
+ version: 3,
70
+ toolchain: facts.python && !facts.typescript ? 'python' : facts.typescript ? 'typescript' : 'javascript',
71
+ globalLock: true,
72
+ lock: { staleAfterMinutes: 45 },
73
+ sourceExtensions: facts.python ? ['.py'] : ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
74
+ checks,
75
+ };
76
+ }
77
+
78
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
79
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
80
+ const { scripts, ...facts } = detectFacts(repoRoot);
81
+ console.log(`\n code-quality detect — ${repoRoot}\n`);
82
+ for (const [k, v] of Object.entries(facts)) {
83
+ const val = Array.isArray(v) ? (v.length ? v.join(', ') : '—') : v === true ? 'yes' : v === false ? 'no' : (v ?? '—');
84
+ console.log(` ${k.padEnd(16)} ${val}`);
85
+ }
86
+ console.log('\n── proposed config ──');
87
+ console.log(JSON.stringify(detect({ repoRoot }), null, 2));
88
+ console.log('\nNothing written. Record it with: total-recall skill config code-quality detect --apply');
89
+ }
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * code-quality / report-data.mjs — the data adapter behind the report UI.
4
+ *
5
+ * Reads reports/latest.json exactly as check.mjs wrote it and returns a view
6
+ * with an honest status. It never runs a gate and never invents a number: every
7
+ * count comes from the report file, and a missing, unreadable, stale or
8
+ * partial report can never produce status "clean".
9
+ *
10
+ * node report-data.mjs print the view as JSON (exit 0 if clean, 1 otherwise)
11
+ *
12
+ * Status, most severe first:
13
+ * missing no report file
14
+ * unreadable report file is not valid JSON or has an unknown shape
15
+ * infrastructure a gate failed to spawn or timed out; results are incomplete
16
+ * unparsed-failure a gate exited non-zero with no parseable findings
17
+ * failed findings or a failing gate
18
+ * zero-scope a grep gate scanned zero files, or nothing ran
19
+ * stale source changed after the run started, or HEAD moved
20
+ * unknown-freshness git could not be read
21
+ * partial clean, but higher-tier gates were skipped
22
+ * clean every gate ran, nothing changed since
23
+ */
24
+ import { readFileSync, existsSync, statSync } from 'node:fs';
25
+ import { execFileSync } from 'node:child_process';
26
+ import path from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.vue', '.svelte']);
30
+
31
+ function git(cwd, args) {
32
+ return execFileSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 20_000 });
33
+ }
34
+
35
+ /** Returns { head, changed } or null when git cannot be read. */
36
+ function freshness(report) {
37
+ try {
38
+ const head = git(report.repoRoot, ['rev-parse', 'HEAD']).trim();
39
+ const startedMs = Date.parse(report.startedAt);
40
+ const changed = [];
41
+ for (const rel of git(report.repoRoot, ['ls-files', '-z']).split('\0')) {
42
+ if (!rel || !SOURCE_EXTS.has(path.extname(rel))) continue;
43
+ try {
44
+ if (statSync(path.join(report.repoRoot, rel)).mtimeMs > startedMs) changed.push(rel);
45
+ } catch { /* deleted since the run */ }
46
+ }
47
+ return { head, changed };
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ export function buildReportView(reportFile, { now = Date.now(), inspectGit = true } = {}) {
54
+ const base = { reportFile, status: 'missing', reasons: [] };
55
+ if (!existsSync(reportFile)) {
56
+ return { ...base, reasons: ['No report file. Run code-quality-check, then reload.'] };
57
+ }
58
+ let report;
59
+ try {
60
+ report = JSON.parse(readFileSync(reportFile, 'utf8'));
61
+ } catch (err) {
62
+ return { ...base, status: 'unreadable', reasons: [`Report is not valid JSON: ${err.message}`] };
63
+ }
64
+ if (!report || !Array.isArray(report.checks) || !Array.isArray(report.ranChecks) || !report.startedAt) {
65
+ return { ...base, status: 'unreadable', reasons: ['Report does not have the code-quality schema (checks, ranChecks, startedAt).'] };
66
+ }
67
+
68
+ const findings = Array.isArray(report.findings) ? report.findings : [];
69
+ const hard = findings.filter((f) => f.severity !== 'warning' && f.severity !== 'info');
70
+ const skipped = Array.isArray(report.skippedChecks) ? report.skippedChecks : [];
71
+ const gates = report.checks.map((c) => ({
72
+ id: c.id,
73
+ ok: Boolean(c.ok),
74
+ exitCode: c.exitCode ?? null,
75
+ durationMs: c.durationMs ?? null,
76
+ scanned: typeof c.scanned === 'number' ? c.scanned : null,
77
+ spawnError: c.spawnError || null,
78
+ timedOut: Boolean(c.timedOut),
79
+ findings: findings.filter((f) => f.check === c.id).length,
80
+ }));
81
+ for (const s of skipped) gates.push({ id: s.id, skipped: true, tier: s.tier });
82
+
83
+ const infra = report.checks.filter((c) => c.spawnError || c.timedOut);
84
+ const unparsed = report.checks.filter(
85
+ (c) => !c.ok && !c.spawnError && !c.timedOut && !findings.some((f) => f.check === c.id)
86
+ );
87
+ const zeroScope = report.checks.filter((c) => c.scanned === 0);
88
+ const fresh = inspectGit ? freshness(report) : null;
89
+ const headMoved = Boolean(fresh && report.gitHead && fresh.head !== report.gitHead);
90
+
91
+ const reasons = [];
92
+ let status = 'clean';
93
+ const set = (s, why) => { if (status === 'clean' || SEVERITY.indexOf(s) < SEVERITY.indexOf(status)) status = s; reasons.push(why); };
94
+ const SEVERITY = ['infrastructure', 'unparsed-failure', 'failed', 'zero-scope', 'stale', 'unknown-freshness', 'partial', 'clean'];
95
+
96
+ if (infra.length) set('infrastructure', `${infra.map((c) => c.id).join(', ')}: spawn failure or timeout; results are incomplete`);
97
+ if (unparsed.length) set('unparsed-failure', `${unparsed.map((c) => `${c.id} (exit ${c.exitCode})`).join(', ')}: failed without parseable findings`);
98
+ if (hard.length || report.checks.some((c) => !c.ok)) {
99
+ if (!infra.length && !unparsed.length) set('failed', `${hard.length} blocking finding(s)`);
100
+ }
101
+ if (report.ranChecks.length === 0) set('zero-scope', 'No gates ran');
102
+ if (zeroScope.length) set('zero-scope', `${zeroScope.map((c) => c.id).join(', ')}: scanned 0 files`);
103
+ if (fresh === null && inspectGit) set('unknown-freshness', 'git could not be read; cannot tell whether this report matches the tree');
104
+ else if (fresh && (fresh.changed.length || headMoved)) {
105
+ set('stale', headMoved
106
+ ? `HEAD moved from ${String(report.gitHead).slice(0, 8)} to ${fresh.head.slice(0, 8)}`
107
+ : `${fresh.changed.length} source file(s) changed after the run started`);
108
+ }
109
+ if (skipped.length) set('partial', `Not run (tier ${report.tier}): ${skipped.map((s) => `${s.id}(${s.tier})`).join(', ')}`);
110
+
111
+ return {
112
+ reportFile,
113
+ status,
114
+ reasons,
115
+ clean: status === 'clean',
116
+ toolchain: report.toolchain,
117
+ repoRoot: report.repoRoot,
118
+ tier: report.tier,
119
+ gitHead: report.gitHead ?? null,
120
+ currentHead: fresh?.head ?? null,
121
+ startedAt: report.startedAt,
122
+ finishedAt: report.finishedAt ?? null,
123
+ ageMs: report.finishedAt ? Math.max(0, now - Date.parse(report.finishedAt)) : null,
124
+ durationMs: report.durationMs ?? null,
125
+ sourceFileCount: report.sourceFileCount ?? null,
126
+ ranChecks: report.ranChecks,
127
+ skippedChecks: skipped,
128
+ gates,
129
+ totalFindings: report.totalFindings ?? findings.length,
130
+ blockingFindings: hard.length,
131
+ changedSinceRun: fresh ? fresh.changed : null,
132
+ findings: findings.slice(0, 200),
133
+ findingsTruncated: findings.length > 200,
134
+ };
135
+ }
136
+
137
+ export function defaultReportFile() {
138
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'reports', 'latest.json');
139
+ }
140
+
141
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
142
+ const view = buildReportView(process.argv[2] || defaultReportFile());
143
+ console.log(JSON.stringify(view, null, 2));
144
+ process.exitCode = view.clean ? 0 : 1;
145
+ }
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * code-quality / report.mjs — read the last check report and pick the next work.
4
+ *
5
+ * This never runs a checker and never spawns anything. It reads
6
+ * reports/latest.json, tells you whether that report still describes your
7
+ * working tree, and orders the remaining findings so you know what to fix next.
8
+ *
9
+ * Views:
10
+ * report.mjs worst files first (default)
11
+ * report.mjs worst <skip> page deeper into the worst-files list
12
+ * report.mjs type group by error code
13
+ * report.mjs file <pattern> everything matching one path
14
+ * report.mjs count totals per code and per check
15
+ * report.mjs raw <checkId> raw tool output for one check
16
+ * report.mjs json machine-readable view with honest status (feeds the UI)
17
+ */
18
+
19
+ import { readFileSync, existsSync, statSync } from 'node:fs';
20
+ import { execFileSync } from 'node:child_process';
21
+ import path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { stateDir } from './state.mjs';
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+ const SKILL_DIR = stateDir();
28
+ const REPORT_JSON = path.join(SKILL_DIR, 'reports', 'latest.json');
29
+ const REPORT_TXT = path.join(SKILL_DIR, 'reports', 'latest.txt');
30
+ const CHECK_CMD = `node ${path.relative(process.cwd(), path.join(__dirname, 'check.mjs')) || 'check.mjs'}`;
31
+
32
+ const [mode = 'worst', arg] = process.argv.slice(2);
33
+ const MAX_SHOWN = 150;
34
+
35
+ // `json` is the data feed for the plugin's report UI: the same report, with an
36
+ // honest status, as machine-readable JSON. Exit 0 only when status is clean.
37
+ if (mode === 'json') {
38
+ const { buildReportView } = await import('./report-data.mjs');
39
+ const view = buildReportView(REPORT_JSON);
40
+ console.log(JSON.stringify(view, null, 2));
41
+ process.exit(view.clean ? 0 : 1);
42
+ }
43
+
44
+ if (!existsSync(REPORT_JSON)) {
45
+ console.log(' No report yet.\n');
46
+ console.log('Run a check as a BACKGROUND job, then come back:');
47
+ console.log(` ${CHECK_CMD}`);
48
+ process.exit(0);
49
+ }
50
+
51
+ const report = JSON.parse(readFileSync(REPORT_JSON, 'utf8'));
52
+ const startedMs = Date.parse(report.startedAt);
53
+
54
+ // ─── Staleness: which files changed after the run started? ────────────────────
55
+
56
+ function changedSinceRun() {
57
+ let files = [];
58
+ try {
59
+ const out = execFileSync('git', ['ls-files', '-z'], {
60
+ cwd: report.repoRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 20_000
61
+ });
62
+ const exts = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.vue', '.svelte']);
63
+ files = out.split('\0').filter((f) => f && exts.has(path.extname(f)));
64
+ } catch {
65
+ return null; // cannot determine — say so rather than implying freshness
66
+ }
67
+ const changed = [];
68
+ for (const rel of files) {
69
+ try {
70
+ if (statSync(path.join(report.repoRoot, rel)).mtimeMs > startedMs) changed.push(rel);
71
+ } catch { /* deleted since */ }
72
+ }
73
+ return changed;
74
+ }
75
+
76
+ const changed = changedSinceRun();
77
+ const ageMin = Math.round((Date.now() - Date.parse(report.finishedAt)) / 60000);
78
+
79
+ // ─── Header ───────────────────────────────────────────────────────────────────
80
+
81
+ // Errors block a push; warnings and infos are reported but do not. Showing the
82
+ // split keeps "17 findings" from reading as "17 things I must fix now".
83
+ const sevCount = (report.findings || []).reduce((m, f) => {
84
+ const k = f.severity === 'warning' || f.severity === 'info' ? f.severity : 'error';
85
+ m[k] = (m[k] || 0) + 1;
86
+ return m;
87
+ }, {});
88
+ const sevLabel = ['error', 'warning', 'info']
89
+ .filter((k) => sevCount[k])
90
+ .map((k) => `${sevCount[k]} ${k}`)
91
+ .join(', ');
92
+ console.log(
93
+ `\n code-quality — ${report.toolchain} — ${report.totalFindings} finding(s)${sevLabel ? ` (${sevLabel})` : ''}`
94
+ );
95
+ console.log(` ran ${report.ranChecks.join(', ')} at tier=${report.tier} in ${(report.durationMs / 1000).toFixed(1)}s, ${ageMin}min ago`);
96
+
97
+ if (report.skippedChecks?.length) {
98
+ console.log(` NOT run: ${report.skippedChecks.map((c) => `${c.id}(${c.tier})`).join(', ')} — this is partial coverage.`);
99
+ }
100
+ if (report.infrastructureFailure) {
101
+ const bad = report.checks.filter((c) => c.spawnError || c.timedOut);
102
+ console.log(` INFRASTRUCTURE FAILURE — findings below are incomplete:`);
103
+ for (const c of bad) console.log(` ${c.id}: ${c.spawnError ? `spawn failed (${c.spawnError})` : 'timed out'}`);
104
+ }
105
+
106
+ // A gate that exited non-zero but produced no parsed findings means the tool
107
+ // reported a problem in a format the parser did not understand. Silently
108
+ // showing "0 findings" there would be a false clean bill of health.
109
+ const opaque = (report.checks || []).filter(
110
+ (c) => !c.ok && !c.spawnError && !c.timedOut && !(report.findings || []).some((f) => f.check === c.id)
111
+ );
112
+ if (opaque.length) {
113
+ console.log(` FAILED WITHOUT PARSEABLE FINDINGS — the tool objected, the parser did not understand it:`);
114
+ for (const c of opaque) {
115
+ console.log(` ${c.id} (exit ${c.exitCode}) → inspect with: report.mjs raw ${c.id}`);
116
+ }
117
+ }
118
+
119
+ if (changed === null) {
120
+ console.log(` Could not read git — cannot tell whether this report matches your tree.`);
121
+ } else if (changed.length === 0) {
122
+ console.log(` FRESH — no tracked source file has changed since this run started.`);
123
+ } else {
124
+ console.log(` STALE for ${changed.length} file(s) edited after the run started:`);
125
+ for (const f of changed.slice(0, 8)) console.log(` ${f}`);
126
+ if (changed.length > 8) console.log(` … and ${changed.length - 8} more`);
127
+ console.log(` Findings in those files may already be fixed. Re-run: ${CHECK_CMD}`);
128
+ }
129
+ console.log('');
130
+
131
+ // ─── Views ────────────────────────────────────────────────────────────────────
132
+
133
+ const findings = report.findings || [];
134
+ const byFile = () => {
135
+ const m = new Map();
136
+ for (const f of findings) {
137
+ if (!m.has(f.file)) m.set(f.file, []);
138
+ m.get(f.file).push(f);
139
+ }
140
+ return m;
141
+ };
142
+ const fmt = (f) => ` ${f.file}:${f.line}:${f.col} ${f.code} ${f.message}${f.snippet ? `\n ↳ ${f.snippet}` : ''}`;
143
+ const staleMark = (file) => (changed?.includes(file) ? ' ' : '');
144
+
145
+ // `raw` reads the stored tool output, not the parsed findings, so it must be
146
+ // reachable when there are zero findings — that is precisely the "failed
147
+ // without parseable findings" case the header just told you to inspect.
148
+ if (mode === 'raw') {
149
+ if (!existsSync(REPORT_TXT)) { console.error('No raw output stored.'); process.exit(1); }
150
+ const raw = readFileSync(REPORT_TXT, 'utf8');
151
+ if (!arg) { console.log(raw.slice(0, 20000)); process.exit(0); }
152
+ const section = raw.split(/^# /m).find((s) => s.startsWith(arg));
153
+ console.log(section ? `# ${section}`.slice(0, 40000) : `No raw section for check "${arg}".`);
154
+ process.exit(section ? 0 : 1);
155
+ }
156
+
157
+ if (findings.length === 0) {
158
+ console.log(' No findings.');
159
+ process.exit(0);
160
+ }
161
+
162
+ switch (mode) {
163
+ case 'count': {
164
+ const codes = {};
165
+ const checks = {};
166
+ for (const f of findings) {
167
+ codes[f.code] = (codes[f.code] || 0) + 1;
168
+ checks[f.check] = (checks[f.check] || 0) + 1;
169
+ }
170
+ console.log(`── by check ──`);
171
+ for (const [k, n] of Object.entries(checks).sort((a, b) => b[1] - a[1])) console.log(` ${String(n).padStart(5)} ${k}`);
172
+ console.log(`\n── by code ──`);
173
+ for (const [k, n] of Object.entries(codes).sort((a, b) => b[1] - a[1])) console.log(` ${String(n).padStart(5)} ${k}`);
174
+ break;
175
+ }
176
+ case 'type': {
177
+ const codes = {};
178
+ for (const f of findings) (codes[f.code] ||= []).push(f);
179
+ for (const [code, list] of Object.entries(codes).sort((a, b) => b[1].length - a[1].length)) {
180
+ console.log(`\n ${code} — ${list.length}`);
181
+ for (const f of list.slice(0, 6)) console.log(fmt(f));
182
+ if (list.length > 6) console.log(` … ${list.length - 6} more`);
183
+ }
184
+ break;
185
+ }
186
+ case 'file': {
187
+ if (!arg) { console.error('Usage: report.mjs file <pattern>'); process.exit(1); }
188
+ const hits = findings.filter((f) => f.file.includes(arg));
189
+ console.log(` ${hits.length} finding(s) matching "${arg}"${staleMark(hits[0]?.file || '')}`);
190
+ hits.slice(0, MAX_SHOWN).forEach((f) => console.log(fmt(f)));
191
+ break;
192
+ }
193
+ default: {
194
+ const skip = parseInt(arg, 10) || 0;
195
+ const files = [...byFile().entries()].sort((a, b) => b[1].length - a[1].length);
196
+ const page = files.slice(skip, skip + 4);
197
+ console.log(`── worst files (${skip + 1}–${skip + page.length} of ${files.length}) ──`);
198
+ for (const [file, list] of page) {
199
+ console.log(`\n ${file} (${list.length})${staleMark(file)}`);
200
+ list.slice(0, 40).forEach((f) => console.log(fmt(f)));
201
+ if (list.length > 40) console.log(` … ${list.length - 40} more`);
202
+ }
203
+ if (files.length > skip + 4) {
204
+ console.log(`\n ${files.length - (skip + 4)} more file(s). Next page:`);
205
+ console.log(` node ${path.relative(process.cwd(), __filename)} worst ${skip + 4}`);
206
+ }
207
+ }
208
+ }
209
+
210
+ console.log('');
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Where a repository keeps its code-quality state (config.json, reports, lock).
3
+ * The runner itself lives in this plugin and is never copied into a repo.
4
+ * TR_CODE_QUALITY_STATE overrides; otherwise the nearest enclosing
5
+ * .agent/skills/code-quality directory, or one beside the current directory.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ export function stateDir(start = process.cwd()) {
11
+ if (process.env.TR_CODE_QUALITY_STATE) return path.resolve(process.env.TR_CODE_QUALITY_STATE);
12
+ let dir = path.resolve(start);
13
+ for (;;) {
14
+ const candidate = path.join(dir, '.agent', 'skills', 'code-quality');
15
+ if (fs.existsSync(candidate)) return candidate;
16
+ const parent = path.dirname(dir);
17
+ if (parent === dir) return path.join(path.resolve(start), '.agent', 'skills', 'code-quality');
18
+ dir = parent;
19
+ }
20
+ }