simt-dev 0.2.0__py3-none-any.whl

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 (138) hide show
  1. python/simt_dev/__init__.py +32 -0
  2. python/simt_dev/cli.py +225 -0
  3. simt_dev/__init__.py +32 -0
  4. simt_dev/cli.py +225 -0
  5. simt_dev/vendored/CHANGELOG.md +166 -0
  6. simt_dev/vendored/bin/simt-dev.mjs +28 -0
  7. simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
  8. simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
  9. simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
  10. simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
  11. simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
  12. simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
  13. simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
  14. simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
  15. simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
  16. simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
  17. simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
  18. simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
  19. simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
  20. simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
  21. simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
  22. simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
  23. simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
  24. simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
  25. simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
  26. simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
  27. simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
  28. simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
  29. simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
  30. simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
  31. simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
  32. simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
  33. simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
  34. simt_dev/vendored/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +131 -0
  35. simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
  36. simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
  37. simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
  38. simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
  39. simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
  40. simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
  41. simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
  42. simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
  43. simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
  44. simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
  45. simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
  46. simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
  47. simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
  48. simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
  49. simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
  50. simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
  51. simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
  52. simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
  53. simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
  54. simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
  55. simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
  56. simt_dev/vendored/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +102 -0
  57. simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
  58. simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
  59. simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
  60. simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
  61. simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
  62. simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
  63. simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
  64. simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
  65. simt_dev/vendored/docs/decisions/README.md +530 -0
  66. simt_dev/vendored/graph/skills.graph.json +1622 -0
  67. simt_dev/vendored/package.json +43 -0
  68. simt_dev/vendored/rules/core.md +76 -0
  69. simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
  70. simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
  71. simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
  72. simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
  73. simt_dev/vendored/rules/stacks/android.md +63 -0
  74. simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
  75. simt_dev/vendored/rules/stacks/connectiq.md +62 -0
  76. simt_dev/vendored/rules/stacks/container.md +31 -0
  77. simt_dev/vendored/rules/stacks/jvm.md +54 -0
  78. simt_dev/vendored/rules/stacks/node.md +45 -0
  79. simt_dev/vendored/rules/stacks/python.md +48 -0
  80. simt_dev/vendored/rules/stacks/react-native.md +58 -0
  81. simt_dev/vendored/rules/stacks/react-web.md +61 -0
  82. simt_dev/vendored/rules/stacks/typescript.md +21 -0
  83. simt_dev/vendored/rules/stacks/uv.md +32 -0
  84. simt_dev/vendored/src/argv.mjs +116 -0
  85. simt_dev/vendored/src/ci/dependabot.mjs +268 -0
  86. simt_dev/vendored/src/ci/workflow.mjs +360 -0
  87. simt_dev/vendored/src/commands/ci.mjs +285 -0
  88. simt_dev/vendored/src/commands/compare.mjs +195 -0
  89. simt_dev/vendored/src/commands/graph.mjs +73 -0
  90. simt_dev/vendored/src/commands/help.mjs +108 -0
  91. simt_dev/vendored/src/commands/hooks.mjs +471 -0
  92. simt_dev/vendored/src/commands/index.mjs +241 -0
  93. simt_dev/vendored/src/commands/init.mjs +632 -0
  94. simt_dev/vendored/src/commands/mcp.mjs +63 -0
  95. simt_dev/vendored/src/commands/rules.mjs +374 -0
  96. simt_dev/vendored/src/commands/skills.mjs +647 -0
  97. simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
  98. simt_dev/vendored/src/commands/sync.mjs +331 -0
  99. simt_dev/vendored/src/commands/upgrade.mjs +202 -0
  100. simt_dev/vendored/src/compare/discover.mjs +212 -0
  101. simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
  102. simt_dev/vendored/src/compare/row.mjs +299 -0
  103. simt_dev/vendored/src/decisions/index.mjs +536 -0
  104. simt_dev/vendored/src/graph/load.mjs +541 -0
  105. simt_dev/vendored/src/hooks/capability.mjs +129 -0
  106. simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
  107. simt_dev/vendored/src/hooks/gates.mjs +144 -0
  108. simt_dev/vendored/src/hooks/resolve.mjs +194 -0
  109. simt_dev/vendored/src/hooks/shim.mjs +140 -0
  110. simt_dev/vendored/src/hooks/where.mjs +118 -0
  111. simt_dev/vendored/src/identity.mjs +218 -0
  112. simt_dev/vendored/src/locate.mjs +75 -0
  113. simt_dev/vendored/src/mcp/server.mjs +167 -0
  114. simt_dev/vendored/src/mcp/tools.mjs +410 -0
  115. simt_dev/vendored/src/name.mjs +95 -0
  116. simt_dev/vendored/src/output/index.mjs +96 -0
  117. simt_dev/vendored/src/projection/baselines.mjs +209 -0
  118. simt_dev/vendored/src/projection/conflicts.mjs +205 -0
  119. simt_dev/vendored/src/projection/manifest.mjs +478 -0
  120. simt_dev/vendored/src/projection/markers.mjs +349 -0
  121. simt_dev/vendored/src/projection/mdc.mjs +327 -0
  122. simt_dev/vendored/src/projection/project.mjs +504 -0
  123. simt_dev/vendored/src/projection/rules.mjs +640 -0
  124. simt_dev/vendored/src/projection/surfaces.mjs +398 -0
  125. simt_dev/vendored/src/projection/targets.mjs +221 -0
  126. simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
  127. simt_dev/vendored/src/resolve/near.mjs +61 -0
  128. simt_dev/vendored/src/resolve/resolve.mjs +457 -0
  129. simt_dev/vendored/src/resolve/source.mjs +105 -0
  130. simt_dev/vendored/src/toolchain.mjs +202 -0
  131. simt_dev/vendored/src/tsconfig.mjs +314 -0
  132. simt_dev/vendored/src/upgrade/plan.mjs +203 -0
  133. simt_dev/vendored/src/workspace.mjs +289 -0
  134. simt_dev-0.2.0.dist-info/METADATA +150 -0
  135. simt_dev-0.2.0.dist-info/RECORD +138 -0
  136. simt_dev-0.2.0.dist-info/WHEEL +4 -0
  137. simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
  138. simt_dev-0.2.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,195 @@
