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,22 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { bootstrapTopic } from '../topics/bootstrap.js';
4
+ import { findThread } from './threads.js';
5
+ export async function promoteThreadToTopic({ rootDir, name, request, }) {
6
+ const threadSummary = await findThread({ rootDir, name });
7
+ if (!threadSummary) {
8
+ throw new Error(`Thread not found: ${name}`);
9
+ }
10
+ const thread = await readFile(threadSummary.path, 'utf8');
11
+ const topic = await bootstrapTopic({
12
+ rootDir,
13
+ request,
14
+ });
15
+ const supportPath = join(topic.topicDir, 'support-thread.md');
16
+ await writeFile(supportPath, `# Imported Support Thread\n\n- source_thread: ${threadSummary.name}\n- source_slug: ${threadSummary.slug}\n\n${thread.trim()}\n`, 'utf8');
17
+ return {
18
+ topicDir: topic.topicDir,
19
+ topicSlug: topic.topicSlug,
20
+ supportPath,
21
+ };
22
+ }
@@ -0,0 +1,66 @@
1
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ export function slugifyThreadName(value) {
4
+ return String(value || '')
5
+ .trim()
6
+ .toLowerCase()
7
+ .replace(/[^a-z0-9]+/g, '-')
8
+ .replace(/-+/g, '-')
9
+ .replace(/^-|-$/g, '') || 'thread';
10
+ }
11
+ export function threadPath(rootDir, name) {
12
+ return join(rootDir, '.ax', 'threads', `${slugifyThreadName(name)}.md`);
13
+ }
14
+ export async function saveThreadNote({ rootDir, name, summary, note, now = new Date(), }) {
15
+ const path = threadPath(rootDir, name);
16
+ const existing = await readFile(path, 'utf8').catch(() => '');
17
+ const lines = existing.trim().length > 0
18
+ ? existing
19
+ .replace(/- updated_at:\s*.+$/m, `- updated_at: ${now.toISOString()}`)
20
+ .trimEnd()
21
+ .concat(`\n\n- ${now.toISOString()}: ${note.trim()}`)
22
+ : [
23
+ `# Thread: ${name}`,
24
+ '',
25
+ `- updated_at: ${now.toISOString()}`,
26
+ '',
27
+ '## Summary',
28
+ '',
29
+ (summary || `Thread for ${name}`).trim(),
30
+ '',
31
+ '## Notes',
32
+ '',
33
+ `- ${now.toISOString()}: ${note.trim()}`,
34
+ ].join('\n');
35
+ await mkdir(dirname(path), { recursive: true });
36
+ await writeFile(path, `${lines}\n`, 'utf8');
37
+ return { path };
38
+ }
39
+ export async function listThreads({ rootDir, }) {
40
+ const dir = join(rootDir, '.ax', 'threads');
41
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
42
+ const result = [];
43
+ for (const entry of entries) {
44
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
45
+ continue;
46
+ const path = join(dir, entry.name);
47
+ const content = await readFile(path, 'utf8').catch(() => '');
48
+ const updatedAt = content.match(/- updated_at:\s*(.+)\s*$/m)?.[1]?.trim() ||
49
+ content.match(/- (\d{4}-\d{2}-\d{2}T[^:]+:[^\n]+)/)?.[1]?.trim() ||
50
+ '';
51
+ result.push({
52
+ name: content.match(/^# Thread:\s*(.+)\s*$/m)?.[1]?.trim() || entry.name.replace(/\.md$/, ''),
53
+ slug: entry.name.replace(/\.md$/, ''),
54
+ path,
55
+ updated_at: updatedAt,
56
+ });
57
+ }
58
+ return result.sort((left, right) => right.updated_at.localeCompare(left.updated_at));
59
+ }
60
+ export async function findThread({ rootDir, name, }) {
61
+ const slug = slugifyThreadName(name);
62
+ const threads = await listThreads({ rootDir });
63
+ return (threads.find((thread) => thread.slug === slug) ??
64
+ threads.find((thread) => thread.name.toLowerCase() === name.toLowerCase()) ??
65
+ null);
66
+ }
@@ -0,0 +1,52 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } 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
+ function scoreTopic(query, content) {
11
+ const haystack = new Set(tokenize(content));
12
+ return tokenize(query).reduce((score, token) => score + (haystack.has(token) ? 1 : 0), 0);
13
+ }
14
+ export async function searchPastTopics({ rootDir, query, limit = 5, }) {
15
+ const topicsRoot = join(rootDir, '.ax', 'topics');
16
+ const topicEntries = await readdir(topicsRoot, { withFileTypes: true }).catch(() => []);
17
+ const matches = [];
18
+ for (const entry of topicEntries) {
19
+ if (!entry.isDirectory())
20
+ continue;
21
+ const topicDir = join(topicsRoot, entry.name);
22
+ const [request, summary, spec, workflowRaw] = await Promise.all([
23
+ readFile(join(topicDir, 'request.md'), 'utf8').catch(() => ''),
24
+ readFile(join(topicDir, 'request-summary.md'), 'utf8').catch(() => ''),
25
+ readFile(join(topicDir, 'spec.md'), 'utf8').catch(() => ''),
26
+ readFile(join(topicDir, 'workflow-state.json'), 'utf8').catch(() => ''),
27
+ ]);
28
+ if (!workflowRaw)
29
+ continue;
30
+ const workflow = JSON.parse(workflowRaw);
31
+ if (workflow.phase !== 'committed')
32
+ continue;
33
+ const score = scoreTopic(query, [request, summary, spec].join('\n'));
34
+ if (score <= 0)
35
+ continue;
36
+ matches.push({
37
+ topic_slug: entry.name,
38
+ summary: summary.trim(),
39
+ request: request.trim(),
40
+ score,
41
+ updated_at: workflow.updated_at,
42
+ });
43
+ }
44
+ return matches
45
+ .sort((a, b) => {
46
+ const scoreDiff = b.score - a.score;
47
+ if (scoreDiff !== 0)
48
+ return scoreDiff;
49
+ return String(b.updated_at ?? '').localeCompare(String(a.updated_at ?? ''));
50
+ })
51
+ .slice(0, limit);
52
+ }
@@ -0,0 +1,15 @@
1
+ import { buildContextBundle, classifyContextBundle, } from '../context/context-bundle.js';
2
+ export async function assessContextHealth({ rootDir, topicDir, query, maxChars = 6000, }) {
3
+ const bundle = await buildContextBundle({
4
+ ...(rootDir ? { rootDir } : {}),
5
+ ...(topicDir ? { topicDir } : {}),
6
+ query,
7
+ maxChars,
8
+ });
9
+ const classification = classifyContextBundle(bundle);
10
+ return {
11
+ status: classification.status,
12
+ recommendation: classification.recommendation,
13
+ bundle,
14
+ };
15
+ }
@@ -0,0 +1,29 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { assessContextHealth } from './context-health.js';
4
+ import { getRootDirFromTopicDir } from '../topics/topic-artifacts.js';
5
+ export async function writeContextMonitorSnapshot({ rootDir, topicDir, query, maxChars = 6000, outputPath, }) {
6
+ const report = await assessContextHealth({
7
+ ...(rootDir ? { rootDir } : {}),
8
+ ...(topicDir ? { topicDir } : {}),
9
+ query,
10
+ maxChars,
11
+ });
12
+ const effectiveRoot = rootDir || (topicDir ? getRootDirFromTopicDir(topicDir) : '');
13
+ const targetPath = outputPath || join(effectiveRoot, '.ax', 'context-monitor.json');
14
+ await mkdir(dirname(targetPath), { recursive: true });
15
+ await writeFile(targetPath, `${JSON.stringify({
16
+ status: report.status,
17
+ should_pause: report.status === 'critical',
18
+ recommendation: report.recommendation,
19
+ query,
20
+ max_chars: report.bundle.max_chars,
21
+ total_source_chars: report.bundle.total_source_chars,
22
+ truncated: report.bundle.truncated,
23
+ issues: report.bundle.issues,
24
+ }, null, 2)}\n`, 'utf8');
25
+ return {
26
+ output_path: targetPath,
27
+ status: report.status,
28
+ };
29
+ }
@@ -0,0 +1,78 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { listTopicsStatus } from './topics-status.js';
4
+ import { summarizeTopicStatus } from './topic-status.js';
5
+ import { getRootDirFromTopicDir, topicArtifactPath } from '../topics/topic-artifacts.js';
6
+ export async function writeRootStateSummary({ rootDir, limit = 10, }) {
7
+ const topics = await listTopicsStatus({ rootDir, limit });
8
+ const lines = [
9
+ '# Shift AX State',
10
+ '',
11
+ '## Active Topics',
12
+ '',
13
+ ];
14
+ if (topics.length === 0) {
15
+ lines.push('- No tracked topics yet.');
16
+ }
17
+ else {
18
+ for (const topic of topics) {
19
+ lines.push(`- ${topic.topic_slug}`);
20
+ lines.push(` - phase: ${topic.phase}`);
21
+ lines.push(` - review: ${topic.review_status}`);
22
+ lines.push(` - execution: ${topic.execution_status}`);
23
+ if (topic.last_failure_reason) {
24
+ lines.push(` - latest issue: ${topic.last_failure_reason}`);
25
+ }
26
+ }
27
+ }
28
+ const outputPath = join(rootDir, '.ax', 'STATE.md');
29
+ await mkdir(dirname(outputPath), { recursive: true });
30
+ await writeFile(outputPath, `${lines.join('\n')}\n`, 'utf8');
31
+ return { output_path: outputPath };
32
+ }
33
+ export async function writeTopicHandoff({ topicDir, summary, nextStep, commands = [], }) {
34
+ const status = await summarizeTopicStatus(topicDir);
35
+ const lines = [
36
+ '# Topic Handoff',
37
+ '',
38
+ `- topic: ${status.topic_slug}`,
39
+ `- phase: ${status.phase}`,
40
+ `- review: ${status.review_status}`,
41
+ `- execution: ${status.execution_status}`,
42
+ '',
43
+ '## Summary',
44
+ '',
45
+ summary.trim(),
46
+ '',
47
+ ];
48
+ if (nextStep?.trim()) {
49
+ lines.push('## Next Step', '', nextStep.trim(), '');
50
+ }
51
+ if (commands.length > 0) {
52
+ lines.push('## Suggested Commands', '');
53
+ for (const command of commands) {
54
+ lines.push(`- \`${command}\``);
55
+ }
56
+ lines.push('');
57
+ }
58
+ if (status.last_failure_reason) {
59
+ lines.push('## Latest Failure Reason', '', status.last_failure_reason, '');
60
+ }
61
+ const outputPath = topicArtifactPath(topicDir, 'handoff');
62
+ await writeFile(outputPath, `${lines.join('\n')}\n`, 'utf8');
63
+ return { output_path: outputPath };
64
+ }
65
+ export async function pauseTopicWork({ topicDir, summary, nextStep, commands = [], }) {
66
+ const handoff = await writeTopicHandoff({
67
+ topicDir,
68
+ summary,
69
+ nextStep,
70
+ commands,
71
+ });
72
+ const rootDir = getRootDirFromTopicDir(topicDir);
73
+ const state = await writeRootStateSummary({ rootDir });
74
+ return {
75
+ handoff_path: handoff.output_path,
76
+ state_path: state.output_path,
77
+ };
78
+ }
@@ -0,0 +1,40 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ async function readJson(path, fallback) {
4
+ try {
5
+ const raw = await readFile(path, 'utf8');
6
+ return JSON.parse(raw);
7
+ }
8
+ catch {
9
+ return fallback;
10
+ }
11
+ }
12
+ export async function summarizeTopicStatus(topicDir) {
13
+ const [workflow, executionState, aggregate, policyContextSync, lifecycle, reactions] = await Promise.all([
14
+ readJson(join(topicDir, 'workflow-state.json'), {}),
15
+ readJson(join(topicDir, 'execution-state.json'), {}),
16
+ readJson(join(topicDir, 'review', 'aggregate.json'), {}),
17
+ readJson(join(topicDir, 'policy-context-sync.json'), {}),
18
+ readJson(join(topicDir, 'lifecycle-log.json'), []),
19
+ readJson(join(topicDir, 'reaction-log.json'), []),
20
+ ]);
21
+ const lastEvent = lifecycle.length > 0 ? lifecycle[lifecycle.length - 1] : undefined;
22
+ const lastReaction = reactions.length > 0 ? reactions[reactions.length - 1] : undefined;
23
+ return {
24
+ topic_slug: workflow.topic_slug ?? topicDir.split('/').pop() ?? 'unknown-topic',
25
+ phase: workflow.phase ?? 'unknown',
26
+ review_status: workflow.review?.overall_status ?? aggregate.overall_status ?? 'unknown',
27
+ execution_status: executionState.overall_status ?? 'unknown',
28
+ ...(policyContextSync.status ? { policy_context_status: policyContextSync.status } : {}),
29
+ ...(lastEvent ? { last_event: lastEvent } : {}),
30
+ ...(lastReaction ? { last_reaction: lastReaction } : {}),
31
+ ...(policyContextSync.status === 'required'
32
+ ? { last_failure_reason: 'policy context sync is required before implementation can start' }
33
+ : {}),
34
+ ...(lastEvent && /fail|blocked|changes|requested/i.test(lastEvent.summary)
35
+ ? { last_failure_reason: lastEvent.summary }
36
+ : lastReaction?.outcome && lastReaction.outcome !== 'approved'
37
+ ? { last_failure_reason: `${lastReaction.key}: ${lastReaction.outcome}` }
38
+ : {}),
39
+ };
40
+ }
@@ -0,0 +1,26 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { summarizeTopicStatus } from './topic-status.js';
4
+ export async function listTopicsStatus({ rootDir, limit = 10, }) {
5
+ const topicsRoot = join(rootDir, '.ax', 'topics');
6
+ const entries = await readdir(topicsRoot, { withFileTypes: true }).catch(() => []);
7
+ const results = [];
8
+ for (const entry of entries) {
9
+ if (!entry.isDirectory())
10
+ continue;
11
+ const topicDir = join(topicsRoot, entry.name);
12
+ const workflowRaw = await readFile(join(topicDir, 'workflow-state.json'), 'utf8').catch(() => '');
13
+ if (!workflowRaw)
14
+ continue;
15
+ const workflow = JSON.parse(workflowRaw);
16
+ const summary = await summarizeTopicStatus(topicDir);
17
+ results.push({
18
+ ...summary,
19
+ topic_dir: topicDir,
20
+ updated_at: workflow.updated_at ?? '',
21
+ });
22
+ }
23
+ return results
24
+ .sort((left, right) => String(right.updated_at).localeCompare(String(left.updated_at)))
25
+ .slice(0, limit);
26
+ }
@@ -0,0 +1,82 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ async function readMaybe(path) {
4
+ try {
5
+ return await readFile(path, 'utf8');
6
+ }
7
+ catch {
8
+ return '';
9
+ }
10
+ }
11
+ async function readTopicDebt(topicDir) {
12
+ const workflowRaw = await readMaybe(join(topicDir, 'workflow-state.json'));
13
+ if (!workflowRaw)
14
+ return [];
15
+ const workflow = JSON.parse(workflowRaw);
16
+ const topicSlug = workflow.topic_slug || topicDir.split('/').pop() || 'unknown-topic';
17
+ const debt = [];
18
+ if ((workflow.verification ?? []).length === 0) {
19
+ debt.push({
20
+ topic_slug: topicSlug,
21
+ kind: 'verification_command',
22
+ message: 'Verification was skipped or not recorded.',
23
+ });
24
+ }
25
+ for (const item of workflow.verification ?? []) {
26
+ if ((item.exit_code ?? 0) !== 0) {
27
+ debt.push({
28
+ topic_slug: topicSlug,
29
+ kind: 'verification_command',
30
+ message: `Verification command failed: ${item.command ?? 'unknown command'}`,
31
+ });
32
+ }
33
+ }
34
+ const reviewDir = join(topicDir, 'review');
35
+ const reviewFiles = await readdir(reviewDir).catch(() => []);
36
+ for (const file of reviewFiles) {
37
+ if (!file.endsWith('.json') || file === 'aggregate.json')
38
+ continue;
39
+ const raw = await readMaybe(join(reviewDir, file));
40
+ if (!raw)
41
+ continue;
42
+ const parsed = JSON.parse(raw);
43
+ if (parsed.status === 'approved')
44
+ continue;
45
+ if ((parsed.issues ?? []).length > 0) {
46
+ for (const issue of parsed.issues ?? []) {
47
+ debt.push({
48
+ topic_slug: topicSlug,
49
+ kind: 'review_issue',
50
+ message: `${parsed.lane ?? 'review'}: ${issue.message ?? parsed.summary ?? 'review issue'}`,
51
+ });
52
+ }
53
+ }
54
+ else if (parsed.summary) {
55
+ debt.push({
56
+ topic_slug: topicSlug,
57
+ kind: 'review_issue',
58
+ message: `${parsed.lane ?? 'review'}: ${parsed.summary}`,
59
+ });
60
+ }
61
+ for (const item of parsed.deferred_items ?? []) {
62
+ debt.push({
63
+ topic_slug: topicSlug,
64
+ kind: 'review_issue',
65
+ message: `${parsed.lane ?? 'review'} deferred verification: ${item}`,
66
+ });
67
+ }
68
+ }
69
+ return debt;
70
+ }
71
+ export async function listVerificationDebt({ rootDir, topicDir, }) {
72
+ if (topicDir) {
73
+ return readTopicDebt(topicDir);
74
+ }
75
+ if (!rootDir) {
76
+ throw new Error('rootDir or topicDir is required');
77
+ }
78
+ const topicsRoot = join(rootDir, '.ax', 'topics');
79
+ const entries = await readdir(topicsRoot, { withFileTypes: true }).catch(() => []);
80
+ const all = await Promise.all(entries.filter((entry) => entry.isDirectory()).map((entry) => readTopicDebt(join(topicsRoot, entry.name))));
81
+ return all.flat();
82
+ }
@@ -0,0 +1,120 @@
1
+ function bulletize(value) {
2
+ return String(value || '')
3
+ .split(/\r?\n|;/)
4
+ .map((item) => item.trim())
5
+ .filter(Boolean)
6
+ .map((item) => (item.startsWith('-') ? item : `- ${item}`));
7
+ }
8
+ function sentenceOrFallback(value, fallback) {
9
+ const trimmed = String(value || '').trim();
10
+ return trimmed === '' ? fallback : trimmed;
11
+ }
12
+ export function buildPlanningArtifactsFromInterview({ request, matchedContextLabels, answers, engineeringDefaults, }) {
13
+ const relevantContext = matchedContextLabels.length > 0
14
+ ? matchedContextLabels.map((label) => `- ${label}`)
15
+ : ['- No matched context documents yet.'];
16
+ const constraints = bulletize(answers.constraints);
17
+ const outOfScope = bulletize(answers.outOfScope);
18
+ const verification = bulletize(answers.verification);
19
+ const implementationAreas = bulletize(answers.implementationAreas);
20
+ const longRunningWork = bulletize(answers.longRunningWork);
21
+ const policyUpdates = bulletize(answers.policyUpdates);
22
+ const brainstormContent = [
23
+ '# Brainstorm',
24
+ '',
25
+ '## Request',
26
+ '',
27
+ request.trim(),
28
+ '',
29
+ '## Relevant Context',
30
+ '',
31
+ ...relevantContext,
32
+ '',
33
+ '## Clarified Outcome',
34
+ '',
35
+ ...bulletize(answers.outcome),
36
+ '',
37
+ '## Constraints',
38
+ '',
39
+ ...constraints,
40
+ '',
41
+ '## Out of Scope',
42
+ '',
43
+ ...outOfScope,
44
+ '',
45
+ '## Verification Expectations',
46
+ '',
47
+ ...verification,
48
+ '',
49
+ '## Global Knowledge Updates',
50
+ '',
51
+ ...(policyUpdates.length > 0 ? policyUpdates : ['- None yet.']),
52
+ '',
53
+ '## Implementation Areas',
54
+ '',
55
+ ...implementationAreas,
56
+ '',
57
+ '## Long-running Work',
58
+ '',
59
+ ...longRunningWork,
60
+ '',
61
+ ].join('\n');
62
+ const specContent = [
63
+ '# Topic Spec',
64
+ '',
65
+ '## Goal',
66
+ '',
67
+ sentenceOrFallback(answers.outcome, request.trim()),
68
+ '',
69
+ '## Relevant Context',
70
+ '',
71
+ ...relevantContext,
72
+ '',
73
+ '## Constraints',
74
+ '',
75
+ ...constraints,
76
+ '',
77
+ '## Out of Scope',
78
+ '',
79
+ ...outOfScope,
80
+ '',
81
+ '## Verification Expectations',
82
+ '',
83
+ ...verification,
84
+ '',
85
+ '## Global Knowledge Updates',
86
+ '',
87
+ ...(policyUpdates.length > 0 ? policyUpdates : ['- None yet.']),
88
+ '',
89
+ ].join('\n');
90
+ const implementationPlanContent = [
91
+ '# Implementation Plan',
92
+ '',
93
+ '## Delivery Tasks',
94
+ '',
95
+ `1. Add or update tests first using ${engineeringDefaults.test_strategy.toUpperCase()} for: ${sentenceOrFallback(answers.verification, 'the clarified outcome')}`,
96
+ `2. Implement ${sentenceOrFallback(answers.outcome, request.trim())} inside: ${sentenceOrFallback(answers.implementationAreas, 'the affected service boundary')}.`,
97
+ `3. Respect ${engineeringDefaults.architecture.replace(/-/g, ' ')} and keep these constraints visible: ${sentenceOrFallback(answers.constraints, 'No extra constraints recorded.')}`,
98
+ `4. Keep these items out of scope: ${sentenceOrFallback(answers.outOfScope, 'No out-of-scope items recorded.')}`,
99
+ `5. Capture verification evidence for: ${sentenceOrFallback(answers.verification, 'the agreed happy path and regressions')}`,
100
+ '',
101
+ '## Execution Routing',
102
+ '',
103
+ `- Short slices should use ${engineeringDefaults.short_task_execution}.`,
104
+ `- Long-running or cross-cutting work should use ${engineeringDefaults.long_task_execution}.`,
105
+ ...longRunningWork.map((item) => `${item} -> ${engineeringDefaults.long_task_execution}`),
106
+ ...implementationAreas
107
+ .filter((item) => !longRunningWork.some((longItem) => longItem.includes(item.replace(/^-\s*/, ''))))
108
+ .map((item) => `${item} -> ${engineeringDefaults.short_task_execution}`),
109
+ '',
110
+ '## Global Knowledge Updates',
111
+ '',
112
+ ...(policyUpdates.length > 0 ? policyUpdates : ['- None yet.']),
113
+ '',
114
+ ].join('\n');
115
+ return {
116
+ brainstormContent: `${brainstormContent.trimEnd()}\n`,
117
+ specContent: `${specContent.trimEnd()}\n`,
118
+ implementationPlanContent: `${implementationPlanContent.trimEnd()}\n`,
119
+ };
120
+ }
@@ -0,0 +1,69 @@
1
+ import { SHIFT_AX_ESCALATION_KINDS, readWorkflowState, writeWorkflowState, } from './workflow-state.js';
2
+ export function defaultEscalationSummary(kind) {
3
+ switch (kind) {
4
+ case 'new-user-flow':
5
+ return 'Implementation requires a new user flow that is not in the reviewed plan.';
6
+ case 'policy-conflict':
7
+ return 'A domain or policy document conflicts with the implementation approach.';
8
+ case 'risky-data-or-permission-change':
9
+ return 'A risky data or permission change needs human review before continuing.';
10
+ }
11
+ }
12
+ export function parseEscalationArgument(raw) {
13
+ const [kindPart, ...summaryParts] = String(raw || '').split(':');
14
+ const kind = kindPart?.trim();
15
+ if (!kind || !SHIFT_AX_ESCALATION_KINDS.includes(kind)) {
16
+ throw new Error(`Invalid escalation trigger "${raw}". Expected one of: ${SHIFT_AX_ESCALATION_KINDS.join(', ')}`);
17
+ }
18
+ const summary = summaryParts.join(':').trim() || defaultEscalationSummary(kind);
19
+ return {
20
+ kind,
21
+ summary,
22
+ };
23
+ }
24
+ function nextPhaseAfterEscalationClear(workflow) {
25
+ if (workflow.phase !== 'awaiting_human_escalation') {
26
+ return workflow.phase;
27
+ }
28
+ return workflow.plan_review_status === 'approved'
29
+ ? 'approved'
30
+ : 'awaiting_plan_review';
31
+ }
32
+ export async function recordWorkflowEscalations({ topicDir, triggers, now = new Date(), }) {
33
+ if (!triggers || triggers.length === 0) {
34
+ throw new Error('At least one escalation trigger is required.');
35
+ }
36
+ const workflow = await readWorkflowState(topicDir);
37
+ const resolvedHistory = workflow.escalation?.triggers.filter((trigger) => trigger.resolved_at) ?? [];
38
+ const activeTriggers = triggers.map((trigger) => ({
39
+ kind: trigger.kind,
40
+ summary: trigger.summary.trim(),
41
+ detected_at: now.toISOString(),
42
+ }));
43
+ workflow.phase = 'awaiting_human_escalation';
44
+ workflow.updated_at = now.toISOString();
45
+ workflow.escalation = {
46
+ status: 'required',
47
+ triggers: [...resolvedHistory, ...activeTriggers],
48
+ };
49
+ await writeWorkflowState(topicDir, workflow);
50
+ return workflow;
51
+ }
52
+ export async function clearWorkflowEscalations({ topicDir, resolution, now = new Date(), }) {
53
+ const workflow = await readWorkflowState(topicDir);
54
+ const updatedTriggers = (workflow.escalation?.triggers ?? []).map((trigger) => trigger.resolved_at
55
+ ? trigger
56
+ : {
57
+ ...trigger,
58
+ resolved_at: now.toISOString(),
59
+ ...(resolution?.trim() ? { resolution: resolution.trim() } : {}),
60
+ });
61
+ workflow.updated_at = now.toISOString();
62
+ workflow.phase = nextPhaseAfterEscalationClear(workflow);
63
+ workflow.escalation = {
64
+ status: 'clear',
65
+ triggers: updatedTriggers,
66
+ };
67
+ await writeWorkflowState(topicDir, workflow);
68
+ return workflow;
69
+ }
@@ -0,0 +1,61 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { readProjectProfile } from '../policies/project-profile.js';
3
+ import { getRootDirFromTopicDir, topicArtifactPath } from '../topics/topic-artifacts.js';
4
+ function extractPlanTasks(plan) {
5
+ const lines = String(plan || '')
6
+ .split(/\r?\n/)
7
+ .map((line) => line.trim())
8
+ .filter(Boolean)
9
+ .filter((line) => !line.startsWith('#'));
10
+ const taskLines = lines.filter((line) => /^[-*]\s+|^\d+\.\s+/.test(line));
11
+ if (taskLines.length > 0) {
12
+ return taskLines.map((line) => line.replace(/^[-*]\s+|^\d+\.\s+/, '').trim());
13
+ }
14
+ return lines;
15
+ }
16
+ function pickExecutionMode(task, shortExecution, longExecution) {
17
+ const normalized = task.toLowerCase();
18
+ const longSignals = ['migration', 'long-running', 'cross-cutting', 'multi-service', 'analysis', 'tmux'];
19
+ const isLong = longSignals.some((signal) => normalized.includes(signal));
20
+ return isLong
21
+ ? {
22
+ executionMode: 'tmux',
23
+ reason: `Matched long-running signal; route through ${longExecution}.`,
24
+ }
25
+ : {
26
+ executionMode: 'subagent',
27
+ reason: `Fits a shorter bounded slice; route through ${shortExecution}.`,
28
+ };
29
+ }
30
+ export async function buildExecutionHandoff(topicDir, now = new Date()) {
31
+ const [plan, workflow, profile] = await Promise.all([
32
+ readFile(topicArtifactPath(topicDir, 'implementation_plan'), 'utf8'),
33
+ readFile(topicArtifactPath(topicDir, 'workflow_state'), 'utf8'),
34
+ readProjectProfile(getRootDirFromTopicDir(topicDir)),
35
+ ]);
36
+ const parsedWorkflow = JSON.parse(workflow);
37
+ const shortExecution = profile?.engineering_defaults.short_task_execution ?? 'subagent';
38
+ const longExecution = profile?.engineering_defaults.long_task_execution ?? 'tmux';
39
+ const tasks = extractPlanTasks(plan).map((task, index) => {
40
+ const routing = pickExecutionMode(task, shortExecution, longExecution);
41
+ return {
42
+ id: `task-${index + 1}`,
43
+ source_text: task,
44
+ execution_mode: routing.executionMode,
45
+ reason: routing.reason,
46
+ };
47
+ });
48
+ return {
49
+ version: 1,
50
+ generated_at: now.toISOString(),
51
+ topic_slug: parsedWorkflow.topic_slug,
52
+ default_short_execution: shortExecution,
53
+ default_long_execution: longExecution,
54
+ tasks,
55
+ };
56
+ }
57
+ export async function writeExecutionHandoff(topicDir, now = new Date()) {
58
+ const handoff = await buildExecutionHandoff(topicDir, now);
59
+ await writeFile(topicArtifactPath(topicDir, 'execution_handoff'), `${JSON.stringify(handoff, null, 2)}\n`, 'utf8');
60
+ return handoff;
61
+ }