@ai-content-space/loopx 0.1.2 → 0.1.4

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 (69) hide show
  1. package/README.md +422 -57
  2. package/README.zh-CN.md +485 -0
  3. package/assets/logo.svg +89 -0
  4. package/package.json +5 -1
  5. package/plugins/loopx/.codex-plugin/plugin.json +1 -1
  6. package/plugins/loopx/scripts/plugin-install.test.mjs +14 -0
  7. package/plugins/loopx/skills/archive/SKILL.md +49 -0
  8. package/plugins/loopx/skills/build/SKILL.md +111 -9
  9. package/plugins/loopx/skills/clarify/SKILL.md +129 -8
  10. package/plugins/loopx/skills/debug/SKILL.md +296 -0
  11. package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
  12. package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
  13. package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
  14. package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
  15. package/plugins/loopx/skills/go-style/SKILL.md +71 -0
  16. package/plugins/loopx/skills/kratos/SKILL.md +74 -0
  17. package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
  18. package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
  19. package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
  20. package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
  21. package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
  22. package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
  23. package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
  24. package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
  25. package/plugins/loopx/skills/plan/SKILL.md +24 -3
  26. package/plugins/loopx/skills/review/SKILL.md +98 -1
  27. package/plugins/loopx/skills/tdd/SKILL.md +371 -0
  28. package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
  29. package/plugins/loopx/skills/verify/SKILL.md +139 -0
  30. package/scripts/codex-stop-hook.mjs +71 -0
  31. package/scripts/codex-workflow-hook.mjs +248 -0
  32. package/skills/archive/SKILL.md +49 -0
  33. package/skills/build/SKILL.md +111 -9
  34. package/skills/clarify/SKILL.md +129 -8
  35. package/skills/debug/SKILL.md +296 -0
  36. package/skills/debug/condition-based-waiting.md +115 -0
  37. package/skills/debug/defense-in-depth.md +122 -0
  38. package/skills/debug/find-polluter.sh +63 -0
  39. package/skills/debug/root-cause-tracing.md +169 -0
  40. package/skills/go-style/SKILL.md +71 -0
  41. package/skills/kratos/SKILL.md +74 -0
  42. package/skills/kratos/references/advanced-features.md +314 -0
  43. package/skills/kratos/references/architecture.md +488 -0
  44. package/skills/kratos/references/configuration.md +399 -0
  45. package/skills/kratos/references/http-customization.md +512 -0
  46. package/skills/kratos/references/middleware-logging.md +400 -0
  47. package/skills/kratos/references/proto-api-design.md +432 -0
  48. package/skills/kratos/references/security-auth.md +411 -0
  49. package/skills/kratos/references/troubleshooting.md +385 -0
  50. package/skills/plan/SKILL.md +20 -3
  51. package/skills/review/SKILL.md +98 -1
  52. package/skills/tdd/SKILL.md +371 -0
  53. package/skills/tdd/testing-anti-patterns.md +299 -0
  54. package/skills/verify/SKILL.md +139 -0
  55. package/src/build-runtime.mjs +311 -26
  56. package/src/build-stop-gate.mjs +94 -0
  57. package/src/cli.mjs +57 -5
  58. package/src/codex-exec-runtime.mjs +105 -5
  59. package/src/context-manifest.mjs +172 -0
  60. package/src/html-views.mjs +316 -0
  61. package/src/install-discovery.mjs +352 -5
  62. package/src/next-skill.mjs +57 -5
  63. package/src/plan-runtime.mjs +102 -122
  64. package/src/review-runtime.mjs +558 -0
  65. package/src/runtime-maintenance.mjs +429 -14
  66. package/src/template-governance.mjs +223 -0
  67. package/src/workflow.mjs +2341 -120
  68. package/src/workspace-context.mjs +166 -0
  69. package/src/workspace-memory.mjs +69 -0
package/src/cli.mjs CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { autopilotStage, approveStage, buildStage, clarifyStage, initWorkspace, planStage, reviewStage, statusSummary } from './workflow.mjs';
3
+ import { archiveStage, autopilotStage, approveStage, buildStage, clarifyStage, initWorkspace, planStage, reviewStage, statusSummary } from './workflow.mjs';
4
+ import { renderHtmlViews } from './html-views.mjs';
4
5
  import { installBundledSkills } from './install-discovery.mjs';
