@ai-content-space/loopx 0.1.0 → 0.1.1

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 (43) hide show
  1. package/README.md +26 -26
  2. package/package.json +6 -2
  3. package/plugins/loopx/.codex-plugin/plugin.json +6 -6
  4. package/plugins/loopx/scripts/plugin-install.test.mjs +25 -8
  5. package/plugins/loopx/skills/autopilot/SKILL.md +90 -0
  6. package/plugins/loopx/skills/build/SKILL.md +118 -0
  7. package/plugins/loopx/skills/clarify/SKILL.md +219 -0
  8. package/plugins/loopx/skills/plan/SKILL.md +238 -0
  9. package/plugins/loopx/skills/{loopx-review → review}/SKILL.md +9 -4
  10. package/skills/ai-slop-cleaner/SKILL.md +114 -0
  11. package/skills/autopilot/SKILL.md +90 -0
  12. package/skills/autoresearch/SKILL.md +68 -0
  13. package/skills/build/SKILL.md +118 -0
  14. package/skills/clarify/SKILL.md +219 -0
  15. package/skills/deep-interview/SKILL.md +461 -0
  16. package/skills/deepsearch/SKILL.md +38 -0
  17. package/skills/plan/SKILL.md +238 -0
  18. package/skills/ralph/SKILL.md +271 -0
  19. package/skills/ralplan/SKILL.md +49 -0
  20. package/skills/{loopx-review → review}/SKILL.md +9 -4
  21. package/src/autopilot-runtime.mjs +152 -0
  22. package/src/build-runtime.mjs +146 -0
  23. package/src/cli.mjs +43 -7
  24. package/src/codex-exec-runtime.mjs +97 -0
  25. package/src/install-discovery.mjs +7 -7
  26. package/src/plan-runtime.mjs +456 -0
  27. package/src/runtime-maintenance.mjs +36 -8
  28. package/src/workflow.mjs +825 -123
  29. package/templates/architecture.md +3 -3
  30. package/templates/development-plan.md +1 -1
  31. package/templates/execution-record.md +1 -1
  32. package/templates/plan.md +4 -4
  33. package/templates/review-report.md +1 -1
  34. package/templates/spec.md +38 -2
  35. package/templates/test-plan.md +1 -1
  36. package/plugins/loopx/skills/loopx-autopilot/SKILL.md +0 -30
  37. package/plugins/loopx/skills/loopx-build/SKILL.md +0 -25
  38. package/plugins/loopx/skills/loopx-clarify/SKILL.md +0 -25
  39. package/plugins/loopx/skills/loopx-plan/SKILL.md +0 -25
  40. package/skills/loopx-autopilot/SKILL.md +0 -30
  41. package/skills/loopx-build/SKILL.md +0 -25
  42. package/skills/loopx-clarify/SKILL.md +0 -25
  43. package/skills/loopx-plan/SKILL.md +0 -25
