@ps-neko/nekowork 0.2.0-alpha.6 → 0.2.0-alpha.8

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 (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -8
  3. package/package.json +9 -3
  4. package/scripts/cli.js +18 -8
  5. package/scripts/core/git-mutation-guard.js +65 -0
  6. package/scripts/lib/acceptance-criteria.js +2 -9
  7. package/scripts/lib/ast/analyze.js +986 -0
  8. package/scripts/lib/ast/parse.js +131 -0
  9. package/scripts/lib/decision.js +4 -58
  10. package/scripts/lib/diff-parser.js +75 -4
  11. package/scripts/lib/risk-classifier.js +1 -0
  12. package/scripts/lib/rules/_helpers.js +90 -10
  13. package/scripts/lib/rules/ast-dataflow.js +103 -0
  14. package/scripts/lib/rules/auto-apply-commit-push.js +44 -0
  15. package/scripts/lib/rules/command-injection.js +72 -0
  16. package/scripts/lib/rules/cors-wildcard.js +84 -0
  17. package/scripts/lib/rules/eval-usage.js +102 -0
  18. package/scripts/lib/rules/hardcoded-credential.js +134 -2
  19. package/scripts/lib/rules/insecure-tls.js +86 -0
  20. package/scripts/lib/rules/package-lockfile-risk.js +23 -0
  21. package/scripts/lib/rules/secret-fallback.js +206 -24
  22. package/scripts/lib/rules/sql-injection.js +68 -0
  23. package/scripts/lib/rules/test-or-security-disable.js +102 -0
  24. package/scripts/lib/session-constants.js +30 -0
  25. package/scripts/lib/session-io.js +81 -0
  26. package/scripts/lib/session-resolver.js +17 -0
  27. package/scripts/lib/verify-helpers.js +442 -0
  28. package/scripts/orchestrators/_handoff-utils.js +45 -0
  29. package/scripts/orchestrators/apply.js +33 -17
  30. package/scripts/orchestrators/gate.js +17 -18
  31. package/scripts/orchestrators/report.js +4 -48
  32. package/scripts/orchestrators/verify-pr.js +49 -313
  33. package/scripts/benchmark/capture-live-ai-diff.js +0 -230
  34. package/scripts/benchmark/rules.js +0 -214
  35. package/scripts/benchmark/scrape-oss-positives.js +0 -237
  36. package/scripts/benchmark/verify-candidates.js +0 -110
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HARNESS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -4,9 +4,21 @@
4
4
 
5
5
  AI can write 100 lines in 10 seconds. Who checks them before they hit `main`?
6
6
 
7
- This package reviews every change your AI tool makes, flags the risky parts with
8
- deterministic rules, and lets **you** make the final call. It never commits,
9
- pushes, or deploys on its own.
7
+ This package reviews every change your AI tool makes, **flags a defined set of
8
+ AI-introduced risk patterns** (11 deterministic rules: secrets, secret fallbacks,
9
+ hardcoded credentials, auto-push/commit, test/security disables, risky package
10
+ hooks, eval, insecure TLS, CORS wildcard, basic SQL/command injection, and AST
11
+ dataflow taint for variable-mediated injection) and routes everything else to a
12
+ human decision. It is **not an exhaustive security audit** — the AST rule is
13
+ intraprocedural (single-function, JS/TS); cross-function and whole-program dataflow
14
+ are out of scope. The verdict is deterministic (same diff, same result), and it never
15
+ commits, pushes, or deploys on its own. **You** make the final call.
16
+
17
+ > Note: the published `@alpha` (0.2.0-alpha.7) now ships all **11 rules** described
18
+ > above (incl. eval, insecure TLS, CORS wildcard, SQL/command injection, AST dataflow)
19
+ > and adds **one tiny, well-known dependency** (`acorn`, the JS parser — MIT, zero
20
+ > transitive dependencies) for the AST engine. Always install with the **`@alpha`**
21
+ > tag: the `latest` dist-tag is a stale `0.2.0-alpha.0` (5 rules, zero deps).
10
22
 
11
23
  ## Status
12
24
 
@@ -33,14 +45,21 @@ npx -y @ps-neko/nekowork@alpha verify-pr # scan the diff → get a verdict
33
45
  `verify-pr` reads the diff, writes a plain-English `REPORT.md`, and tells you
34
46
  whether the change is safe to merge.
35
47
 
36
- ## The 4 verbs
48
+ ## The verbs
49
+
50
+ **Primary — the 1.0 front surface. Start here:**
37
51
 
38
52
  | Verb | What it does |
39
53
  |---|---|
40
54
  | `check` | Probe environment readiness (Node version, git repo, etc.) |
41
- | `verify-pr` | Scan working-tree diff. Produce REPORT.md + .nekowork/decision.json |
42
- | `report` | Session-based compatibility command. Requires `--session <id>` and renders that session's evidence to REPORT.md. The normal `verify-pr` path already writes REPORT.md directly — you don't need `report` for it. |
43
- | `apply` | Session-based compatibility apply. Requires a completed work cycle (SHIP_READY marker + cleared Human Gate). NOT driven by verify-pr's decision.json. See [ADVANCED.md](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/ADVANCED.md). |
55
+ | `verify-pr` | Scan the working-tree diff. Produce REPORT.md + .nekowork/decision.json |
56
+
57
+ **Compatibility session-based (legacy/advanced; not needed for the normal flow):**
58
+
59
+ | Verb | What it does |
60
+ |---|---|
61
+ | `report --session <id>` | Render that session's evidence to REPORT.md. The normal `verify-pr` path already writes REPORT.md directly — you don't need `report` for it. |
62
+ | `apply --session <id>` | Apply a stored `.diff`. Requires a completed work cycle (SHIP_READY marker + cleared Human Gate). NOT driven by verify-pr's decision.json. See [ADVANCED.md](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/ADVANCED.md). |
44
63
 
45
64
  Anything else (`ask`, `plan`, `team`, `work`, `ship`, `build`, `auto`,
46
65
  `pr-prep`, `review`, ...) belongs to `@ps-neko/nekowork-harness` (legacy and
@@ -66,7 +85,7 @@ step — it is not triggered by `decision.json`.
66
85
 
67
86
  - [Quickstart](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/QUICKSTART.md)
68
87
  - [How verification works](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/SCOPE-1.0.md)
69
- - [Benchmark](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/BENCHMARK.md) — 85/86 (99%) recall, 0/47 FP, 30 real OSS positives (secret-fallback)
88
+ - [Benchmark](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/BENCHMARK.md) — 11 rules, 184/184 (100%) recall, 0/120 FP; 30 real OSS positives on `secret-fallback`, the newer rules (incl. sql/command injection and `ast-dataflow`) are synthetic-only
70
89
  - [Integration](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/INTEGRATION.md)
71
90
 
72
91
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ps-neko/nekowork",
3
- "version": "0.2.0-alpha.6",
3
+ "version": "0.2.0-alpha.8",
4
4
  "description": "Local verification gate for AI-written code diffs. Deterministic rules decide the verdict, never the LLM. No auto-commit, push, or deploy — you decide at the Human Gate.",
5
5
  "keywords": [
6
6
  "ai-code-review",
@@ -33,7 +33,11 @@
33
33
  "nekowork": "scripts/cli.js"
34
34
  },
35
35
  "files": [
36
- "scripts/",
36
+ "scripts/cli.js",
37
+ "scripts/check.js",
38
+ "scripts/core/",
39
+ "scripts/lib/",
40
+ "scripts/orchestrators/",
37
41
  "README.md",
38
42
  "LICENSE"
39
43
  ],
@@ -43,5 +47,7 @@
43
47
  "test": "node --test tests/unit/*.test.js",
44
48
  "bench:rules": "node scripts/benchmark/rules.js"
45
49
  },
46
- "dependencies": {}
50
+ "dependencies": {
51
+ "acorn": "^8.16.0"
52
+ }
47
53
  }