5
- import { withNextSkill } from './next-skill.mjs';
6
+ import { nextSkillCommand, withNextSkill } from './next-skill.mjs';
6
7
  import { doctorRuntime, migrateLegacyRuntime } from './runtime-maintenance.mjs';
8
+ import { setupWorkspaceContext } from './workspace-context.mjs';
7
9
 
8
10
  function usage() {
9
11
  return [
@@ -13,9 +15,13 @@ function usage() {
13
15
  ' loopx approve <slug> --from <stage> --to <stage>',
14
16
  ' loopx plan [slug] [--direct <spec-path>] [--interactive] [--deliberate]',
15
17
  ' loopx build <slug> [--no-deslop]',
18
+ ' loopx build --from-review <review-report-path> [--no-deslop]',
16
19
  ' loopx review <slug> [--reviewer <name>]',
20
+ ' loopx archive <slug>',
17
21
  ' loopx autopilot <slug> [--reviewer <name>]',
22
+ ' loopx render [slug|--all]',
18
23
  ' loopx status [slug] [--json]',
24
+ ' loopx setup-context',
19
25
  ' loopx doctor',
20
26
  ' loopx migrate',
21
27
  ' loopx repair-install',
@@ -78,7 +84,7 @@ function printHumanStatus(status) {
78
84
  console.log(`plan_deliberate_mode: ${status.state.plan_deliberate_mode}`);
79
85
  console.log(`plan_architect_review_status: ${status.state.plan_architect_review_status}`);
80
86
  console.log(`plan_critic_verdict: ${status.state.plan_critic_verdict}`);
81
- console.log(`plan_docs_status: ${status.state.plan_docs_status}`);
87
+ console.log(`plan_artifact_status: ${status.state.plan_docs_status}`);
82
88
  console.log(`plan_blockers: ${Array.isArray(status.state.plan_blockers) && status.state.plan_blockers.length > 0 ? status.state.plan_blockers.join(', ') : '(none)'}`);
83
89
  }
84
90
  if (status.state?.current_stage === 'build') {
@@ -88,8 +94,31 @@ function printHumanStatus(status) {
88
94
  console.log(`build_architect_verification_status: ${status.state.build_architect_verification_status}`);
89
95
  console.log(`build_deslop_status: ${status.state.build_deslop_status}`);
90
96
  console.log(`build_regression_status: ${status.state.build_regression_status}`);
97
+ console.log(`context_manifest_status: ${status.state.context_manifest_status ?? 'unknown'}`);
91
98
  console.log(`build_blockers: ${Array.isArray(status.state.build_blockers) && status.state.build_blockers.length > 0 ? status.state.build_blockers.join(', ') : '(none)'}`);
92
99
  }
100
+ if (status.state?.workspace_journal_path) {
101
+ console.log(`workspace_journal_path: ${status.state.workspace_journal_path}`);
102
+ }
103
+ if (status.state?.change_artifacts_status) {
104
+ console.log(`change_artifacts_status: ${status.state.change_artifacts_status}`);
105
+ console.log(`spec_delta_status: ${status.state.spec_delta_status ?? 'unknown'}`);
106
+ console.log(`spec_sync_status: ${status.state.spec_sync_status ?? 'unknown'}`);
107
+ console.log(`archive_status: ${status.state.archive_status ?? 'unknown'}`);
108
+ }
109
+ if (status.state?.readiness && status.state?.authorization) {
110
+ for (const key of ['plan', 'build', 'review', 'done', 'archive']) {
111
+ if (status.state.readiness[key]) {
112
+ console.log(`readiness_${key}: ${status.state.readiness[key].ready}`);
113
+ }
114
+ if (status.state.authorization[key]) {
115
+ console.log(`authorization_${key}: ${status.state.authorization[key].authorized}`);
116
+ }
117
+ }
118
+ }
119
+ if (status.hook) {
120
+ console.log(`hook_enabled: ${status.hook.enabled}`);
121
+ }
93
122
  if (status.state?.autopilot_current_phase && status.state.autopilot_current_phase !== 'none') {
94
123
  console.log(`autopilot_current_phase: ${status.state.autopilot_current_phase}`);
95
124
  console.log(`autopilot_completed: ${status.state.autopilot_completed}`);
@@ -99,6 +128,10 @@ function printHumanStatus(status) {
99
128
  console.log(`last_confirmed_transition: ${status.state?.last_confirmed_transition ?? 'none'}`);
100
129
  console.log(`pending_user_decision: ${status.state?.pending_user_decision ?? 'none'}`);
101
130
  console.log(`missing artifacts: ${status.missing_artifacts.length > 0 ? status.missing_artifacts.join(', ') : '(none)'}`);
131
+ const nextSkill = nextSkillCommand(status.state);
132
+ if (nextSkill) {
133
+ console.log(`next skill: ${nextSkill}`);
134
+ }
102
135
  console.log(`next: ${status.next_action}`);
103
136
  }
104
137
 
@@ -116,6 +149,11 @@ async function main() {
116
149
  console.log(JSON.stringify({ ok: true, command, workspaceRoot: result.workspaceRoot, workflow: result.workflow?.state ?? null }, null, 2));
117
150
  return;
118
151
  }
152
+ case 'setup-context': {
153
+ const contextSetup = await setupWorkspaceContext(process.cwd());
154
+ console.log(JSON.stringify({ ok: true, command, contextSetup }, null, 2));
155
+ return;
156
+ }
119
157
  case 'clarify': {
120
158
  const profile = options.get('--deep') ? 'deep' : 'standard';
121
159
  const result = await clarifyStage(process.cwd(), positionals[0], { profile });
@@ -140,8 +178,9 @@ async function main() {
140
178
  return;
141
179
  }
142
180
  case 'build': {
143
- const result = await buildStage(process.cwd(), positionals[0], {
181
+ const result = await buildStage(process.cwd(), options.get('--from-review') ? undefined : positionals[0], {
144
182
  noDeslop: Boolean(options.get('--no-deslop')),
183
+ fromReviewPath: options.get('--from-review'),
145
184
  });
146
185
  console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state }, result.state), null, 2));
147
186
  return;
@@ -150,7 +189,12 @@ async function main() {
150
189
  const result = await reviewStage(process.cwd(), positionals[0], {
151
190
  reviewer: options.get('--reviewer') || 'independent-reviewer',
152
191
  });
153
- console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state, verdict: result.verdict }, result.state), null, 2));
192
+ console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state, verdict: result.verdict, review_message_zh: result.reviewMessageZh }, result.state), null, 2));
193
+ return;
194
+ }
195
+ case 'archive': {
196
+ const result = await archiveStage(process.cwd(), positionals[0]);
197
+ console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
154
198
  return;
155
199
  }
