shift-ax 0.3.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +145 -0
  3. package/README.md +143 -0
  4. package/dist/adapters/claude-code/adapter.js +90 -0
  5. package/dist/adapters/codex/adapter.js +94 -0
  6. package/dist/adapters/contracts.js +7 -0
  7. package/dist/adapters/index.js +12 -0
  8. package/dist/core/context/context-bundle.js +300 -0
  9. package/dist/core/context/discovery.js +82 -0
  10. package/dist/core/context/global-index-authoring.js +199 -0
  11. package/dist/core/context/global-knowledge-updates.js +116 -0
  12. package/dist/core/context/glossary.js +73 -0
  13. package/dist/core/context/guided-onboarding.js +233 -0
  14. package/dist/core/context/index-authoring.js +47 -0
  15. package/dist/core/context/index-resolver.js +78 -0
  16. package/dist/core/context/onboarding.js +186 -0
  17. package/dist/core/diagnostics/doctor.js +154 -0
  18. package/dist/core/finalization/commit-message.js +76 -0
  19. package/dist/core/finalization/commit-workflow.js +131 -0
  20. package/dist/core/memory/consolidation.js +99 -0
  21. package/dist/core/memory/decision-register.js +141 -0
  22. package/dist/core/memory/entity-memory.js +25 -0
  23. package/dist/core/memory/learned-debug.js +52 -0
  24. package/dist/core/memory/summary-checkpoints.js +9 -0
  25. package/dist/core/memory/thread-promotion.js +22 -0
  26. package/dist/core/memory/threads.js +66 -0
  27. package/dist/core/memory/topic-recall.js +52 -0
  28. package/dist/core/observability/context-health.js +15 -0
  29. package/dist/core/observability/context-monitor.js +29 -0
  30. package/dist/core/observability/state-handoff.js +78 -0
  31. package/dist/core/observability/topic-status.js +40 -0
  32. package/dist/core/observability/topics-status.js +26 -0
  33. package/dist/core/observability/verification-debt.js +82 -0
  34. package/dist/core/planning/brainstorm.js +120 -0
  35. package/dist/core/planning/escalation.js +69 -0
  36. package/dist/core/planning/execution-handoff.js +61 -0
  37. package/dist/core/planning/execution-launch.js +156 -0
  38. package/dist/core/planning/execution-orchestrator.js +87 -0
  39. package/dist/core/planning/feedback-reactions.js +75 -0
  40. package/dist/core/planning/lifecycle-events.js +45 -0
  41. package/dist/core/planning/plan-review.js +76 -0
  42. package/dist/core/planning/policy-context-sync.js +154 -0
  43. package/dist/core/planning/request-pipeline.js +386 -0
  44. package/dist/core/planning/workflow-state.js +18 -0
  45. package/dist/core/policies/project-profile.js +28 -0
  46. package/dist/core/policies/team-preferences.js +17 -0
  47. package/dist/core/review/aggregate-reviews.js +129 -0
  48. package/dist/core/review/run-lanes.js +376 -0
  49. package/dist/core/settings/global-context-home.js +28 -0
  50. package/dist/core/settings/project-settings.js +37 -0
  51. package/dist/core/shell/platform-shell.js +144 -0
  52. package/dist/core/topics/bootstrap.js +119 -0
  53. package/dist/core/topics/topic-artifacts.js +36 -0
  54. package/dist/core/topics/worktree-runtime.js +141 -0
  55. package/dist/core/topics/worktree.js +8 -0
  56. package/dist/platform/claude-code/bootstrap.js +66 -0
  57. package/dist/platform/claude-code/execution.js +157 -0
  58. package/dist/platform/claude-code/scaffold/CLAUDE.template.md +40 -0
  59. package/dist/platform/claude-code/scaffold/commands/doctor.template.md +11 -0
  60. package/dist/platform/claude-code/scaffold/commands/export-context.template.md +20 -0
  61. package/dist/platform/claude-code/scaffold/commands/onboard.template.md +43 -0
  62. package/dist/platform/claude-code/scaffold/commands/onboarding.template.md +43 -0
  63. package/dist/platform/claude-code/scaffold/commands/request.template.md +19 -0
  64. package/dist/platform/claude-code/scaffold/commands/resume.template.md +12 -0
  65. package/dist/platform/claude-code/scaffold/commands/review.template.md +10 -0
  66. package/dist/platform/claude-code/scaffold/commands/status.template.md +14 -0
  67. package/dist/platform/claude-code/scaffold/commands/topics.template.md +10 -0
  68. package/dist/platform/claude-code/scaffold/hooks/shift-ax-session-start.template.md +29 -0
  69. package/dist/platform/claude-code/tmux.js +35 -0
  70. package/dist/platform/claude-code/upstream/tmux/imported/detached-session.js +40 -0
  71. package/dist/platform/claude-code/upstream/tmux/imported/session-name.js +19 -0
  72. package/dist/platform/claude-code/upstream/worktree/imported/get-worktree-root.js +39 -0
  73. package/dist/platform/claude-code/upstream/worktree/imported/managed-worktree.js +77 -0
  74. package/dist/platform/claude-code/worktree.js +79 -0
  75. package/dist/platform/codex/bootstrap.js +69 -0
  76. package/dist/platform/codex/execution.js +163 -0
  77. package/dist/platform/codex/scaffold/AGENTS.template.md +40 -0
  78. package/dist/platform/codex/scaffold/prompts/doctor.template.md +11 -0
  79. package/dist/platform/codex/scaffold/prompts/export-context.template.md +20 -0
  80. package/dist/platform/codex/scaffold/prompts/onboard.template.md +43 -0
  81. package/dist/platform/codex/scaffold/prompts/onboarding.template.md +43 -0
  82. package/dist/platform/codex/scaffold/prompts/request.template.md +19 -0
  83. package/dist/platform/codex/scaffold/prompts/resume.template.md +14 -0
  84. package/dist/platform/codex/scaffold/prompts/review.template.md +10 -0
  85. package/dist/platform/codex/scaffold/prompts/shift-ax-bootstrap.template.md +23 -0
  86. package/dist/platform/codex/scaffold/prompts/status.template.md +14 -0
  87. package/dist/platform/codex/scaffold/prompts/topics.template.md +10 -0
  88. package/dist/platform/codex/scaffold/skills/doctor/SKILL.template.md +11 -0
  89. package/dist/platform/codex/scaffold/skills/export-context/SKILL.template.md +20 -0
  90. package/dist/platform/codex/scaffold/skills/onboard/SKILL.template.md +43 -0
  91. package/dist/platform/codex/scaffold/skills/request/SKILL.template.md +19 -0
  92. package/dist/platform/codex/scaffold/skills/resume/SKILL.template.md +14 -0
  93. package/dist/platform/codex/scaffold/skills/review/SKILL.template.md +10 -0
  94. package/dist/platform/codex/scaffold/skills/status/SKILL.template.md +14 -0
  95. package/dist/platform/codex/scaffold/skills/topics/SKILL.template.md +10 -0
  96. package/dist/platform/codex/tmux.js +45 -0
  97. package/dist/platform/codex/upstream/tmux/imported/resize-hook-registration.js +37 -0
  98. package/dist/platform/codex/upstream/tmux/imported/resize-hooks.js +29 -0
  99. package/dist/platform/codex/upstream/tmux/imported/sanitize-team-name.js +18 -0
  100. package/dist/platform/codex/upstream/worktree/imported/managed-worktree.js +208 -0
  101. package/dist/platform/codex/upstream/worktree/imported/resolve-repo-root.js +14 -0
  102. package/dist/platform/codex/worktree.js +99 -0
  103. package/dist/platform/index.js +10 -0
  104. package/dist/platform/product-shell-commands.js +17 -0
  105. package/dist/platform/scaffold.js +16 -0
  106. package/dist/platform/upstream-imports.js +5 -0
  107. package/dist/scripts/ax-approve-plan.js +30 -0
  108. package/dist/scripts/ax-bootstrap-assets.js +19 -0
  109. package/dist/scripts/ax-bootstrap-topic.js +24 -0
  110. package/dist/scripts/ax-build-context-bundle.js +35 -0
  111. package/dist/scripts/ax-checkpoint-context.js +22 -0
  112. package/dist/scripts/ax-consolidate-memory.js +7 -0
  113. package/dist/scripts/ax-context-health.js +26 -0
  114. package/dist/scripts/ax-decisions.js +32 -0
  115. package/dist/scripts/ax-doctor.js +25 -0
  116. package/dist/scripts/ax-entity-memory.js +19 -0
  117. package/dist/scripts/ax-export-context.js +8 -0
  118. package/dist/scripts/ax-finalize-commit.js +23 -0
  119. package/dist/scripts/ax-init-context.js +41 -0
  120. package/dist/scripts/ax-launch-execution.js +24 -0
  121. package/dist/scripts/ax-learned-debug-save.js +30 -0
  122. package/dist/scripts/ax-learned-debug.js +12 -0
  123. package/dist/scripts/ax-monitor-context.js +28 -0
  124. package/dist/scripts/ax-onboard-context.js +112 -0
  125. package/dist/scripts/ax-pause-work.js +33 -0
  126. package/dist/scripts/ax-platform-manifest.js +19 -0
  127. package/dist/scripts/ax-promote-thread.js +20 -0
  128. package/dist/scripts/ax-react-feedback.js +28 -0
  129. package/dist/scripts/ax-recall-topics.js +20 -0
  130. package/dist/scripts/ax-recall.js +58 -0
  131. package/dist/scripts/ax-refresh-state.js +15 -0
  132. package/dist/scripts/ax-resolve-context.js +34 -0
  133. package/dist/scripts/ax-review.js +24 -0
  134. package/dist/scripts/ax-run-request.js +198 -0
  135. package/dist/scripts/ax-scaffold-build.js +19 -0
  136. package/dist/scripts/ax-shell.js +123 -0
  137. package/dist/scripts/ax-sync-policy-context.js +40 -0
  138. package/dist/scripts/ax-team-preferences.js +20 -0
  139. package/dist/scripts/ax-thread-save.js +26 -0
  140. package/dist/scripts/ax-threads.js +11 -0
  141. package/dist/scripts/ax-topic-status.js +18 -0
  142. package/dist/scripts/ax-topics-status.js +22 -0
  143. package/dist/scripts/ax-verification-debt.js +22 -0
  144. package/dist/scripts/ax-worktree-create.js +22 -0
  145. package/dist/scripts/ax-worktree-plan.js +18 -0
  146. package/dist/scripts/ax-worktree-remove.js +18 -0
  147. package/dist/scripts/ax.js +132 -0
  148. package/package.json +71 -0
