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,376 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { readProjectProfile } from '../policies/project-profile.js';
5
+ import { readPlanReviewArtifact, verifyApprovedPlanFingerprint } from '../planning/plan-review.js';
6
+ import { getRootDirFromTopicDir } from '../topics/topic-artifacts.js';
7
+ function containsPlaceholder(content) {
8
+ return /Shift AX placeholder/i.test(content);
9
+ }
10
+ async function readMaybe(path) {
11
+ try {
12
+ return await readFile(path, 'utf8');
13
+ }
14
+ catch {
15
+ return '';
16
+ }
17
+ }
18
+ function verdictBase(lane, status, summary, issues = []) {
19
+ return {
20
+ version: 1,
21
+ lane,
22
+ status,
23
+ checked_at: new Date().toISOString(),
24
+ summary,
25
+ ...(issues.length > 0 ? { issues } : {}),
26
+ };
27
+ }
28
+ function tokenizeReviewWords(value) {
29
+ return String(value || '')
30
+ .toLowerCase()
31
+ .split(/[^a-z0-9]+/)
32
+ .map((token) => token.trim())
33
+ .filter((token) => token.length >= 4)
34
+ .filter((token) => !['that', 'this', 'with', 'from', 'into', 'when', 'then', 'keep'].includes(token));
35
+ }
36
+ function parseMarkdownSections(content) {
37
+ const lines = String(content || '').split(/\r?\n/);
38
+ const sections = new Map();
39
+ let current = null;
40
+ const buffer = [];
41
+ const flush = () => {
42
+ if (current) {
43
+ sections.set(current, buffer.join('\n').trim());
44
+ buffer.length = 0;
45
+ }
46
+ };
47
+ for (const line of lines) {
48
+ const match = line.match(/^##\s+(.+)$/);
49
+ if (match) {
50
+ flush();
51
+ current = match[1].trim();
52
+ continue;
53
+ }
54
+ if (current) {
55
+ buffer.push(line);
56
+ }
57
+ }
58
+ flush();
59
+ return sections;
60
+ }
61
+ function hasTokenOverlap(content, reference) {
62
+ const haystack = String(content || '').toLowerCase();
63
+ return tokenizeReviewWords(reference).some((token) => haystack.includes(token));
64
+ }
65
+ async function readWorkflowStateMaybe(topicDir) {
66
+ const raw = await readMaybe(join(topicDir, 'workflow-state.json'));
67
+ if (!raw)
68
+ return null;
69
+ return JSON.parse(raw);
70
+ }
71
+ async function readExecutionStateMaybe(topicDir) {
72
+ const raw = await readMaybe(join(topicDir, 'execution-state.json'));
73
+ if (!raw)
74
+ return null;
75
+ return JSON.parse(raw);
76
+ }
77
+ async function readExecutionResultArtifacts(topicDir) {
78
+ const executionState = await readExecutionStateMaybe(topicDir);
79
+ const outputPaths = (executionState?.tasks ?? [])
80
+ .map((task) => task.output_path)
81
+ .filter((path) => Boolean(path));
82
+ return Promise.all(outputPaths.map(async (path) => {
83
+ const resolved = path.startsWith('/') ? path : join(topicDir, path);
84
+ return readMaybe(resolved);
85
+ }));
86
+ }
87
+ function extractMentionedFilesFromExecutionResults(results) {
88
+ const mentioned = new Set();
89
+ for (const result of results) {
90
+ if (!result.trim())
91
+ continue;
92
+ try {
93
+ const parsed = JSON.parse(result);
94
+ for (const file of parsed.changed_files ?? []) {
95
+ if (file)
96
+ mentioned.add(file);
97
+ }
98
+ const summary = parsed.summary ?? '';
99
+ const summaryMatches = summary.match(/[A-Za-z0-9_./-]+\.[A-Za-z0-9]+/g) ?? [];
100
+ for (const file of summaryMatches) {
101
+ mentioned.add(file);
102
+ }
103
+ continue;
104
+ }
105
+ catch {
106
+ const matches = result.match(/[A-Za-z0-9_./-]+\.[A-Za-z0-9]+/g) ?? [];
107
+ for (const file of matches) {
108
+ mentioned.add(file);
109
+ }
110
+ }
111
+ }
112
+ return [...mentioned];
113
+ }
114
+ function listChangedFiles(worktreePath) {
115
+ const output = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], {
116
+ cwd: worktreePath,
117
+ encoding: 'utf8',
118
+ stdio: ['ignore', 'pipe', 'pipe'],
119
+ });
120
+ return output
121
+ .split(/\r?\n/)
122
+ .map((line) => line.trim())
123
+ .filter(Boolean)
124
+ .map((line) => line.replace(/^[A-Z?]{1,2}\s+/, ''))
125
+ .filter(Boolean);
126
+ }
127
+ function isTestFile(path) {
128
+ return /(^|\/)(tests?|__tests__)\//i.test(path) || /\.(test|spec)\.[cm]?[jt]sx?$/i.test(path);
129
+ }
130
+ function strategyPattern(value) {
131
+ const normalized = value.toLowerCase();
132
+ if (normalized === 'tdd') {
133
+ return /\btdd\b|test[- ]driven/i;
134
+ }
135
+ return new RegExp(normalized.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&').replace(/-/g, '[- ]?'), 'i');
136
+ }
137
+ async function readProjectProfileForTopic(topicDir) {
138
+ try {
139
+ return await readProjectProfile(getRootDirFromTopicDir(topicDir));
140
+ }
141
+ catch {
142
+ return readProjectProfile(topicDir);
143
+ }
144
+ }
145
+ function architecturePattern(value) {
146
+ const normalized = value.toLowerCase();
147
+ if (normalized === 'clean-boundaries') {
148
+ return /\b(boundary|boundaries|architecture)\b/i;
149
+ }
150
+ if (normalized === 'layered-boundaries') {
151
+ return /\b(layered|boundary|boundaries|architecture)\b/i;
152
+ }
153
+ return new RegExp(normalized.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&').replace(/-/g, '[- ]?'), 'i');
154
+ }
155
+ async function runDomainPolicyLane(topicDir) {
156
+ const raw = await readMaybe(join(topicDir, 'resolved-context.json'));
157
+ if (!raw) {
158
+ return verdictBase('domain-policy', 'changes_requested', 'Resolved context artifact is missing.', [
159
+ { severity: 'high', message: 'resolved-context.json is required before review.' },
160
+ ]);
161
+ }
162
+ const parsed = JSON.parse(raw);
163
+ const unresolved = parsed.unresolved_paths ?? [];
164
+ if (unresolved.length > 0) {
165
+ return verdictBase('domain-policy', 'changes_requested', 'Some base-context paths could not be resolved.', unresolved.map((path) => ({
166
+ severity: 'high',
167
+ message: `Unresolved base-context path: ${path}`,
168
+ })));
169
+ }
170
+ return verdictBase('domain-policy', 'approved', parsed.matches && parsed.matches.length > 0
171
+ ? 'Relevant base-context documents were resolved and no unresolved paths were recorded.'
172
+ : 'No relevant base-context documents matched this topic, and no unresolved paths were recorded.');
173
+ }
174
+ async function runSpecConformanceLane(topicDir) {
175
+ const spec = await readMaybe(join(topicDir, 'spec.md'));
176
+ const plan = await readMaybe(join(topicDir, 'implementation-plan.md'));
177
+ const planReview = await readPlanReviewArtifact(topicDir);
178
+ const workflow = await readWorkflowStateMaybe(topicDir);
179
+ if (planReview.status !== 'approved') {
180
+ return verdictBase('spec-conformance', 'changes_requested', 'Human plan review has not approved the current implementation plan yet.', [
181
+ {
182
+ severity: 'high',
183
+ message: 'Approve plan-review.json before review can pass.',
184
+ },
185
+ ]);
186
+ }
187
+ const fingerprint = await verifyApprovedPlanFingerprint({ topicDir });
188
+ if (!fingerprint.matches) {
189
+ return verdictBase('spec-conformance', 'changes_requested', 'The implementation plan changed after approval and needs re-review.', [
190
+ {
191
+ severity: 'high',
192
+ message: fingerprint.reason ?? 'Approved plan fingerprint no longer matches.',
193
+ },
194
+ ]);
195
+ }
196
+ if (containsPlaceholder(spec) || containsPlaceholder(plan)) {
197
+ return verdictBase('spec-conformance', 'changes_requested', 'Spec or implementation plan still contains placeholders.', [
198
+ {
199
+ severity: 'high',
200
+ message: 'Replace placeholder content in spec.md and implementation-plan.md before review can pass.',
201
+ },
202
+ ]);
203
+ }
204
+ const worktreePath = workflow?.worktree?.worktree_path;
205
+ if (worktreePath) {
206
+ const changedFiles = listChangedFiles(worktreePath);
207
+ const executionState = await readExecutionStateMaybe(topicDir);
208
+ const outOfScopeContent = parseMarkdownSections(spec).get('Out of Scope') ??
209
+ parseMarkdownSections(await readMaybe(join(topicDir, 'brainstorm.md'))).get('Out of Scope') ??
210
+ '';
211
+ if (changedFiles.length > 0 && executionState && executionState.overall_status !== 'completed') {
212
+ return verdictBase('spec-conformance', 'changes_requested', 'Execution state is not completed for the current changed files.', [
213
+ {
214
+ severity: 'high',
215
+ message: `execution-state.json reports ${executionState.overall_status ?? 'unknown'} instead of completed.`,
216
+ },
217
+ ]);
218
+ }
219
+ const outOfScopeTouched = changedFiles.find((file) => hasTokenOverlap(file, outOfScopeContent));
220
+ if (outOfScopeTouched) {
221
+ return verdictBase('spec-conformance', 'changes_requested', 'Changed files touch an area that is explicitly out of scope for the reviewed plan.', [
222
+ {
223
+ severity: 'high',
224
+ message: `Out-of-scope file changed: ${outOfScopeTouched}`,
225
+ },
226
+ ]);
227
+ }
228
+ }
229
+ return verdictBase('spec-conformance', 'approved', 'Spec and implementation plan are approved, fingerprint-matched, and free of unresolved placeholders.');
230
+ }
231
+ async function runTestAdequacyLane(topicDir) {
232
+ const plan = await readMaybe(join(topicDir, 'implementation-plan.md'));
233
+ const workflow = await readWorkflowStateMaybe(topicDir);
234
+ const worktreePath = workflow?.worktree?.worktree_path;
235
+ if (worktreePath) {
236
+ const successfulTestCommand = (workflow?.verification ?? []).some((item) => item.exit_code === 0 && /\b(test|jest|vitest|pytest|go test|cargo test|phpunit|rspec)\b/i.test(item.command));
237
+ if (!successfulTestCommand) {
238
+ return verdictBase('test-adequacy', 'changes_requested', 'Verification evidence does not include a successful test command.', [
239
+ {
240
+ severity: 'high',
241
+ message: 'Run a passing automated test command before review can approve test adequacy.',
242
+ },
243
+ ]);
244
+ }
245
+ const changedFiles = listChangedFiles(worktreePath);
246
+ const changedCodeFiles = changedFiles.filter((file) => !isTestFile(file));
247
+ const changedTestFiles = changedFiles.filter((file) => isTestFile(file));
248
+ if (changedCodeFiles.length > 0 && changedTestFiles.length === 0) {
249
+ return verdictBase('test-adequacy', 'changes_requested', 'Code changed in the worktree, but no corresponding test file changes were found.', [
250
+ {
251
+ severity: 'high',
252
+ message: 'Add or update tests for changed implementation files before review can pass.',
253
+ },
254
+ ]);
255
+ }
256
+ if (changedTestFiles.length > 0) {
257
+ const [spec, brainstorm, resolvedContext] = await Promise.all([
258
+ readMaybe(join(topicDir, 'spec.md')),
259
+ readMaybe(join(topicDir, 'brainstorm.md')),
260
+ readMaybe(join(topicDir, 'resolved-context.json')),
261
+ ]);
262
+ const contextLabels = (() => {
263
+ try {
264
+ const parsed = JSON.parse(resolvedContext);
265
+ return (parsed.matches ?? []).map((item) => item.label).filter(Boolean).join('\n');
266
+ }
267
+ catch {
268
+ return '';
269
+ }
270
+ })();
271
+ const desiredCoverage = [spec, brainstorm, contextLabels].join('\n');
272
+ const testContents = await Promise.all(changedTestFiles.map((file) => readMaybe(join(worktreePath, file))));
273
+ const hasRelevantCoverage = testContents.some((content) => hasTokenOverlap(content, desiredCoverage));
274
+ if (!hasRelevantCoverage) {
275
+ return verdictBase('test-adequacy', 'changes_requested', 'Changed tests do not clearly reflect the spec, brainstorm, or domain-policy language yet.', [
276
+ {
277
+ severity: 'medium',
278
+ message: 'Make the changed tests reference the agreed outcome, constraints, or domain-policy terms more explicitly.',
279
+ },
280
+ ]);
281
+ }
282
+ }
283
+ }
284
+ if (!/\b(test|tdd)\b/i.test(plan) || containsPlaceholder(plan)) {
285
+ return verdictBase('test-adequacy', 'changes_requested', 'Implementation plan does not yet provide reviewable test evidence expectations.', [
286
+ {
287
+ severity: 'medium',
288
+ message: 'Implementation plan should explicitly reference tests or TDD expectations.',
289
+ },
290
+ ]);
291
+ }
292
+ return verdictBase('test-adequacy', 'approved', 'Implementation plan explicitly references test or TDD expectations.');
293
+ }
294
+ async function runEngineeringDisciplineLane(topicDir) {
295
+ const plan = await readMaybe(join(topicDir, 'implementation-plan.md'));
296
+ const profile = await readProjectProfileForTopic(topicDir);
297
+ const requiredTestStrategy = profile?.engineering_defaults.test_strategy ?? 'tdd';
298
+ const requiredArchitecture = profile?.engineering_defaults.architecture ?? 'clean-boundaries';
299
+ const hasTdd = strategyPattern(requiredTestStrategy).test(plan);
300
+ const hasArchitecture = architecturePattern(requiredArchitecture).test(plan);
301
+ if (containsPlaceholder(plan) || !hasTdd || !hasArchitecture) {
302
+ return verdictBase('engineering-discipline', 'changes_requested', 'Engineering-discipline expectations are not yet explicit enough.', [
303
+ {
304
+ severity: 'medium',
305
+ message: `Implementation plan should explicitly reference ${requiredTestStrategy} and ${requiredArchitecture}.`,
306
+ },
307
+ ]);
308
+ }
309
+ return verdictBase('engineering-discipline', 'approved', 'Implementation plan references the configured engineering-method guardrails.');
310
+ }
311
+ async function runConversationTraceLane(topicDir) {
312
+ const request = await readMaybe(join(topicDir, 'request.md'));
313
+ const summary = await readMaybe(join(topicDir, 'request-summary.md'));
314
+ const spec = await readMaybe(join(topicDir, 'spec.md'));
315
+ const brainstorm = await readMaybe(join(topicDir, 'brainstorm.md'));
316
+ const plan = await readMaybe(join(topicDir, 'implementation-plan.md'));
317
+ const workflow = await readWorkflowStateMaybe(topicDir);
318
+ if (!request.trim() || !summary.trim() || !brainstorm.trim() || containsPlaceholder(spec)) {
319
+ return verdictBase('conversation-trace', 'changes_requested', 'Request artifacts exist, but the spec is not yet traceable enough to the original request.', [
320
+ {
321
+ severity: 'medium',
322
+ message: 'A reviewable brainstorm + spec must exist before conversation trace can pass.',
323
+ },
324
+ ]);
325
+ }
326
+ const brainstormSections = parseMarkdownSections(brainstorm);
327
+ const missingSpecSections = ['Clarified Outcome', 'Constraints', 'Out of Scope'].filter((section) => {
328
+ const content = brainstormSections.get(section);
329
+ return content && !hasTokenOverlap(spec, content);
330
+ });
331
+ const missingPlanSections = ['Verification Expectations', 'Implementation Areas', 'Long-running Work'].filter((section) => {
332
+ const content = brainstormSections.get(section);
333
+ return content && !hasTokenOverlap(plan, content);
334
+ });
335
+ if (missingSpecSections.length > 0 || missingPlanSections.length > 0) {
336
+ return verdictBase('conversation-trace', 'changes_requested', 'The spec or implementation plan does not yet reflect all of the clarified brainstorming details.', [
337
+ ...missingSpecSections.map((section) => ({
338
+ severity: 'medium',
339
+ message: `Spec is missing clarified brainstorming details from section: ${section}.`,
340
+ })),
341
+ ...missingPlanSections.map((section) => ({
342
+ severity: 'medium',
343
+ message: `Implementation plan is missing clarified brainstorming details from section: ${section}.`,
344
+ })),
345
+ ]);
346
+ }
347
+ const worktreePath = workflow?.worktree?.worktree_path;
348
+ if (worktreePath) {
349
+ const changedFiles = listChangedFiles(worktreePath);
350
+ if (changedFiles.length > 0) {
351
+ const executionResults = await readExecutionResultArtifacts(topicDir);
352
+ const mentionedFiles = extractMentionedFilesFromExecutionResults(executionResults);
353
+ const unmentionedChangedFile = changedFiles.find((file) => !mentionedFiles.includes(file));
354
+ if (unmentionedChangedFile) {
355
+ return verdictBase('conversation-trace', 'changes_requested', 'Execution results do not describe all of the changed files yet.', [
356
+ {
357
+ severity: 'medium',
358
+ message: `Changed file is missing from execution result artifacts: ${unmentionedChangedFile}.`,
359
+ },
360
+ ]);
361
+ }
362
+ }
363
+ }
364
+ return verdictBase('conversation-trace', 'approved', 'Request, summary, brainstorm, and spec artifacts are traceable to the original request.');
365
+ }
366
+ export async function runReviewLanes({ topicDir, }) {
367
+ const verdicts = await Promise.all([
368
+ runDomainPolicyLane(topicDir),
369
+ runSpecConformanceLane(topicDir),
370
+ runTestAdequacyLane(topicDir),
371
+ runEngineeringDisciplineLane(topicDir),
372
+ runConversationTraceLane(topicDir),
373
+ ]);
374
+ await Promise.all(verdicts.map((verdict) => writeFile(join(topicDir, 'review', `${verdict.lane}.json`), `${JSON.stringify(verdict, null, 2)}\n`, 'utf8')));
375
+ return verdicts;
376
+ }
@@ -0,0 +1,28 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ export function resolveGlobalContextRoot({ env = process.env, homeDir = homedir(), } = {}) {
4
+ const override = env.SHIFT_AX_HOME?.trim();
5
+ if (override)
6
+ return override;
7
+ return join(homeDir, '.shift-ax');
8
+ }
9
+ export function getGlobalContextHome(options = {}) {
10
+ const root = resolveGlobalContextRoot(options);
11
+ return {
12
+ root,
13
+ indexPath: join(root, 'index.md'),
14
+ workTypesDir: join(root, 'work-types'),
15
+ reposDir: join(root, 'repos'),
16
+ proceduresDir: join(root, 'procedures'),
17
+ domainLanguageDir: join(root, 'domain-language'),
18
+ backupsDir: join(root, 'backups'),
19
+ profilePath: join(root, 'profile.json'),
20
+ settingsPath: join(root, 'settings.json'),
21
+ };
22
+ }
23
+ export function toGlobalRelativePath(absolutePath, home = getGlobalContextHome()) {
24
+ if (!absolutePath.startsWith(home.root)) {
25
+ return absolutePath;
26
+ }
27
+ return absolutePath.slice(home.root.length + 1);
28
+ }
@@ -0,0 +1,37 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { getGlobalContextHome } from './global-context-home.js';
3
+ export function getProjectSettingsPath(rootDir) {
4
+ return getGlobalContextHome().settingsPath;
5
+ }
6
+ export async function readProjectSettings(rootDir) {
7
+ try {
8
+ const raw = JSON.parse(await readFile(getProjectSettingsPath(rootDir), 'utf8'));
9
+ const locale = raw.locale ??
10
+ (raw.preferred_language === 'korean'
11
+ ? 'ko'
12
+ : raw.preferred_language === 'english'
13
+ ? 'en'
14
+ : undefined);
15
+ if (!locale)
16
+ return null;
17
+ return {
18
+ version: 1,
19
+ updated_at: raw.updated_at ?? new Date(0).toISOString(),
20
+ locale,
21
+ preferred_language: raw.preferred_language ?? (locale === 'ko' ? 'korean' : 'english'),
22
+ ...(typeof raw.default_full_auto === 'boolean' ? { default_full_auto: raw.default_full_auto } : {}),
23
+ ...(raw.preferred_platform ? { preferred_platform: raw.preferred_platform } : {}),
24
+ };
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ export async function writeProjectSettings({ rootDir, settings, }) {
31
+ const path = getProjectSettingsPath(rootDir);
32
+ await mkdir(getGlobalContextHome().root, { recursive: true });
33
+ await writeFile(path, `${JSON.stringify({
34
+ ...settings,
35
+ preferred_language: settings.preferred_language ?? (settings.locale === 'ko' ? 'korean' : 'english'),
36
+ }, null, 2)}\n`, 'utf8');
37
+ }
@@ -0,0 +1,144 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createInterface } from 'node:readline/promises';
3
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { constants } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { stdin, stdout } from 'node:process';
7
+ import { getPlatformBootstrapAssets } from '../../platform/index.js';
8
+ import { readProjectProfile } from '../policies/project-profile.js';
9
+ import { readProjectSettings, writeProjectSettings, } from '../settings/project-settings.js';
10
+ import { getGlobalContextHome } from '../settings/global-context-home.js';
11
+ const SHELL_COPY = {
12
+ en: {
13
+ chooseLanguage: 'Choose language:\n1. English (default)\n2. Korean\n> ',
14
+ chooseFullAuto: 'Enable full-auto mode by default?\n1. No (default)\n2. Yes\n> ',
15
+ localeRule: 'Preferred user language: English. Respond in English unless the user explicitly asks to switch.',
16
+ },
17
+ ko: {
18
+ chooseLanguage: '언어를 선택하세요:\n1. English (default)\n2. Korean\n> ',
19
+ chooseFullAuto: '기본으로 full-auto 모드를 켤까요?\n1. 아니오 (기본값)\n2. 예\n> ',
20
+ localeRule: '선호 사용자 언어: 한국어. 사용자가 명시적으로 바꾸라고 하지 않으면 한국어로 응답하세요.',
21
+ },
22
+ };
23
+ let nonTtyAnswerLinesPromise = null;
24
+ let nonTtyAnswerIndex = 0;
25
+ async function readNonTtyAnswers() {
26
+ if (!nonTtyAnswerLinesPromise) {
27
+ nonTtyAnswerLinesPromise = new Promise((resolve, reject) => {
28
+ let raw = '';
29
+ stdin.setEncoding('utf8');
30
+ stdin.on('data', (chunk) => {
31
+ raw += chunk;
32
+ });
33
+ stdin.on('end', () => resolve(raw.split(/\r?\n/)));
34
+ stdin.on('error', reject);
35
+ stdin.resume();
36
+ });
37
+ }
38
+ return nonTtyAnswerLinesPromise;
39
+ }
40
+ async function promptChoice({ question, fallback, }) {
41
+ if (stdin.isTTY) {
42
+ const rl = createInterface({ input: stdin, output: stdout });
43
+ try {
44
+ return (await rl.question(question)).trim() || fallback;
45
+ }
46
+ finally {
47
+ rl.close();
48
+ }
49
+ }
50
+ const answers = await readNonTtyAnswers().catch(() => []);
51
+ const answer = answers[nonTtyAnswerIndex] ?? '';
52
+ nonTtyAnswerIndex += 1;
53
+ return answer.trim() || fallback;
54
+ }
55
+ async function pathExists(path) {
56
+ try {
57
+ await access(path, constants.F_OK);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ export async function isProjectOnboarded(rootDir) {
65
+ const home = getGlobalContextHome();
66
+ return (await pathExists(home.indexPath)) && (await readProjectProfile(rootDir)) !== null;
67
+ }
68
+ export async function resolveShellLocale({ rootDir, requestedLocale, }) {
69
+ if (requestedLocale)
70
+ return requestedLocale;
71
+ const settings = await readProjectSettings(rootDir);
72
+ if (settings?.locale)
73
+ return settings.locale;
74
+ const answer = await promptChoice({
75
+ question: SHELL_COPY.en.chooseLanguage,
76
+ fallback: '1',
77
+ });
78
+ return answer === '2' ? 'ko' : 'en';
79
+ }
80
+ export async function resolveShellPlatform({ rootDir, requestedPlatform, locale, }) {
81
+ if (requestedPlatform)
82
+ return requestedPlatform;
83
+ const settings = await readProjectSettings(rootDir);
84
+ if (settings?.preferred_platform)
85
+ return settings.preferred_platform;
86
+ return 'codex';
87
+ }
88
+ export async function resolveShellDefaultFullAuto({ rootDir, locale, }) {
89
+ const settings = await readProjectSettings(rootDir);
90
+ if (typeof settings?.default_full_auto === 'boolean') {
91
+ return settings.default_full_auto;
92
+ }
93
+ const answer = await promptChoice({
94
+ question: SHELL_COPY[locale].chooseFullAuto,
95
+ fallback: '1',
96
+ });
97
+ return answer === '2';
98
+ }
99
+ export async function persistShellSettings({ rootDir, locale, defaultFullAuto, platform, }) {
100
+ const existing = (await readProjectSettings(rootDir));
101
+ await writeProjectSettings({
102
+ rootDir,
103
+ settings: {
104
+ ...(existing ?? {}),
105
+ version: 1,
106
+ updated_at: new Date().toISOString(),
107
+ locale,
108
+ preferred_language: locale === 'ko' ? 'korean' : 'english',
109
+ ...(typeof defaultFullAuto === 'boolean'
110
+ ? { default_full_auto: defaultFullAuto }
111
+ : typeof existing?.default_full_auto === 'boolean'
112
+ ? { default_full_auto: existing.default_full_auto }
113
+ : {}),
114
+ preferred_platform: platform,
115
+ },
116
+ });
117
+ }
118
+ export async function launchPlatformShell({ rootDir, platform, fullAuto = false, initialPrompt, }) {
119
+ const locale = (await readProjectSettings(rootDir))?.locale ?? 'en';
120
+ await ensurePlatformShellAssets({ platform, rootDir, locale });
121
+ const args = platform === 'codex'
122
+ ? [...(fullAuto ? ['--yolo'] : []), '-C', rootDir, ...(initialPrompt?.trim() ? [initialPrompt.trim()] : [])]
123
+ : [...(fullAuto ? ['--dangerously-skip-permissions'] : []), ...(initialPrompt?.trim() ? [initialPrompt.trim()] : [])];
124
+ const child = platform === 'codex'
125
+ ? spawn('codex', args, { stdio: 'inherit' })
126
+ : spawn('claude', args, { cwd: rootDir, stdio: 'inherit' });
127
+ return await new Promise((resolve, reject) => {
128
+ child.on('error', reject);
129
+ child.on('exit', (code) => resolve(code ?? 0));
130
+ });
131
+ }
132
+ async function ensurePlatformShellAssets({ platform, rootDir, locale, }) {
133
+ const assets = getPlatformBootstrapAssets(platform, rootDir, locale);
134
+ await Promise.all(assets.map(async (asset) => {
135
+ const absolutePath = join(rootDir, asset.path);
136
+ const current = await readFile(absolutePath, 'utf8').catch(() => '');
137
+ const isTopLevelBootstrap = asset.path === 'AGENTS.md' || asset.path === 'CLAUDE.md';
138
+ if (isTopLevelBootstrap && current.trim() && !current.includes('Shift AX')) {
139
+ return;
140
+ }
141
+ await mkdir(dirname(absolutePath), { recursive: true });
142
+ await writeFile(absolutePath, asset.content, 'utf8');
143
+ }));
144
+ }