@ai-content-space/loopx 0.1.0 → 0.1.2

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 (44) 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 +242 -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 +49 -12
  24. package/src/codex-exec-runtime.mjs +97 -0
  25. package/src/install-discovery.mjs +7 -7
  26. package/src/next-skill.mjs +33 -0
  27. package/src/plan-runtime.mjs +477 -0
  28. package/src/runtime-maintenance.mjs +36 -8
  29. package/src/workflow.mjs +831 -124
  30. package/templates/architecture.md +3 -3
  31. package/templates/development-plan.md +1 -1
  32. package/templates/execution-record.md +1 -1
  33. package/templates/plan.md +10 -4
  34. package/templates/review-report.md +1 -1
  35. package/templates/spec.md +38 -2
  36. package/templates/test-plan.md +1 -1
  37. package/plugins/loopx/skills/loopx-autopilot/SKILL.md +0 -30
  38. package/plugins/loopx/skills/loopx-build/SKILL.md +0 -25
  39. package/plugins/loopx/skills/loopx-clarify/SKILL.md +0 -25
  40. package/plugins/loopx/skills/loopx-plan/SKILL.md +0 -25
  41. package/skills/loopx-autopilot/SKILL.md +0 -30
  42. package/skills/loopx-build/SKILL.md +0 -25
  43. package/skills/loopx-clarify/SKILL.md +0 -25
  44. 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
@@ -2,16 +2,17 @@
2
2
 
3
3
  import { autopilotStage, approveStage, buildStage, clarifyStage, initWorkspace, planStage, reviewStage, statusSummary } from './workflow.mjs';
4
4
  import { installBundledSkills } from './install-discovery.mjs';
5
+ import { withNextSkill } from './next-skill.mjs';
5
6
  import { doctorRuntime, migrateLegacyRuntime } from './runtime-maintenance.mjs';
6
7
 