1
+ // simt-dev compare - the org-wide matrix.
2
+ //
3
+ // This command exists because the 2026-10-05 audit had to be reconstructed by
4
+ // hand, from 16 repositories, into 40 KB of prose. Everything it found is a fact
5
+ // this command computes: which repos are adopted, which have drifted, which route
6
+ // hooks, which declare gates the dispatcher cannot run.
7
+ //
8
+ // Two decisions make it useful rather than decorative:
9
+ //
10
+ // 1. It is a READ of the workspace, never a write. No `sync`, no `upgrade`, no
11
+ // tag. A dashboard that can change state is not a dashboard; an agent running
12
+ // `compare` to orient itself must not be able to mutate 16 repositories.
13
+ // 2. The exit code is a SIGNAL, not a score. It fails when something is wrong
14
+ // (drift, malformed region, a gate that cannot run), and succeeds when repos
15
+ // are merely not adopted yet - an unadopted repo is the expected mid-migration
16
+ // state, not a defect, and a command that went red for it would train people
17
+ // to ignore it.
18
+ //
19
+ // Output is one fact per line in fixed field order (plan 0002 section 6): a
20
+ // human can read the table, an agent can grep it, and two runs diff cleanly.
21
+
22
+ import { basename, resolve } from 'node:path';
23
+
24
+ import { EXIT, emit, emitJson, fail, isJson, note } from '../output/index.mjs';
25
+ import { provenanceDigest } from '../projection/project.mjs';
26
+ import { GATES } from '../hooks/gates.mjs';
27
+ import { buildRow, summarise } from '../compare/row.mjs';
28
+ import { discover } from '../compare/discover.mjs';
29
+ import { targetRoot, WORKSPACE_REMEDY } from '../workspace.mjs';
30
+
31
+ /** Gate kinds, so a row can tell a command gate with no command from a script gate. */
32
+ const GATE_KINDS = new Map(Object.entries(GATES).map(([name, g]) => [name, g.kind]));
33
+
34
+ export async function compareCommand(positional, flags) {
35
+ // The estate root. `--root` is explicit and wins; otherwise it is the resolved
36
+ // workspace, which may be none (ADR-014: a single repo is a first-class case, and
37
+ // "no workspace" is a fact to report rather than a failure).
38
+ //
39
+ // A path that IS a repository is compared as itself, not scanned for children:
40
+ // naming one repo almost certainly meant that repo. Discovery decides which,
41
+ // rather than this command guessing.
42
+ //
43
+ // `targetRoot` owns the precedence so `compare` and `upgrade` cannot disagree about
44
+ // which source named the directory.
45
+ const target = await targetRoot(flags, positional);
46
+ const root = resolve(target.root);
47
+ const found = await discover(root);
48
+ if (found.error) {
49
+ return fail(`${root}: ${found.error}`, EXIT.CONFIG, 'pass --root <path>');
50
+ }
51
+ if (found.repos.length === 0) {
52
+ return fail(
53
+ `${root} contains no git repositories`,
54
+ EXIT.CONFIG,
55
+ `point --root at the directory holding the repos, or at one repo to see just it`,
56
+ );
57
+ }
58
+
59
+ const expectedSha = await provenanceDigest();
60
+ const context = { expectedSha, gates: new Set(Object.keys(GATES)), gateKinds: GATE_KINDS };
61
+
62
+ // Sequential on purpose. 16 fingerprint+region computations each spawn a git
63
+ // process; running them concurrently produces a burst of subprocesses and no
64
+ // measurable speedup, because the work is process-spawn bound rather than CPU
65
+ // bound. Determinism of the output matters more than wall clock on a dashboard.
66
+ const rows = [];
67
+ for (const repo of found.repos) rows.push(await buildRow(repo, context));
68
+
69
+ const summary = summarise(rows, {
70
+ root,
71
+ shape: found.shape,
72
+ source: target.source,
73
+ expectedSha,
74
+ worktrees: found.worktrees.length,
75
+ directories: found.directories.length,
76
+ });
77
+
78
+ if (isJson(flags)) {
79
+ emitJson({
80
+ ok: !rows.some((r) => r.problems.length > 0),
81
+ command: 'compare',
82
+ root: summary.root,
83
+ shape: summary.shape,
84
+ source: summary.source,
85
+ expectedSha: summary.expectedSha,
86
+ // Spread, not a hand-listed copy. A copy is a second place to forget: a field
87
+ // added to `summarise` renders in the text output and silently vanishes from
88
+ // `--json`, which is the combination automation trusts most. That is not
89
+ // hypothetical — `withOwnHooks` and `staleRuleFiles` both did it, and both
90
+ // were caught only because a test asserts the two shapes agree.
91
+ summary: {
92
+ repos: summary.repos,
93
+ adopted: summary.adopted,
94
+ unmanaged: summary.unmanaged,
95
+ broken: summary.broken,
96
+ drifted: summary.drifted,
97
+ current: summary.current,
98
+ noRegion: summary.noRegion,
99
+ malformed: summary.malformed,
100
+ staleRuleFiles: summary.staleRuleFiles,
101
+ hooksRouted: summary.hooksRouted,
102
+ withProblems: summary.withProblems,
103
+ withOwnHooks: summary.withOwnHooks,
104
+ shas: summary.shas,
105
+ },
106
+ rows,
107
+ worktrees: found.worktrees.map((w) => ({
108
+ name: w.name,
109
+ path: w.path,
110
+ owner: w.ownerName,
111
+ ownerMatchesDirName: w.ownerMatches,
112
+ })),
113
+ directories: found.directories.map((d) => basename(d)),
114
+ });
115
+ return rows.some((r) => r.problems.length > 0) ? EXIT.FAIL : EXIT.OK;
116
+ }
117
+
118
+ emit(render(rows, summary, found), { maxBytes: flags.maxBytes ?? 0 });
119
+ if (summary.shape === 'repo') {
120
+ note('note one repository compared; pass --root at a workspace directory to compare its siblings');
121
+ } else if (target.source === 'none') {
122
+ note(`note no workspace configured, so this compared the current directory; ${WORKSPACE_REMEDY}`);
123
+ }
124
+ for (const w of found.worktrees) {
125
+ // A worktree is reported, not compared. Its files are the parent repo's HEAD
126
+ // checked out elsewhere, so its projection state is the parent's - reporting
127
+ // it as an independent consumer would double-count one repo's drift.
128
+ note(`note ${w.name} is a worktree of ${w.ownerName ?? '(unknown)'}, not compared separately`);
129
+ if (!w.ownerMatches) {
130
+ note(`note ${w.name}: git slug does not match its directory name; owner inferred from the gitdir path`);
131
+ }
132
+ }
133
+ if (found.directories.length > 0) {
134
+ note(
135
+ `note ${found.directories.length} non-repo director${found.directories.length === 1 ? 'y' : 'ies'}: ${found.directories.map((d) => basename(d)).join(', ')}`,
136
+ );
137
+ }
138
+ return rows.some((r) => r.problems.length > 0) ? EXIT.FAIL : EXIT.OK;
139
+ }
140
+
141
+ /**
142
+ * Fixed field order, one repo per block. A single aligned table would be prettier
143
+ * and worse: variable-length stacks and gate lists force padding that breaks on
144
+ * the longest value, and an agent cannot tell which column is which without
145
+ * counting spaces.
146
+ */
147
+ function render(rows, summary, found) {
148
+ const lines = [
149
+ // `workspace` and `repo` are the same field with different names, and the name is the
150
+ // honest one: a reader who sees "workspace" and gets one repo was told a lie. `root`
151
+ // plus `shape` is the one reading that is true in both cases (ADR-014).
152
+ `${summary.shape === 'repo' ? 'repo ' : 'workspace'} ${summary.root}`,
153
+ `shape ${summary.shape}${summary.shape === 'workspace' ? ` (root from ${summary.source})` : ''}`,
154
+ `simt-dev ${summary.expectedSha}`,
155
+ `repos ${summary.repos} (${summary.adopted} adopted, ${summary.unmanaged} unmanaged${summary.broken > 0 ? `, ${summary.broken} broken` : ''})`,
156
+ `region ${summary.current} current, ${summary.drifted} drifted, ${summary.noRegion} with no region, ${summary.malformed} malformed`,
157
+ `rules ${summary.staleRuleFiles === 0 ? 'every rule file current' : `${summary.staleRuleFiles} rule file${summary.staleRuleFiles === 1 ? '' : 's'} not current`}`,
158
+ `hooks ${summary.hooksRouted}/${summary.adopted} adopted repos route the shared dispatcher`,
159
+ ` ${summary.withOwnHooks} repo${summary.withOwnHooks === 1 ? '' : 's'} run their own hooks; adopting replaces them (a note, not a problem)`,
160
+ `problems ${summary.withProblems} repo${summary.withProblems === 1 ? '' : 's'}`,
161
+ `pinned ${summary.shas.map(([sha, n]) => `${sha} x${n}`).join(', ')}`,
162
+ '',
163
+ ];
164
+
165
+ for (const r of rows) {
166
+ lines.push(`repo ${r.name}`);
167
+ lines.push(` adoption ${r.adoption}${r.id && r.id !== r.name ? ` (id ${r.id})` : ''}`);
168
+ lines.push(` stack ${r.stack.length > 0 ? r.stack.join(', ') : '(none)'}${r.declaredStack.length > 0 ? ` (manifest declares ${r.declaredStack.join(', ')})` : ''}`);
169
+ if (r.facts.length > 0) lines.push(` facts ${r.facts.join(', ')}`);
170
+ lines.push(` region ${r.region}${r.regionDetail ? ` (${r.regionDetail})` : ''}`);
171
+ // The rule files are the answer to "does every editor see the same rule?".
172
+ lines.push(
173
+ ` rules ${r.ruleFiles.total === 0 ? '(none)' : `${r.ruleFiles.current}/${r.ruleFiles.total} current`}${r.ruleFiles.stale.length > 0 ? ` - ${r.ruleFiles.stale.join(', ')}` : ''}`,
174
+ );
175
+ lines.push(` sha pinned ${r.pinnedSha ?? '(none)'} region ${r.recordedSha ?? '(none)'}`);
176
+ lines.push(
177
+ ` hooks ${r.hooksRouted ? 'shared dispatcher' : r.adoption === 'adopted' ? 'NOT ROUTED' : '(n/a)'}${r.hooksRouted && !r.hooksComplete ? ' (incomplete)' : ''}`,
178
+ );
179
+ lines.push(
180
+ ` gates ${r.gatesDeclared.length > 0 ? r.gatesDeclared.join(', ') : r.adoption === 'adopted' ? '(none declared)' : '(n/a)'}`,
181
+ );
182
+ if (r.problems.length > 0) {
183
+ lines.push(' PROBLEMS');
184
+ for (const p of r.problems) lines.push(` ${p}`);
185
+ }
186
+ if (r.notes.length > 0) {
187
+ for (const n of r.notes) lines.push(` note ${n}`);
188
+ }
189
+ lines.push('');
190
+ }
191
+
192
+ return lines.join('\n').trimEnd();
193
+ }
194
+
195
+ export { discover };
@@ -0,0 +1,73 @@
1
+ // `simt-dev graph lint` - verify the hand-authored registry against reality.
2
+ //
3
+ // Implementation sequence step 2. This command is the reason hand-authoring the
4
+ // registry is safe: 38 nodes across 6 repos is bookkeeping that rots silently.
5
+ //
6
+ // Output is line-oriented and bounded (plan 0002 section 6). A lint failure names
7
+ // the offending node, the file it disagrees with, and both numbers.
8
+
9
+ import { EXIT, emit, fail, isJson } from '../output/index.mjs';
10
+ import { loadRegistry, REGISTRY_PATH } from '../graph/load.mjs';
11
+
12
+ export async function lintCommand(positional, flags) {
13
+ const path = positional[0] ?? REGISTRY_PATH;
14
+ const { ok, registry, errors, warnings, verified, workspace } = await loadRegistry(path, flags);
15
+
16
+ if (isJson(flags)) {
17
+ emit(
18
+ `${JSON.stringify(
19
+ {
20
+ ok,
21
+ registry: path,
22
+ capabilities: registry?.capabilities?.length ?? 0,
23
+ skills: registry?.skills?.length ?? 0,
24
+ // `verified: false` means the byte counts in the registry were NOT
25
+ // checked against files on disk. It is not a failure - it is the difference
26
+ // between "the registry is right" and "the registry could not be checked",
27
+ // and an agent that cannot see the difference will trust a number nobody
28
+ // measured (ADR-014).
29
+ verified,
30
+ workspace,
31
+ errors,
32
+ warnings,
33
+ },
34
+ null,
35
+ 2,
36
+ )}\n`,
37
+ { maxBytes: flags.maxBytes ?? 0 },
38
+ );
39
+ return ok ? EXIT.OK : EXIT.REGISTRY;
40
+ }
41
+
42
+ if (!registry) {
43
+ return fail(errors[0] ?? `${path}: unreadable`, EXIT.REGISTRY);
44
+ }
45
+
46
+ const lines = [
47
+ `registry ${path}`,
48
+ `nodes ${registry.skills.length} skills, ${registry.capabilities.length} capabilities`,
49
+ `verified ${verified ? 'byte counts checked against disk' : 'NOT checked - see WARN below'}`,
50
+ ];
51
+
52
+ const deferred = registry.skills.filter((s) => s.deferred);
53
+ lines.push(
54
+ `deferred ${deferred.length} over ${registry.deferredThreshold} bytes ` +
55
+ `(${deferred.map((s) => s.name).join(', ') || 'none'})`,
56
+ );
57
+
58
+ if (warnings.length > 0) {
59
+ lines.push('', `WARN ${warnings.length}`);
60
+ for (const warning of warnings) lines.push(` ${warning}`);
61
+ }
62
+
63
+ if (errors.length > 0) {
64
+ lines.push('', `FAIL ${errors.length}`);
65
+ for (const error of errors) lines.push(` ${error}`);
66
+ emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
67
+ return EXIT.REGISTRY;
68
+ }
69
+
70
+ lines.push('', verified ? 'OK no findings' : 'OK structural checks only');
71
+ emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
72
+ return EXIT.OK;
73
+ }
@@ -0,0 +1,108 @@
1
+ import { emit } from '../output/index.mjs';
2
+ import { parse } from '../argv.mjs';
3
+ import { CLI } from '../name.mjs';
4
+
5
+ const COMMANDS = `${CLI} init [path] adopt or scaffold a repo into the ecosystem
6
+ (dry-run unless --yes; conflicts block without
7
+ --accept-conflicts; never commits or pushes)
8
+ ${CLI} doctor classify a repo and check config health
9
+ ${CLI} check the gate: projection drift, manifest validity, policy
10
+ ${CLI} sync [--yes] regenerate the projection (idempotent)
11
+ (prints the plan and exits 1 on drift without --yes,
12
+ so it also works as a check)
13
+ ${CLI} upgrade move every consumer to this simt-dev SHA, as one set
14
+ (dry-run unless --yes; moves a pin only - it never
15
+ adopts or repairs a repo; never commits or pushes)
16
+
17
+ ${CLI} skills index tier-0 catalog: every skill, one line each
18
+ ${CLI} skills brief --task tier-1 pre-assembled brief <- the common case
19
+ ${CLI} skills show <name> tier-2 body (respects --max-bytes)
20
+ ${CLI} skills why <name> the signal chain that selected it
21
+ ${CLI} skills find <query> search across capability and keyword
22
+ ${CLI} skills install <name> pin a skill into the repo as a local override
23
+ (dry-run unless --yes; a multi-variant skill needs
24
+ --variant or --from rather than a guess)
25
+
26
+ ${CLI} rules list the rule catalogue (--root marks what applies to a repo)
27
+ ${CLI} rules show <id> one rule body
28
+ ${CLI} rules why <id|ADR-NNN> the decision that settled it, and whether it is still in force
29
+ ${CLI} hooks install [--yes] install the shared dispatcher; report what it would run
30
+ (dry-run unless --yes; refuses to take over routing git
31
+ already uses, and --yes is that consent)
32
+ ${CLI} ci simt-dev's own .github files: create or refresh them
33
+ (dry-run unless --yes; refreshes the templates' own copies
34
+ and no repository else's)
35
+ ${CLI} compare org-wide matrix across every sibling repo
36
+ (read-only; exits non-zero on defects, not on
37
+ repos that have not adopted yet)
38
+ ${CLI} mcp the read-only surface as MCP tools over stdio
39
+ (--list prints it without reading stdin; init, sync,
40
+ upgrade and the installers stay CLI-only by design)`;
41
+
42
+ const FLAGS = `--json structured output
43
+ --max-bytes <n> hard ceiling on emitted bytes (0 = unlimited, default 0)
44
+ --budget <n> context budget for skills brief (default 20480)
45
+ --plan emit a fetch plan instead of inlining skill bodies
46
+ --skill <name> force a skill into the brief (repeatable)
47
+ --task "<text>" the task text used for capability resolution
48
+ --paths <globs> comma-separated path globs used for path resolution
49
+ --root <path> repository root (default: cwd)
50
+ --surfaces <ids> comma-separated editors to write rule files for;
51
+ "" means AGENTS.md only (default: every surface,
52
+ or the "surfaces" list in simt-dev.json)
53
+ --workspace <path> where the skill sources live; "off" declines it
54
+ (default: SIMT_DEV_WORKSPACE, then the user config
55
+ file, then the sibling heuristic if it verifies)
56
+ --dry-run report what would change, change nothing
57
+ --yes assume yes; required in automation, never prompts
58
+ --all ignore the budget and resolve everything
59
+ --variant <id> which body of a multi-variant skill to use
60
+ --from <repo/path> install from a declared source by name`;
61
+
62
+ const PRINCIPLES = `design rules:
63
+ deterministic same repo + same task = same answer, forever
64
+ explainable skills why prints the signal chain for any selection;
65
+ rules why prints the decision behind any rule
66
+ zero dependencies a DX tool that needs an install step fails on a fresh machine
67
+ bounded output one fact per line, fixed field order, explicit TRUNCATED lines`;
68
+
69
+ export async function help(argv) {
70
+ const { ok, value } = parse(argv);
71
+ if (!ok) return value.code;
72
+ const { flags } = value;
73
+ if (flags.version) {
74
+ emit((await version()).trim());
75
+ return 0;
76
+ }
77
+ emit(
78
+ [
79
+ 'simt-dev - shared agent rules, skills and gates for a repo ecosystem',
80
+ '',
81
+ 'usage: simt-dev <command> [args] [flags]',
82
+ '',
83
+ 'commands:',
84
+ COMMANDS,
85
+ '',
86
+ 'flags:',
87
+ FLAGS,
88
+ '',
89
+ PRINCIPLES,
90
+ '',
91
+ // The plans ship inside the installed package but have no command that surfaces
92
+ // them, so naming the path would again be advice the reader cannot take. They
93
+ // are an implementation record; the rulings are the part a reader can act on.
94
+ `rulings ${CLI} rules list the decisions behind the rules you just read`,
95
+ ].join('\n'),
96
+ { maxBytes: flags.maxBytes ?? 0 },
97
+ );
98
+ return 0;
99
+ }
100
+
101
+ async function version() {
102
+ const { readFile } = await import('node:fs/promises');
103
+ const { fileURLToPath } = await import('node:url');
104
+ const { dirname, join } = await import('node:path');
105
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..');
106
+ const pkg = JSON.parse(await readFile(join(root, '..', 'package.json'), 'utf8'));
107
+ return `simt-dev ${pkg.version}\n`;
108
+ }