package/scripts/cli.js CHANGED
@@ -13,8 +13,6 @@ import {
13
13
  verifyPrCycle,
14
14
  parseVerifyPrArgs,
15
15
  printVerifyPrSummary,
16
- EXIT_CODE,
17
- VERDICT,
18
16
  } from './orchestrators/verify-pr.js';
19
17
 
20
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -38,6 +36,7 @@ verify-pr options:
38
36
  --from-staged scan staged diff
39
37
  --range <baseSha...head> scan commit range
40
38
  --from-patch <file> scan a patch file
39
+ --full-scan scan the whole tree (onboarding; no PR/diff yet)
41
40
  --include <path> force-scan a path even if gitignored
42
41
  --comment-file <path> write a PR-comment markdown
43
42
  --ci-exit-soft NEEDS_HUMAN_REVIEW / INSUFFICIENT_EVIDENCE → exit 0
@@ -93,7 +92,20 @@ async function runInternal(verb, rest) {
93
92
 
94
93
  if (verb === 'verify-pr') {
95
94
  const opts = parseVerifyPrArgs(rest);
96
- const result = await verifyPrCycle(opts);
95
+ let result;
96
+ try {
97
+ result = await verifyPrCycle(opts);
98
+ } catch (e) {
99
+ const msg = String(e?.message || e);
100
+ if (/not a git repository/i.test(msg)) {
101
+ console.error('verify-pr could not find a git repository here.');
102
+ console.error(' Run it inside a git repo, or scan a patch file instead:');
103
+ console.error(' nekowork verify-pr --from-patch <file>');
104
+ process.exit(2);
105
+ }
106
+ console.error(msg);
107
+ process.exit(1);
108
+ }
97
109
  if (opts.json) {
98
110
  console.log(JSON.stringify({
99
111
  decision: result.decision,
@@ -104,11 +116,9 @@ async function runInternal(verb, rest) {
104
116
  } else {
105
117
  printVerifyPrSummary(result);
106
118
  }
107
- let exitCode = EXIT_CODE[result.decision.verdict] ?? 1;
108
- if (opts.ciExitSoft && (result.decision.verdict === VERDICT.NEEDS_HUMAN_REVIEW || result.decision.verdict === VERDICT.INSUFFICIENT_EVIDENCE)) {
109
- exitCode = 0;
110
- }
111
- process.exit(exitCode);
119
+ // Single source of truth: verifyPrCycle already computed exitCode and
120
+ // honored --ci-exit-soft. Do not recompute here.
121
+ process.exit(result.exitCode ?? 1);
112
122
  }
113
123
 
114
124
  if (verb === 'report') {
@@ -47,6 +47,71 @@ export async function withGitMutationGuard(root, fn, options = {}) {
47
47
  return result;
48
48
  }
49
49
 
50
+ /**
51
+ * Synchronous mutation guard for `apply`: the apply step DOES legitimately
52
+ * mutate the working tree, so a blunt before≠after check is wrong here. Instead
53
+ * we sanction the EXPECTED files (the diff's own files) and reject only changes
54
+ * to git-tracked paths OUTSIDE that set — i.e. an unexpected EXTRA mutation
55
+ * (a stray commit/checkout/branch op or an edit to an unrelated file the apply
56
+ * was not supposed to touch).
57
+ *
58
+ * Behavior-preserving: when only the expected files changed (the normal case)
59
+ * this returns fn()'s result unchanged. No-op outside a git worktree or when
60
+ * `allowEnvKey` is set to '1'.
61
+ *
62
+ * @param {string} root
63
+ * @param {() => T} fn synchronous function performing the apply
64
+ * @param {object} [options]
65
+ * @param {string[]} [options.expectedPaths] repo-relative paths the apply may touch
66
+ * @param {string} [options.label]
67
+ * @param {string} [options.allowEnvKey]
68
+ * @param {NodeJS.ProcessEnv} [options.env]
69
+ * @returns {T}
70
+ * @template T
71
+ */
72
+ export function withGitMutationGuardSync(root, fn, options = {}) {
73
+ const label = options.label || 'apply';
74
+ const allowEnvKey = options.allowEnvKey || 'HARNESS_ALLOW_WORKSPACE_MUTATION';
75
+ const env = options.env || process.env;
76
+ const expected = new Set((options.expectedPaths || []).map(p => String(p).split('\\').join('/')));
77
+
78
+ if (!root || env[allowEnvKey] === '1' || !isGitWorkTree(root)) {
79
+ return fn();
80
+ }
81
+
82
+ const beforeSet = changedPathSet(root);
83
+ const result = fn();
84
+ const afterSet = changedPathSet(root);
85
+
86
+ const unexpected = [...afterSet].filter(p => !beforeSet.has(p) && !expected.has(p));
87
+ if (unexpected.length) {
88
+ throw new Error([
89
+ `${label} produced unexpected git changes outside the applied diff.`,
90
+ `Set ${allowEnvKey}=1 only when these extra mutations are intentional.`,
91
+ '',
92
+ 'Unexpected paths:',
93
+ ...unexpected.map(p => ` ${p}`),
94
+ ].join('\n'));
95
+ }
96
+ return result;
97
+ }
98
+
99
+ function changedPathSet(root) {
100
+ const text = runGit(root, ['status', '--porcelain=v1']);
101
+ const set = new Set();
102
+ for (const line of (text || '').split(/\r?\n/)) {
103
+ if (!line.trim()) continue;
104
+ // porcelain v1: "XY <path>" (and "XY <old> -> <new>" for renames).
105
+ let p = line.slice(3).trim();
106
+ const arrow = p.indexOf(' -> ');
107
+ if (arrow !== -1) p = p.slice(arrow + 4);
108
+ // strip optional surrounding quotes git adds for unusual paths
109
+ p = p.replace(/^"(.*)"$/, '$1').split('\\').join('/');
110
+ set.add(p);
111
+ }
112
+ return set;
113
+ }
114
+
50
115
  export function readGitStatus(root) {
51
116
  if (!isGitWorkTree(root)) return null;
52
117
 
@@ -1,5 +1,7 @@
1
+ // Shared library module — consumed by the heavy @ps-neko/nekowork-harness package; kept in slim as the single source of truth.
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
4
+ import { readJson } from './session-io.js';
3
5
 
4
6
  const DEFAULT_COUNT = 3;
5
7
 
@@ -94,12 +96,3 @@ export function buildDefaultAcceptanceCriteria(task = '', minimum = DEFAULT_COUN
94
96
  source: 'task-derived-minimum',
95
97
  }));
96
98
  }
97
-
98
- function readJson(file) {
99
- if (!fs.existsSync(file)) return null;
100
- try {
101
- return JSON.parse(fs.readFileSync(file, 'utf8'));
102
- } catch {
103
- return null;
104
- }
105
- }