@gitset-dev/cli 2.4.0 → 2.4.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.
package/README.md CHANGED
@@ -87,6 +87,38 @@ Local tools (no AI):
87
87
  Common flags: `--provider` `--model` `--yes` `--print` `--json`. Run
88
88
  `gitset help <command>` for everything else.
89
89
 
90
+ ## Knowledge Mapper
91
+
92
+ `gitset knowledge` builds and maintains `docs/gitset-knowledge/` — a
93
+ structured, always-current map of your codebase (architecture, module
94
+ boundaries, commands, dependency graph) that both developers and AI coding
95
+ agents can read before touching your code. It's generated from your source
96
+ code, manifests, and CI configuration — never from your existing prose docs,
97
+ so it can't inherit their drift.
98
+
99
+ ```sh
100
+ gitset knowledge init # scaffold optional .gitset-knowledge.json (include/exclude globs)
101
+ gitset knowledge scan # zero AI calls — discovers files, prints the plan + exact cost estimate
102
+ gitset knowledge generate # shows the estimate again, asks to confirm, then writes the knowledge base
103
+ gitset knowledge update # incremental — only changed modules (and their direct importers) are re-summarized
104
+ gitset knowledge automate # writes a GitHub Actions workflow that runs `update` in CI and opens a review PR
105
+ ```
106
+
107
+ It's a six-stage local pipeline (Discover → Map → Summarize → Plan → Write →
108
+ Validate): Discover and Map walk the repository and build an import graph
109
+ with zero AI calls; only Summarize and Write touch your configured provider
110
+ — there's no separate "default model" for this tool, it uses whatever you
111
+ picked with `gitset config`; and a deterministic Validate pass checks every
112
+ generated link, command, and script before anything is written to disk.
113
+ Secrets are redacted locally before any file content is sent, and prose
114
+ docs / test file bodies are listed structurally but never sent at all.
115
+
116
+ `gitset knowledge automate` needs "Allow GitHub Actions to create and
117
+ approve pull requests" enabled under the repo's **Settings > Actions >
118
+ General > Workflow permissions** — without it, the scheduled update still
119
+ runs and commits safely, but the review-PR step fails, and the workflow run
120
+ will show that failure clearly rather than hide it.
121
+
90
122
  ## Templates
91
123
 
92
124
  Define your commit style, issue structure, PR format, README skeleton, or