@@ -0,0 +1,73 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ function uniq(items) {
4
+ return [...new Set(items)];
5
+ }
6
+ function scoreTerm(term) {
7
+ const noSpaces = term.replace(/\s+/g, '');
8
+ if (/^[A-Z][a-z]+(?:[A-Z][a-z]+)+$/.test(noSpaces))
9
+ return 3; // CamelCase
10
+ if (/^[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+$/.test(term))
11
+ return 2; // Title case phrase
12
+ if (/^[A-Z][a-z]+(?:[A-Z][A-Za-z0-9]+)+$/.test(term))
13
+ return 3;
14
+ return 1;
15
+ }
16
+ function extractCandidateTerms(content) {
17
+ const candidates = new Set();
18
+ const headingMatches = content.match(/^#\s+(.+)$/gm) ?? [];
19
+ for (const match of headingMatches) {
20
+ candidates.add(match.replace(/^#\s+/, '').trim());
21
+ }
22
+ const camelMatches = content.match(/\b[A-Z][a-z]+(?:[A-Z][A-Za-z0-9]+)+\b/g) ?? [];
23
+ for (const term of camelMatches) {
24
+ candidates.add(term.trim());
25
+ }
26
+ const titlePhraseMatches = content.match(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3}\b/g) ?? [];
27
+ for (const term of titlePhraseMatches) {
28
+ candidates.add(term.trim());
29
+ }
30
+ return [...candidates]
31
+ .filter((term) => term.length >= 4)
32
+ .filter((term) => !['Base Context Index', 'Domain Glossary'].includes(term))
33
+ .sort((a, b) => scoreTerm(b) - scoreTerm(a) || a.localeCompare(b));
34
+ }
35
+ function buildDefinition(term, sourcePath) {
36
+ return `Detected from ${sourcePath} as a likely domain or architecture term.`;
37
+ }
38
+ export async function extractDomainGlossaryEntries({ rootDir, documentPaths, }) {
39
+ const byTerm = new Map();
40
+ for (const path of documentPaths) {
41
+ const content = await readFile(join(rootDir, path), 'utf8').catch(() => '');
42
+ if (!content.trim())
43
+ continue;
44
+ for (const term of extractCandidateTerms(content)) {
45
+ const existing = byTerm.get(term);
46
+ if (existing) {
47
+ existing.sources = uniq([...existing.sources, path]);
48
+ continue;
49
+ }
50
+ byTerm.set(term, {
51
+ term,
52
+ definition: buildDefinition(term, path),
53
+ sources: [path],
54
+ });
55
+ }
56
+ }
57
+ return [...byTerm.values()].sort((a, b) => a.term.localeCompare(b.term));
58
+ }
59
+ export async function writeDomainGlossaryDocument({ rootDir, entries, path = 'docs/base-context/domain-glossary.md', }) {
60
+ const absolutePath = join(rootDir, path);
61
+ await mkdir(dirname(absolutePath), { recursive: true });
62
+ const lines = ['# Domain Glossary', ''];
63
+ for (const entry of entries) {
64
+ lines.push(`## ${entry.term}`);
65
+ lines.push('');
66
+ lines.push(entry.definition);
67
+ lines.push('');
68
+ lines.push(`Sources: ${entry.sources.join(', ')}`);
69
+ lines.push('');
70
+ }
71
+ await writeFile(absolutePath, `${lines.join('\n').trimEnd()}\n`, 'utf8');
72
+ return { path };
73
+ }
@@ -0,0 +1,233 @@
1
+ import { access, readdir, readFile } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { onboardProjectContext, } from './onboarding.js';
5
+ import { defaultEngineeringDefaults, } from '../policies/project-profile.js';
6
+ import { getGlobalContextHome } from '../settings/global-context-home.js';
7
+ const COPY = {
8
+ en: {
9
+ intro: 'This step matters most. Please invest 10 minutes so Shift AX can understand how you work.',
10
+ roleSummary: '1. What kind of work do you usually do? ',
11
+ workTypes: 'List your main work types (comma-separated, e.g. API development, incident response): ',
12
+ workTypeSummary: (workType) => `Summarize how "${workType}" work usually looks for you: `,
13
+ repositories: (workType) => `Which repositories are involved in "${workType}"? (comma-separated): `,
14
+ repositoryPath: (repository, defaultPath) => `What path should Shift AX inspect for "${repository}"? [${defaultPath} or blank if unknown]: `,
15
+ repositoryPurpose: (repository) => `What does "${repository}" mainly do in your work? `,
16
+ directories: (workType, repository) => `For "${workType}" in "${repository}", which directories do you touch? (comma-separated): `,
17
+ inferredWorkflow: (repository, inference) => [
18
+ `I inspected "${repository}" and inferred this workflow:`,
19
+ inference,
20
+ 'What is wrong or missing? Be explicit.',
21
+ '> ',
22
+ ].join('\n'),
23
+ confirmedWorkflow: (workType, repository) => `Describe the actual working method for "${workType}" in "${repository}": `,
24
+ glossaryTerms: 'List company-specific terms or aliases Shift AX should know. (comma-separated): ',
25
+ glossaryDefinition: (term) => `What does "${term}" mean in your company/domain? `,
26
+ verificationCommands: 'Which verification commands should Shift AX run by default? (comma-separated)',
27
+ overwrite: (path) => `Global knowledge already exists at ${path}. Overwrite and back up the previous files? [y/N]: `,
28
+ shareMessage: (path) => `Onboarding finished. Please share ${path} with teammates who do similar work and ask them to place it in the same location.`,
29
+ },
30
+ ko: {
31
+ intro: '이 절차가 가장 중요합니다. 당신을 잘 이해하기 위해 10분의 시간을 투자해주세요.',
32
+ roleSummary: '1. 당신은 어떤 업무를 주로 하나요? ',
33
+ workTypes: '주요 작업 유형을 적어주세요. (쉼표 구분, 예: API 개발, 장애 대응): ',
34
+ workTypeSummary: (workType) => `"${workType}" 업무는 보통 어떤 식으로 진행되나요? `,
35
+ repositories: (workType) => `"${workType}" 업무에 관련된 레포는 무엇인가요? (쉼표 구분): `,
36
+ repositoryPath: (repository, defaultPath) => `"${repository}"를 Shift AX가 어디서 읽어야 하나요? [${defaultPath} 또는 모르면 공백]: `,
37
+ repositoryPurpose: (repository) => `"${repository}"는 당신의 업무에서 어떤 역할을 하나요? `,
38
+ directories: (workType, repository) => `"${workType}" 업무를 "${repository}"에서 할 때 주로 만지는 디렉토리는 무엇인가요? (쉼표 구분): `,
39
+ inferredWorkflow: (repository, inference) => [
40
+ `"${repository}"를 읽어보고 제가 이렇게 추론했습니다:`,
41
+ inference,
42
+ '틀린 부분이나 빠진 부분을 꼭 짚어주세요.',
43
+ '> ',
44
+ ].join('\n'),
45
+ confirmedWorkflow: (workType, repository) => `"${workType}" 업무를 "${repository}"에서 실제로 어떤 절차로 진행하나요? `,
46
+ glossaryTerms: '회사/도메인에서만 쓰는 용어를 적어주세요. (쉼표 구분): ',
47
+ glossaryDefinition: (term) => `"${term}"는 무엇인가요? `,
48
+ verificationCommands: 'Shift AX가 기본으로 돌려야 하는 검증 명령은 무엇인가요? (쉼표 구분)',
49
+ overwrite: (path) => `${path} 아래에 기존 글로벌 지식이 있습니다. 이전 파일을 백업하고 덮어쓸까요? [y/N]: `,
50
+ shareMessage: (path) => `온보딩이 끝났습니다. 비슷한 업무를 하는 동료에게 ${path} 를 공유하고 같은 위치에 넣어달라고 안내해주세요.`,
51
+ },
52
+ };
53
+ function normalizeCommaList(value) {
54
+ return value
55
+ .split(',')
56
+ .map((item) => item.trim())
57
+ .filter(Boolean);
58
+ }
59
+ async function pathExists(path) {
60
+ try {
61
+ await access(path, constants.F_OK);
62
+ return true;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ async function homeHasExistingKnowledge() {
69
+ const home = getGlobalContextHome();
70
+ return ((await pathExists(home.indexPath)) ||
71
+ (await pathExists(home.profilePath)) ||
72
+ (await pathExists(home.settingsPath)));
73
+ }
74
+ async function promptNonEmpty(ask, question) {
75
+ while (true) {
76
+ const answer = (await ask(question)).trim();
77
+ if (answer)
78
+ return answer;
79
+ }
80
+ }
81
+ async function promptWithDefault(ask, question, defaultValue) {
82
+ const answer = (await ask(`${question} [${defaultValue}]: `)).trim();
83
+ return answer || defaultValue;
84
+ }
85
+ async function listInterestingFiles(rootDir, directories) {
86
+ const candidates = directories.length > 0 ? directories : ['.'];
87
+ const interesting = [];
88
+ for (const directory of candidates.slice(0, 6)) {
89
+ const absolute = resolve(rootDir, directory);
90
+ if (!(await pathExists(absolute)))
91
+ continue;
92
+ const entries = await readdir(absolute, { withFileTypes: true }).catch(() => []);
93
+ for (const entry of entries.slice(0, 20)) {
94
+ if (entry.name.startsWith('.'))
95
+ continue;
96
+ const relative = directory === '.' ? entry.name : join(directory, entry.name);
97
+ if (entry.isDirectory()) {
98
+ if (/controller|service|dto|schema|prisma|route|api|worker|job/i.test(entry.name)) {
99
+ interesting.push(relative);
100
+ }
101
+ }
102
+ else if (/\.(ts|tsx|js|jsx|prisma|sql|md|yaml|yml)$/.test(entry.name)) {
103
+ interesting.push(relative);
104
+ }
105
+ if (interesting.length >= 8)
106
+ return interesting;
107
+ }
108
+ }
109
+ return interesting;
110
+ }
111
+ async function inferWorkflow({ rootDir, repository, directories, }) {
112
+ const interestingFiles = await listInterestingFiles(rootDir, directories);
113
+ const heuristics = [];
114
+ const haystack = interestingFiles.join(' ');
115
+ if (/controller/i.test(haystack) && /service/i.test(haystack)) {
116
+ heuristics.push('It looks like controller/service boundaries matter here.');
117
+ }
118
+ if (/dto/i.test(haystack)) {
119
+ heuristics.push('DTO definitions appear to be part of the change flow.');
120
+ }
121
+ if (/prisma/i.test(haystack)) {
122
+ heuristics.push('It looks like Prisma schema files may define database changes here.');
123
+ }
124
+ if (/worker|job/i.test(haystack)) {
125
+ heuristics.push('There appear to be worker/job paths that may need runtime or queue-specific handling.');
126
+ }
127
+ let excerpt = '';
128
+ const firstInterestingFile = interestingFiles.find((path) => /\.(ts|tsx|js|jsx|prisma)$/.test(path));
129
+ if (firstInterestingFile) {
130
+ excerpt = await readFile(resolve(rootDir, firstInterestingFile), 'utf8')
131
+ .then((content) => content.slice(0, 220).trim())
132
+ .catch(() => '');
133
+ }
134
+ return [
135
+ `Repository: ${repository}`,
136
+ directories.length > 0 ? `Directories: ${directories.join(', ')}` : 'Directories: none recorded yet.',
137
+ interestingFiles.length > 0
138
+ ? `Observed files/dirs: ${interestingFiles.join(', ')}`
139
+ : 'Observed files/dirs: none',
140
+ heuristics.length > 0 ? `Inferences: ${heuristics.join(' ')}` : 'Inferences: no strong file-pattern inference yet.',
141
+ excerpt ? `First code excerpt hint: ${excerpt}` : '',
142
+ ]
143
+ .filter(Boolean)
144
+ .join('\n');
145
+ }
146
+ async function buildRepositoryInput({ ask, locale, rootDir, workType, repository, defaultPath, }) {
147
+ const copy = COPY[locale];
148
+ const requestedPath = (await ask(copy.repositoryPath(repository, defaultPath))).trim();
149
+ const repositoryPath = requestedPath === ''
150
+ ? defaultPath
151
+ : isAbsolute(requestedPath)
152
+ ? requestedPath
153
+ : resolve(rootDir, requestedPath);
154
+ const purpose = await promptNonEmpty(ask, copy.repositoryPurpose(repository));
155
+ const directories = normalizeCommaList(await ask(copy.directories(workType, repository)));
156
+ const inference = await inferWorkflow({
157
+ rootDir: repositoryPath,
158
+ repository,
159
+ directories,
160
+ });
161
+ const correction = await ask(copy.inferredWorkflow(repository, inference));
162
+ const workflow = await promptNonEmpty(ask, copy.confirmedWorkflow(workType, repository));
163
+ return {
164
+ repository,
165
+ repositoryPath,
166
+ purpose,
167
+ directories,
168
+ workflow,
169
+ inferredNotes: correction.trim() ? [correction.trim()] : [inference],
170
+ confirmationNotes: correction.trim() || 'User accepted the inferred workflow with no further edits.',
171
+ volatility: 'volatile',
172
+ };
173
+ }
174
+ export async function runGuidedOnboarding({ rootDir, locale, ask, }) {
175
+ const copy = COPY[locale];
176
+ const defaults = defaultEngineeringDefaults();
177
+ const home = getGlobalContextHome();
178
+ await ask(`${copy.intro}\n\n(press Enter to continue)\n> `);
179
+ const primaryRoleSummary = await promptNonEmpty(ask, copy.roleSummary);
180
+ const workTypeNames = normalizeCommaList(await promptNonEmpty(ask, copy.workTypes));
181
+ const workTypes = [];
182
+ for (const workType of workTypeNames) {
183
+ const summary = await promptNonEmpty(ask, copy.workTypeSummary(workType));
184
+ const repositories = normalizeCommaList(await promptNonEmpty(ask, copy.repositories(workType)));
185
+ const repositoryInputs = [];
186
+ for (let index = 0; index < repositories.length; index += 1) {
187
+ const repository = repositories[index];
188
+ repositoryInputs.push(await buildRepositoryInput({
189
+ ask,
190
+ locale,
191
+ rootDir,
192
+ workType,
193
+ repository,
194
+ defaultPath: index === 0 ? rootDir : '',
195
+ }));
196
+ }
197
+ workTypes.push({
198
+ name: workType,
199
+ summary,
200
+ repositories: repositoryInputs,
201
+ });
202
+ }
203
+ const glossaryTerms = normalizeCommaList(await ask(copy.glossaryTerms));
204
+ const domainLanguage = [];
205
+ for (const term of glossaryTerms) {
206
+ domainLanguage.push({
207
+ term,
208
+ definition: await promptNonEmpty(ask, copy.glossaryDefinition(term)),
209
+ });
210
+ }
211
+ const verificationCommands = normalizeCommaList(await promptWithDefault(ask, copy.verificationCommands, defaults.verification_commands?.join(', ') || 'npm test, npm run build'));
212
+ const overwriteAnswer = (await homeHasExistingKnowledge())
213
+ ? (await ask(copy.overwrite(home.root))).trim().toLowerCase()
214
+ : 'y';
215
+ const result = await onboardProjectContext({
216
+ rootDir,
217
+ primaryRoleSummary,
218
+ workTypes,
219
+ domainLanguage,
220
+ onboardingContext: {
221
+ primary_role_summary: primaryRoleSummary,
222
+ work_types: workTypes.map((item) => item.name),
223
+ domain_language: domainLanguage.map((item) => item.term),
224
+ },
225
+ engineeringDefaults: {
226
+ ...defaults,
227
+ verification_commands: verificationCommands.length > 0 ? verificationCommands : defaults.verification_commands,
228
+ },
229
+ overwrite: ['y', 'yes'].includes(overwriteAnswer),
230
+ });
231
+ process.stderr.write(`${copy.shareMessage(result.sharePath)}\n`);
232
+ return result;
233
+ }
@@ -0,0 +1,47 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { parseIndexDocument, } from './index-resolver.js';
4
+ function dedupeEntries(entries) {
5
+ const seen = new Set();
6
+ const result = [];
7
+ for (const entry of entries) {
8
+ const key = `${entry.label}::${entry.path}`;
9
+ if (seen.has(key))
10
+ continue;
11
+ seen.add(key);
12
+ result.push(entry);
13
+ }
14
+ return result;
15
+ }
16
+ export function renderIndexDocument(entries) {
17
+ const lines = ['# Base Context Index', ''];
18
+ for (const entry of entries) {
19
+ lines.push(`- ${entry.label} -> ${entry.path}`);
20
+ }
21
+ lines.push('');
22
+ lines.push('Notes:');
23
+ lines.push('');
24
+ lines.push('- each line should point to a concrete tracked document');
25
+ lines.push('- documents in this directory are shared and versioned');
26
+ lines.push('- request-local planning or review artifacts should not live here');
27
+ lines.push('');
28
+ return lines.join('\n');
29
+ }
30
+ export async function authorBaseContextIndex({ rootDir, indexPath = join(rootDir, 'docs', 'base-context', 'index.md'), entries, }) {
31
+ if (!rootDir)
32
+ throw new Error('rootDir is required');
33
+ let existing = [];
34
+ try {
35
+ existing = parseIndexDocument(await readFile(indexPath, 'utf8'));
36
+ }
37
+ catch {
38
+ existing = [];
39
+ }
40
+ const merged = dedupeEntries([...existing, ...entries]);
41
+ await mkdir(join(rootDir, 'docs', 'base-context'), { recursive: true });
42
+ await writeFile(indexPath, renderIndexDocument(merged), 'utf8');
43
+ return {
44
+ indexPath,
45
+ entries: merged,
46
+ };
47
+ }
@@ -0,0 +1,78 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ function tokenize(value) {
4
+ return String(value || '')
5
+ .toLowerCase()
6
+ .split(/[^a-z0-9]+/)
7
+ .map((token) => token.trim())
8
+ .filter((token) => token.length >= 3);
9
+ }
10
+ export function parseIndexDocument(markdown) {
11
+ const lines = String(markdown || '').split(/\r?\n/);
12
+ const entries = [];
13
+ for (const line of lines) {
14
+ const trimmed = line.trim();
15
+ if (!trimmed.startsWith('-'))
16
+ continue;
17
+ const body = trimmed.slice(1).trim();
18
+ const match = body.match(/^(.+?)\s*->\s*(.+)$/);
19
+ if (!match)
20
+ continue;
21
+ entries.push({
22
+ label: match[1].trim(),
23
+ path: match[2].trim(),
24
+ });
25
+ }
26
+ return entries;
27
+ }
28
+ function scoreEntry(entry, queryTokens) {
29
+ const haystack = new Set([...tokenize(entry.label), ...tokenize(entry.path)]);
30
+ let score = 0;
31
+ for (const token of queryTokens) {
32
+ if (haystack.has(token))
33
+ score += 1;
34
+ }
35
+ return score;
36
+ }
37
+ export async function resolveContextFromIndex({ rootDir, indexPath, indexRootDir, query, maxMatches = 5, }) {
38
+ if (!rootDir)
39
+ throw new Error('rootDir is required');
40
+ if (!indexPath)
41
+ throw new Error('indexPath is required');
42
+ if (!query || String(query).trim() === '')
43
+ throw new Error('query is required');
44
+ const rawIndex = await readFile(indexPath, 'utf8');
45
+ const entries = parseIndexDocument(rawIndex);
46
+ const queryTokens = tokenize(query);
47
+ const scored = entries
48
+ .map((entry) => ({ entry, score: scoreEntry(entry, queryTokens) }))
49
+ .filter((item) => item.score > 0)
50
+ .sort((left, right) => right.score - left.score)
51
+ .slice(0, maxMatches);
52
+ const matches = [];
53
+ const unresolvedPaths = [];
54
+ const effectiveIndexRootDir = indexRootDir || rootDir;
55
+ for (const item of scored) {
56
+ const absolutePath = resolve(effectiveIndexRootDir, item.entry.path);
57
+ try {
58
+ const content = await readFile(absolutePath, 'utf8');
59
+ matches.push({
60
+ label: item.entry.label,
61
+ path: item.entry.path,
62
+ absolute_path: absolutePath,
63
+ score: item.score,
64
+ content,
65
+ });
66
+ }
67
+ catch {
68
+ unresolvedPaths.push(item.entry.path);
69
+ }
70
+ }
71
+ return {
72
+ version: 1,
73
+ index_path: indexPath,
74
+ query: String(query).trim(),
75
+ matches,
76
+ unresolved_paths: unresolvedPaths,
77
+ };
78
+ }
@@ -0,0 +1,186 @@
1
+ import { access, cp, mkdir, readFile } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { basename, join, relative } from 'node:path';
4
+ import { authorGlobalKnowledgeBase, } from './global-index-authoring.js';
5
+ import { discoverBaseContextEntries } from './discovery.js';
6
+ import { defaultEngineeringDefaults, writeProjectProfile, } from '../policies/project-profile.js';
7
+ import { getGlobalContextHome } from '../settings/global-context-home.js';
8
+ async function pathExists(path) {
9
+ try {
10
+ await access(path, constants.F_OK);
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ function slugify(value) {
18
+ return String(value || '')
19
+ .trim()
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9]+/g, '-')
22
+ .replace(/-+/g, '-')
23
+ .replace(/^-|-$/g, '') || 'context';
24
+ }
25
+ function deriveLegacyWorkTypes({ rootDir, documents, }) {
26
+ const repoName = basename(rootDir);
27
+ return [
28
+ {
29
+ name: 'Repository Context',
30
+ summary: 'Migrated legacy repository-local context.',
31
+ repositories: [
32
+ {
33
+ repository: repoName,
34
+ repositoryPath: rootDir,
35
+ directories: documents.map((document) => document.path || `docs/base-context/${slugify(document.label)}.md`),
36
+ workflow: documents
37
+ .map((document) => `${document.label}: ${document.content.replace(/^# .*$/m, '').trim()}`)
38
+ .join('\n\n'),
39
+ volatility: 'volatile',
40
+ },
41
+ ],
42
+ },
43
+ ];
44
+ }
45
+ function deriveLegacyDomainLanguage(documents) {
46
+ return documents.map((document) => ({
47
+ term: document.label,
48
+ definition: `Migrated legacy context entry stored under ${document.path || `docs/base-context/${slugify(document.label)}.md`}.`,
49
+ }));
50
+ }
51
+ async function backupFileIfPresent(path) {
52
+ const home = getGlobalContextHome();
53
+ if (!(await pathExists(path)))
54
+ return;
55
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
56
+ await mkdir(home.backupsDir, { recursive: true });
57
+ const backupName = `${stamp}-${basename(path)}`;
58
+ await cp(path, join(home.backupsDir, backupName));
59
+ }
60
+ async function ensureOverwriteAllowed({ overwrite, candidatePaths, }) {
61
+ if (!overwrite) {
62
+ for (const path of candidatePaths) {
63
+ if (await pathExists(path)) {
64
+ throw new Error(`global context file already exists and requires overwrite confirmation: ${path}`);
65
+ }
66
+ }
67
+ return;
68
+ }
69
+ for (const path of candidatePaths) {
70
+ await backupFileIfPresent(path);
71
+ }
72
+ }
73
+ function normalizeOnboardingContext({ primaryRoleSummary, workTypes, domainLanguage, onboardingContext, }) {
74
+ return (onboardingContext ?? {
75
+ primary_role_summary: primaryRoleSummary,
76
+ work_types: workTypes.map((workType) => workType.name),
77
+ domain_language: domainLanguage.map((entry) => entry.term),
78
+ });
79
+ }
80
+ export async function persistProjectContextProfile({ rootDir, entries, onboardingContext, engineeringDefaults, now, }) {
81
+ const home = getGlobalContextHome();
82
+ const profile = {
83
+ version: 1,
84
+ updated_at: now.toISOString(),
85
+ docs_root: home.root,
86
+ index_path: relative(home.root, home.indexPath) || 'index.md',
87
+ context_docs: entries,
88
+ ...(onboardingContext ? { onboarding_context: onboardingContext } : {}),
89
+ engineering_defaults: engineeringDefaults,
90
+ };
91
+ await writeProjectProfile(rootDir, profile);
92
+ return {
93
+ index: {
94
+ indexPath: home.indexPath,
95
+ entries,
96
+ },
97
+ profile,
98
+ };
99
+ }
100
+ export async function onboardProjectContext({ rootDir, primaryRoleSummary, workTypes, domainLanguage, onboardingContext, engineeringDefaults = defaultEngineeringDefaults(), overwrite = true, documents, }) {
101
+ if (!rootDir)
102
+ throw new Error('rootDir is required');
103
+ const derivedWorkTypes = workTypes && workTypes.length > 0
104
+ ? workTypes
105
+ : documents && documents.length > 0
106
+ ? deriveLegacyWorkTypes({ rootDir, documents })
107
+ : [];
108
+ const derivedDomainLanguage = domainLanguage && domainLanguage.length > 0
109
+ ? domainLanguage
110
+ : documents && documents.length > 0
111
+ ? deriveLegacyDomainLanguage(documents)
112
+ : [];
113
+ const derivedPrimaryRoleSummary = primaryRoleSummary?.trim() ||
114
+ onboardingContext?.primary_role_summary?.trim() ||
115
+ `Primary workflow owner for ${basename(rootDir)}.`;
116
+ if (derivedWorkTypes.length === 0) {
117
+ throw new Error('workTypes are required');
118
+ }
119
+ const home = getGlobalContextHome();
120
+ const candidatePaths = [
121
+ home.indexPath,
122
+ home.profilePath,
123
+ home.settingsPath,
124
+ ...derivedWorkTypes.flatMap((workType) => [
125
+ join(home.workTypesDir, `${slugify(workType.name)}.md`),
126
+ ...workType.repositories.map((repository) => join(home.reposDir, `${slugify(repository.repository || basename(repository.repositoryPath || 'repo'))}.md`)),
127
+ ...workType.repositories.map((repository) => join(home.proceduresDir, `${slugify(workType.name)}--${slugify(repository.repository || basename(repository.repositoryPath || 'repo'))}.md`)),
128
+ ]),
129
+ ...derivedDomainLanguage.map((entry) => join(home.domainLanguageDir, `${slugify(entry.term)}.md`)),
130
+ ];
131
+ await ensureOverwriteAllowed({
132
+ overwrite,
133
+ candidatePaths,
134
+ });
135
+ const index = await authorGlobalKnowledgeBase({
136
+ primaryRoleSummary: derivedPrimaryRoleSummary,
137
+ workTypes: derivedWorkTypes,
138
+ domainLanguage: derivedDomainLanguage,
139
+ });
140
+ const { profile } = await persistProjectContextProfile({
141
+ rootDir,
142
+ entries: index.contextDocs,
143
+ onboardingContext: normalizeOnboardingContext({
144
+ primaryRoleSummary: derivedPrimaryRoleSummary,
145
+ workTypes: derivedWorkTypes,
146
+ domainLanguage: derivedDomainLanguage,
147
+ onboardingContext,
148
+ }),
149
+ engineeringDefaults,
150
+ now: new Date(),
151
+ });
152
+ return {
153
+ documents: index.contextDocs,
154
+ index: {
155
+ indexPath: index.indexPath,
156
+ entries: index.contextDocs,
157
+ },
158
+ profile,
159
+ sharePath: home.root,
160
+ };
161
+ }
162
+ export async function onboardProjectContextFromDiscovery({ rootDir, onboardingContext, engineeringDefaults = defaultEngineeringDefaults(), includeGlossary = false, overwrite = true, }) {
163
+ const discovered = await discoverBaseContextEntries({ rootDir });
164
+ if (discovered.length === 0) {
165
+ throw new Error('no discoverable base-context documents were found');
166
+ }
167
+ const documents = await Promise.all(discovered.map(async (entry) => ({
168
+ label: entry.label,
169
+ path: entry.path,
170
+ content: await readFile(join(rootDir, entry.path), 'utf8'),
171
+ })));
172
+ const domainLanguage = includeGlossary
173
+ ? discovered.map((entry) => ({
174
+ term: entry.label,
175
+ definition: `Migrated from ${entry.path}.`,
176
+ }))
177
+ : [];
178
+ return onboardProjectContext({
179
+ rootDir,
180
+ documents,
181
+ domainLanguage,
182
+ onboardingContext,
183
+ engineeringDefaults,
184
+ overwrite,
185
+ });
186
+ }