7
8
  function usage() {
8
9
  return [
9
10
  'Usage:',
10
11
  ' loopx init [--slug <slug>]',
11
- ' loopx clarify <slug>',
12
+ ' loopx clarify <slug> [--standard|--deep]',
12
13
  ' loopx approve <slug> --from <stage> --to <stage>',
13
- ' loopx plan <slug>',
14
- ' loopx build <slug>',
14
+ ' loopx plan [slug] [--direct <spec-path>] [--interactive] [--deliberate]',
15
+ ' loopx build <slug> [--no-deslop]',
15
16
  ' loopx review <slug> [--reviewer <name>]',
16
17
  ' loopx autopilot <slug> [--reviewer <name>]',
17
18
  ' loopx status [slug] [--json]',
@@ -46,7 +47,7 @@ function parseArgs(argv) {
46
47
 
47
48
  function printHumanStatus(status) {
48
49
  if (!status.initialized) {
49
- console.log('LoopX workspace is not initialized.');
50
+ console.log('loopx workspace is not initialized.');
50
51
  console.log(status.next_action);
51
52
  return;
52
53
  }
@@ -65,6 +66,35 @@ function printHumanStatus(status) {
65
66
  console.log(`contract: ${status.contract}`);
66
67
  console.log(`schema_version: ${status.schema_version}`);
67
68
  console.log(`stage: ${status.state?.current_stage ?? '(none)'}`);
69
+ if (status.state?.current_stage === 'clarify') {
70
+ console.log(`clarify_round: ${status.state.clarify_current_round}/${status.state.clarify_max_rounds}`);
71
+ console.log(`clarify_ambiguity_score: ${status.state.clarify_ambiguity_score}`);
72
+ console.log(`clarify_target_ambiguity_threshold: ${status.state.clarify_target_ambiguity_threshold}`);
73
+ 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}`);
74
+ }
75
+ if (status.state?.current_stage === 'plan') {
76
+ console.log(`plan_iteration: ${status.state.plan_current_iteration}/${status.state.plan_max_iterations}`);
77
+ console.log(`plan_consensus_mode: ${status.state.plan_consensus_mode}`);
78
+ console.log(`plan_deliberate_mode: ${status.state.plan_deliberate_mode}`);
79
+ console.log(`plan_architect_review_status: ${status.state.plan_architect_review_status}`);
80
+ console.log(`plan_critic_verdict: ${status.state.plan_critic_verdict}`);
81
+ console.log(`plan_docs_status: ${status.state.plan_docs_status}`);
82
+ console.log(`plan_blockers: ${Array.isArray(status.state.plan_blockers) && status.state.plan_blockers.length > 0 ? status.state.plan_blockers.join(', ') : '(none)'}`);
83
+ }
84
+ if (status.state?.current_stage === 'build') {
85
+ console.log(`build_iteration: ${status.state.build_current_iteration}/${status.state.build_max_iterations}`);
86
+ console.log(`build_parallel_mode: ${status.state.build_parallel_mode}`);
87
+ console.log(`build_verification_status: ${status.state.build_verification_status}`);
88
+ console.log(`build_architect_verification_status: ${status.state.build_architect_verification_status}`);
89
+ console.log(`build_deslop_status: ${status.state.build_deslop_status}`);
90
+ console.log(`build_regression_status: ${status.state.build_regression_status}`);
91
+ console.log(`build_blockers: ${Array.isArray(status.state.build_blockers) && status.state.build_blockers.length > 0 ? status.state.build_blockers.join(', ') : '(none)'}`);
92
+ }
93
+ if (status.state?.autopilot_current_phase && status.state.autopilot_current_phase !== 'none') {
94
+ console.log(`autopilot_current_phase: ${status.state.autopilot_current_phase}`);
95
+ console.log(`autopilot_completed: ${status.state.autopilot_completed}`);
96
+ console.log(`autopilot_blockers: ${Array.isArray(status.state.autopilot_blockers) && status.state.autopilot_blockers.length > 0 ? status.state.autopilot_blockers.join(', ') : '(none)'}`);
97
+ }
68
98
  console.log(`requested_transition: ${status.state?.requested_transition ?? 'none'}`);
69
99
  console.log(`last_confirmed_transition: ${status.state?.last_confirmed_transition ?? 'none'}`);
70
100
  console.log(`pending_user_decision: ${status.state?.pending_user_decision ?? 'none'}`);
@@ -87,8 +117,9 @@ async function main() {
87
117
  return;
88
118
  }
89
119
  case 'clarify': {
90
- const result = await clarifyStage(process.cwd(), positionals[0]);
91
- console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
120
+ const profile = options.get('--deep') ? 'deep' : 'standard';
121
+ const result = await clarifyStage(process.cwd(), positionals[0], { profile });
122
+ console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state }, result.state), null, 2));
92
123
  return;
93
124
  }
94
125
  case 'approve': {
@@ -96,24 +127,30 @@ async function main() {
96
127
  from: options.get('--from'),
97
128
  to: options.get('--to'),
98
129
  });
99
- console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
130
+ console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state }, result.state), null, 2));
100
131
  return;
101
132
  }
102
133
  case 'plan': {
103
- const result = await planStage(process.cwd(), positionals[0]);
104
- console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
134
+ const result = await planStage(process.cwd(), positionals[0], {
135
+ directSpecPath: options.get('--direct'),
136
+ interactive: Boolean(options.get('--interactive')),
137
+ deliberate: Boolean(options.get('--deliberate')),
138
+ });
139
+ console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state }, result.state), null, 2));
105
140
  return;
106
141
  }
107
142
  case 'build': {
108
- const result = await buildStage(process.cwd(), positionals[0]);
109
- console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
143
+ const result = await buildStage(process.cwd(), positionals[0], {
144
+ noDeslop: Boolean(options.get('--no-deslop')),
145
+ });
146
+ console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state }, result.state), null, 2));
110
147
  return;
111
148
  }
112
149
  case 'review': {
113
150
  const result = await reviewStage(process.cwd(), positionals[0], {
114
151
  reviewer: options.get('--reviewer') || 'independent-reviewer',
115
152
  });
116
- console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state, verdict: result.verdict }, null, 2));
153
+ console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state, verdict: result.verdict }, result.state), null, 2));
117
154
  return;
118
155
  }
119
156
  case 'autopilot': {
@@ -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
@@ -0,0 +1,33 @@
1
+ export function nextSkillCommand(state) {
2
+ if (!state || state.stage_status !== 'awaiting-approval' || !state.slug) {
3
+ return null;
4
+ }
5
+ if (state.current_stage === 'clarify') {
6
+ return `$plan ${state.slug}`;
7
+ }
8
+ if (state.current_stage === 'plan' && Array.isArray(state.plan_blockers) && state.plan_blockers.length === 0) {
9
+ return `$build ${state.slug}`;
10
+ }
11
+ if (state.current_stage === 'build' && Array.isArray(state.build_blockers) && state.build_blockers.length === 0) {
12
+ return `$review ${state.slug}`;
13
+ }
14
+ return null;
15
+ }
16
+
17
+ export function nextSkillHint(state) {
18
+ const command = nextSkillCommand(state);
19
+ if (!command) {
20
+ return null;
21
+ }
22
+ return `Next: ${command}`;
23
+ }
24
+
25
+ export function withNextSkill(payload, state) {
26
+ const nextCommand = nextSkillCommand(state);
27
+ const nextHint = nextSkillHint(state);
28
+ return {
29
+ ...payload,
30
+ next_skill_command: nextCommand,
31
+ next_skill_hint: nextHint,
32
+ };
33
+ }