@navels/neal 0.1.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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +527 -0
  3. package/SECURITY.md +91 -0
  4. package/config.yml +104 -0
  5. package/dist/neal/activity-footer.js +177 -0
  6. package/dist/neal/activity-reporting.js +1 -0
  7. package/dist/neal/adjudicator/artifacts.js +58 -0
  8. package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
  9. package/dist/neal/adjudicator/contracts.js +139 -0
  10. package/dist/neal/adjudicator/execute.js +611 -0
  11. package/dist/neal/adjudicator/final-completion.js +104 -0
  12. package/dist/neal/adjudicator/planning.js +145 -0
  13. package/dist/neal/adjudicator/specs.js +453 -0
  14. package/dist/neal/agents/prompts.js +120 -0
  15. package/dist/neal/agents/rounds.js +706 -0
  16. package/dist/neal/agents/schemas.js +832 -0
  17. package/dist/neal/agents/structured-coder.js +82 -0
  18. package/dist/neal/agents/structured-json.js +528 -0
  19. package/dist/neal/agents.js +4 -0
  20. package/dist/neal/atomic-write.js +18 -0
  21. package/dist/neal/blocked-guidance.js +406 -0
  22. package/dist/neal/cli.js +471 -0
  23. package/dist/neal/commands/check.js +401 -0
  24. package/dist/neal/commands/compat.js +807 -0
  25. package/dist/neal/commands/interactive-activity.js +57 -0
  26. package/dist/neal/commands/new-run.js +79 -0
  27. package/dist/neal/commands/plan-and-execute.js +44 -0
  28. package/dist/neal/commands/recovery-guidance.js +217 -0
  29. package/dist/neal/commands/resume-run.js +395 -0
  30. package/dist/neal/commands/review.js +21 -0
  31. package/dist/neal/commands/runtime.js +557 -0
  32. package/dist/neal/commands/setup.js +596 -0
  33. package/dist/neal/commands/squash.js +113 -0
  34. package/dist/neal/commands/status.js +33 -0
  35. package/dist/neal/commands/writer-exit-codes.js +42 -0
  36. package/dist/neal/commit-message.js +17 -0
  37. package/dist/neal/config.js +432 -0
  38. package/dist/neal/context/artifacts.js +140 -0
  39. package/dist/neal/context/context.js +324 -0
  40. package/dist/neal/context/inline-review-context.js +131 -0
  41. package/dist/neal/context/reviewer-context.js +166 -0
  42. package/dist/neal/context/shared.js +117 -0
  43. package/dist/neal/context/types.js +1 -0
  44. package/dist/neal/diagnostic.js +208 -0
  45. package/dist/neal/execute-finalization.js +5 -0
  46. package/dist/neal/final-completion-review.js +188 -0
  47. package/dist/neal/final-completion.js +229 -0
  48. package/dist/neal/git.js +339 -0
  49. package/dist/neal/index.js +135 -0
  50. package/dist/neal/interactive-controls.js +85 -0
  51. package/dist/neal/logger.js +102 -0
  52. package/dist/neal/manual-gates.js +121 -0
  53. package/dist/neal/orchestrator/artifacts.js +70 -0
  54. package/dist/neal/orchestrator/completion.js +531 -0
  55. package/dist/neal/orchestrator/failures.js +31 -0
  56. package/dist/neal/orchestrator/notifications.js +175 -0
  57. package/dist/neal/orchestrator/phases/coder.js +516 -0
  58. package/dist/neal/orchestrator/phases/planning.js +540 -0
  59. package/dist/neal/orchestrator/phases/recovery.js +798 -0
  60. package/dist/neal/orchestrator/phases/review.js +136 -0
  61. package/dist/neal/orchestrator/phases/shared.js +279 -0
  62. package/dist/neal/orchestrator/run-loop.js +113 -0
  63. package/dist/neal/orchestrator/split-plan.js +235 -0
  64. package/dist/neal/orchestrator/transitions.js +309 -0
  65. package/dist/neal/orchestrator.js +215 -0
  66. package/dist/neal/phase-display.js +27 -0
  67. package/dist/neal/plan-doc.js +154 -0
  68. package/dist/neal/plan-queue.js +1092 -0
  69. package/dist/neal/plan-refinement.js +39 -0
  70. package/dist/neal/plan-validation.js +525 -0
  71. package/dist/neal/progress.js +237 -0
  72. package/dist/neal/prompts/assert-builder.js +13 -0
  73. package/dist/neal/prompts/execute.js +290 -0
  74. package/dist/neal/prompts/guidance.js +70 -0
  75. package/dist/neal/prompts/planning.js +313 -0
  76. package/dist/neal/prompts/review-doctrine.js +142 -0
  77. package/dist/neal/prompts/shared.js +101 -0
  78. package/dist/neal/prompts/specialized.js +212 -0
  79. package/dist/neal/prompts/specs.js +572 -0
  80. package/dist/neal/providers/anthropic-claude.js +1599 -0
  81. package/dist/neal/providers/detection.js +139 -0
  82. package/dist/neal/providers/generic-agentic-tools.js +586 -0
  83. package/dist/neal/providers/generic-agentic.js +1238 -0
  84. package/dist/neal/providers/liveness.js +151 -0
  85. package/dist/neal/providers/openai-codex.js +1014 -0
  86. package/dist/neal/providers/openai-compatible.js +654 -0
  87. package/dist/neal/providers/registry.js +389 -0
  88. package/dist/neal/providers/telemetry.js +208 -0
  89. package/dist/neal/providers/types.js +21 -0
  90. package/dist/neal/recovery-artifacts.js +50 -0
  91. package/dist/neal/resume-decision.js +220 -0
  92. package/dist/neal/resume-planner.js +265 -0
  93. package/dist/neal/retrospective.js +391 -0
  94. package/dist/neal/review-debt.js +18 -0
  95. package/dist/neal/review-findings/artifacts.js +173 -0
  96. package/dist/neal/review-findings/prompts.js +172 -0
  97. package/dist/neal/review-findings/provider.js +330 -0
  98. package/dist/neal/review-findings/run.js +373 -0
  99. package/dist/neal/review-findings/types.js +1 -0
  100. package/dist/neal/review-mode.js +67 -0
  101. package/dist/neal/review.js +137 -0
  102. package/dist/neal/run-lock.js +334 -0
  103. package/dist/neal/run-metrics.js +355 -0
  104. package/dist/neal/run-narrative-types.js +1 -0
  105. package/dist/neal/run-narrative.js +1374 -0
  106. package/dist/neal/run-registry.js +218 -0
  107. package/dist/neal/run-status.js +25 -0
  108. package/dist/neal/scopes.js +451 -0
  109. package/dist/neal/sensitive-text.js +8 -0
  110. package/dist/neal/squash-message.js +379 -0
  111. package/dist/neal/squash.js +591 -0
  112. package/dist/neal/state-invariants.js +496 -0
  113. package/dist/neal/state-views.js +344 -0
  114. package/dist/neal/state.js +887 -0
  115. package/dist/neal/status-footer.js +258 -0
  116. package/dist/neal/status.js +1260 -0
  117. package/dist/neal/storage-paths.js +57 -0
  118. package/dist/neal/support.js +58 -0
  119. package/dist/neal/terminal-narrator.js +435 -0
  120. package/dist/neal/types.js +1 -0
  121. package/dist/neal/verification-events.js +81 -0
  122. package/dist/neal/version.js +37 -0
  123. package/dist/neal/worktree-status.js +137 -0
  124. package/dist/notifier.js +44 -0
  125. package/docs/ADJUDICATOR_INVENTORY.md +310 -0
  126. package/docs/PROMPT_SPECS.md +266 -0
  127. package/docs/README.md +22 -0
  128. package/docs/architecture.md +113 -0
  129. package/docs/assets/neal-execution-flow.png +0 -0
  130. package/docs/automation.md +65 -0
  131. package/docs/comparison.md +105 -0
  132. package/docs/compat.md +269 -0
  133. package/docs/compatible-models.md +135 -0
  134. package/docs/demo.md +55 -0
  135. package/docs/maintenance.md +64 -0
  136. package/docs/plan-format.md +213 -0
  137. package/docs/providers.md +751 -0
  138. package/docs/release.md +147 -0
  139. package/docs/state-machine.md +266 -0
  140. package/docs/storage.md +207 -0
  141. package/docs/troubleshooting.md +152 -0
  142. package/examples/compat/add-edit-verify/PLAN.md +29 -0
  143. package/examples/compat/add-edit-verify/broken.diff +8 -0
  144. package/examples/compat/add-edit-verify/good.diff +8 -0
  145. package/examples/compat/add-edit-verify/package.json +5 -0
  146. package/examples/compat/add-edit-verify/src/add.js +2 -0
  147. package/examples/compat/add-edit-verify/test/add.test.js +9 -0
  148. package/examples/compat/is-even-add-test/PLAN.md +30 -0
  149. package/examples/compat/is-even-add-test/broken.diff +11 -0
  150. package/examples/compat/is-even-add-test/good.diff +11 -0
  151. package/examples/compat/is-even-add-test/package.json +5 -0
  152. package/examples/compat/is-even-add-test/src/is-even.js +3 -0
  153. package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
  154. package/examples/compat/manifest.json +60 -0
  155. package/examples/compat/plan-greeting/ISSUE.md +25 -0
  156. package/examples/compat/plan-greeting/package.json +5 -0
  157. package/examples/compat/plan-greeting/src/greet.js +2 -0
  158. package/examples/compat/plan-greeting/test/greet.test.js +8 -0
  159. package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
  160. package/examples/compat/reverse-grep-edit/broken.diff +12 -0
  161. package/examples/compat/reverse-grep-edit/good.diff +12 -0
  162. package/examples/compat/reverse-grep-edit/package.json +5 -0
  163. package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
  164. package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
  165. package/examples/issue-triage-js/PLAN.md +83 -0
  166. package/examples/issue-triage-js/README.md +76 -0
  167. package/examples/issue-triage-js/package.json +9 -0
  168. package/examples/issue-triage-js/src/issue-triage.js +87 -0
  169. package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
  170. package/package.json +70 -0
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import process from 'node:process';
4
+ import { buildUsageLines, } from './cli.js';
5
+ import { runCheckCommand } from './commands/check.js';
6
+ import { runCompatCommand } from './commands/compat.js';
7
+ import { runNewRunCommand } from './commands/new-run.js';
8
+ import { runPlanAndExecuteCommand } from './commands/plan-and-execute.js';
9
+ import { runResumeRunCommand } from './commands/resume-run.js';
10
+ import { getCurrentRunLogger } from './commands/runtime.js';
11
+ import { runReviewCommand } from './commands/review.js';
12
+ import { runSetupCommand } from './commands/setup.js';
13
+ import { runSquashCommand } from './commands/squash.js';
14
+ import { runStatusCommand } from './commands/status.js';
15
+ import { formatWriterProviderSetupRequiredLines, isWriterProvidersNotConfiguredError, } from './config.js';
16
+ import { clearDiagnosticFooter, writeErrorDetail, writeNarrative } from './diagnostic.js';
17
+ import { CoderRoundError, ReviewerRoundError } from './agents.js';
18
+ import { getAppVersion } from './version.js';
19
+ function printUsage(lines) {
20
+ for (const line of lines) {
21
+ console.error(line);
22
+ }
23
+ }
24
+ function usage() {
25
+ printUsage(buildUsageLines(getAppVersion()));
26
+ process.exit(1);
27
+ }
28
+ function help(lines) {
29
+ for (const line of lines) {
30
+ console.log(line);
31
+ }
32
+ }
33
+ function getProcessArgs() {
34
+ const args = process.argv.slice(2);
35
+ if (args[0] === '--' && process.env.npm_lifecycle_event === 'start') {
36
+ return args.slice(1);
37
+ }
38
+ return args;
39
+ }
40
+ async function main() {
41
+ const args = getProcessArgs();
42
+ const version = getAppVersion();
43
+ if (args.length === 0) {
44
+ usage();
45
+ }
46
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
47
+ if (args.length === 1) {
48
+ help(buildUsageLines(version));
49
+ return;
50
+ }
51
+ throw new Error('Usage: neal help');
52
+ }
53
+ if (args[0] === 'version' || args[0] === '--version' || args[0] === '-V') {
54
+ if (args.length !== 1) {
55
+ throw new Error('Usage: neal version');
56
+ }
57
+ process.stdout.write(`${version}\n`);
58
+ return;
59
+ }
60
+ if (args[0] === 'status') {
61
+ await runStatusCommand(args);
62
+ return;
63
+ }
64
+ if (args[0] === 'review') {
65
+ await runReviewCommand(args);
66
+ return;
67
+ }
68
+ if (args[0] === 'setup') {
69
+ await runSetupCommand(args);
70
+ return;
71
+ }
72
+ if (args[0] === 'plan') {
73
+ await runNewRunCommand(args);
74
+ return;
75
+ }
76
+ if (args[0] === 'execute') {
77
+ await runNewRunCommand(args);
78
+ return;
79
+ }
80
+ if (args[0] === 'run') {
81
+ await runPlanAndExecuteCommand(args);
82
+ return;
83
+ }
84
+ if (args[0] === 'resume') {
85
+ await runResumeRunCommand(args);
86
+ return;
87
+ }
88
+ if (args[0] === 'squash') {
89
+ await runSquashCommand(args);
90
+ return;
91
+ }
92
+ if (args[0] === 'check') {
93
+ await runCheckCommand(args);
94
+ return;
95
+ }
96
+ if (args[0] === 'compat') {
97
+ await runCompatCommand(args);
98
+ return;
99
+ }
100
+ throw new Error(`Unknown command: ${args[0]}. Run \`neal help\` for supported commands.`);
101
+ }
102
+ void main().catch((error) => {
103
+ clearDiagnosticFooter();
104
+ const runLogger = getCurrentRunLogger();
105
+ const message = error instanceof Error ? error.message : String(error);
106
+ const coderSessionHandle = error instanceof CoderRoundError ? error.sessionHandle : null;
107
+ const reviewerSessionHandle = error instanceof ReviewerRoundError ? error.sessionHandle : null;
108
+ void runLogger?.event('run.failed', {
109
+ message,
110
+ coderSessionHandle,
111
+ reviewerSessionHandle,
112
+ reviewerSubtype: error instanceof ReviewerRoundError ? error.subtype : null,
113
+ });
114
+ if (error instanceof Error && error.stack) {
115
+ void runLogger?.stderr(`[fatal] ${error.stack}\n`);
116
+ }
117
+ else {
118
+ void runLogger?.stderr(`[fatal] ${message}\n`);
119
+ }
120
+ if (coderSessionHandle) {
121
+ writeErrorDetail(`[neal:detail] coder session: ${coderSessionHandle}\n`, runLogger, { role: 'coder' });
122
+ }
123
+ else if (reviewerSessionHandle) {
124
+ writeErrorDetail(`[neal:detail] reviewer session: ${reviewerSessionHandle}\n`, runLogger, { role: 'reviewer' });
125
+ }
126
+ if (isWriterProvidersNotConfiguredError(error)) {
127
+ writeNarrative(`${formatWriterProviderSetupRequiredLines(error, '[neal]').join('\n')}\n`);
128
+ }
129
+ else {
130
+ // Some thrown messages already carry the prefix (e.g. run-lock guidance);
131
+ // only prepend it when absent so every failure reports it exactly once.
132
+ writeNarrative(message.startsWith('[neal] ') ? `${message}\n` : `[neal] ${message}\n`);
133
+ }
134
+ process.exit(1);
135
+ });
@@ -0,0 +1,85 @@
1
+ import { existsSync } from 'node:fs';
2
+ import readline from 'node:readline';
3
+ import { clearDiagnosticFooter, getBufferedDetailSnapshot, isDiagnosticDetailVisible, showDiagnosticDetailView, showDiagnosticNarrativeView, writeNarrative, } from './diagnostic.js';
4
+ export function createInteractiveKeyController(options = {}) {
5
+ let stopRequested = false;
6
+ let cleanedUp = false;
7
+ const input = options.input ?? process.stdin;
8
+ const stopRequestFile = options.stopRequestFile ?? process.env.NEAL_STOP_AFTER_CURRENT_SCOPE_FILE;
9
+ const allowStopRequest = options.allowStopRequest ?? true;
10
+ if (!input.isTTY) {
11
+ return {
12
+ cleanup() { },
13
+ isStopRequested() {
14
+ return allowStopRequest && (stopRequested || hasStopRequestFile(stopRequestFile));
15
+ },
16
+ };
17
+ }
18
+ const emitKeypressEvents = options.emitKeypressEvents ?? readline.emitKeypressEvents;
19
+ const exit = options.exit ?? ((code) => process.exit(code));
20
+ emitKeypressEvents(input);
21
+ input.setRawMode?.(true);
22
+ input.resume();
23
+ const onKeypress = (_input, key) => {
24
+ if (key.ctrl && key.name === 'c') {
25
+ cleanup();
26
+ clearDiagnosticFooter();
27
+ exit(130);
28
+ return;
29
+ }
30
+ if (key.name === 'q' && allowStopRequest) {
31
+ stopRequested = !stopRequested;
32
+ writeNarrative(stopRequested
33
+ ? '\n[neal] stop requested after the current scope\n'
34
+ : '\n[neal] stop request cleared; continuing after the current scope\n');
35
+ return;
36
+ }
37
+ if (key.name === 'v') {
38
+ toggleDetailVisibility(options.getDetailFilter?.() ?? {});
39
+ }
40
+ };
41
+ input.on('keypress', onKeypress);
42
+ function cleanup() {
43
+ if (cleanedUp) {
44
+ return;
45
+ }
46
+ cleanedUp = true;
47
+ input.off('keypress', onKeypress);
48
+ if (input.isTTY) {
49
+ input.setRawMode?.(false);
50
+ }
51
+ input.pause();
52
+ }
53
+ return {
54
+ cleanup,
55
+ isStopRequested() {
56
+ return allowStopRequest && (stopRequested || hasStopRequestFile(stopRequestFile));
57
+ },
58
+ };
59
+ }
60
+ function hasStopRequestFile(stopRequestFile) {
61
+ return stopRequestFile ? existsSync(stopRequestFile) : false;
62
+ }
63
+ function toggleDetailVisibility(filter) {
64
+ if (isDiagnosticDetailVisible()) {
65
+ showDiagnosticNarrativeView();
66
+ return;
67
+ }
68
+ showDiagnosticDetailView(selectDetailReplayFilter(filter));
69
+ }
70
+ function selectDetailReplayFilter(filter) {
71
+ if (!filter.runId) {
72
+ return filter;
73
+ }
74
+ const preferredFilter = {
75
+ runId: filter.runId,
76
+ phase: filter.phase,
77
+ scopeLabel: filter.scopeLabel,
78
+ scopeNumber: filter.scopeNumber,
79
+ };
80
+ const preferredSnapshot = getBufferedDetailSnapshot(preferredFilter);
81
+ if (preferredSnapshot.entries.length > 0) {
82
+ return preferredFilter;
83
+ }
84
+ return { runId: filter.runId };
85
+ }
@@ -0,0 +1,102 @@
1
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { writeJsonAtomic } from './atomic-write.js';
5
+ import { RUNS_DIR_NAME } from './storage-paths.js';
6
+ import { getNealBuildMetadata } from './version.js';
7
+ export function createRunId() {
8
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
9
+ return `${timestamp}-${randomUUID().slice(0, 8)}`;
10
+ }
11
+ async function safeWrite(action) {
12
+ try {
13
+ await action();
14
+ }
15
+ catch {
16
+ // Best-effort diagnostics only.
17
+ }
18
+ }
19
+ async function readExistingMeta(path) {
20
+ try {
21
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
22
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function appendResumeMeta(meta, resume) {
29
+ const resumes = Array.isArray(meta.resumes) ? meta.resumes : [];
30
+ return {
31
+ ...meta,
32
+ resumes: [
33
+ ...resumes,
34
+ resume,
35
+ ],
36
+ };
37
+ }
38
+ export class RunLogger {
39
+ runDir;
40
+ eventsPath;
41
+ stderrPath;
42
+ metaPath;
43
+ constructor(runDir) {
44
+ this.runDir = runDir;
45
+ this.eventsPath = join(runDir, 'events.ndjson');
46
+ this.stderrPath = join(runDir, 'stderr.log');
47
+ this.metaPath = join(runDir, 'meta.json');
48
+ }
49
+ async init(init) {
50
+ await safeWrite(async () => {
51
+ await mkdir(this.runDir, { recursive: true });
52
+ const now = new Date().toISOString();
53
+ const build = await getNealBuildMetadata();
54
+ const resumeRecord = init.resumedFromStatePath
55
+ ? {
56
+ resumedAt: now,
57
+ resumedFromStatePath: init.resumedFromStatePath,
58
+ build,
59
+ }
60
+ : null;
61
+ if (resumeRecord) {
62
+ const existing = await readExistingMeta(this.metaPath);
63
+ if (existing) {
64
+ await writeJsonAtomic(this.metaPath, appendResumeMeta(existing, resumeRecord));
65
+ return;
66
+ }
67
+ }
68
+ await writeJsonAtomic(this.metaPath, {
69
+ version: 1,
70
+ createdAt: now,
71
+ cwd: init.cwd,
72
+ planDoc: init.planDoc,
73
+ planName: basename(init.planDoc),
74
+ topLevelMode: init.topLevelMode,
75
+ resumedFromStatePath: init.resumedFromStatePath ?? null,
76
+ runDir: this.runDir,
77
+ ...(resumeRecord ? { resumes: [resumeRecord] } : { build }),
78
+ });
79
+ });
80
+ }
81
+ async event(type, data) {
82
+ const payload = {
83
+ ts: new Date().toISOString(),
84
+ type,
85
+ data,
86
+ };
87
+ await safeWrite(async () => {
88
+ await appendFile(this.eventsPath, JSON.stringify(payload) + '\n', 'utf8');
89
+ });
90
+ }
91
+ async stderr(message) {
92
+ await safeWrite(async () => {
93
+ await appendFile(this.stderrPath, message, 'utf8');
94
+ });
95
+ }
96
+ }
97
+ export async function createRunLogger(init) {
98
+ const runDir = init.runDir ?? join(init.stateDir, RUNS_DIR_NAME, createRunId());
99
+ const logger = new RunLogger(runDir);
100
+ await logger.init(init);
101
+ return logger;
102
+ }
@@ -0,0 +1,121 @@
1
+ import { spawn } from 'node:child_process';
2
+ const DEFAULT_TIMEOUT_MS = 30_000;
3
+ const OUTPUT_TAIL_LIMIT = 4_000;
4
+ const REDACTED = '[redacted]';
5
+ export async function runManualGateResumeChecks(state) {
6
+ const gate = state.manualGate;
7
+ if (state.phase !== 'manual_gate' || gate === null) {
8
+ throw new Error('Cannot run manual gate resume checks without an active manual gate.');
9
+ }
10
+ const results = [];
11
+ for (const check of gate.resumeChecks) {
12
+ const result = await runManualGateResumeCheck(state, check);
13
+ results.push(result);
14
+ if (!result.ok) {
15
+ return {
16
+ ok: false,
17
+ failure: result,
18
+ results,
19
+ };
20
+ }
21
+ }
22
+ return {
23
+ ok: true,
24
+ results: results,
25
+ };
26
+ }
27
+ export function redactManualGateOutput(value, env = process.env) {
28
+ let redacted = value;
29
+ for (const [key, secret] of Object.entries(env)) {
30
+ if (!secret || !isSensitiveEnvKey(key)) {
31
+ continue;
32
+ }
33
+ redacted = redacted.split(secret).join(REDACTED);
34
+ }
35
+ return redacted;
36
+ }
37
+ function isSensitiveEnvKey(key) {
38
+ return /TOKEN|SECRET|PASSWORD|KEY/i.test(key);
39
+ }
40
+ async function runManualGateResumeCheck(state, check) {
41
+ const [command, ...args] = check.command;
42
+ if (!command) {
43
+ throw new Error(`Manual gate check ${check.name} has an empty command.`);
44
+ }
45
+ const timeoutMs = check.timeoutMs ?? DEFAULT_TIMEOUT_MS;
46
+ const cwd = check.cwd === 'run_dir' ? state.runDir : state.cwd;
47
+ const raw = await runCommand({
48
+ command,
49
+ args,
50
+ cwd,
51
+ timeoutMs,
52
+ });
53
+ const stdoutTail = boundedTail(redactManualGateOutput(raw.stdout));
54
+ const stderrTail = boundedTail(redactManualGateOutput(raw.stderr));
55
+ if (raw.exitCode === 0 && raw.signal === null) {
56
+ return {
57
+ ok: true,
58
+ checkName: check.name,
59
+ exitCode: 0,
60
+ signal: null,
61
+ stdoutTail,
62
+ stderrTail,
63
+ };
64
+ }
65
+ return {
66
+ ok: false,
67
+ checkName: check.name,
68
+ exitCode: raw.exitCode,
69
+ signal: raw.signal,
70
+ stdoutTail,
71
+ stderrTail,
72
+ };
73
+ }
74
+ function runCommand(args) {
75
+ return new Promise((resolve, reject) => {
76
+ const child = spawn(args.command, args.args, {
77
+ cwd: args.cwd,
78
+ shell: false,
79
+ env: process.env,
80
+ stdio: ['ignore', 'pipe', 'pipe'],
81
+ });
82
+ const stdout = [];
83
+ const stderr = [];
84
+ let settled = false;
85
+ let timedOut = false;
86
+ const timer = setTimeout(() => {
87
+ timedOut = true;
88
+ child.kill('SIGTERM');
89
+ }, args.timeoutMs);
90
+ child.stdout?.on('data', (chunk) => {
91
+ stdout.push(chunk);
92
+ });
93
+ child.stderr?.on('data', (chunk) => {
94
+ stderr.push(chunk);
95
+ });
96
+ child.once('error', (error) => {
97
+ if (settled) {
98
+ return;
99
+ }
100
+ settled = true;
101
+ clearTimeout(timer);
102
+ reject(error);
103
+ });
104
+ child.once('close', (exitCode, signal) => {
105
+ if (settled) {
106
+ return;
107
+ }
108
+ settled = true;
109
+ clearTimeout(timer);
110
+ resolve({
111
+ exitCode,
112
+ signal: signal ?? (timedOut ? 'SIGTERM' : null),
113
+ stdout: Buffer.concat(stdout).toString('utf8'),
114
+ stderr: Buffer.concat(stderr).toString('utf8'),
115
+ });
116
+ });
117
+ });
118
+ }
119
+ function boundedTail(value) {
120
+ return value.length <= OUTPUT_TAIL_LIMIT ? value : value.slice(value.length - OUTPUT_TAIL_LIMIT);
121
+ }
@@ -0,0 +1,70 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import { writeRecoveryMarkdown } from '../support.js';
4
+ import { getFinalCompletionReviewArtifactPath, writeFinalCompletionReviewMarkdown } from '../final-completion-review.js';
5
+ import { writePlanProgressArtifacts } from '../progress.js';
6
+ import { writeReviewMarkdown } from '../review.js';
7
+ import { writeRunNarrativeArtifacts } from '../run-narrative.js';
8
+ import { hasFinalCompletionReviewState } from '../state-views.js';
9
+ export async function writeExecutionArtifacts(state) {
10
+ await writeReviewMarkdown(state.reviewMarkdownPath, state);
11
+ await writeRecoveryMarkdown(state.recoveryMarkdownPath, state);
12
+ await writePlanProgressArtifacts(state);
13
+ if (hasFinalCompletionReviewState(state)) {
14
+ await writeFinalCompletionReviewMarkdown(getFinalCompletionReviewArtifactPath(state.runDir), state);
15
+ }
16
+ await writeRunNarrativeArtifacts(state);
17
+ }
18
+ export function validateManualGateId(id) {
19
+ const trimmed = id.trim();
20
+ if (!trimmed) {
21
+ throw new Error('Manual gate id must be non-empty.');
22
+ }
23
+ if (trimmed.includes('..') ||
24
+ trimmed.includes('/') ||
25
+ trimmed.includes('\\') ||
26
+ !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed)) {
27
+ throw new Error(`Manual gate id ${JSON.stringify(id)} cannot safely form a gate artifact filename.`);
28
+ }
29
+ return trimmed;
30
+ }
31
+ export function getManualGateArtifactPath(runDir, gateId) {
32
+ return join(runDir, `GATE-${validateManualGateId(gateId)}.md`);
33
+ }
34
+ export async function writeManualGateArtifact(args) {
35
+ const id = validateManualGateId(args.id);
36
+ const artifactPath = getManualGateArtifactPath(args.state.runDir, id);
37
+ const runId = basename(args.state.runDir);
38
+ const lines = [
39
+ `# ${args.title.trim()}`,
40
+ '',
41
+ '## Reason',
42
+ '',
43
+ args.reason.trim(),
44
+ '',
45
+ '## Instructions',
46
+ '',
47
+ args.instructionsMarkdown.trim(),
48
+ '',
49
+ '## Resume Checks',
50
+ '',
51
+ ...args.resumeChecks.flatMap((check) => renderResumeCheck(check)),
52
+ '## Resume',
53
+ '',
54
+ `Run \`neal resume --run ${runId}\` after the manual work is complete.`,
55
+ '',
56
+ 'Neal will resume the same `coder_scope` after these checks pass.',
57
+ '',
58
+ ];
59
+ await writeFile(artifactPath, `${lines.join('\n').trimEnd()}\n`, 'utf8');
60
+ return artifactPath;
61
+ }
62
+ function renderResumeCheck(check) {
63
+ return [
64
+ `- ${check.name}`,
65
+ ` - argv: \`${JSON.stringify(check.command)}\``,
66
+ ` - cwd: \`${check.cwd ?? 'repo'}\``,
67
+ ...(check.timeoutMs === undefined ? [] : [` - timeoutMs: \`${check.timeoutMs}\``]),
68
+ '',
69
+ ];
70
+ }