@@ -53,6 +53,12 @@ function buildKnowledgeWorkflow({ mode, provider, envKey, model, defaultBranch =
53
53
  '# provider — never to Gitset. Runs whose mapped source files are',
54
54
  '# unchanged exit without any AI call (content-hash diff), so harmless',
55
55
  '# triggers cost nothing beyond a few CI seconds.',
56
+ '#',
57
+ '# Also requires "Allow GitHub Actions to create and approve pull',
58
+ '# requests" to be enabled: Settings > Actions > General > Workflow',
59
+ '# permissions. Without it, the update still commits and pushes',
60
+ '# safely, but opening the review PR fails and this job reports it',
61
+ '# as a failure so it is never silently missed.',
56
62
  ...buildTrigger(mode, defaultBranch),
57
63
  'permissions:',
58
64
  ' contents: write',
@@ -87,7 +93,17 @@ function buildKnowledgeWorkflow({ mode, provider, envKey, model, defaultBranch =
87
93
  ' git add docs/gitset-knowledge AGENTS.md',
88
94
  ' git commit -m "docs: refresh knowledge base"',
89
95
  ' git push -f origin gitset/knowledge-update',
90
- ' gh pr create --title "docs: refresh knowledge base" --body "Automated incremental update by [Gitset](https://gitset.dev) Knowledge Mapper. Only changed modules were re-analyzed; review like any other docs change." || true',
96
+ ' echo "Committed and pushed gitset/knowledge-update this work is now safe on GitHub regardless of the next step."',
97
+ ' if PR_OUTPUT=$(gh pr create --title "docs: refresh knowledge base" --body "Automated incremental update by [Gitset](https://gitset.dev) Knowledge Mapper. Only changed modules were re-analyzed; review like any other docs change." 2>&1); then',
98
+ ' echo "$PR_OUTPUT"',
99
+ ' elif echo "$PR_OUTPUT" | grep -qi "already exists"; then',
100
+ ' echo "A pull request for gitset/knowledge-update already exists — nothing further to do."',
101
+ ' echo "$PR_OUTPUT"',
102
+ ' else',
103
+ ' echo "$PR_OUTPUT"',
104
+ ' echo "::error::Could not open the pull request automatically (the branch and commit ARE safely pushed — no work was lost). This is usually because \'Allow GitHub Actions to create and approve pull requests\' is disabled for this repository: enable it under Settings > Actions > General > Workflow permissions, then either re-run this workflow or open the PR yourself: https://github.com/${{ github.repository }}/pull/new/gitset/knowledge-update"',
105
+ ' exit 1',
106
+ ' fi',
91
107
  '',
92
108
  ].join('\n');
93
109
  }
@@ -151,10 +151,14 @@ const knowledgeSummarize = {
151
151
  'exposes — module.exports, export statements, __all__; an executable or ' +
152
152
  'script with no exports gets [], never its internal functions; max 12), ' +
153
153
  '"dependencies": string[] (notable internal files or external ' +
154
- 'packages this file relies on, max 10), "notes": string (<= 160 chars ' +
155
- 'gotchas, side effects, or "" if none)}]. One entry per FILE block in the ' +
156
- 'input, using the exact path given. For a dispatcher/entry-point file, ' +
157
- 'note in "notes" the exact command or route names it registers.',
154
+ 'packages this file relies on, max 10 each entry must be a distinct, ' +
155
+ 'real import copied from the file\'s own import/require statements; if ' +
156
+ 'you are not certain of one, omit it rather than guessing, and never ' +
157
+ 'repeat or extend a name into a longer variant), "notes": string ' +
158
+ '(<= 160 chars — gotchas, side effects, or "" if none)}]. One entry per ' +
159
+ 'FILE block in the input, using the exact path given. For a dispatcher/' +
160
+ 'entry-point file, note in "notes" the exact command or route names it ' +
161
+ 'registers.',
158
162
  user: [
159
163
  repo && `Repository: ${clip(repo, 200)}`,
160
164
  module && `Module: ${clip(module, 200)}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitset-dev/cli",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "Gitset CLI — drafts commits, PRs, issues, READMEs and release notes with your own AI key. Fully local: your code and keys never touch a Gitset server. Draft. Refine. Ship.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -125,6 +125,12 @@ async function confirmOrAbort(question, argv) {
125
125
  return answer === 'y' || answer === 'yes';
126
126
  }
127
127
 
128
+ function appendModuleEntries(moduleSummaries, moduleName, entries) {
129
+ const prev = moduleSummaries[moduleName];
130
+ const prevArray = Array.isArray(prev) ? prev : [];
131
+ moduleSummaries[moduleName] = [...prevArray, ...entries];
132
+ }
133
+
128
134
  async function summarizeBatches({ batches, repo, argv, cachedSummaries = {}, usage }) {
129
135
  const moduleSummaries = { ...cachedSummaries };
130
136
  let providerUsed = null;
@@ -133,7 +139,7 @@ async function summarizeBatches({ batches, repo, argv, cachedSummaries = {}, usa
133
139
  for (const batch of batches) {
134
140
  done += 1;
135
141
  log(` [${done}/${batches.length}] Summarizing ${batch.module}${batch.part > 1 ? ` (part ${batch.part})` : ''}…`, 'dim');
136
- const result = await genLocal.generate({
142
+ const summarizeBatch = (temperature) => genLocal.generate({
137
143
  tool: 'knowledgeSummarize',
138
144
  ctx: {
139
145
  repo,
@@ -143,19 +149,37 @@ async function summarizeBatches({ batches, repo, argv, cachedSummaries = {}, usa
143
149
  provider: flag(argv, '--provider'),
144
150
  model: flag(argv, '--model'),
145
151
  maxTokens: km.DEFAULTS.summarizeMaxTokens,
146
- temperature: 0.2,
152
+ temperature,
147
153
  interactive: true,
148
154
  });
155
+
156
+ let result = await summarizeBatch(0.2);
157
+ trackUsage(usage, result);
149
158
  providerUsed = result.provider;
150
159
  modelUsed = result.model;
151
- trackUsage(usage, result);
152
- const entries = km.parseSummarizeResponse(result.raw);
153
- const prev = moduleSummaries[batch.module];
160
+ let entries = km.parseSummarizeResponse(result.raw);
161
+
162
+ if (!entries) {
163
+ log(` Structured parse failed for ${batch.module}${batch.part > 1 ? ` (part ${batch.part})` : ''}; retrying once…`, 'yellow');
164
+ result = await summarizeBatch(0.5);
165
+ trackUsage(usage, result);
166
+ providerUsed = result.provider;
167
+ modelUsed = result.model;
168
+ entries = km.parseSummarizeResponse(result.raw);
169
+ }
170
+
154
171
  if (entries) {
155
- moduleSummaries[batch.module] = Array.isArray(prev) ? [...prev, ...entries] : entries;
172
+ appendModuleEntries(moduleSummaries, batch.module, entries);
156
173
  } else {
157
- log(` Structured parse failed for ${batch.module}; keeping raw summary text.`, 'yellow');
158
- moduleSummaries[batch.module] = typeof prev === 'string' ? `${prev}\n${result.text}` : result.text;
174
+ log(` Still unparseable after retry; recording ${batch.files.length} file(s) as summary-unavailable (no data lost for the rest of the module).`, 'yellow');
175
+ const placeholders = batch.files.map((f) => ({
176
+ path: f.path,
177
+ purpose: '(AI summary unavailable — the model\'s response for this batch could not be parsed after a retry)',
178
+ exports: [],
179
+ dependencies: [],
180
+ notes: '',
181
+ }));
182
+ appendModuleEntries(moduleSummaries, batch.module, placeholders);
159
183
  }
160
184
  }
161
185
  return { moduleSummaries, providerUsed, modelUsed };
@@ -468,6 +492,39 @@ async function runUpdate(rootDir, argv) {
468
492
  }
469
493
  }
470
494
 
495
+ function ghApiJson(args) {
496
+ try {
497
+ const out = execFileSync('gh', ['api', ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
498
+ return JSON.parse(out);
499
+ } catch {
500
+ return null;
501
+ }
502
+ }
503
+
504
+ async function ensurePrPermission(repo, argv) {
505
+ if (!repo.includes('/')) return 'unknown';
506
+ const current = ghApiJson([`/repos/${repo}/actions/permissions/workflow`]);
507
+ if (!current) return 'unknown';
508
+ if (current.can_approve_pull_request_reviews) return 'already-enabled';
509
+
510
+ log('\n"Allow GitHub Actions to create and approve pull requests" is currently OFF for this repository. The update step will still run and commit safely without it — only opening the review PR will fail.', 'yellow');
511
+ const proceed = await confirmOrAbort('Enable it now via the GitHub API? (only this one setting changes)', argv);
512
+ if (!proceed) return 'declined';
513
+
514
+ try {
515
+ execFileSync('gh', [
516
+ 'api', '--method', 'PUT', `/repos/${repo}/actions/permissions/workflow`,
517
+ '-f', `default_workflow_permissions=${current.default_workflow_permissions}`,
518
+ '-F', 'can_approve_pull_request_reviews=true',
519
+ ], { stdio: 'pipe' });
520
+ log('Enabled — CI will be able to open the review PR.', 'green');
521
+ return 'enabled';
522
+ } catch {
523
+ log('Could not change it automatically (you may not have admin access on this repo, or an organization policy may lock this setting).', 'yellow');
524
+ return 'failed';
525
+ }
526
+ }
527
+
471
528
  async function runAutomate(rootDir, argv) {
472
529
  let cfg;
473
530
  try {
@@ -534,6 +591,8 @@ async function runAutomate(rootDir, argv) {
534
591
  log(`\nWrote ${km.WORKFLOW_PATH}.`, 'green');
535
592
 
536
593
  const repo = repoLabel(rootDir);
594
+ const prPermission = await ensurePrPermission(repo, argv);
595
+
537
596
  log('\nNext steps:', 'cyan');
538
597
  log(` 1. Add the repository secret ${envKey} with your ${cfg.provider} API key:`, 'reset');
539
598
  if (repo.includes('/')) {
@@ -541,8 +600,19 @@ async function runAutomate(rootDir, argv) {
541
600
  } else {
542
601
  log(' GitHub repo > Settings > Secrets and variables > Actions', 'dim');
543
602
  }
544
- log(' 2. Make sure the knowledge base itself is committed (gitset knowledge generate, then commit docs/gitset-knowledge/ and AGENTS.md).', 'reset');
545
- log(' 3. Commit and push the workflow file.', 'reset');
603
+ if (prPermission === 'enabled' || prPermission === 'already-enabled') {
604
+ log(' 2. "Allow GitHub Actions to create and approve pull requests" is already on — the update PR step is ready.', 'reset');
605
+ } else {
606
+ log(' 2. Enable "Allow GitHub Actions to create and approve pull requests" (required for the update PR):', 'reset');
607
+ if (repo.includes('/')) {
608
+ log(` https://github.com/${repo}/settings/actions`, 'dim');
609
+ } else {
610
+ log(' GitHub repo > Settings > Actions > General > Workflow permissions', 'dim');
611
+ }
612
+ log(' Without this, the update still runs and commits safely — only opening the review PR fails, and the workflow run will show that failure clearly rather than hide it.', 'dim');
613
+ }
614
+ log(' 3. Make sure the knowledge base itself is committed (gitset knowledge generate, then commit docs/gitset-knowledge/ and AGENTS.md).', 'reset');
615
+ log(' 4. Commit and push the workflow file.', 'reset');
546
616
  log('\nWhen mapped source changes land, CI opens a PR on branch gitset/knowledge-update — you review it like any docs change. Runs with no mapped changes make zero AI calls.', 'dim');
547
617
  return 0;
548
618
  }