@@ -0,0 +1,152 @@
1
+ import { join } from 'node:path';
2
+ import { mkdir } from 'node:fs/promises';
3
+
4
+ import { runCodexExecJson } from './codex-exec-runtime.mjs';
5
+
6
+ export const AUTOPILOT_PHASES = ['expansion', 'planning', 'execution', 'qa', 'validation'];
7
+
8
+ function phaseResult(phase, status, details = {}) {
9
+ return {
10
+ phase,
11
+ status,
12
+ ...details,
13
+ };
14
+ }
15
+
16
+ function normalizeStatus(value, fallback = 'complete') {
17
+ const normalized = String(value ?? fallback).trim().toLowerCase();
18
+ if (['complete', 'blocked', 'failed', 'skipped'].includes(normalized)) {
19
+ return normalized;
20
+ }
21
+ return fallback;
22
+ }
23
+
24
+ function scriptedEntry(script, phase) {
25
+ if (!script || typeof script !== 'object') {
26
+ return {};
27
+ }
28
+ return script[phase] || {};
29
+ }
30
+
31
+ export function createDefaultAutopilotAdapter() {
32
+ return createRealAutopilotAdapter();
33
+ }
34
+
35
+ export function createRealAutopilotAdapter({ model } = {}) {
36
+ return {
37
+ async expansion({ state }) {
38
+ if (state.unresolved_ambiguity_count === 0) {
39
+ return phaseResult('expansion', 'complete', {
40
+ source: state.spec_artifact_path ? 'workflow-spec' : 'generated-spec',
41
+ summary: 'Autopilot reused the existing loopx spec input.',
42
+ });
43
+ }
44
+
45
+ const outputPath = join(state.root || state.cwd || '.', 'autopilot', `${state.slug}-expansion.json`);
46
+ await mkdir(join(state.root || state.cwd || '.', 'autopilot'), { recursive: true });
47
+ const prompt = [
48
+ `You are acting as the real loopx autopilot expansion phase for workflow "${state.slug}".`,
49
+ 'Read the current spec.md template and rewrite it into a resolved loopx spec for this workflow.',
50
+ 'Also return only raw JSON with this shape:',
51
+ '{',
52
+ ' "status": "complete" | "blocked",',
53
+ ' "summary": string',
54
+ '}',
55
+ 'Required spec frontmatter updates:',
56
+ '- unresolved_ambiguity_count: 0',
57
+ '- current_round: at least 1',
58
+ '- ambiguity_score: <= target threshold',
59
+ '- non_goals_resolved: true',
60
+ '- decision_boundaries_resolved: true',
61
+ '- pressure_pass_complete: true',
62
+ 'Do not ask questions. Return JSON only, no markdown.',
63
+ ].join('\n');
64
+ const report = await runCodexExecJson({
65
+ cwd: state.cwd || process.cwd(),
66
+ prompt,
67
+ outputPath,
68
+ model,
69
+ });
70
+ return phaseResult('expansion', report.status, {
71
+ source: 'codex-exec',
72
+ summary: report.summary,
73
+ });
74
+ },
75
+ async planning({ planResult }) {
76
+ return phaseResult(
77
+ 'planning',
78
+ planResult.state.plan_critic_verdict === 'approve' ? 'complete' : 'blocked',
79
+ {
80
+ planIteration: planResult.state.plan_current_iteration,
81
+ criticVerdict: planResult.state.plan_critic_verdict,
82
+ },
83
+ );
84
+ },
85
+ async execution({ buildResult }) {
86
+ return phaseResult(
87
+ 'execution',
88
+ Array.isArray(buildResult.state.build_blockers) && buildResult.state.build_blockers.some((item) => item.startsWith('lane_incomplete_'))
89
+ ? 'blocked'
90
+ : 'complete',
91
+ {
92
+ buildIteration: buildResult.state.build_current_iteration,
93
+ laneCount: Array.isArray(buildResult.state.build_lane_statuses) ? buildResult.state.build_lane_statuses.length : 0,
94
+ },
95
+ );
96
+ },
97
+ async qa({ buildResult }) {
98
+ const ok = buildResult.state.build_verification_status === 'complete'
99
+ && buildResult.state.build_architect_verification_status === 'approve'
100
+ && ['complete', 'skipped'].includes(buildResult.state.build_deslop_status)
101
+ && ['complete', 'skipped'].includes(buildResult.state.build_regression_status)
102
+ && buildResult.state.execution_record_status === 'complete';
103
+ return phaseResult('qa', ok ? 'complete' : 'blocked', {
104
+ verificationStatus: buildResult.state.build_verification_status,
105
+ architectStatus: buildResult.state.build_architect_verification_status,
106
+ deslopStatus: buildResult.state.build_deslop_status,
107
+ regressionStatus: buildResult.state.build_regression_status,
108
+ });
109
+ },
110
+ async validation({ reviewResult }) {
111
+ return phaseResult(
112
+ 'validation',
113
+ reviewResult.verdict === 'APPROVE' ? 'complete' : 'blocked',
114
+ {
115
+ reviewVerdict: reviewResult.verdict,
116
+ rollbackTarget: reviewResult.rollbackTarget,
117
+ },
118
+ );
119
+ },
120
+ };
121
+ }
122
+
123
+ export function createScriptedAutopilotAdapter(script = {}) {
124
+ const defaults = createDefaultAutopilotAdapter();
125
+ return {
126
+ async expansion(context) {
127
+ const base = await defaults.expansion(context);
128
+ const override = scriptedEntry(script, 'expansion');
129
+ return { ...base, ...override, status: normalizeStatus(override.status, base.status) };
130
+ },
131
+ async planning(context) {
132
+ const base = await defaults.planning(context);
133
+ const override = scriptedEntry(script, 'planning');
134
+ return { ...base, ...override, status: normalizeStatus(override.status, base.status) };
135
+ },
136
+ async execution(context) {
137
+ const base = await defaults.execution(context);
138
+ const override = scriptedEntry(script, 'execution');
139
+ return { ...base, ...override, status: normalizeStatus(override.status, base.status) };
140
+ },
141
+ async qa(context) {
142
+ const base = await defaults.qa(context);
143
+ const override = scriptedEntry(script, 'qa');
144
+ return { ...base, ...override, status: normalizeStatus(override.status, base.status) };
145
+ },
146
+ async validation(context) {
147
+ const base = await defaults.validation(context);
148
+ const override = scriptedEntry(script, 'validation');
149
+ return { ...base, ...override, status: normalizeStatus(override.status, base.status) };
150
+ },
151
+ };
152
+ }
@@ -0,0 +1,146 @@
1
+ import { join } from 'node:path';
2
+ import { mkdir } from 'node:fs/promises';
3
+
4
+ import { runCodexExecJson } from './codex-exec-runtime.mjs';
5
+
6
+ const DEFAULT_BUILD_MAX_ITERATIONS = 10;
7
+ const DEFAULT_BUILD_LANES = ['execution', 'evidence', 'verification'];
8
+
9
+ function defaultLaneResult(name, iteration) {
10
+ return {
11
+ name,
12
+ status: 'complete',
13
+ summary: `${name} lane completed in iteration ${iteration}.`,
14
+ evidence: [
15
+ {
16
+ id: `${name}-${iteration}`,
17
+ kind: 'artifact',
18
+ summary: `${name} lane evidence`,
19
+ ref: `${name}-lane-${iteration}.md`,
20
+ },
21
+ ],
22
+ };
23
+ }
24
+
25
+ function scriptedValue(value, index, fallback) {
26
+ if (!Array.isArray(value) || value.length === 0) {
27
+ return fallback;
28
+ }
29
+ return value[Math.min(index, value.length - 1)];
30
+ }
31
+
32
+ function normalizeVerdict(raw, fallback = 'approve') {
33
+ const value = String(raw ?? fallback).trim().toLowerCase();
34
+ if (['approve', 'reject', 'iterate'].includes(value)) {
35
+ return value;
36
+ }
37
+ return fallback;
38
+ }
39
+
40
+ function normalizeStatus(raw, fallback = 'complete') {
41
+ const value = String(raw ?? fallback).trim().toLowerCase();
42
+ if (['complete', 'failed', 'pending', 'skipped'].includes(value)) {
43
+ return value;
44
+ }
45
+ return fallback;
46
+ }
47
+
48
+ function buildIterationData({ slug, iteration, noDeslop = false }, scriptEntry = {}) {
49
+ const laneNames = Array.isArray(scriptEntry.lanes) && scriptEntry.lanes.length > 0
50
+ ? scriptEntry.lanes.map((lane) => lane.name)
51
+ : DEFAULT_BUILD_LANES;
52
+ const lanes = laneNames.map((name) => {
53
+ const scriptedLane = Array.isArray(scriptEntry.lanes)
54
+ ? scriptEntry.lanes.find((lane) => lane.name === name)
55
+ : null;
56
+ return {
57
+ ...defaultLaneResult(name, iteration),
58
+ ...(scriptedLane || {}),
59
+ status: normalizeStatus(scriptedLane?.status, 'complete'),
60
+ };
61
+ });
62
+ const verificationStatus = normalizeStatus(scriptEntry.verificationStatus, 'complete');
63
+ const architectVerdict = normalizeVerdict(scriptEntry.architectVerdict, 'approve');
64
+ const deslopStatus = noDeslop ? 'skipped' : normalizeStatus(scriptEntry.deslopStatus, 'complete');
65
+ const regressionStatus = noDeslop ? 'skipped' : normalizeStatus(scriptEntry.regressionStatus, 'complete');
66
+
67
+ return {
68
+ slug,
69
+ iteration,
70
+ runId: `${slug}-build-run-${iteration}`,
71
+ actorId: `${slug}-builder-1`,
72
+ lanes,
73
+ verificationStatus,
74
+ architectVerdict,
75
+ deslopStatus,
76
+ regressionStatus,
77
+ executionEvidence: [
78
+ `lane_count=${lanes.length}`,
79
+ ...lanes.map((lane) => `${lane.name}:${lane.status}`),
80
+ ],
81
+ verificationEvidence: scriptEntry.verificationEvidence || [
82
+ `verification=${verificationStatus}`,
83
+ `architect=${architectVerdict}`,
84
+ `deslop=${deslopStatus}`,
85
+ `regression=${regressionStatus}`,
86
+ ],
87
+ architectFindings: scriptEntry.architectFindings || (
88
+ architectVerdict === 'approve'
89
+ ? ['Architect gate approved the build iteration.']
90
+ : ['Architect gate rejected the build iteration.']
91
+ ),
92
+ limitations: scriptEntry.limitations || (architectVerdict === 'approve' ? ['none'] : ['Architect review did not approve this iteration.']),
93
+ };
94
+ }
95
+
96
+ export function createDefaultBuildAdapter() {
97
+ return createRealBuildAdapter();
98
+ }
99
+
100
+ export function createRealBuildAdapter({ model } = {}) {
101
+ return {
102
+ maxIterations: DEFAULT_BUILD_MAX_ITERATIONS,
103
+ async executeLanes(context) {
104
+ const outputPath = join(context.root || context.cwd, 'build-support', `runtime-build-iteration-${context.iteration}.json`);
105
+ await mkdir(join(context.root || context.cwd, 'build-support'), { recursive: true });
106
+ const prompt = [
107
+ `You are acting as the real loopx build runtime for workflow "${context.slug}".`,
108
+ 'Execute the approved build work in this repository and return only raw JSON with this shape:',
109
+ '{',
110
+ ' "lanes": [{"name": string, "status": "complete" | "failed" | "pending" | "skipped", "summary": string, "evidence": [{"id": string, "kind": string, "summary": string, "ref": string}]}],',
111
+ ' "verificationStatus": "complete" | "failed" | "pending" | "skipped",',
112
+ ' "architectVerdict": "approve" | "reject" | "iterate",',
113
+ ' "deslopStatus": "complete" | "failed" | "pending" | "skipped",',
114
+ ' "regressionStatus": "complete" | "failed" | "pending" | "skipped",',
115
+ ' "executionEvidence": string[],',
116
+ ' "verificationEvidence": string[],',
117
+ ' "architectFindings": string[],',
118
+ ' "limitations": string[]',
119
+ '}',
120
+ `noDeslop: ${Boolean(context.noDeslop)}`,
121
+ `planArtifactPath: ${context.planArtifactPath}`,
122
+ `testSpecArtifactPath: ${context.testSpecArtifactPath}`,
123
+ 'Do not ask questions. Real code edits are allowed. Keep execution-record.md as the sole canonical execution artifact; return JSON only, no markdown.',
124
+ ].join('\n');
125
+ const report = await runCodexExecJson({
126
+ cwd: context.cwd,
127
+ prompt,
128
+ outputPath,
129
+ model,
130
+ });
131
+ return buildIterationData(context, report);
132
+ },
133
+ };
134
+ }
135
+
136
+ export function createScriptedBuildAdapter(script = {}) {
137
+ return {
138
+ maxIterations: script.maxIterations || DEFAULT_BUILD_MAX_ITERATIONS,
139
+ async executeLanes(context) {
140
+ const entry = scriptedValue(script.iterations, context.iteration - 1, {});
141
+ return buildIterationData(context, entry || {});
142
+ },
143
+ };
144
+ }
145
+
146
+ export { DEFAULT_BUILD_MAX_ITERATIONS };
package/src/cli.mjs CHANGED
@@ -8,10 +8,10 @@ function usage() {
8
8
  return [
9
9
  'Usage:',
10
10
  ' loopx init [--slug <slug>]',
11
- ' loopx clarify <slug>',
11
+ ' loopx clarify <slug> [--standard|--deep]',
12
12
  ' loopx approve <slug> --from <stage> --to <stage>',
13
- ' loopx plan <slug>',
14
- ' loopx build <slug>',
13
+ ' loopx plan [slug] [--direct <spec-path>] [--interactive] [--deliberate]',
14
+ ' loopx build <slug> [--no-deslop]',
15
15
  ' loopx review <slug> [--reviewer <name>]',
16
16
  ' loopx autopilot <slug> [--reviewer <name>]',
17
17
  ' loopx status [slug] [--json]',
@@ -46,7 +46,7 @@ function parseArgs(argv) {
46
46
 
47
47
  function printHumanStatus(status) {
48
48
  if (!status.initialized) {
49
- console.log('LoopX workspace is not initialized.');
49
+ console.log('loopx workspace is not initialized.');
50
50
  console.log(status.next_action);
51
51
  return;
52
52
  }
@@ -65,6 +65,35 @@ function printHumanStatus(status) {
65
65
  console.log(`contract: ${status.contract}`);
66
66
  console.log(`schema_version: ${status.schema_version}`);
67
67
  console.log(`stage: ${status.state?.current_stage ?? '(none)'}`);
68
+ if (status.state?.current_stage === 'clarify') {
69
+ console.log(`clarify_round: ${status.state.clarify_current_round}/${status.state.clarify_max_rounds}`);
70
+ console.log(`clarify_ambiguity_score: ${status.state.clarify_ambiguity_score}`);
71
+ console.log(`clarify_target_ambiguity_threshold: ${status.state.clarify_target_ambiguity_threshold}`);
72
+ console.log(`clarify_gates: non_goals=${status.state.clarify_non_goals_resolved} decision_boundaries=${status.state.clarify_decision_boundaries_resolved} pressure_pass=${status.state.clarify_pressure_pass_complete}`);
73
+ }
74
+ if (status.state?.current_stage === 'plan') {
75
+ console.log(`plan_iteration: ${status.state.plan_current_iteration}/${status.state.plan_max_iterations}`);
76
+ console.log(`plan_consensus_mode: ${status.state.plan_consensus_mode}`);
77
+ console.log(`plan_deliberate_mode: ${status.state.plan_deliberate_mode}`);
78
+ console.log(`plan_architect_review_status: ${status.state.plan_architect_review_status}`);
79
+ console.log(`plan_critic_verdict: ${status.state.plan_critic_verdict}`);
80
+ console.log(`plan_docs_status: ${status.state.plan_docs_status}`);
81
+ console.log(`plan_blockers: ${Array.isArray(status.state.plan_blockers) && status.state.plan_blockers.length > 0 ? status.state.plan_blockers.join(', ') : '(none)'}`);
82
+ }
83
+ if (status.state?.current_stage === 'build') {
84
+ console.log(`build_iteration: ${status.state.build_current_iteration}/${status.state.build_max_iterations}`);
85
+ console.log(`build_parallel_mode: ${status.state.build_parallel_mode}`);
86
+ console.log(`build_verification_status: ${status.state.build_verification_status}`);
87
+ console.log(`build_architect_verification_status: ${status.state.build_architect_verification_status}`);
88
+ console.log(`build_deslop_status: ${status.state.build_deslop_status}`);
89
+ console.log(`build_regression_status: ${status.state.build_regression_status}`);
90
+ console.log(`build_blockers: ${Array.isArray(status.state.build_blockers) && status.state.build_blockers.length > 0 ? status.state.build_blockers.join(', ') : '(none)'}`);
91
+ }
92
+ if (status.state?.autopilot_current_phase && status.state.autopilot_current_phase !== 'none') {
93
+ console.log(`autopilot_current_phase: ${status.state.autopilot_current_phase}`);
94
+ console.log(`autopilot_completed: ${status.state.autopilot_completed}`);
95
+ console.log(`autopilot_blockers: ${Array.isArray(status.state.autopilot_blockers) && status.state.autopilot_blockers.length > 0 ? status.state.autopilot_blockers.join(', ') : '(none)'}`);
96
+ }
68
97
  console.log(`requested_transition: ${status.state?.requested_transition ?? 'none'}`);
69
98
  console.log(`last_confirmed_transition: ${status.state?.last_confirmed_transition ?? 'none'}`);
70
99
  console.log(`pending_user_decision: ${status.state?.pending_user_decision ?? 'none'}`);
@@ -87,7 +116,8 @@ async function main() {
87
116
  return;
88
117
  }
89
118
  case 'clarify': {
90
- const result = await clarifyStage(process.cwd(), positionals[0]);
119
+ const profile = options.get('--deep') ? 'deep' : 'standard';
120
+ const result = await clarifyStage(process.cwd(), positionals[0], { profile });
91
121
  console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
92
122
  return;
93
123
  }
@@ -100,12 +130,18 @@ async function main() {
100
130
  return;
101
131
  }
102
132
  case 'plan': {
103
- const result = await planStage(process.cwd(), positionals[0]);
133
+ const result = await planStage(process.cwd(), positionals[0], {
134
+ directSpecPath: options.get('--direct'),
135
+ interactive: Boolean(options.get('--interactive')),
136
+ deliberate: Boolean(options.get('--deliberate')),
137
+ });
104
138
  console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
105
139
  return;
106
140
  }
107
141
  case 'build': {
108
- const result = await buildStage(process.cwd(), positionals[0]);
142
+ const result = await buildStage(process.cwd(), positionals[0], {
143
+ noDeslop: Boolean(options.get('--no-deslop')),
144
+ });
109
145
  console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
110
146
  return;
111
147
  }
@@ -0,0 +1,97 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ const DEFAULT_CODEX_MODEL = 'gpt-5.4';
5
+ const DEFAULT_CODEX_TIMEOUT_MS = 120000;
6
+ const DEFAULT_CODEX_REASONING = 'low';
7
+
8
+ function codexBinary() {
9
+ return process.env.LOOPX_CODEX_BIN || 'codex';
10
+ }
11
+
12
+ export async function readJsonArtifact(path) {
13
+ const text = await readFile(path, 'utf8');
14
+ return JSON.parse(text);
15
+ }
16
+
17
+ export async function runCodexExec({
18
+ cwd,
19
+ prompt,
20
+ outputPath,
21
+ model = DEFAULT_CODEX_MODEL,
22
+ extraArgs = [],
23
+ timeoutMs = DEFAULT_CODEX_TIMEOUT_MS,
24
+ reasoningEffort = DEFAULT_CODEX_REASONING,
25
+ }) {
26
+ const args = [
27
+ 'exec',
28
+ '--dangerously-bypass-approvals-and-sandbox',
29
+ '-C',
30
+ cwd,
31
+ '-m',
32
+ model,
33
+ '-c',
34
+ `model_reasoning_effort=\"${reasoningEffort}\"`,
35
+ '-o',
36
+ outputPath,
37
+ ...extraArgs,
38
+ prompt,
39
+ ];
40
+
41
+ const child = spawn(codexBinary(), args, {
42
+ cwd,
43
+ env: {
44
+ ...process.env,
45
+ CODEX_DISABLE_UPDATE: '1',
46
+ },
47
+ stdio: ['pipe', 'pipe', 'pipe'],
48
+ });
49
+
50
+ child.stdin.end();
51
+
52
+ const stdoutChunks = [];
53
+ const stderrChunks = [];
54
+ child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
55
+ child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
56
+
57
+ const timeout = setTimeout(() => {
58
+ child.kill('SIGTERM');
59
+ }, timeoutMs);
60
+
61
+ try {
62
+ const exitCode = await new Promise((resolve, reject) => {
63
+ child.on('error', reject);
64
+ child.on('close', resolve);
65
+ });
66
+ clearTimeout(timeout);
67
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8');
68
+ const stderr = Buffer.concat(stderrChunks).toString('utf8');
69
+ const finalMessage = existsSync(outputPath) ? await readFile(outputPath, 'utf8') : '';
70
+ if (exitCode !== 0) {
71
+ throw new Error(`exit_${exitCode}`);
72
+ }
73
+ return {
74
+ ok: true,
75
+ finalMessage,
76
+ stdout,
77
+ stderr,
78
+ };
79
+ } catch (error) {
80
+ clearTimeout(timeout);
81
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8');
82
+ const stderr = Buffer.concat(stderrChunks).toString('utf8');
83
+ const finalMessage = existsSync(outputPath) ? await readFile(outputPath, 'utf8') : '';
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ throw new Error(`codex_exec_failed:${message}\nstdout:${stdout}\nstderr:${stderr}\nfinal:${finalMessage}`);
86
+ }
87
+ }
88
+
89
+ export async function runCodexExecJson(options) {
90
+ const result = await runCodexExec(options);
91
+ const text = result.finalMessage.trim();
92
+ try {
93
+ return JSON.parse(text);
94
+ } catch (error) {
95
+ throw new Error(`codex_exec_invalid_json:${error instanceof Error ? error.message : String(error)}\nbody:${text}`);
96
+ }
97
+ }
@@ -7,11 +7,11 @@ import { fileURLToPath } from 'node:url';
7
7
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
8
8
  const PROJECT_ROOT = resolve(MODULE_DIR, '..');
9
9
  const LOOPX_SKILLS = [
10
- 'loopx-clarify',
11
- 'loopx-plan',
12
- 'loopx-build',
13
- 'loopx-review',
14
- 'loopx-autopilot',
10
+ 'clarify',
11
+ 'plan',
12
+ 'build',
13
+ 'review',
14
+ 'autopilot',
15
15
  ];
16
16
  const LOOPX_INSTALLATION_IDENTITY = 'loopx';
17
17
 
@@ -150,7 +150,7 @@ async function materializeSkill(skillName, env = process.env, options = {}) {
150
150
 
151
151
  function buildRegistryRow(record, env = process.env, options = {}) {
152
152
  return {
153
- source: 'LoopX',
153
+ source: 'loopx',
154
154
  sourceType: 'local',
155
155
  installationIdentity: options.installationIdentity,
156
156
  distributionChannel: options.distributionChannel,
@@ -173,7 +173,7 @@ function buildRegistryRow(record, env = process.env, options = {}) {
173
173
  function isLoopxOwnedIdentity(skillName, row, env = process.env) {
174
174
  return Boolean(
175
175
  row
176
- && row.source === 'LoopX'
176
+ && row.source === 'loopx'
177
177
  && row.sourceType === 'local'
178
178
  && (
179
179
  row.installationIdentity === LOOPX_INSTALLATION_IDENTITY