156
200
  case 'autopilot': {
@@ -160,6 +204,14 @@ async function main() {
160
204
  console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state, runPath: result.runPath }, null, 2));
161
205
  return;
162
206
  }
207
+ case 'render': {
208
+ const result = await renderHtmlViews(process.cwd(), {
209
+ slug: positionals[0],
210
+ all: Boolean(options.get('--all')),
211
+ });
212
+ console.log(JSON.stringify({ ok: true, command, ...result }, null, 2));
213
+ return;
214
+ }
163
215
  case 'status': {
164
216
  const result = await statusSummary(process.cwd(), positionals[0]);
165
217
  if (options.get('--json')) {
@@ -1,9 +1,16 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
- import { readFile } from 'node:fs/promises';
3
+ import { mkdtemp, readFile, rm, unlink, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
4
6
  const DEFAULT_CODEX_MODEL = 'gpt-5.4';
5
7
  const DEFAULT_CODEX_TIMEOUT_MS = 120000;
6
8
  const DEFAULT_CODEX_REASONING = 'low';
9
+ const DIAGNOSTIC_TAIL_CHARS = 4000;
10
+
11
+ function diagnosticTail(value) {
12
+ return String(value || '').slice(-DIAGNOSTIC_TAIL_CHARS);
13
+ }
7
14
 
8
15
  function codexBinary() {
9
16
  return process.env.LOOPX_CODEX_BIN || 'codex';
@@ -22,7 +29,14 @@ export async function runCodexExec({
22
29
  extraArgs = [],
23
30
  timeoutMs = DEFAULT_CODEX_TIMEOUT_MS,
24
31
  reasoningEffort = DEFAULT_CODEX_REASONING,
32
+ outputSchema = null,
33
+ promptViaStdin = false,
25
34
  }) {
35
+ await unlink(outputPath).catch((error) => {
36
+ if (error?.code !== 'ENOENT') {
37
+ throw error;
38
+ }
39
+ });
26
40
  const args = [
27
41
  'exec',
28
42
  '--dangerously-bypass-approvals-and-sandbox',
@@ -34,8 +48,9 @@ export async function runCodexExec({
34
48
  `model_reasoning_effort=\"${reasoningEffort}\"`,
35
49
  '-o',
36
50
  outputPath,
51
+ ...(outputSchema ? ['--output-schema', outputSchema] : []),
37
52
  ...extraArgs,
38
- prompt,
53
+ promptViaStdin ? '-' : prompt,
39
54
  ];
40
55
 
41
56
  const child = spawn(codexBinary(), args, {
@@ -47,14 +62,16 @@ export async function runCodexExec({
47
62
  stdio: ['pipe', 'pipe', 'pipe'],
48
63
  });
49
64
 
50
- child.stdin.end();
65
+ child.stdin.end(promptViaStdin ? prompt : undefined);
51
66
 
52
67
  const stdoutChunks = [];
53
68
  const stderrChunks = [];
54
69
  child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
55
70
  child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
56
71
 
72
+ let timedOut = false;
57
73
  const timeout = setTimeout(() => {
74
+ timedOut = true;
58
75
  child.kill('SIGTERM');
59
76
  }, timeoutMs);
60
77
 
@@ -67,6 +84,9 @@ export async function runCodexExec({
67
84
  const stdout = Buffer.concat(stdoutChunks).toString('utf8');
68
85
  const stderr = Buffer.concat(stderrChunks).toString('utf8');
69
86
  const finalMessage = existsSync(outputPath) ? await readFile(outputPath, 'utf8') : '';
87
+ if (timedOut) {
88
+ throw new Error('timeout');
89
+ }
70
90
  if (exitCode !== 0) {
71
91
  throw new Error(`exit_${exitCode}`);
72
92
  }
@@ -82,7 +102,7 @@ export async function runCodexExec({
82
102
  const stderr = Buffer.concat(stderrChunks).toString('utf8');
83
103
  const finalMessage = existsSync(outputPath) ? await readFile(outputPath, 'utf8') : '';
84
104
  const message = error instanceof Error ? error.message : String(error);
85
- throw new Error(`codex_exec_failed:${message}\nstdout:${stdout}\nstderr:${stderr}\nfinal:${finalMessage}`);
105
+ throw new Error(`codex_exec_failed:${message}\nstdout:${diagnosticTail(stdout)}\nstderr:${diagnosticTail(stderr)}\nfinal:${diagnosticTail(finalMessage)}`);
86
106
  }
87
107
  }
88
108
 
@@ -92,6 +112,86 @@ export async function runCodexExecJson(options) {
92
112
  try {
93
113
  return JSON.parse(text);
94
114
  } catch (error) {
95
- throw new Error(`codex_exec_invalid_json:${error instanceof Error ? error.message : String(error)}\nbody:${text}`);
115
+ const stdout = diagnosticTail(result.stdout);
116
+ const stderr = diagnosticTail(result.stderr);
117
+ throw new Error(`codex_exec_invalid_json:${error instanceof Error ? error.message : String(error)}\nbody:${text}\nstdout:${stdout}\nstderr:${stderr}`);
118
+ }
119
+ }
120
+
121
+ export async function runCodexReviewJson({
122
+ cwd,
123
+ prompt,
124
+ outputPath,
125
+ model = DEFAULT_CODEX_MODEL,
126
+ timeoutMs = DEFAULT_CODEX_TIMEOUT_MS,
127
+ reasoningEffort = DEFAULT_CODEX_REASONING,
128
+ uncommitted = true,
129
+ outputSchema = null,
130
+ }) {
131
+ let schemaDir = null;
132
+ let schemaPath = outputSchema;
133
+ try {
134
+ if (!schemaPath) {
135
+ schemaDir = await mkdtemp(join(tmpdir(), 'loopx-code-review-schema-'));
136
+ schemaPath = join(schemaDir, 'schema.json');
137
+ await writeFile(schemaPath, JSON.stringify({
138
+ type: 'object',
139
+ additionalProperties: false,
140
+ required: ['status', 'verdict', 'summary', 'findings'],
141
+ properties: {
142
+ status: { enum: ['complete', 'skipped'] },
143
+ verdict: { enum: ['approve', 'request-changes'] },
144
+ summary: { type: 'string' },
145
+ rollbackTarget: {
146
+ anyOf: [
147
+ { enum: ['build', 'plan', 'clarify'] },
148
+ { type: 'null' },
149
+ ],
150
+ },
151
+ findings: {
152
+ type: 'array',
153
+ items: {
154
+ type: 'object',
155
+ additionalProperties: false,
156
+ required: ['severity', 'file', 'line', 'message'],
157
+ properties: {
158
+ severity: { enum: ['high', 'medium', 'low'] },
159
+ file: { anyOf: [{ type: 'string' }, { type: 'null' }] },
160
+ line: { anyOf: [{ type: 'number' }, { type: 'null' }] },
161
+ message: { type: 'string' },
162
+ },
163
+ },
164
+ },
165
+ },
166
+ }));
167
+ }
168
+ try {
169
+ return await runCodexExecJson({
170
+ cwd,
171
+ prompt,
172
+ outputPath,
173
+ model,
174
+ timeoutMs,
175
+ reasoningEffort,
176
+ outputSchema: schemaPath,
177
+ promptViaStdin: true,
178
+ extraArgs: uncommitted ? [] : [],
179
+ });
180
+ } catch (error) {
181
+ return await runCodexExecJson({
182
+ cwd,
183
+ prompt,
184
+ outputPath,
185
+ model,
186
+ timeoutMs,
187
+ reasoningEffort,
188
+ promptViaStdin: true,
189
+ extraArgs: uncommitted ? [] : [],
190
+ });
191
+ }
192
+ } finally {
193
+ if (schemaDir) {
194
+ await rm(schemaDir, { recursive: true, force: true });
195
+ }
96
196
  }
97
197
  }
@@ -0,0 +1,172 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+
5
+ import { inspectWorkspaceContext, resolveWorkspaceContextPaths } from './workspace-context.mjs';
6
+
7
+ export const CONTEXT_MANIFEST_SCHEMA_VERSION = 1;
8
+ const MAX_MANIFEST_ROWS = 80;
9
+
10
+ function normalizePath(cwd, path) {
11
+ const resolved = resolve(cwd, path);
12
+ const rel = relative(cwd, resolved);
13
+ return rel && !rel.startsWith('..') ? rel : resolved;
14
+ }
15
+
16
+ function normalizeReason(reason) {
17
+ if (Array.isArray(reason)) {
18
+ return reason.filter(Boolean).map(String).join(',');
19
+ }
20
+ return String(reason || 'context');
21
+ }
22
+
23
+ function row(cwd, { stage, kind, path, reason, priority, required = true }) {
24
+ const normalizedPath = normalizePath(cwd, path);
25
+ return {
26
+ schema_version: CONTEXT_MANIFEST_SCHEMA_VERSION,
27
+ stage,
28
+ kind,
29
+ path: normalizedPath,
30
+ reason: normalizeReason(reason),
31
+ priority,
32
+ required: Boolean(required),
33
+ exists: existsSync(resolve(cwd, normalizedPath)),
34
+ };
35
+ }
36
+
37
+ function stableRows(rows) {
38
+ const byPath = new Map();
39
+ for (const item of rows) {
40
+ const key = `${item.stage}:${item.kind}:${item.path}`;
41
+ const existing = byPath.get(key);
42
+ if (!existing) {
43
+ byPath.set(key, item);
44
+ continue;
45
+ }
46
+ const reasons = new Set([
47
+ ...existing.reason.split(',').map((value) => value.trim()).filter(Boolean),
48
+ ...item.reason.split(',').map((value) => value.trim()).filter(Boolean),
49
+ ]);
50
+ byPath.set(key, {
51
+ ...existing,
52
+ priority: Math.min(existing.priority, item.priority),
53
+ required: existing.required || item.required,
54
+ reason: [...reasons].sort().join(','),
55
+ });
56
+ }
57
+ return [...byPath.values()]
58
+ .sort((left, right) => (
59
+ left.priority - right.priority
60
+ || left.stage.localeCompare(right.stage)
61
+ || left.kind.localeCompare(right.kind)
62
+ || left.path.localeCompare(right.path)
63
+ ))
64
+ .slice(0, MAX_MANIFEST_ROWS);
65
+ }
66
+
67
+ export async function writeContextManifest(path, rows) {
68
+ const text = stableRows(rows).map((item) => JSON.stringify(item)).join('\n');
69
+ await mkdir(dirname(path), { recursive: true });
70
+ await writeFile(path, `${text}\n`);
71
+ }
72
+
73
+ export async function readContextManifest(path, options = {}) {
74
+ if (!existsSync(path)) {
75
+ return { status: 'fallback', rows: [], error: null };
76
+ }
77
+ try {
78
+ const text = await readFile(path, 'utf8');
79
+ const cwd = resolve(options.cwd || process.cwd());
80
+ const rows = text.trim()
81
+ ? text.trim().split('\n').map((line) => JSON.parse(line))
82
+ : [];
83
+ if (rows.length === 0) {
84
+ return { status: 'invalid', rows: [], error: 'empty_manifest' };
85
+ }
86
+ const valid = rows.every((item) => (
87
+ item
88
+ && item.schema_version === CONTEXT_MANIFEST_SCHEMA_VERSION
89
+ && typeof item.path === 'string'
90
+ && item.path.length > 0
91
+ && typeof item.kind === 'string'
92
+ && item.kind.length > 0
93
+ && typeof item.stage === 'string'
94
+ && item.stage.length > 0
95
+ && typeof item.reason === 'string'
96
+ && item.reason.length > 0
97
+ && typeof item.priority === 'number'
98
+ && Number.isFinite(item.priority)
99
+ && typeof item.required === 'boolean'
100
+ && typeof item.exists === 'boolean'
101
+ ));
102
+ if (!valid) {
103
+ return { status: 'invalid', rows: [], error: 'invalid_manifest_row' };
104
+ }
105
+ const missingRequired = rows.find((item) => item.required && (!item.exists || !existsSync(resolve(cwd, item.path))));
106
+ if (missingRequired) {
107
+ return { status: 'invalid', rows, error: `missing_required_context:${missingRequired.kind}` };
108
+ }
109
+ return { status: 'hit', rows, error: null };
110
+ } catch (error) {
111
+ return { status: 'invalid', rows: [], error: error instanceof Error ? error.message : String(error) };
112
+ }
113
+ }
114
+
115
+ export function buildContextManifestPath(root) {
116
+ return join(root, 'build-context.jsonl');
117
+ }
118
+
119
+ export function reviewContextManifestPath(root) {
120
+ return join(root, 'review-context.jsonl');
121
+ }
122
+
123
+ export async function generateBuildContextManifest({ cwd, root, state, slug }) {
124
+ const contextPaths = resolveWorkspaceContextPaths(cwd);
125
+ const contextSetup = await inspectWorkspaceContext(cwd);
126
+ const reviewReworkPath = state.review_rework_artifact_path || join(root, 'review-report.md');
127
+ const requiresReviewRework = state.last_confirmed_transition === 'review->build'
128
+ || (state.current_stage === 'review' && state.rollback_target === 'build');
129
+ const rows = [
130
+ row(cwd, { stage: 'build', kind: 'spec', path: join(root, 'spec.md'), reason: 'clarified_requirements', priority: 10 }),
131
+ row(cwd, { stage: 'build', kind: 'plan', path: join(root, 'plan.md'), reason: 'implementation_strategy', priority: 20 }),
132
+ row(cwd, { stage: 'build', kind: 'architecture', path: join(root, 'architecture.md'), reason: 'architecture_constraints', priority: 21 }),
133
+ row(cwd, { stage: 'build', kind: 'development-plan', path: join(root, 'development-plan.md'), reason: 'execution_steps', priority: 22 }),
134
+ row(cwd, { stage: 'build', kind: 'test-plan', path: join(root, 'test-plan.md'), reason: 'verification_strategy', priority: 23 }),
135
+ row(cwd, { stage: 'build', kind: 'prd', path: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `prd-${slug}.md`), reason: 'requirements', priority: 30 }),
136
+ row(cwd, { stage: 'build', kind: 'test-spec', path: state.test_spec_artifact_path || join(cwd, '.loopx', 'plans', `test-spec-${slug}.md`), reason: 'test_requirements', priority: 31 }),
137
+ row(cwd, { stage: 'build', kind: 'vertical-slices', path: state.change_artifact_paths?.slices || join(cwd, '.loopx', 'changes', 'active', state.change_id || `chg-${slug}`, 'slices.json'), reason: 'end_to_end_delivery_slices', priority: 32 }),
138
+ row(cwd, { stage: 'build', kind: 'review-rework', path: reviewReworkPath, reason: 'review_requested_implementation_fixes', priority: 33, required: requiresReviewRework }),
139
+ row(cwd, { stage: 'build', kind: 'domain-context', path: contextPaths.domainGlossary, reason: 'domain_vocabulary', priority: 34, required: contextSetup.status !== 'missing' }),
140
+ row(cwd, { stage: 'build', kind: 'agent-domain', path: contextPaths.agentDomain, reason: 'agent_context_rules', priority: 35, required: false }),
141
+ ];
142
+ const manifestPath = buildContextManifestPath(root);
143
+ await writeContextManifest(manifestPath, rows);
144
+ return { path: manifestPath, rows: stableRows(rows) };
145
+ }
146
+
147
+ export async function generateReviewContextManifest({ cwd, root, state, slug }) {
148
+ const contextPaths = resolveWorkspaceContextPaths(cwd);
149
+ const contextSetup = await inspectWorkspaceContext(cwd);
150
+ const rows = [
151
+ row(cwd, { stage: 'review', kind: 'execution-record', path: join(root, 'execution-record.md'), reason: 'execution_evidence', priority: 10 }),
152
+ row(cwd, { stage: 'review', kind: 'test-spec', path: state.test_spec_artifact_path || join(cwd, '.loopx', 'plans', `test-spec-${slug}.md`), reason: 'acceptance_tests', priority: 20 }),
153
+ row(cwd, { stage: 'review', kind: 'prd', path: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `prd-${slug}.md`), reason: 'requirements', priority: 21 }),
154
+ row(cwd, { stage: 'review', kind: 'vertical-slices', path: state.change_artifact_paths?.slices || join(cwd, '.loopx', 'changes', 'active', state.change_id || `chg-${slug}`, 'slices.json'), reason: 'slice_verification_contract', priority: 22 }),
155
+ row(cwd, { stage: 'review', kind: 'domain-context', path: contextPaths.domainGlossary, reason: 'terminology_and_boundary_review', priority: 23, required: contextSetup.status !== 'missing' }),
156
+ row(cwd, { stage: 'review', kind: 'changed-files', path: join(root, 'review-support', 'changed-files.json'), reason: 'changed_file_evidence', priority: 25, required: false }),
157
+ row(cwd, { stage: 'review', kind: 'residual-risks', path: join(root, 'execution-record.md'), reason: 'residual_risk_reference', priority: 26, required: false }),
158
+ row(cwd, { stage: 'review', kind: 'build-support', path: join(root, 'build-support'), reason: 'build_gate_evidence', priority: 30, required: false }),
159
+ row(cwd, { stage: 'review', kind: 'agent-domain', path: contextPaths.agentDomain, reason: 'agent_context_rules', priority: 31, required: false }),
160
+ row(cwd, { stage: 'review', kind: 'state', path: join(root, 'state.json'), reason: 'workflow_state', priority: 40 }),
161
+ ];
162
+ const manifestPath = reviewContextManifestPath(root);
163
+ await writeContextManifest(manifestPath, rows);
164
+ return { path: manifestPath, rows: stableRows(rows) };
165
+ }
166
+
167
+ export function manifestRowsToInputManifest(rows, fallback = []) {
168
+ if (!Array.isArray(rows) || rows.length === 0) {
169
+ return fallback;
170
+ }
171
+ return rows.map((item) => `${item.kind}:${item.path}:${item.reason}`);
172
+ }