@nanocollective/sentinel 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +66 -0
  3. package/dist/cli.d.ts +12 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +254 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config/parse.d.ts +13 -0
  8. package/dist/config/parse.d.ts.map +1 -0
  9. package/dist/config/parse.js +271 -0
  10. package/dist/config/parse.js.map +1 -0
  11. package/dist/config/repo-override.d.ts +13 -0
  12. package/dist/config/repo-override.d.ts.map +1 -0
  13. package/dist/config/repo-override.js +128 -0
  14. package/dist/config/repo-override.js.map +1 -0
  15. package/dist/config/types.d.ts +86 -0
  16. package/dist/config/types.d.ts.map +1 -0
  17. package/dist/config/types.js +8 -0
  18. package/dist/config/types.js.map +1 -0
  19. package/dist/dedup/hash.d.ts +10 -0
  20. package/dist/dedup/hash.d.ts.map +1 -0
  21. package/dist/dedup/hash.js +18 -0
  22. package/dist/dedup/hash.js.map +1 -0
  23. package/dist/dedup/markers.d.ts +13 -0
  24. package/dist/dedup/markers.d.ts.map +1 -0
  25. package/dist/dedup/markers.js +42 -0
  26. package/dist/dedup/markers.js.map +1 -0
  27. package/dist/dedup/plan.d.ts +45 -0
  28. package/dist/dedup/plan.d.ts.map +1 -0
  29. package/dist/dedup/plan.js +83 -0
  30. package/dist/dedup/plan.js.map +1 -0
  31. package/dist/dedup/reconcile.d.ts +29 -0
  32. package/dist/dedup/reconcile.d.ts.map +1 -0
  33. package/dist/dedup/reconcile.js +73 -0
  34. package/dist/dedup/reconcile.js.map +1 -0
  35. package/dist/findings/types.d.ts +53 -0
  36. package/dist/findings/types.d.ts.map +1 -0
  37. package/dist/findings/types.js +28 -0
  38. package/dist/findings/types.js.map +1 -0
  39. package/dist/findings/validate.d.ts +36 -0
  40. package/dist/findings/validate.d.ts.map +1 -0
  41. package/dist/findings/validate.js +178 -0
  42. package/dist/findings/validate.js.map +1 -0
  43. package/dist/index.d.ts +48 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +39 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/init/args.d.ts +20 -0
  48. package/dist/init/args.d.ts.map +1 -0
  49. package/dist/init/args.js +80 -0
  50. package/dist/init/args.js.map +1 -0
  51. package/dist/init/plan.d.ts +9 -0
  52. package/dist/init/plan.d.ts.map +1 -0
  53. package/dist/init/plan.js +17 -0
  54. package/dist/init/plan.js.map +1 -0
  55. package/dist/init/scaffold.d.ts +17 -0
  56. package/dist/init/scaffold.d.ts.map +1 -0
  57. package/dist/init/scaffold.js +27 -0
  58. package/dist/init/scaffold.js.map +1 -0
  59. package/dist/init/templates.d.ts +28 -0
  60. package/dist/init/templates.d.ts.map +1 -0
  61. package/dist/init/templates.js +187 -0
  62. package/dist/init/templates.js.map +1 -0
  63. package/dist/init/types.d.ts +29 -0
  64. package/dist/init/types.d.ts.map +1 -0
  65. package/dist/init/types.js +15 -0
  66. package/dist/init/types.js.map +1 -0
  67. package/dist/issues/body.d.ts +12 -0
  68. package/dist/issues/body.d.ts.map +1 -0
  69. package/dist/issues/body.js +53 -0
  70. package/dist/issues/body.js.map +1 -0
  71. package/dist/issues/file.d.ts +24 -0
  72. package/dist/issues/file.d.ts.map +1 -0
  73. package/dist/issues/file.js +61 -0
  74. package/dist/issues/file.js.map +1 -0
  75. package/dist/issues/gh-client.d.ts +21 -0
  76. package/dist/issues/gh-client.d.ts.map +1 -0
  77. package/dist/issues/gh-client.js +133 -0
  78. package/dist/issues/gh-client.js.map +1 -0
  79. package/dist/issues/types.d.ts +85 -0
  80. package/dist/issues/types.d.ts.map +1 -0
  81. package/dist/issues/types.js +8 -0
  82. package/dist/issues/types.js.map +1 -0
  83. package/dist/orchestrator/audit.d.ts +14 -0
  84. package/dist/orchestrator/audit.d.ts.map +1 -0
  85. package/dist/orchestrator/audit.js +47 -0
  86. package/dist/orchestrator/audit.js.map +1 -0
  87. package/dist/orchestrator/auto-fix.d.ts +31 -0
  88. package/dist/orchestrator/auto-fix.d.ts.map +1 -0
  89. package/dist/orchestrator/auto-fix.js +59 -0
  90. package/dist/orchestrator/auto-fix.js.map +1 -0
  91. package/dist/orchestrator/extract.d.ts +14 -0
  92. package/dist/orchestrator/extract.d.ts.map +1 -0
  93. package/dist/orchestrator/extract.js +70 -0
  94. package/dist/orchestrator/extract.js.map +1 -0
  95. package/dist/orchestrator/nanocoder-runner.d.ts +33 -0
  96. package/dist/orchestrator/nanocoder-runner.d.ts.map +1 -0
  97. package/dist/orchestrator/nanocoder-runner.js +125 -0
  98. package/dist/orchestrator/nanocoder-runner.js.map +1 -0
  99. package/dist/orchestrator/types.d.ts +51 -0
  100. package/dist/orchestrator/types.d.ts.map +1 -0
  101. package/dist/orchestrator/types.js +8 -0
  102. package/dist/orchestrator/types.js.map +1 -0
  103. package/dist/prompt/build.d.ts +14 -0
  104. package/dist/prompt/build.d.ts.map +1 -0
  105. package/dist/prompt/build.js +123 -0
  106. package/dist/prompt/build.js.map +1 -0
  107. package/dist/prompt/types.d.ts +34 -0
  108. package/dist/prompt/types.d.ts.map +1 -0
  109. package/dist/prompt/types.js +8 -0
  110. package/dist/prompt/types.js.map +1 -0
  111. package/dist/rule-packs/dependencies.d.ts +20 -0
  112. package/dist/rule-packs/dependencies.d.ts.map +1 -0
  113. package/dist/rule-packs/dependencies.js +58 -0
  114. package/dist/rule-packs/dependencies.js.map +1 -0
  115. package/dist/rule-packs/glob.d.ts +23 -0
  116. package/dist/rule-packs/glob.d.ts.map +1 -0
  117. package/dist/rule-packs/glob.js +66 -0
  118. package/dist/rule-packs/glob.js.map +1 -0
  119. package/dist/rule-packs/parse.d.ts +21 -0
  120. package/dist/rule-packs/parse.d.ts.map +1 -0
  121. package/dist/rule-packs/parse.js +230 -0
  122. package/dist/rule-packs/parse.js.map +1 -0
  123. package/dist/rule-packs/types.d.ts +55 -0
  124. package/dist/rule-packs/types.d.ts.map +1 -0
  125. package/dist/rule-packs/types.js +10 -0
  126. package/dist/rule-packs/types.js.map +1 -0
  127. package/dist/run/audit.d.ts +20 -0
  128. package/dist/run/audit.d.ts.map +1 -0
  129. package/dist/run/audit.js +28 -0
  130. package/dist/run/audit.js.map +1 -0
  131. package/dist/run/clone.d.ts +23 -0
  132. package/dist/run/clone.d.ts.map +1 -0
  133. package/dist/run/clone.js +51 -0
  134. package/dist/run/clone.js.map +1 -0
  135. package/dist/run/preview.d.ts +34 -0
  136. package/dist/run/preview.d.ts.map +1 -0
  137. package/dist/run/preview.js +68 -0
  138. package/dist/run/preview.js.map +1 -0
  139. package/dist/run/report.d.ts +10 -0
  140. package/dist/run/report.d.ts.map +1 -0
  141. package/dist/run/report.js +73 -0
  142. package/dist/run/report.js.map +1 -0
  143. package/dist/run/run.d.ts +74 -0
  144. package/dist/run/run.d.ts.map +1 -0
  145. package/dist/run/run.js +131 -0
  146. package/dist/run/run.js.map +1 -0
  147. package/dist/run/select.d.ts +14 -0
  148. package/dist/run/select.d.ts.map +1 -0
  149. package/dist/run/select.js +31 -0
  150. package/dist/run/select.js.map +1 -0
  151. package/dist/run/sources.d.ts +11 -0
  152. package/dist/run/sources.d.ts.map +1 -0
  153. package/dist/run/sources.js +106 -0
  154. package/dist/run/sources.js.map +1 -0
  155. package/dist/run/types.d.ts +55 -0
  156. package/dist/run/types.d.ts.map +1 -0
  157. package/dist/run/types.js +7 -0
  158. package/dist/run/types.js.map +1 -0
  159. package/dist/suppression/apply.d.ts +33 -0
  160. package/dist/suppression/apply.d.ts.map +1 -0
  161. package/dist/suppression/apply.js +55 -0
  162. package/dist/suppression/apply.js.map +1 -0
  163. package/package.json +118 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ # License
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2026 Nano Collective
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Sentinel
2
+
3
+ Built by the [Nano Collective](https://nanocollective.org) — a community collective building AI tooling not for profit, but for the community.
4
+
5
+ Sentinel is an installable, Nanocoder-driven workflow that runs continuous, configurable security and code audits across the repositories in a GitHub organisation, and files what it finds as issues for a human to act on.
6
+
7
+ You install Sentinel into your own organisation, point it at the repositories you care about, write the rule packs that describe what to look for, and a scheduled GitHub Actions workflow does the audit pass. Findings land as issues on the affected repository, written up for a reviewer. Local models are a first-class path, so the audited code never has to leave hardware you own.
8
+
9
+ > Sentinel is in active development toward its v1. This repository and its documentation describe the v1 design settled in the [Sentinel whitepaper](https://docs.nanocollective.org/collective/whitepapers/sentinel). Features are being built; where something is planned rather than shipped, the docs say so.
10
+
11
+ ---
12
+
13
+ ![Build Status](https://github.com/Nano-Collective/sentinel/raw/main/badges/build.svg)
14
+ ![Coverage](https://github.com/Nano-Collective/sentinel/raw/main/badges/coverage.svg)
15
+ ![Version](https://github.com/Nano-Collective/sentinel/raw/main/badges/npm-version.svg)
16
+ ![NPM Downloads](https://github.com/Nano-Collective/sentinel/raw/main/badges/npm-downloads-monthly.svg)
17
+ ![NPM License](https://github.com/Nano-Collective/sentinel/raw/main/badges/npm-license.svg)
18
+ ![Stars](https://github.com/Nano-Collective/sentinel/raw/main/badges/stars.svg)
19
+
20
+ ## Quick Start
21
+
22
+ From inside a fresh repository in your GitHub organisation:
23
+
24
+ ```bash
25
+ npx @nanocollective/sentinel init
26
+ # or
27
+ pnpm dlx @nanocollective/sentinel init
28
+ ```
29
+
30
+ `init` scaffolds the configuration (`sentinel.yaml`), the scheduled GitHub Actions workflow, an empty `rule-packs/` directory, and a starter pack template. Answer the prompts (model provider, schedule, first repositories), review the generated files, write your first rule pack, commit, and push. The first scheduled run lands later that day, or you can dispatch the workflow manually.
31
+
32
+ Sentinel ships **no rule packs of its own**. The value comes from the packs you write for the code you actually ship — see [Writing a Rule Pack](docs/rule-packs/authoring.md).
33
+
34
+ You can also run an audit locally, off-cycle, for calibrating a pack:
35
+
36
+ ```bash
37
+ npx @nanocollective/sentinel run \
38
+ --rule-pack ./rule-packs/solana-anchor.md \
39
+ --repo ../my-program \
40
+ --output findings.md
41
+ ```
42
+
43
+ ## Documentation
44
+
45
+ Full documentation is available online at **[docs.nanocollective.org](https://docs.nanocollective.org/sentinel/docs)** or in the [docs/](docs/) folder:
46
+
47
+ - **[Getting Started](docs/getting-started/index.md)** — Install Sentinel into your organisation and get the first run green
48
+ - **[Rule Packs](docs/rule-packs/index.md)** — The pack format, the authoring guide, and worked examples
49
+ - **[Configuration](docs/configuration/index.md)** — The `sentinel.yaml` reference
50
+ - **[Workflow](docs/workflow/index.md)** — The scheduled audit, run modes, and the execution model
51
+ - **[Findings & Issues](docs/findings/index.md)** — The severity model, issue filing, dedup, and suppression
52
+ - **[CLI](docs/cli/index.md)** — `init` and `run` command reference
53
+ - **[Community](docs/community.md)** — Contributing, Discord, and how to help
54
+
55
+ ## What Sentinel is not (in v1)
56
+
57
+ Sentinel is a triage layer, not a replacement for a real audit. It is **not** a substitute for a formal security audit, **not** a SAST replacement (run Semgrep/CodeQL alongside it), **not** a secret scanner, **not** a hosted service, **not** a rule pack catalogue, and **not** a fix-it tool — it files issues, it does not open PRs. See the [whitepaper](https://docs.nanocollective.org/collective/whitepapers/sentinel) for the full scope.
58
+
59
+ ## Community
60
+
61
+ The Nano Collective is a community collective building AI tooling for the community, not for profit. We'd love your help.
62
+
63
+ - **Contribute**: See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
64
+ - **The collective**: [nanocollective.org](https://nanocollective.org) · [docs](https://docs.nanocollective.org) · [GitHub](https://github.com/Nano-Collective) · [Discord](https://discord.gg/ktPDV6rekE)
65
+ - **Support the work**: The [Support page](https://docs.nanocollective.org/collective/organisation/support) covers donations and sponsorship.
66
+ - **Paid contribution**: The [Economics Charter](https://docs.nanocollective.org/collective/organisation/economics-charter) sets out how scoped paid bounties work.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sentinel CLI entry point.
4
+ *
5
+ * sentinel init scaffold a configuration repo
6
+ * sentinel run perform an audit pass (not yet implemented)
7
+ *
8
+ * The init command's logic lives in ./init; this file is the interactive glue
9
+ * (prompting, printing) and is excluded from coverage.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../source/cli.ts"],"names":[],"mappings":";AAEA;;;;;;;;GAQG"}
package/dist/cli.js ADDED
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sentinel CLI entry point.
4
+ *
5
+ * sentinel init scaffold a configuration repo
6
+ * sentinel run perform an audit pass (not yet implemented)
7
+ *
8
+ * The init command's logic lives in ./init; this file is the interactive glue
9
+ * (prompting, printing) and is excluded from coverage.
10
+ */
11
+ import { readFileSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import { createInterface } from 'node:readline/promises';
14
+ import { parseConfig } from './config/parse.js';
15
+ import { parseInitArgs } from './init/args.js';
16
+ import { scaffold } from './init/scaffold.js';
17
+ import { ghIssueClient } from './issues/gh-client.js';
18
+ import { nanocoderRunner } from './orchestrator/nanocoder-runner.js';
19
+ import { ensureTargets } from './run/clone.js';
20
+ import { renderPreview } from './run/preview.js';
21
+ import { renderReport } from './run/report.js';
22
+ import { runFromConfig, runLocal } from './run/run.js';
23
+ import { fsPackLoader, fsRepoFiles } from './run/sources.js';
24
+ const USAGE = `sentinel <command>
25
+
26
+ Commands:
27
+ init Scaffold a Sentinel configuration into the current repository
28
+ run Perform an audit pass against a rule pack and a repository
29
+
30
+ Run 'sentinel <command> --help' for command-specific options.`;
31
+ const INIT_USAGE = `sentinel init [options]
32
+
33
+ Scaffold sentinel.yaml, the audit workflow, an empty rule-packs/ directory,
34
+ and a disabled starter pack into the current (or given) directory.
35
+
36
+ Options:
37
+ --provider <name> Model provider (ollama, lmstudio, a cloud provider)
38
+ --model <id> Model identifier
39
+ --schedule <cron> Cron schedule, UTC (default "0 6 * * *")
40
+ --targets <a/b,c/d> Comma-separated owner/repo targets
41
+ --severity-threshold <s> low | medium | high | critical (default medium)
42
+ --label <name> Issue label (default "sentinel")
43
+ --dir <path> Directory to scaffold into (default ".")
44
+ --force Overwrite existing files
45
+ --yes Non-interactive; accept defaults`;
46
+ async function promptMissing(options) {
47
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
48
+ try {
49
+ const ask = async (label, current) => {
50
+ const answer = (await rl.question(`${label} [${current}]: `)).trim();
51
+ return answer.length > 0 ? answer : current;
52
+ };
53
+ const provider = await ask('Model provider', options.provider);
54
+ const model = await ask('Model', options.model);
55
+ const schedule = await ask('Schedule (cron, UTC)', options.schedule);
56
+ const label = await ask('Issue label', options.label);
57
+ const targetsRaw = await ask('Repositories to audit (comma-separated owner/repo)', options.targets.join(','));
58
+ const targets = targetsRaw
59
+ .split(',')
60
+ .map(target => target.trim())
61
+ .filter(target => target.length > 0);
62
+ return { ...options, provider, model, schedule, label, targets };
63
+ }
64
+ finally {
65
+ rl.close();
66
+ }
67
+ }
68
+ async function runInit(argv) {
69
+ if (argv.includes('--help') || argv.includes('-h')) {
70
+ console.log(INIT_USAGE);
71
+ return 0;
72
+ }
73
+ const parsed = parseInitArgs(argv);
74
+ if (parsed.errors.length > 0) {
75
+ for (const error of parsed.errors) {
76
+ console.error(`error: ${error}`);
77
+ }
78
+ return 1;
79
+ }
80
+ const interactive = !parsed.yes && Boolean(process.stdin.isTTY);
81
+ const options = interactive
82
+ ? await promptMissing(parsed.options)
83
+ : parsed.options;
84
+ const result = scaffold(options, parsed.dir, parsed.force);
85
+ for (const path of result.written) {
86
+ console.log(` created ${path}`);
87
+ }
88
+ for (const path of result.skipped) {
89
+ console.log(` skipped ${path} (exists; pass --force to overwrite)`);
90
+ }
91
+ console.log('\nNext steps:\n 1. Edit sentinel.yaml — set your real targets and model.\n 2. Write your first rule pack in rule-packs/ (see the disabled example).\n 3. Commit and push. The audit runs on schedule, or dispatch it manually.');
92
+ return 0;
93
+ }
94
+ const RUN_USAGE = `sentinel run [options]
95
+
96
+ Config-driven (default): reads sentinel.yaml and audits every target. Files
97
+ issues when GITHUB_TOKEN is set and --dry-run is not passed.
98
+
99
+ Local (calibration): audit one pack against one repo, write findings to
100
+ Markdown, and file nothing.
101
+
102
+ Options:
103
+ --rule-pack <path> Local mode: the rule pack to run
104
+ --repo <path> Local mode: the repository directory to audit
105
+ --output <path> Write the Markdown report here (default stdout)
106
+ --config <path> Path to sentinel.yaml (default ./sentinel.yaml)
107
+ --packs-dir <path> Rule packs directory (default ./rule-packs)
108
+ --workspace <path> Where target repos are checked out (default .)
109
+ --config-repo <o/n> Config repo, for routing (default $GITHUB_REPOSITORY)
110
+ --provider <name> Local mode model provider (default ollama)
111
+ --model <id> Local mode model id (default llama3.1:70b)
112
+ --dry-run Audit but file no issues`;
113
+ function flagMap(argv) {
114
+ const map = new Map();
115
+ for (let i = 0; i < argv.length; i++) {
116
+ const token = argv[i];
117
+ if (!token || !token.startsWith('--')) {
118
+ continue;
119
+ }
120
+ const body = token.slice(2);
121
+ const eq = body.indexOf('=');
122
+ if (eq !== -1) {
123
+ map.set(body.slice(0, eq), body.slice(eq + 1));
124
+ continue;
125
+ }
126
+ const next = argv[i + 1];
127
+ if (next !== undefined && !next.startsWith('--')) {
128
+ map.set(body, next);
129
+ i++;
130
+ }
131
+ else {
132
+ map.set(body, true);
133
+ }
134
+ }
135
+ return map;
136
+ }
137
+ function flagStr(flags, key) {
138
+ const value = flags.get(key);
139
+ return typeof value === 'string' ? value : undefined;
140
+ }
141
+ function writeReport(markdown, output) {
142
+ if (output) {
143
+ writeFileSync(output, markdown);
144
+ console.log(`Wrote report to ${output}`);
145
+ }
146
+ else {
147
+ console.log(markdown);
148
+ }
149
+ }
150
+ async function runRun(argv) {
151
+ if (argv.includes('--help') || argv.includes('-h')) {
152
+ console.log(RUN_USAGE);
153
+ return 0;
154
+ }
155
+ const flags = flagMap(argv);
156
+ const output = flagStr(flags, 'output');
157
+ // Local calibration mode.
158
+ const rulePack = flagStr(flags, 'rule-pack');
159
+ const repo = flagStr(flags, 'repo');
160
+ if (rulePack && repo) {
161
+ const model = {
162
+ provider: flagStr(flags, 'provider') ?? 'ollama',
163
+ model: flagStr(flags, 'model') ?? 'llama3.1:70b',
164
+ };
165
+ const localConfigDir = flagStr(flags, 'config-dir');
166
+ const outcome = await runLocal(rulePack, repo, model, { runner: nanocoderRunner, files: fsRepoFiles },
167
+ // Absolute — nanocoder runs with cwd set to the audited repo.
168
+ { configDir: localConfigDir ? resolve(localConfigDir) : undefined });
169
+ writeReport(renderReport(outcome), output);
170
+ return 0;
171
+ }
172
+ // Config-driven mode.
173
+ const configPath = flagStr(flags, 'config') ?? 'sentinel.yaml';
174
+ const parsed = parseConfig(readFileSync(configPath, 'utf8'));
175
+ if (!parsed.valid || !parsed.config) {
176
+ for (const error of parsed.errors) {
177
+ console.error(`config error — ${error.field}: ${error.message}`);
178
+ }
179
+ return 1;
180
+ }
181
+ const dryRun = flags.get('dry-run') === true;
182
+ const workspace = flagStr(flags, 'workspace') ?? '.';
183
+ // Clone any target repos not already present in the workspace.
184
+ if (flags.get('no-clone') !== true) {
185
+ const repos = parsed.config.targets
186
+ .map(target => target.repo)
187
+ .filter((repo) => Boolean(repo));
188
+ const ensured = ensureTargets(repos, workspace);
189
+ for (const repo of ensured.cloned) {
190
+ console.log(`cloned ${repo}`);
191
+ }
192
+ for (const { repo, error } of ensured.errors) {
193
+ console.error(`clone failed for ${repo}: ${error}`);
194
+ }
195
+ }
196
+ // The client is available whenever a token is present — a dry run uses it to
197
+ // read existing issues for the preview, a live run to file.
198
+ const hasToken = Boolean(process.env.GITHUB_TOKEN);
199
+ const report = await runFromConfig(parsed.config, {
200
+ runner: nanocoderRunner,
201
+ files: fsRepoFiles,
202
+ packs: fsPackLoader,
203
+ client: hasToken ? ghIssueClient : undefined,
204
+ now: new Date().toISOString(),
205
+ }, {
206
+ workspaceDir: workspace,
207
+ packsDir: flagStr(flags, 'packs-dir') ?? join(dirname(configPath), 'rule-packs'),
208
+ // nanocoder's agents.config.json lives beside sentinel.yaml. Absolute —
209
+ // nanocoder runs with cwd set to the audited repo.
210
+ configDir: resolve(flagStr(flags, 'config-dir') ?? dirname(configPath)),
211
+ configRepo: flagStr(flags, 'config-repo') ?? process.env.GITHUB_REPOSITORY,
212
+ dryRun,
213
+ });
214
+ // Dry run with a token renders the grouped preview; otherwise the plain
215
+ // findings report.
216
+ const markdown = dryRun && report.previews.length > 0
217
+ ? renderPreview(report.previews)
218
+ : renderReport(report.outcome);
219
+ writeReport(markdown, output);
220
+ if (report.filed) {
221
+ for (const { repo: repoName, result } of report.reconciled) {
222
+ console.log(`${repoName}: filed ${result.created.length}, touched ${result.touched}, resolved ${result.resolved}`);
223
+ }
224
+ }
225
+ else if (dryRun) {
226
+ console.log('Dry run — no issues filed.');
227
+ }
228
+ else {
229
+ console.log('No GITHUB_TOKEN — no issues filed.');
230
+ }
231
+ return 0;
232
+ }
233
+ async function main(argv) {
234
+ const [command, ...rest] = argv;
235
+ switch (command) {
236
+ case 'init':
237
+ return runInit(rest);
238
+ case 'run':
239
+ return runRun(rest);
240
+ case undefined:
241
+ case '--help':
242
+ case '-h':
243
+ console.log(USAGE);
244
+ return 0;
245
+ default:
246
+ console.log(`Unknown command: ${command}\n`);
247
+ console.log(USAGE);
248
+ return 1;
249
+ }
250
+ }
251
+ main(process.argv.slice(2)).then(code => {
252
+ process.exit(code);
253
+ });
254
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../source/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Parse and validate `sentinel.yaml`. Applies sensible defaults (schedule,
3
+ * severity threshold, issue routing), enforces the target and model contracts,
4
+ * and returns a structured {@link ConfigResult} — the same error shape used by
5
+ * the findings validator and the rule pack parser.
6
+ */
7
+ import type { ConfigResult } from './types.js';
8
+ /**
9
+ * Parse `sentinel.yaml`. On success, returns the normalised config with
10
+ * defaults applied; on failure, returns every reason it was rejected.
11
+ */
12
+ export declare function parseConfig(raw: string): ConfigResult;
13
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../source/config/parse.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAEX,YAAY,EAKZ,MAAM,YAAY,CAAC;AAgNpB;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAiFrD"}
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Parse and validate `sentinel.yaml`. Applies sensible defaults (schedule,
3
+ * severity threshold, issue routing), enforces the target and model contracts,
4
+ * and returns a structured {@link ConfigResult} — the same error shape used by
5
+ * the findings validator and the rule pack parser.
6
+ */
7
+ import { parse as parseYaml } from 'yaml';
8
+ import { isSeverity } from '../findings/types.js';
9
+ const DEFAULT_SCHEDULE = '0 6 * * *';
10
+ const DEFAULT_SEVERITY_THRESHOLD = 'medium';
11
+ const DEFAULT_LABEL = 'sentinel';
12
+ /** `owner/name`, with no whitespace or extra slashes. */
13
+ const REPO_PATTERN = /^[^/\s]+\/[^/\s]+$/;
14
+ /** Kebab-case rule pack name. */
15
+ const PACK_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
16
+ /** A single cron field: digits, and the `* , - /` operators. */
17
+ const CRON_FIELD_PATTERN = /^[\d*,/-]+$/;
18
+ function isObject(value) {
19
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
20
+ }
21
+ function isNonEmptyString(value) {
22
+ return typeof value === 'string' && value.trim().length > 0;
23
+ }
24
+ /** Light cron validation: five fields, each using only cron characters. */
25
+ function isValidCron(value) {
26
+ const fields = value.trim().split(/\s+/);
27
+ if (fields.length !== 5) {
28
+ return false;
29
+ }
30
+ return fields.every(field => CRON_FIELD_PATTERN.test(field));
31
+ }
32
+ function validateTarget(value, index, errors) {
33
+ const field = `targets[${index}]`;
34
+ if (!isObject(value)) {
35
+ errors.push({ field, message: 'each target must be a mapping' });
36
+ return null;
37
+ }
38
+ const hasRepo = value.repo !== undefined;
39
+ const hasPattern = value.pattern !== undefined;
40
+ if (hasRepo === hasPattern) {
41
+ errors.push({
42
+ field,
43
+ message: 'each target must set exactly one of repo or pattern',
44
+ });
45
+ }
46
+ if (hasRepo &&
47
+ !(isNonEmptyString(value.repo) && REPO_PATTERN.test(value.repo))) {
48
+ errors.push({
49
+ field: `${field}.repo`,
50
+ message: 'repo must be in owner/name form',
51
+ });
52
+ }
53
+ if (hasPattern && !isNonEmptyString(value.pattern)) {
54
+ errors.push({
55
+ field: `${field}.pattern`,
56
+ message: 'pattern must be a non-empty string',
57
+ });
58
+ }
59
+ const rulePacks = [];
60
+ if (!Array.isArray(value.rule_packs) || value.rule_packs.length === 0) {
61
+ errors.push({
62
+ field: `${field}.rule_packs`,
63
+ message: 'rule_packs must be a non-empty list of pack names',
64
+ });
65
+ }
66
+ else {
67
+ for (const [i, name] of value.rule_packs.entries()) {
68
+ if (typeof name === 'string' && PACK_NAME_PATTERN.test(name)) {
69
+ rulePacks.push(name);
70
+ }
71
+ else {
72
+ errors.push({
73
+ field: `${field}.rule_packs[${i}]`,
74
+ message: 'each rule pack name must be kebab-case',
75
+ });
76
+ }
77
+ }
78
+ }
79
+ const target = { rulePacks };
80
+ if (hasRepo && typeof value.repo === 'string') {
81
+ target.repo = value.repo;
82
+ }
83
+ if (hasPattern && typeof value.pattern === 'string') {
84
+ target.pattern = value.pattern;
85
+ }
86
+ return target;
87
+ }
88
+ function validateModel(value, errors) {
89
+ if (!isObject(value)) {
90
+ errors.push({
91
+ field: 'model',
92
+ message: 'model is required and must be a mapping',
93
+ });
94
+ return null;
95
+ }
96
+ if (!isNonEmptyString(value.provider)) {
97
+ errors.push({
98
+ field: 'model.provider',
99
+ message: 'model.provider is required',
100
+ });
101
+ }
102
+ if (!isNonEmptyString(value.model)) {
103
+ errors.push({ field: 'model.model', message: 'model.model is required' });
104
+ }
105
+ const model = {
106
+ provider: isNonEmptyString(value.provider) ? value.provider : '',
107
+ model: isNonEmptyString(value.model) ? value.model : '',
108
+ };
109
+ if (value.fallback !== undefined) {
110
+ if (!isObject(value.fallback)) {
111
+ errors.push({
112
+ field: 'model.fallback',
113
+ message: 'model.fallback must be a mapping',
114
+ });
115
+ }
116
+ else {
117
+ const fb = value.fallback;
118
+ if (!isNonEmptyString(fb.provider) || !isNonEmptyString(fb.model)) {
119
+ errors.push({
120
+ field: 'model.fallback',
121
+ message: 'model.fallback requires provider and model',
122
+ });
123
+ }
124
+ else {
125
+ model.fallback = { provider: fb.provider, model: fb.model };
126
+ if (fb.endpoint_secret !== undefined) {
127
+ if (isNonEmptyString(fb.endpoint_secret)) {
128
+ model.fallback.endpointSecret = fb.endpoint_secret;
129
+ }
130
+ else {
131
+ errors.push({
132
+ field: 'model.fallback.endpoint_secret',
133
+ message: 'endpoint_secret must be a non-empty string',
134
+ });
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ return model;
141
+ }
142
+ function validateIssues(value, errors) {
143
+ const issues = {
144
+ label: DEFAULT_LABEL,
145
+ assignee: null,
146
+ aggregateToConfigRepo: false,
147
+ };
148
+ if (value === undefined) {
149
+ return issues;
150
+ }
151
+ if (!isObject(value)) {
152
+ errors.push({ field: 'issues', message: 'issues must be a mapping' });
153
+ return issues;
154
+ }
155
+ if (value.label !== undefined) {
156
+ if (isNonEmptyString(value.label)) {
157
+ issues.label = value.label;
158
+ }
159
+ else {
160
+ errors.push({
161
+ field: 'issues.label',
162
+ message: 'label must be a non-empty string',
163
+ });
164
+ }
165
+ }
166
+ if (value.assignee !== undefined && value.assignee !== null) {
167
+ if (isNonEmptyString(value.assignee)) {
168
+ issues.assignee = value.assignee;
169
+ }
170
+ else {
171
+ errors.push({
172
+ field: 'issues.assignee',
173
+ message: 'assignee must be a GitHub login or null',
174
+ });
175
+ }
176
+ }
177
+ if (value.aggregate_to_config_repo !== undefined) {
178
+ if (typeof value.aggregate_to_config_repo === 'boolean') {
179
+ issues.aggregateToConfigRepo = value.aggregate_to_config_repo;
180
+ }
181
+ else {
182
+ errors.push({
183
+ field: 'issues.aggregate_to_config_repo',
184
+ message: 'aggregate_to_config_repo must be a boolean',
185
+ });
186
+ }
187
+ }
188
+ return issues;
189
+ }
190
+ /**
191
+ * Parse `sentinel.yaml`. On success, returns the normalised config with
192
+ * defaults applied; on failure, returns every reason it was rejected.
193
+ */
194
+ export function parseConfig(raw) {
195
+ let data;
196
+ try {
197
+ data = parseYaml(raw);
198
+ }
199
+ catch (error) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ return {
202
+ valid: false,
203
+ config: null,
204
+ errors: [{ field: 'document', message: `invalid YAML: ${message}` }],
205
+ };
206
+ }
207
+ if (!isObject(data)) {
208
+ return {
209
+ valid: false,
210
+ config: null,
211
+ errors: [{ field: 'document', message: 'sentinel.yaml must be a mapping' }],
212
+ };
213
+ }
214
+ const errors = [];
215
+ // targets (required, non-empty)
216
+ const targets = [];
217
+ if (!Array.isArray(data.targets) || data.targets.length === 0) {
218
+ errors.push({
219
+ field: 'targets',
220
+ message: 'targets is required and must list at least one repository',
221
+ });
222
+ }
223
+ else {
224
+ for (const [index, entry] of data.targets.entries()) {
225
+ const target = validateTarget(entry, index, errors);
226
+ if (target) {
227
+ targets.push(target);
228
+ }
229
+ }
230
+ }
231
+ // schedule (optional, defaulted)
232
+ let schedule = DEFAULT_SCHEDULE;
233
+ if (data.schedule !== undefined) {
234
+ if (isNonEmptyString(data.schedule) && isValidCron(data.schedule)) {
235
+ schedule = data.schedule.trim();
236
+ }
237
+ else {
238
+ errors.push({
239
+ field: 'schedule',
240
+ message: 'schedule must be a 5-field cron expression',
241
+ });
242
+ }
243
+ }
244
+ // severity_threshold (optional, defaulted)
245
+ let severityThreshold = DEFAULT_SEVERITY_THRESHOLD;
246
+ if (data.severity_threshold !== undefined) {
247
+ if (isSeverity(data.severity_threshold)) {
248
+ severityThreshold = data.severity_threshold;
249
+ }
250
+ else {
251
+ errors.push({
252
+ field: 'severity_threshold',
253
+ message: 'severity_threshold must be one of: low, medium, high, critical',
254
+ });
255
+ }
256
+ }
257
+ const model = validateModel(data.model, errors);
258
+ const issues = validateIssues(data.issues, errors);
259
+ if (errors.length > 0 || !model) {
260
+ return { valid: false, config: null, errors };
261
+ }
262
+ const config = {
263
+ targets,
264
+ schedule,
265
+ severityThreshold,
266
+ model,
267
+ issues,
268
+ };
269
+ return { valid: true, config, errors: [] };
270
+ }
271
+ //# sourceMappingURL=parse.js.map