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,154 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import { parseIndexDocument } from '../context/index-resolver.js';
6
+ import { summarizeTopicStatus } from '../observability/topic-status.js';
7
+ import { getProjectProfilePath, readProjectProfile } from '../policies/project-profile.js';
8
+ import { getGlobalContextHome } from '../settings/global-context-home.js';
9
+ function combineStatuses(statuses) {
10
+ if (statuses.includes('fail'))
11
+ return 'fail';
12
+ if (statuses.includes('warn'))
13
+ return 'warn';
14
+ return 'ok';
15
+ }
16
+ function shellQuote(value) {
17
+ return `'${value.replace(/'/g, `'\\''`)}'`;
18
+ }
19
+ function defaultCommandExists(command) {
20
+ try {
21
+ execFileSync('/bin/sh', ['-lc', `command -v ${shellQuote(command)} >/dev/null 2>&1`], {
22
+ stdio: 'pipe',
23
+ });
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ function checkGitRoot(rootDir) {
31
+ try {
32
+ const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
33
+ cwd: rootDir,
34
+ encoding: 'utf8',
35
+ stdio: 'pipe',
36
+ }).trim();
37
+ return {
38
+ status: 'ok',
39
+ message: 'Git repository is available.',
40
+ repo_root: repoRoot,
41
+ };
42
+ }
43
+ catch {
44
+ return {
45
+ status: 'fail',
46
+ message: 'Git repository root could not be resolved.',
47
+ };
48
+ }
49
+ }
50
+ async function checkBaseContext(rootDir) {
51
+ const home = getGlobalContextHome();
52
+ const indexPath = home.indexPath;
53
+ if (!existsSync(indexPath)) {
54
+ return {
55
+ status: 'fail',
56
+ message: 'Global Shift AX index is missing.',
57
+ index_path: indexPath,
58
+ entry_count: 0,
59
+ missing_paths: [],
60
+ };
61
+ }
62
+ const rawIndex = await readFile(indexPath, 'utf8');
63
+ const entries = parseIndexDocument(rawIndex);
64
+ const missingPaths = entries
65
+ .map((entry) => entry.path)
66
+ .filter((path) => !existsSync(resolve(home.root, path)));
67
+ return {
68
+ status: missingPaths.length > 0 ? 'fail' : 'ok',
69
+ message: missingPaths.length > 0
70
+ ? 'Global index includes unresolved or missing document paths.'
71
+ : 'Global index and linked documents are present.',
72
+ index_path: indexPath,
73
+ entry_count: entries.length,
74
+ missing_paths: missingPaths,
75
+ };
76
+ }
77
+ async function checkProfile(rootDir) {
78
+ const path = getProjectProfilePath(rootDir);
79
+ const profile = await readProjectProfile(rootDir);
80
+ return {
81
+ status: profile ? 'ok' : 'fail',
82
+ message: profile
83
+ ? 'Global Shift AX profile is available.'
84
+ : 'Global Shift AX profile is missing. Run `/onboarding` or `shift-ax onboard-context` first.',
85
+ path,
86
+ };
87
+ }
88
+ async function checkTopic(topicDir) {
89
+ if (!existsSync(topicDir)) {
90
+ return {
91
+ topic_dir: topicDir,
92
+ topic_slug: topicDir.split('/').pop() || 'unknown-topic',
93
+ phase: 'missing',
94
+ review_status: 'unknown',
95
+ execution_status: 'unknown',
96
+ status: 'fail',
97
+ message: 'Topic directory does not exist.',
98
+ };
99
+ }
100
+ const summary = await summarizeTopicStatus(topicDir);
101
+ const status = summary.phase === 'awaiting_policy_sync' ||
102
+ summary.review_status === 'changes_requested' ||
103
+ !!summary.last_failure_reason
104
+ ? 'warn'
105
+ : 'ok';
106
+ return {
107
+ ...summary,
108
+ topic_dir: topicDir,
109
+ status,
110
+ message: status === 'warn'
111
+ ? summary.last_failure_reason || 'Topic needs operator attention before it can continue.'
112
+ : 'Topic artifacts look healthy.',
113
+ };
114
+ }
115
+ function checkLaunchers({ platform, commandExists = defaultCommandExists, }) {
116
+ const checkedCommands = platform === 'codex' ? ['codex', 'tmux'] : ['claude', 'tmux'];
117
+ const missingCommands = checkedCommands.filter((command) => !commandExists(command));
118
+ return {
119
+ platform,
120
+ checked_commands: checkedCommands,
121
+ missing_commands: missingCommands,
122
+ status: missingCommands.length > 0 ? 'warn' : 'ok',
123
+ message: missingCommands.length > 0
124
+ ? `Missing launcher commands for ${platform}: ${missingCommands.join(', ')}`
125
+ : `Required launcher commands for ${platform} are available.`,
126
+ };
127
+ }
128
+ export async function runDoctor({ rootDir, topicDir, platform, commandExists, }) {
129
+ const git = checkGitRoot(rootDir);
130
+ const [baseContext, profile, topic] = await Promise.all([
131
+ checkBaseContext(rootDir),
132
+ checkProfile(rootDir),
133
+ topicDir ? checkTopic(topicDir) : Promise.resolve(undefined),
134
+ ]);
135
+ const launchers = platform
136
+ ? checkLaunchers({ platform, commandExists })
137
+ : undefined;
138
+ const overallStatus = combineStatuses([
139
+ git.status,
140
+ baseContext.status,
141
+ profile.status,
142
+ ...(topic ? [topic.status] : []),
143
+ ...(launchers ? [launchers.status] : []),
144
+ ]);
145
+ return {
146
+ root_dir: rootDir,
147
+ overall_status: overallStatus,
148
+ git,
149
+ base_context: baseContext,
150
+ profile,
151
+ ...(topic ? { topic } : {}),
152
+ ...(launchers ? { launchers } : {}),
153
+ };
154
+ }
@@ -0,0 +1,76 @@
1
+ const REQUIRED_TRAILERS = [
2
+ 'Constraint:',
3
+ 'Confidence:',
4
+ 'Scope-risk:',
5
+ 'Tested:',
6
+ 'Not-tested:',
7
+ ];
8
+ export function validateLoreCommitMessage(message) {
9
+ const issues = [];
10
+ const trimmed = String(message || '').trim();
11
+ const lines = trimmed.split(/\r?\n/).filter((line) => line.trim() !== '');
12
+ if (trimmed === '') {
13
+ issues.push('Commit message is empty.');
14
+ }
15
+ if (lines.length === 0 || lines[0].trim() === '') {
16
+ issues.push('Intent line is required.');
17
+ }
18
+ for (const trailer of REQUIRED_TRAILERS) {
19
+ if (!new RegExp(`^${trailer.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+.+$`, 'm').test(trimmed)) {
20
+ issues.push(`Missing required lore trailer: ${trailer}`);
21
+ }
22
+ }
23
+ return {
24
+ valid: issues.length === 0,
25
+ issues,
26
+ };
27
+ }
28
+ export function buildLoreCommitMessage({ intent, body, constraint, confidence, scopeRisk, directive, tested, notTested, rejected, related, reversibility, }) {
29
+ const lines = [intent.trim(), '', body.trim(), '', `Constraint: ${constraint}`];
30
+ if (rejected)
31
+ lines.push(`Rejected: ${rejected}`);
32
+ lines.push(`Confidence: ${confidence}`);
33
+ lines.push(`Scope-risk: ${scopeRisk}`);
34
+ if (reversibility)
35
+ lines.push(`Reversibility: ${reversibility}`);
36
+ if (directive)
37
+ lines.push(`Directive: ${directive}`);
38
+ lines.push(`Tested: ${tested}`);
39
+ lines.push(`Not-tested: ${notTested}`);
40
+ if (related)
41
+ lines.push(`Related: ${related}`);
42
+ return `${lines.join('\n').trimEnd()}\n`;
43
+ }
44
+ function normalizeOneLine(value) {
45
+ return String(value || '')
46
+ .replace(/\s+/g, ' ')
47
+ .trim();
48
+ }
49
+ function truncate(value, maxLength) {
50
+ if (value.length <= maxLength)
51
+ return value;
52
+ return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
53
+ }
54
+ export function buildTopicLoreCommitMessage({ request, requestSummary, topicSlug, verificationCommands = [], }) {
55
+ const normalizedSummary = normalizeOneLine(requestSummary) || normalizeOneLine(request);
56
+ const normalizedRequest = normalizeOneLine(request) || normalizedSummary;
57
+ const intentSource = normalizedSummary || 'reviewed request update';
58
+ const tested = ['Shift AX review lanes', ...verificationCommands]
59
+ .map((command) => normalizeOneLine(command))
60
+ .filter(Boolean)
61
+ .join('; ');
62
+ return buildLoreCommitMessage({
63
+ intent: truncate(`Deliver reviewed change: ${intentSource}`, 72),
64
+ body: [
65
+ `This commit captures the reviewed Shift AX work for "${normalizedRequest || intentSource}".`,
66
+ 'The request passed context resolution, human plan review, and review gates before local finalization.',
67
+ ].join(' '),
68
+ constraint: 'v1 finalization stops at a meaningful local git commit',
69
+ confidence: 'high',
70
+ scopeRisk: 'moderate',
71
+ directive: 'Re-run plan-review, escalation, and review gates before changing finalization semantics',
72
+ tested: tested || 'Shift AX review lanes',
73
+ notTested: 'GitHub push or PR automation beyond the v1 local-commit boundary',
74
+ ...(topicSlug ? { related: `topic:${topicSlug}` } : {}),
75
+ });
76
+ }
@@ -0,0 +1,131 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { buildTopicLoreCommitMessage, validateLoreCommitMessage, } from './commit-message.js';
6
+ import { readWorkflowState, writeWorkflowState, } from '../planning/workflow-state.js';
7
+ import { getRootDirFromTopicDir, topicArtifactPath, } from '../topics/topic-artifacts.js';
8
+ function runGit(cwd, args) {
9
+ return execFileSync('git', args, {
10
+ cwd,
11
+ encoding: 'utf8',
12
+ stdio: ['ignore', 'pipe', 'pipe'],
13
+ }).trim();
14
+ }
15
+ async function resolveGitCwd(topicDir) {
16
+ const rootDir = getRootDirFromTopicDir(topicDir);
17
+ try {
18
+ const raw = await readFile(topicArtifactPath(topicDir, 'worktree_state'), 'utf8');
19
+ const parsed = JSON.parse(raw);
20
+ if (parsed.worktree_path &&
21
+ ['created', 'reused'].includes(String(parsed.status || '')) &&
22
+ existsSync(parsed.worktree_path)) {
23
+ return parsed.worktree_path;
24
+ }
25
+ }
26
+ catch {
27
+ // ignore and fall back
28
+ }
29
+ return rootDir;
30
+ }
31
+ async function readAggregateReview(topicDir) {
32
+ const raw = await readFile(join(topicDir, 'review', 'aggregate.json'), 'utf8');
33
+ return JSON.parse(raw);
34
+ }
35
+ async function readCommitMessage(topicDir, message) {
36
+ if (message && message.trim() !== '') {
37
+ return message;
38
+ }
39
+ return readFile(topicArtifactPath(topicDir, 'commit_message'), 'utf8');
40
+ }
41
+ async function readTopicRequest(topicDir) {
42
+ const [request, requestSummary] = await Promise.all([
43
+ readFile(topicArtifactPath(topicDir, 'request'), 'utf8').catch(() => ''),
44
+ readFile(topicArtifactPath(topicDir, 'request_summary'), 'utf8').catch(() => ''),
45
+ ]);
46
+ try {
47
+ const workflow = await readWorkflowState(topicDir);
48
+ return {
49
+ request,
50
+ requestSummary,
51
+ topicSlug: workflow.topic_slug,
52
+ verification: workflow.verification ?? [],
53
+ };
54
+ }
55
+ catch {
56
+ return {
57
+ request,
58
+ requestSummary,
59
+ verification: [],
60
+ };
61
+ }
62
+ }
63
+ export async function ensureTopicCommitMessageArtifact({ topicDir, verification, }) {
64
+ const artifactPath = topicArtifactPath(topicDir, 'commit_message');
65
+ const existing = await readFile(artifactPath, 'utf8').catch(() => '');
66
+ if (validateLoreCommitMessage(existing).valid) {
67
+ return existing;
68
+ }
69
+ const topic = await readTopicRequest(topicDir);
70
+ const message = buildTopicLoreCommitMessage({
71
+ request: topic.request,
72
+ requestSummary: topic.requestSummary,
73
+ topicSlug: topic.topicSlug,
74
+ verificationCommands: (verification ?? topic.verification)
75
+ .filter((item) => item.exit_code === 0)
76
+ .map((item) => item.command),
77
+ });
78
+ await writeFile(artifactPath, message, 'utf8');
79
+ return message;
80
+ }
81
+ export async function finalizeTopicCommit({ topicDir, message, now = new Date(), }) {
82
+ const aggregate = await readAggregateReview(topicDir);
83
+ if (!aggregate.commit_allowed) {
84
+ throw new Error('aggregate review commit_allowed=false; commit cannot proceed');
85
+ }
86
+ const artifactPath = topicArtifactPath(topicDir, 'commit_message');
87
+ const commitMessage = message && message.trim() !== ''
88
+ ? message
89
+ : await ensureTopicCommitMessageArtifact({ topicDir });
90
+ const validation = validateLoreCommitMessage(commitMessage);
91
+ if (!validation.valid) {
92
+ throw new Error(validation.issues.join('\n'));
93
+ }
94
+ if (message && message.trim() !== '') {
95
+ await writeFile(artifactPath, `${commitMessage.trimEnd()}\n`, 'utf8');
96
+ }
97
+ else {
98
+ const storedMessage = await readCommitMessage(topicDir).catch(() => '');
99
+ if (storedMessage !== commitMessage) {
100
+ await writeFile(artifactPath, `${commitMessage.trimEnd()}\n`, 'utf8');
101
+ }
102
+ }
103
+ const gitCwd = await resolveGitCwd(topicDir);
104
+ const status = runGit(gitCwd, ['status', '--porcelain']);
105
+ if (status.trim() === '') {
106
+ throw new Error('No git changes are available to commit.');
107
+ }
108
+ runGit(gitCwd, ['add', '-A']);
109
+ runGit(gitCwd, ['commit', '-F', artifactPath]);
110
+ const commitSha = runGit(gitCwd, ['rev-parse', 'HEAD']);
111
+ const result = {
112
+ version: 1,
113
+ status: 'committed',
114
+ commit_sha: commitSha,
115
+ committed_at: now.toISOString(),
116
+ git_cwd: gitCwd,
117
+ message_path: artifactPath,
118
+ review_summary_path: join(topicDir, 'review', 'summary.md'),
119
+ };
120
+ await writeFile(topicArtifactPath(topicDir, 'commit_state'), `${JSON.stringify(result, null, 2)}\n`, 'utf8');
121
+ try {
122
+ const workflow = await readWorkflowState(topicDir);
123
+ workflow.phase = 'committed';
124
+ workflow.updated_at = now.toISOString();
125
+ await writeWorkflowState(topicDir, workflow);
126
+ }
127
+ catch {
128
+ // ignore missing workflow state in fixture tests
129
+ }
130
+ return result;
131
+ }
@@ -0,0 +1,99 @@
1
+ import { listDecisionRecords } from './decision-register.js';
2
+ import { listThreads } from './threads.js';
3
+ import { readdir, readFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ const GLOSSARY_STOPWORDS = new Set([
6
+ 'about',
7
+ 'above',
8
+ 'after',
9
+ 'always',
10
+ 'below',
11
+ 'carry',
12
+ 'entry',
13
+ 'issue',
14
+ 'migration',
15
+ 'needs',
16
+ 'notes',
17
+ 'shared',
18
+ 'should',
19
+ 'summary',
20
+ 'thread',
21
+ 'track',
22
+ ]);
23
+ async function readMaybe(path) {
24
+ try {
25
+ return await readFile(path, 'utf8');
26
+ }
27
+ catch {
28
+ return '';
29
+ }
30
+ }
31
+ function collectGlossaryTokens(content) {
32
+ const counts = new Map();
33
+ const relevantLines = content
34
+ .split(/\r?\n/)
35
+ .map((line) => line.trim())
36
+ .filter((line) => line.length > 0)
37
+ .filter((line) => !line.startsWith('#'))
38
+ .filter((line) => !line.startsWith('- updated_at:'));
39
+ for (const line of relevantLines) {
40
+ const normalized = line.replace(/^-\s*\d{4}-\d{2}-\d{2}T[^:]+:\s*/, '');
41
+ for (const token of normalized.match(/\b[a-zA-Z][a-zA-Z-]{5,}\b/g) ?? []) {
42
+ const lower = token.toLowerCase();
43
+ if (GLOSSARY_STOPWORDS.has(lower))
44
+ continue;
45
+ counts.set(lower, (counts.get(lower) ?? 0) + 1);
46
+ }
47
+ }
48
+ return [...counts.entries()]
49
+ .sort((left, right) => {
50
+ const countDiff = right[1] - left[1];
51
+ if (countDiff !== 0)
52
+ return countDiff;
53
+ return left[0].localeCompare(right[0]);
54
+ })
55
+ .map(([token]) => token)
56
+ .slice(0, 10);
57
+ }
58
+ export async function consolidateMemory({ rootDir, }) {
59
+ const decisions = await listDecisionRecords({ rootDir });
60
+ const decisionCounts = new Map();
61
+ for (const decision of decisions) {
62
+ const key = decision.title.trim().toLowerCase();
63
+ decisionCounts.set(key, (decisionCounts.get(key) ?? 0) + 1);
64
+ }
65
+ const duplicateDecisions = [...decisionCounts.entries()]
66
+ .filter(([, count]) => count > 1)
67
+ .map(([title, count]) => ({ title, count }));
68
+ const topicsRoot = join(rootDir, '.ax', 'topics');
69
+ const topicEntries = await readdir(topicsRoot, { withFileTypes: true }).catch(() => []);
70
+ const summaryCounts = new Map();
71
+ for (const entry of topicEntries) {
72
+ if (!entry.isDirectory())
73
+ continue;
74
+ const summary = (await readMaybe(join(topicsRoot, entry.name, 'request-summary.md')))
75
+ .trim()
76
+ .toLowerCase();
77
+ if (!summary)
78
+ continue;
79
+ summaryCounts.set(summary, (summaryCounts.get(summary) ?? 0) + 1);
80
+ }
81
+ const repeatedTopics = [...summaryCounts.entries()]
82
+ .filter(([, count]) => count > 1)
83
+ .map(([summary, count]) => ({ summary, count }));
84
+ const threads = await listThreads({ rootDir });
85
+ const glossaryCandidates = new Set();
86
+ for (const thread of threads) {
87
+ const content = await readMaybe(thread.path);
88
+ for (const token of collectGlossaryTokens(content)) {
89
+ glossaryCandidates.add(token);
90
+ if (glossaryCandidates.size >= 10)
91
+ break;
92
+ }
93
+ }
94
+ return {
95
+ duplicate_decisions: duplicateDecisions,
96
+ repeated_topics: repeatedTopics,
97
+ glossary_candidates: [...glossaryCandidates].slice(0, 10),
98
+ };
99
+ }
@@ -0,0 +1,141 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ function getDecisionRegisterPath(rootDir) {
5
+ return join(rootDir, '.ax', 'memory', 'decision-register.json');
6
+ }
7
+ async function readDecisionRegister(rootDir) {
8
+ try {
9
+ const raw = await readFile(getDecisionRegisterPath(rootDir), 'utf8');
10
+ return JSON.parse(raw);
11
+ }
12
+ catch {
13
+ return [];
14
+ }
15
+ }
16
+ async function writeDecisionRegister(rootDir, records) {
17
+ const path = getDecisionRegisterPath(rootDir);
18
+ await mkdir(dirname(path), { recursive: true });
19
+ await writeFile(path, `${JSON.stringify(records, null, 2)}\n`, 'utf8');
20
+ }
21
+ export async function recordDecision({ rootDir, title, summary, category, validFrom, sourceTopic, sourceDoc, now = new Date(), }) {
22
+ const records = await readDecisionRegister(rootDir);
23
+ const record = {
24
+ id: randomUUID(),
25
+ title,
26
+ summary,
27
+ category,
28
+ valid_from: validFrom,
29
+ status: 'active',
30
+ ...(sourceTopic ? { source_topic: sourceTopic } : {}),
31
+ ...(sourceDoc ? { source_doc: sourceDoc } : {}),
32
+ created_at: now.toISOString(),
33
+ };
34
+ records.push(record);
35
+ await writeDecisionRegister(rootDir, records);
36
+ return record;
37
+ }
38
+ export async function replaceDecision({ rootDir, replacedDecisionId, title, summary, category, validFrom, sourceTopic, sourceDoc, now = new Date(), }) {
39
+ const records = await readDecisionRegister(rootDir);
40
+ const replaced = records.find((record) => record.id === replacedDecisionId);
41
+ if (!replaced) {
42
+ throw new Error(`Decision not found: ${replacedDecisionId}`);
43
+ }
44
+ const replacement = {
45
+ id: randomUUID(),
46
+ title,
47
+ summary,
48
+ category,
49
+ valid_from: validFrom,
50
+ status: 'active',
51
+ ...(sourceTopic ? { source_topic: sourceTopic } : {}),
52
+ ...(sourceDoc ? { source_doc: sourceDoc } : {}),
53
+ created_at: now.toISOString(),
54
+ };
55
+ replaced.status = 'superseded';
56
+ replaced.valid_to = validFrom;
57
+ replaced.replaced_by = replacement.id;
58
+ records.push(replacement);
59
+ await writeDecisionRegister(rootDir, records);
60
+ return replacement;
61
+ }
62
+ function matchesActiveAt(record, activeAt) {
63
+ if (!activeAt)
64
+ return true;
65
+ if (record.valid_from > activeAt)
66
+ return false;
67
+ if (record.valid_to && record.valid_to <= activeAt)
68
+ return false;
69
+ return true;
70
+ }
71
+ function matchesQuery(record, query) {
72
+ if (!query)
73
+ return true;
74
+ const haystack = `${record.title}\n${record.summary}`.toLowerCase();
75
+ return query
76
+ .toLowerCase()
77
+ .split(/[^a-z0-9]+/)
78
+ .map((token) => token.trim())
79
+ .filter((token) => token.length >= 3)
80
+ .every((token) => haystack.includes(token));
81
+ }
82
+ function tokenize(value) {
83
+ return String(value || '')
84
+ .toLowerCase()
85
+ .split(/[^a-z0-9]+/)
86
+ .map((token) => token.trim())
87
+ .filter((token) => token.length >= 3);
88
+ }
89
+ function scoreDecision(query, content) {
90
+ const haystack = new Set(tokenize(content));
91
+ return tokenize(query).reduce((score, token) => score + (haystack.has(token) ? 1 : 0), 0);
92
+ }
93
+ export async function listDecisionRecords({ rootDir, activeAt, query, }) {
94
+ const records = await readDecisionRegister(rootDir);
95
+ return records
96
+ .filter((record) => matchesActiveAt(record, activeAt))
97
+ .filter((record) => matchesQuery(record, query));
98
+ }
99
+ export async function searchDecisionMemory({ rootDir, query, activeAt, limit = 5, }) {
100
+ const records = await listDecisionRecords({
101
+ rootDir,
102
+ activeAt,
103
+ });
104
+ const matches = [];
105
+ for (const record of records) {
106
+ const topicSummary = record.source_topic
107
+ ? await readFile(join(rootDir, '.ax', 'topics', record.source_topic, 'request-summary.md'), 'utf8').catch(() => '')
108
+ : '';
109
+ const topicRequest = record.source_topic
110
+ ? await readFile(join(rootDir, '.ax', 'topics', record.source_topic, 'request.md'), 'utf8').catch(() => '')
111
+ : '';
112
+ const topicSpec = record.source_topic
113
+ ? await readFile(join(rootDir, '.ax', 'topics', record.source_topic, 'spec.md'), 'utf8').catch(() => '')
114
+ : '';
115
+ const score = scoreDecision(query, [
116
+ record.title,
117
+ record.summary,
118
+ record.category,
119
+ record.source_doc ?? '',
120
+ record.source_topic ?? '',
121
+ topicSummary,
122
+ topicRequest,
123
+ topicSpec,
124
+ ].join('\n'));
125
+ if (score <= 0)
126
+ continue;
127
+ matches.push({
128
+ ...record,
129
+ score,
130
+ ...(topicSummary.trim() ? { source_topic_summary: topicSummary.trim() } : {}),
131
+ });
132
+ }
133
+ return matches
134
+ .sort((left, right) => {
135
+ const scoreDiff = right.score - left.score;
136
+ if (scoreDiff !== 0)
137
+ return scoreDiff;
138
+ return String(right.valid_from).localeCompare(String(left.valid_from));
139
+ })
140
+ .slice(0, limit);
141
+ }
@@ -0,0 +1,25 @@
1
+ import { searchDecisionMemory } from './decision-register.js';
2
+ import { listThreads } from './threads.js';
3
+ import { searchPastTopics } from './topic-recall.js';
4
+ import { readFile } from 'node:fs/promises';
5
+ export async function buildEntityMemoryView({ rootDir, entity, }) {
6
+ const [decisions, threads, topics] = await Promise.all([
7
+ searchDecisionMemory({ rootDir, query: entity, limit: 5 }),
8
+ listThreads({ rootDir }),
9
+ searchPastTopics({ rootDir, query: entity, limit: 5 }),
10
+ ]);
11
+ const normalizedEntity = entity.toLowerCase();
12
+ const matchingThreads = (await Promise.all(threads.map(async (thread) => {
13
+ const content = await readFile(thread.path, 'utf8').catch(() => '');
14
+ if (!content.toLowerCase().includes(normalizedEntity)) {
15
+ return null;
16
+ }
17
+ return { name: thread.name, path: thread.path };
18
+ }))).filter((thread) => Boolean(thread));
19
+ return {
20
+ entity,
21
+ decisions,
22
+ threads: matchingThreads,
23
+ topics,
24
+ };
25
+ }
@@ -0,0 +1,52 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ function storePath(rootDir) {
5
+ return join(rootDir, '.ax', 'memory', 'learned-debug.json');
6
+ }
7
+ async function readStore(rootDir) {
8
+ try {
9
+ return JSON.parse(await readFile(storePath(rootDir), 'utf8'));
10
+ }
11
+ catch {
12
+ return [];
13
+ }
14
+ }
15
+ async function writeStore(rootDir, notes) {
16
+ const path = storePath(rootDir);
17
+ await mkdir(dirname(path), { recursive: true });
18
+ await writeFile(path, `${JSON.stringify(notes, null, 2)}\n`, 'utf8');
19
+ }
20
+ function matchesQuery(note, query) {
21
+ if (!query)
22
+ return true;
23
+ const haystack = `${note.summary}\n${note.resolution}`.toLowerCase();
24
+ return query
25
+ .toLowerCase()
26
+ .split(/[^a-z0-9]+/)
27
+ .map((token) => token.trim())
28
+ .filter((token) => token.length >= 3)
29
+ .every((token) => haystack.includes(token));
30
+ }
31
+ export async function recordLearnedDebugNote({ rootDir, summary, resolution, occurrences = 1, approved = false, fixCommit, now = new Date(), }) {
32
+ if (occurrences < 2 && !approved && !fixCommit) {
33
+ throw new Error('learned-debug notes require approval, repeated occurrences, or a fix commit');
34
+ }
35
+ const notes = await readStore(rootDir);
36
+ const note = {
37
+ id: randomUUID(),
38
+ summary,
39
+ resolution,
40
+ occurrences,
41
+ approved,
42
+ ...(fixCommit ? { fix_commit: fixCommit } : {}),
43
+ created_at: now.toISOString(),
44
+ };
45
+ notes.push(note);
46
+ await writeStore(rootDir, notes);
47
+ return note;
48
+ }
49
+ export async function listLearnedDebugNotes({ rootDir, query, }) {
50
+ const notes = await readStore(rootDir);
51
+ return notes.filter((note) => matchesQuery(note, query));
52
+ }
@@ -0,0 +1,9 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ export async function writeTopicSummaryCheckpoint({ topicDir, summary, now = new Date(), }) {
4
+ const stamp = now.toISOString().replace(/[:.]/g, '-');
5
+ const outputPath = join(topicDir, 'checkpoints', `${stamp}-summary.md`);
6
+ await mkdir(dirname(outputPath), { recursive: true });
7
+ await writeFile(outputPath, `# Context Checkpoint\n\n- recorded_at: ${now.toISOString()}\n\n## Summary\n\n${summary.trim()}\n`, 'utf8');
8
+ return { output_path: outputPath };
9
+ }