@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.
- package/LICENSE +21 -0
- package/README.md +527 -0
- package/SECURITY.md +91 -0
- package/config.yml +104 -0
- package/dist/neal/activity-footer.js +177 -0
- package/dist/neal/activity-reporting.js +1 -0
- package/dist/neal/adjudicator/artifacts.js +58 -0
- package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
- package/dist/neal/adjudicator/contracts.js +139 -0
- package/dist/neal/adjudicator/execute.js +611 -0
- package/dist/neal/adjudicator/final-completion.js +104 -0
- package/dist/neal/adjudicator/planning.js +145 -0
- package/dist/neal/adjudicator/specs.js +453 -0
- package/dist/neal/agents/prompts.js +120 -0
- package/dist/neal/agents/rounds.js +706 -0
- package/dist/neal/agents/schemas.js +832 -0
- package/dist/neal/agents/structured-coder.js +82 -0
- package/dist/neal/agents/structured-json.js +528 -0
- package/dist/neal/agents.js +4 -0
- package/dist/neal/atomic-write.js +18 -0
- package/dist/neal/blocked-guidance.js +406 -0
- package/dist/neal/cli.js +471 -0
- package/dist/neal/commands/check.js +401 -0
- package/dist/neal/commands/compat.js +807 -0
- package/dist/neal/commands/interactive-activity.js +57 -0
- package/dist/neal/commands/new-run.js +79 -0
- package/dist/neal/commands/plan-and-execute.js +44 -0
- package/dist/neal/commands/recovery-guidance.js +217 -0
- package/dist/neal/commands/resume-run.js +395 -0
- package/dist/neal/commands/review.js +21 -0
- package/dist/neal/commands/runtime.js +557 -0
- package/dist/neal/commands/setup.js +596 -0
- package/dist/neal/commands/squash.js +113 -0
- package/dist/neal/commands/status.js +33 -0
- package/dist/neal/commands/writer-exit-codes.js +42 -0
- package/dist/neal/commit-message.js +17 -0
- package/dist/neal/config.js +432 -0
- package/dist/neal/context/artifacts.js +140 -0
- package/dist/neal/context/context.js +324 -0
- package/dist/neal/context/inline-review-context.js +131 -0
- package/dist/neal/context/reviewer-context.js +166 -0
- package/dist/neal/context/shared.js +117 -0
- package/dist/neal/context/types.js +1 -0
- package/dist/neal/diagnostic.js +208 -0
- package/dist/neal/execute-finalization.js +5 -0
- package/dist/neal/final-completion-review.js +188 -0
- package/dist/neal/final-completion.js +229 -0
- package/dist/neal/git.js +339 -0
- package/dist/neal/index.js +135 -0
- package/dist/neal/interactive-controls.js +85 -0
- package/dist/neal/logger.js +102 -0
- package/dist/neal/manual-gates.js +121 -0
- package/dist/neal/orchestrator/artifacts.js +70 -0
- package/dist/neal/orchestrator/completion.js +531 -0
- package/dist/neal/orchestrator/failures.js +31 -0
- package/dist/neal/orchestrator/notifications.js +175 -0
- package/dist/neal/orchestrator/phases/coder.js +516 -0
- package/dist/neal/orchestrator/phases/planning.js +540 -0
- package/dist/neal/orchestrator/phases/recovery.js +798 -0
- package/dist/neal/orchestrator/phases/review.js +136 -0
- package/dist/neal/orchestrator/phases/shared.js +279 -0
- package/dist/neal/orchestrator/run-loop.js +113 -0
- package/dist/neal/orchestrator/split-plan.js +235 -0
- package/dist/neal/orchestrator/transitions.js +309 -0
- package/dist/neal/orchestrator.js +215 -0
- package/dist/neal/phase-display.js +27 -0
- package/dist/neal/plan-doc.js +154 -0
- package/dist/neal/plan-queue.js +1092 -0
- package/dist/neal/plan-refinement.js +39 -0
- package/dist/neal/plan-validation.js +525 -0
- package/dist/neal/progress.js +237 -0
- package/dist/neal/prompts/assert-builder.js +13 -0
- package/dist/neal/prompts/execute.js +290 -0
- package/dist/neal/prompts/guidance.js +70 -0
- package/dist/neal/prompts/planning.js +313 -0
- package/dist/neal/prompts/review-doctrine.js +142 -0
- package/dist/neal/prompts/shared.js +101 -0
- package/dist/neal/prompts/specialized.js +212 -0
- package/dist/neal/prompts/specs.js +572 -0
- package/dist/neal/providers/anthropic-claude.js +1599 -0
- package/dist/neal/providers/detection.js +139 -0
- package/dist/neal/providers/generic-agentic-tools.js +586 -0
- package/dist/neal/providers/generic-agentic.js +1238 -0
- package/dist/neal/providers/liveness.js +151 -0
- package/dist/neal/providers/openai-codex.js +1014 -0
- package/dist/neal/providers/openai-compatible.js +654 -0
- package/dist/neal/providers/registry.js +389 -0
- package/dist/neal/providers/telemetry.js +208 -0
- package/dist/neal/providers/types.js +21 -0
- package/dist/neal/recovery-artifacts.js +50 -0
- package/dist/neal/resume-decision.js +220 -0
- package/dist/neal/resume-planner.js +265 -0
- package/dist/neal/retrospective.js +391 -0
- package/dist/neal/review-debt.js +18 -0
- package/dist/neal/review-findings/artifacts.js +173 -0
- package/dist/neal/review-findings/prompts.js +172 -0
- package/dist/neal/review-findings/provider.js +330 -0
- package/dist/neal/review-findings/run.js +373 -0
- package/dist/neal/review-findings/types.js +1 -0
- package/dist/neal/review-mode.js +67 -0
- package/dist/neal/review.js +137 -0
- package/dist/neal/run-lock.js +334 -0
- package/dist/neal/run-metrics.js +355 -0
- package/dist/neal/run-narrative-types.js +1 -0
- package/dist/neal/run-narrative.js +1374 -0
- package/dist/neal/run-registry.js +218 -0
- package/dist/neal/run-status.js +25 -0
- package/dist/neal/scopes.js +451 -0
- package/dist/neal/sensitive-text.js +8 -0
- package/dist/neal/squash-message.js +379 -0
- package/dist/neal/squash.js +591 -0
- package/dist/neal/state-invariants.js +496 -0
- package/dist/neal/state-views.js +344 -0
- package/dist/neal/state.js +887 -0
- package/dist/neal/status-footer.js +258 -0
- package/dist/neal/status.js +1260 -0
- package/dist/neal/storage-paths.js +57 -0
- package/dist/neal/support.js +58 -0
- package/dist/neal/terminal-narrator.js +435 -0
- package/dist/neal/types.js +1 -0
- package/dist/neal/verification-events.js +81 -0
- package/dist/neal/version.js +37 -0
- package/dist/neal/worktree-status.js +137 -0
- package/dist/notifier.js +44 -0
- package/docs/ADJUDICATOR_INVENTORY.md +310 -0
- package/docs/PROMPT_SPECS.md +266 -0
- package/docs/README.md +22 -0
- package/docs/architecture.md +113 -0
- package/docs/assets/neal-execution-flow.png +0 -0
- package/docs/automation.md +65 -0
- package/docs/comparison.md +105 -0
- package/docs/compat.md +269 -0
- package/docs/compatible-models.md +135 -0
- package/docs/demo.md +55 -0
- package/docs/maintenance.md +64 -0
- package/docs/plan-format.md +213 -0
- package/docs/providers.md +751 -0
- package/docs/release.md +147 -0
- package/docs/state-machine.md +266 -0
- package/docs/storage.md +207 -0
- package/docs/troubleshooting.md +152 -0
- package/examples/compat/add-edit-verify/PLAN.md +29 -0
- package/examples/compat/add-edit-verify/broken.diff +8 -0
- package/examples/compat/add-edit-verify/good.diff +8 -0
- package/examples/compat/add-edit-verify/package.json +5 -0
- package/examples/compat/add-edit-verify/src/add.js +2 -0
- package/examples/compat/add-edit-verify/test/add.test.js +9 -0
- package/examples/compat/is-even-add-test/PLAN.md +30 -0
- package/examples/compat/is-even-add-test/broken.diff +11 -0
- package/examples/compat/is-even-add-test/good.diff +11 -0
- package/examples/compat/is-even-add-test/package.json +5 -0
- package/examples/compat/is-even-add-test/src/is-even.js +3 -0
- package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
- package/examples/compat/manifest.json +60 -0
- package/examples/compat/plan-greeting/ISSUE.md +25 -0
- package/examples/compat/plan-greeting/package.json +5 -0
- package/examples/compat/plan-greeting/src/greet.js +2 -0
- package/examples/compat/plan-greeting/test/greet.test.js +8 -0
- package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
- package/examples/compat/reverse-grep-edit/broken.diff +12 -0
- package/examples/compat/reverse-grep-edit/good.diff +12 -0
- package/examples/compat/reverse-grep-edit/package.json +5 -0
- package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
- package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
- package/examples/issue-triage-js/PLAN.md +83 -0
- package/examples/issue-triage-js/README.md +76 -0
- package/examples/issue-triage-js/package.json +9 -0
- package/examples/issue-triage-js/src/issue-triage.js +87 -0
- package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
- package/package.json +70 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
export const NEAL_DIR_NAME = '.neal';
|
|
3
|
+
export const RUNS_DIR_NAME = 'runs';
|
|
4
|
+
export const QUEUES_DIR_NAME = 'queues';
|
|
5
|
+
export const REVIEWS_DIR_NAME = 'reviews';
|
|
6
|
+
export const CURRENT_RUN_POINTER_FILE = 'current.json';
|
|
7
|
+
export const CURRENT_QUEUE_POINTER_FILE = 'current-queue.json';
|
|
8
|
+
export const ACTIVE_RUN_LOCK_FILE = 'active-run.lock';
|
|
9
|
+
export const RUN_STATE_FILE = 'RUN_STATE.json';
|
|
10
|
+
export const QUEUE_STATE_FILE = 'QUEUE_STATE.json';
|
|
11
|
+
export const QUEUE_LINK_FILE = 'QUEUE_LINK.json';
|
|
12
|
+
export const QUEUE_SUMMARY_FILE = 'QUEUE_SUMMARY.md';
|
|
13
|
+
export const PLAN_ORIGINAL_BACKUP_FILE = 'PLAN_ORIGINAL.md';
|
|
14
|
+
export const SCRATCH_DIR_NAME = 'scratch';
|
|
15
|
+
export function getNealDir(cwd) {
|
|
16
|
+
return join(cwd, NEAL_DIR_NAME);
|
|
17
|
+
}
|
|
18
|
+
export function getRunsDir(cwd) {
|
|
19
|
+
return join(getNealDir(cwd), RUNS_DIR_NAME);
|
|
20
|
+
}
|
|
21
|
+
export function getRunDir(cwd, runId) {
|
|
22
|
+
return join(getRunsDir(cwd), runId);
|
|
23
|
+
}
|
|
24
|
+
export function getQueuesDir(cwd) {
|
|
25
|
+
return join(getNealDir(cwd), QUEUES_DIR_NAME);
|
|
26
|
+
}
|
|
27
|
+
export function getReviewsDir(cwd) {
|
|
28
|
+
return join(getNealDir(cwd), REVIEWS_DIR_NAME);
|
|
29
|
+
}
|
|
30
|
+
export function getCurrentRunPointerPath(cwd) {
|
|
31
|
+
return join(getNealDir(cwd), CURRENT_RUN_POINTER_FILE);
|
|
32
|
+
}
|
|
33
|
+
export function getCurrentPlanAndExecuteQueuePointerPath(cwd) {
|
|
34
|
+
return join(getNealDir(cwd), CURRENT_QUEUE_POINTER_FILE);
|
|
35
|
+
}
|
|
36
|
+
export function getActiveRunLockPath(cwd) {
|
|
37
|
+
return join(getNealDir(cwd), ACTIVE_RUN_LOCK_FILE);
|
|
38
|
+
}
|
|
39
|
+
export function getRunStatePath(runDir) {
|
|
40
|
+
return join(runDir, RUN_STATE_FILE);
|
|
41
|
+
}
|
|
42
|
+
export function getPlanDocumentBackupPath(runDir) {
|
|
43
|
+
return join(runDir, PLAN_ORIGINAL_BACKUP_FILE);
|
|
44
|
+
}
|
|
45
|
+
export function getRunScratchDir(runDir) {
|
|
46
|
+
return join(runDir, SCRATCH_DIR_NAME);
|
|
47
|
+
}
|
|
48
|
+
export function getScopeReviewerScratchDir(runDir, scopeLabel, round) {
|
|
49
|
+
return join(getRunScratchDir(runDir), `reviewer-scope-${sanitizePathSegment(scopeLabel)}-round-${round}`);
|
|
50
|
+
}
|
|
51
|
+
export function getFinalCompletionReviewerScratchDir(runDir) {
|
|
52
|
+
return join(getRunScratchDir(runDir), 'final-completion-review');
|
|
53
|
+
}
|
|
54
|
+
function sanitizePathSegment(value) {
|
|
55
|
+
const sanitized = value.replace(/[^A-Za-z0-9._-]/g, '-');
|
|
56
|
+
return sanitized.length > 0 ? sanitized : 'scope';
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { writeTextAtomic } from './atomic-write.js';
|
|
2
|
+
import { formatPublicPhase } from './phase-display.js';
|
|
3
|
+
function appendInteractiveBlockedRecoverySection(lines, title, recovery, options) {
|
|
4
|
+
lines.push('', title, `- Source step: ${formatPublicPhase(recovery.sourcePhase)}`, `- Blocked reason: ${recovery.blockedReason}`, `- Max turns: ${recovery.maxTurns}`);
|
|
5
|
+
if (options?.resolvedAt && options.resolvedByAction && options.resultPhase) {
|
|
6
|
+
lines.push(`- Resolved at: ${options.resolvedAt}`, `- Resolution: ${options.resolvedByAction}`, `- Result step: ${formatPublicPhase(options.resultPhase)}`);
|
|
7
|
+
}
|
|
8
|
+
if (recovery.pendingDirective) {
|
|
9
|
+
lines.push(`- Pending terminal directive at ${recovery.pendingDirective.recordedAt}: ${recovery.pendingDirective.operatorGuidance}`);
|
|
10
|
+
}
|
|
11
|
+
if (recovery.adjudicatorAdvice) {
|
|
12
|
+
const advice = recovery.adjudicatorAdvice;
|
|
13
|
+
lines.push(`- Adjudicator advice at ${advice.recordedAt} (read-only, not auto-applied):`, ` - Recoverable: ${advice.recoverable ? 'yes' : 'no'}`, ` - Triage category: ${advice.triageCategory}`, ` - Suggested directive: ${advice.resolutionDirective || 'n/a'}`, ` - Rationale: ${advice.rationale}`);
|
|
14
|
+
}
|
|
15
|
+
if (recovery.turns.length === 0) {
|
|
16
|
+
lines.push('- Operator guidance: pending');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (const turn of recovery.turns) {
|
|
20
|
+
lines.push(`- Recovery turn ${turn.number} at ${turn.recordedAt}: ${turn.operatorGuidance}`);
|
|
21
|
+
if (turn.disposition) {
|
|
22
|
+
lines.push(`- Recovery turn ${turn.number} coder action: ${turn.disposition.action}`, `- Recovery turn ${turn.number} coder summary: ${turn.disposition.summary}`, `- Recovery turn ${turn.number} coder blocker: ${turn.disposition.blocker || 'n/a'}`, `- Recovery turn ${turn.number} coder rationale: ${turn.disposition.rationale}`, `- Recovery turn ${turn.number} resulting step: ${formatPublicPhase(turn.disposition.resultingPhase)}`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
lines.push(`- Recovery turn ${turn.number} coder response: pending`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function renderRecoveryMarkdown(state) {
|
|
30
|
+
const lines = [
|
|
31
|
+
'# Interactive Blocked Recovery',
|
|
32
|
+
'',
|
|
33
|
+
'## Metadata',
|
|
34
|
+
`- Plan: ${state.planDoc}`,
|
|
35
|
+
`- Current step: ${formatPublicPhase(state.phase)}`,
|
|
36
|
+
`- Coder session: ${state.coderSessionHandle ?? 'pending'}`,
|
|
37
|
+
`- Reviewer session: ${state.reviewerSessionHandle ?? 'pending'}`,
|
|
38
|
+
];
|
|
39
|
+
if (state.interactiveBlockedRecovery) {
|
|
40
|
+
appendInteractiveBlockedRecoverySection(lines, '## Active Recovery', state.interactiveBlockedRecovery);
|
|
41
|
+
}
|
|
42
|
+
if (state.interactiveBlockedRecoveryHistory.length > 0) {
|
|
43
|
+
for (const [index, recovery] of state.interactiveBlockedRecoveryHistory.entries()) {
|
|
44
|
+
appendInteractiveBlockedRecoverySection(lines, `## Recovery History ${index + 1}`, recovery, {
|
|
45
|
+
resolvedAt: recovery.resolvedAt,
|
|
46
|
+
resolvedByAction: recovery.resolvedByAction,
|
|
47
|
+
resultPhase: recovery.resultPhase,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!state.interactiveBlockedRecovery && state.interactiveBlockedRecoveryHistory.length === 0) {
|
|
52
|
+
lines.push('', 'No interactive blocked recovery yet.');
|
|
53
|
+
}
|
|
54
|
+
return `${lines.join('\n')}\n`;
|
|
55
|
+
}
|
|
56
|
+
export async function writeRecoveryMarkdown(path, state) {
|
|
57
|
+
await writeTextAtomic(path, renderRecoveryMarkdown(state));
|
|
58
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { buildBlockedGuidance } from './blocked-guidance.js';
|
|
3
|
+
import { writeNarrative } from './diagnostic.js';
|
|
4
|
+
import { EXECUTE_FINALIZATION_PHASE } from './execute-finalization.js';
|
|
5
|
+
import { formatPublicPhase } from './phase-display.js';
|
|
6
|
+
import { formatPublicRunStatus, getRunDisplayStatus } from './run-status.js';
|
|
7
|
+
import { getCurrentExecutionScopeDescriptor, getCurrentScopeLabel, } from './scopes.js';
|
|
8
|
+
import { getDerivedPlanView, getFinalCompletionView, getInteractiveRecoveryView, } from './state-views.js';
|
|
9
|
+
export function createTerminalNarrator(options = {}) {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
const write = options.write ?? writeNarrative;
|
|
12
|
+
let previous = null;
|
|
13
|
+
let previousScopeDescriptor = null;
|
|
14
|
+
let announcedScopeContextKey = null;
|
|
15
|
+
let scopeIntroSequence = 0;
|
|
16
|
+
let started = false;
|
|
17
|
+
function emit(event) {
|
|
18
|
+
if (seen.has(event.signature)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
seen.add(event.signature);
|
|
22
|
+
write(`${event.line}\n`, options.logger);
|
|
23
|
+
}
|
|
24
|
+
async function observeState(state, includeStartEvent) {
|
|
25
|
+
const scopeDescriptor = await getNarrativeScopeDescriptor(state);
|
|
26
|
+
const currentScopeContextKey = getScopeContextKey(scopeDescriptor);
|
|
27
|
+
const previousScopeContextKey = getScopeContextKey(previousScopeDescriptor);
|
|
28
|
+
const currentIntroAlreadyPrinted = currentScopeContextKey !== null && currentScopeContextKey === announcedScopeContextKey;
|
|
29
|
+
const previousIntroAlreadyPrinted = previousScopeContextKey !== null && previousScopeContextKey === announcedScopeContextKey;
|
|
30
|
+
if (includeStartEvent && !started) {
|
|
31
|
+
started = true;
|
|
32
|
+
emit(renderRunStartEvent(state));
|
|
33
|
+
}
|
|
34
|
+
const events = renderTerminalNarrativeEvents(state, previous, {
|
|
35
|
+
current: scopeDescriptor,
|
|
36
|
+
previous: previousScopeDescriptor,
|
|
37
|
+
currentIntroAlreadyPrinted,
|
|
38
|
+
previousIntroAlreadyPrinted,
|
|
39
|
+
currentIntroSequence: scopeIntroSequence + 1,
|
|
40
|
+
});
|
|
41
|
+
for (const event of events) {
|
|
42
|
+
emit(event);
|
|
43
|
+
}
|
|
44
|
+
if (currentScopeContextKey && events.some((event) => event.signature.startsWith('scope-intro:'))) {
|
|
45
|
+
scopeIntroSequence += 1;
|
|
46
|
+
announcedScopeContextKey = currentScopeContextKey;
|
|
47
|
+
}
|
|
48
|
+
previous = state;
|
|
49
|
+
previousScopeDescriptor = scopeDescriptor;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
async start(state) {
|
|
53
|
+
await observeState(state, true);
|
|
54
|
+
},
|
|
55
|
+
async observe(state) {
|
|
56
|
+
await observeState(state, false);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function renderTerminalNarrativeEvents(state, previous = null, scopeContext = {}) {
|
|
61
|
+
const currentScopeDescriptor = scopeContext.current ?? null;
|
|
62
|
+
const currentIntroAlreadyPrinted = scopeContext.currentIntroAlreadyPrinted ?? false;
|
|
63
|
+
const shouldRenderIntro = shouldRenderCurrentScopeIntro(state, currentScopeDescriptor, currentIntroAlreadyPrinted);
|
|
64
|
+
const scopeIntroEstablished = currentIntroAlreadyPrinted || shouldRenderIntro;
|
|
65
|
+
return [
|
|
66
|
+
...renderCompletedScopeEvents(state, previous, scopeContext.previous ?? currentScopeDescriptor, scopeContext.previousIntroAlreadyPrinted ?? currentIntroAlreadyPrinted),
|
|
67
|
+
...(shouldRenderIntro && currentScopeDescriptor
|
|
68
|
+
? [renderScopeIntroEvent(state, currentScopeDescriptor, scopeContext.currentIntroSequence ?? 0)]
|
|
69
|
+
: []),
|
|
70
|
+
...renderPhaseEvents(state, currentScopeDescriptor, { scopeIntroEstablished }),
|
|
71
|
+
...renderFinalCompletionResolutionEvents(state, currentScopeDescriptor, { scopeIntroEstablished }),
|
|
72
|
+
...renderLifecycleEvents(state, currentScopeDescriptor, { scopeIntroEstablished }),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
async function getNarrativeScopeDescriptor(state) {
|
|
76
|
+
if (state.topLevelMode !== 'execute') {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return getCurrentExecutionScopeDescriptor(state);
|
|
80
|
+
}
|
|
81
|
+
function renderRunStartEvent(state) {
|
|
82
|
+
const runId = basename(state.runDir);
|
|
83
|
+
const planName = basename(state.planDoc);
|
|
84
|
+
const action = hasPriorRunActivity(state) ? 'resuming' : 'starting';
|
|
85
|
+
const mode = state.topLevelMode === 'plan' ? 'plan refinement' : 'plan execution';
|
|
86
|
+
return {
|
|
87
|
+
signature: `run-start:${runId}`,
|
|
88
|
+
line: `[neal] ${action} ${mode} for ${planName}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function hasPriorRunActivity(state) {
|
|
92
|
+
return Boolean(state.rounds.length > 0 ||
|
|
93
|
+
state.completedScopes.length > 0 ||
|
|
94
|
+
state.createdCommits.length > 0 ||
|
|
95
|
+
state.coderSessionHandle ||
|
|
96
|
+
state.reviewerSessionHandle ||
|
|
97
|
+
state.finalCommit ||
|
|
98
|
+
state.interactiveBlockedRecovery ||
|
|
99
|
+
state.interactiveBlockedRecoveryHistory.length > 0 ||
|
|
100
|
+
state.finalCompletionSummary ||
|
|
101
|
+
state.finalCompletionReviewVerdict ||
|
|
102
|
+
state.finalCompletionResolvedAction);
|
|
103
|
+
}
|
|
104
|
+
function renderCompletedScopeEvents(state, previous, scopeDescriptor, scopeIntroEstablished) {
|
|
105
|
+
if (!previous || state.topLevelMode !== 'execute') {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const completedScopes = state.completedScopes.slice(previous.completedScopes.length);
|
|
109
|
+
return completedScopes.map((scope) => ({
|
|
110
|
+
signature: `scope-finalized:${basename(state.runDir)}:${scope.number}:${scope.finalCommit ?? 'pending'}`,
|
|
111
|
+
line: formatCompletedScopeLine(scope.number, scope.result, scopeDescriptor, { scopeIntroEstablished }),
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
function renderPhaseEvents(state, scopeDescriptor, options) {
|
|
115
|
+
const runId = basename(state.runDir);
|
|
116
|
+
const scopeLabel = getCurrentScopeLabel(state);
|
|
117
|
+
const scopeReference = formatScopeReference(scopeLabel, scopeDescriptor);
|
|
118
|
+
const phase = state.phase;
|
|
119
|
+
const round = state.rounds.length + 1;
|
|
120
|
+
const recovery = getInteractiveRecoveryView(state);
|
|
121
|
+
const derivedPlan = getDerivedPlanView(state);
|
|
122
|
+
const finalCompletion = getFinalCompletionView(state);
|
|
123
|
+
const displayStatus = getRunDisplayStatus(state);
|
|
124
|
+
if (displayStatus.waitingForOperatorGuidance) {
|
|
125
|
+
const guidance = buildBlockedGuidance({
|
|
126
|
+
state,
|
|
127
|
+
runId,
|
|
128
|
+
waitingForOperatorGuidance: true,
|
|
129
|
+
});
|
|
130
|
+
const firstOption = guidance?.options[0]?.command;
|
|
131
|
+
if (guidance && firstOption) {
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
signature: `operator-waiting:${runId}:${recovery?.sourcePhase ?? state.blockedFromPhase ?? phase}:${recovery?.handledTurn ?? 0}:${guidance.category}`,
|
|
135
|
+
line: `[neal] waiting for operator guidance: ${guidance.summary}; use: ${firstOption}`,
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
signature: `operator-waiting:${runId}:${recovery?.sourcePhase ?? state.blockedFromPhase ?? phase}:${recovery?.handledTurn ?? 0}`,
|
|
142
|
+
line: `[neal] waiting for operator guidance; use: neal resume --run ${runId} --message "..."`,
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
if (displayStatus.pendingOperatorGuidance) {
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
signature: `operator-pending:${runId}:${recovery?.pendingTurnCount ?? 0}:${recovery?.handledTurn ?? 0}`,
|
|
150
|
+
line: `Operator guidance is recorded; resuming will process it now.`,
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
switch (phase) {
|
|
155
|
+
case 'coder_plan':
|
|
156
|
+
return [
|
|
157
|
+
{
|
|
158
|
+
signature: `phase:${runId}:${phase}:${round}`,
|
|
159
|
+
line: `Planner is refining the plan.`,
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
case 'reviewer_plan':
|
|
163
|
+
return [
|
|
164
|
+
{
|
|
165
|
+
signature: `phase:${runId}:${phase}:${round}:${derivedPlan?.state ?? 'ordinary'}`,
|
|
166
|
+
line: derivedPlan?.reviewActive
|
|
167
|
+
? `Reviewer is checking the derived plan for scope ${derivedPlan.parentScopeNumber ?? scopeLabel}.`
|
|
168
|
+
: `Reviewer is checking the plan.`,
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
case 'coder_plan_response':
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
signature: `phase:${runId}:${phase}:${round}`,
|
|
175
|
+
line: `Planner is addressing requested plan revisions.`,
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
case 'coder_plan_optional_response':
|
|
179
|
+
return [
|
|
180
|
+
{
|
|
181
|
+
signature: `phase:${runId}:${phase}:${round}`,
|
|
182
|
+
line: `Planner is handling optional plan follow-up.`,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
case 'awaiting_derived_plan_execution':
|
|
186
|
+
return [
|
|
187
|
+
{
|
|
188
|
+
signature: `phase:${runId}:${phase}:${derivedPlan?.path ?? 'unknown'}`,
|
|
189
|
+
line: `Derived plan accepted; preparing its first scope.`,
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
case 'coder_scope':
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
signature: `phase:${runId}:${phase}:${scopeLabel}:${state.baseCommit ?? 'no-base'}:${state.createdCommits.length}`,
|
|
196
|
+
line: options.scopeIntroEstablished ? `Coder is implementing.` : `Coder is implementing ${scopeReference}.`,
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
case 'manual_gate':
|
|
200
|
+
return [
|
|
201
|
+
{
|
|
202
|
+
signature: `phase:${runId}:${phase}:${state.manualGate?.id ?? 'unknown'}`,
|
|
203
|
+
line: `Run is waiting for manual gate ${state.manualGate?.id ?? 'unknown'}.`,
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
case 'reviewer_scope':
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
signature: `phase:${runId}:${phase}:${scopeLabel}:${round}:${state.createdCommits.at(-1) ?? 'no-commit'}`,
|
|
210
|
+
line: options.scopeIntroEstablished ? `Reviewer is checking.` : `Reviewer is checking ${scopeReference}.`,
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
case 'coder_response':
|
|
214
|
+
return [
|
|
215
|
+
{
|
|
216
|
+
signature: `phase:${runId}:${phase}:${scopeLabel}:${round}`,
|
|
217
|
+
line: options.scopeIntroEstablished
|
|
218
|
+
? `Reviewer requested revisions; coder is addressing them.`
|
|
219
|
+
: `Reviewer requested revisions for ${scopeReference}; coder is addressing them.`,
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
case 'coder_optional_response':
|
|
223
|
+
return [
|
|
224
|
+
{
|
|
225
|
+
signature: `phase:${runId}:${phase}:${scopeLabel}:${round}`,
|
|
226
|
+
line: options.scopeIntroEstablished
|
|
227
|
+
? `Reviewer left optional follow-up; coder is handling it.`
|
|
228
|
+
: `Reviewer left optional follow-up for ${scopeReference}; coder is handling it.`,
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
case 'interactive_blocked_recovery':
|
|
232
|
+
return [
|
|
233
|
+
{
|
|
234
|
+
signature: `phase:${runId}:${phase}:${recovery?.sourcePhase ?? state.blockedFromPhase ?? 'unknown'}`,
|
|
235
|
+
line: formatScopeRelatedPhase(recovery?.sourcePhase ?? state.blockedFromPhase ?? phase)
|
|
236
|
+
? options.scopeIntroEstablished
|
|
237
|
+
? `Interactive recovery is active for ${formatPublicPhase(recovery?.sourcePhase ?? state.blockedFromPhase ?? phase)}.`
|
|
238
|
+
: `Interactive recovery is active for ${formatPublicPhase(recovery?.sourcePhase ?? state.blockedFromPhase ?? phase)} on ${scopeReference}.`
|
|
239
|
+
: `Interactive recovery is active for ${formatPublicPhase(recovery?.sourcePhase ?? state.blockedFromPhase ?? phase)}.`,
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
case EXECUTE_FINALIZATION_PHASE:
|
|
243
|
+
return [
|
|
244
|
+
{
|
|
245
|
+
signature: `phase:${runId}:${phase}:${scopeLabel}:${state.createdCommits.length}`,
|
|
246
|
+
line: options.scopeIntroEstablished
|
|
247
|
+
? `Reviewer accepted; finalizing.`
|
|
248
|
+
: `Reviewer accepted ${scopeReference}; finalizing.`,
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
case 'final_completion_review':
|
|
252
|
+
return [
|
|
253
|
+
{
|
|
254
|
+
signature: `phase:${runId}:${phase}:${finalCompletion?.continueExecutionCount ?? 0}`,
|
|
255
|
+
line: `Reviewer is checking final completion.`,
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
case 'done':
|
|
259
|
+
case 'blocked':
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function renderFinalCompletionResolutionEvents(state, scopeDescriptor, options) {
|
|
264
|
+
if (state.topLevelMode !== 'execute') {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
const finalCompletion = getFinalCompletionView(state);
|
|
268
|
+
if (!finalCompletion?.hasResolvedAction) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
const runId = basename(state.runDir);
|
|
272
|
+
const action = finalCompletion.effectiveAction;
|
|
273
|
+
if (action === 'accept_complete') {
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
signature: `final-completion:${runId}:${action}`,
|
|
277
|
+
line: `Final completion review accepted the implementation.`,
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
if (action === 'continue_execution') {
|
|
282
|
+
const scopeReference = formatScopeReference(getCurrentScopeLabel(state), scopeDescriptor);
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
signature: `final-completion:${runId}:${action}:${finalCompletion.continueExecutionCount}`,
|
|
286
|
+
line: options.scopeIntroEstablished
|
|
287
|
+
? `Final completion review reopened execution; continuing.`
|
|
288
|
+
: `Final completion review reopened execution; continuing with ${scopeReference}.`,
|
|
289
|
+
},
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
if (action === 'block_for_operator') {
|
|
293
|
+
return [
|
|
294
|
+
{
|
|
295
|
+
signature: `final-completion:${runId}:${action}:${finalCompletion.continueExecutionCount}`,
|
|
296
|
+
line: `Final completion review blocked completion for operator guidance.`,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
function renderLifecycleEvents(state, scopeDescriptor, options) {
|
|
303
|
+
const displayStatus = getRunDisplayStatus(state);
|
|
304
|
+
const publicStatus = formatPublicRunStatus(displayStatus);
|
|
305
|
+
const runId = basename(state.runDir);
|
|
306
|
+
const scopeReference = formatScopeReference(getCurrentScopeLabel(state), scopeDescriptor);
|
|
307
|
+
if (displayStatus.waitingForOperatorGuidance || displayStatus.pendingOperatorGuidance) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
if (state.status === 'paused') {
|
|
311
|
+
return [
|
|
312
|
+
{
|
|
313
|
+
signature: `lifecycle:${runId}:paused:${state.phase}:${getCurrentScopeLabel(state)}`,
|
|
314
|
+
line: options.scopeIntroEstablished ? `Run paused after this scope.` : `Run paused after ${scopeReference}.`,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
if (state.status === 'failed') {
|
|
319
|
+
return [
|
|
320
|
+
{
|
|
321
|
+
signature: `lifecycle:${runId}:failed:${state.phase}`,
|
|
322
|
+
line: `Run failed during ${formatPublicPhase(state.phase)}.`,
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
if (state.status === 'blocked') {
|
|
327
|
+
const blockedPhase = state.blockedFromPhase ?? state.phase;
|
|
328
|
+
return [
|
|
329
|
+
{
|
|
330
|
+
signature: `lifecycle:${runId}:blocked:${blockedPhase}`,
|
|
331
|
+
line: formatScopeRelatedPhase(blockedPhase)
|
|
332
|
+
? options.scopeIntroEstablished
|
|
333
|
+
? `Run is blocked during ${formatPublicPhase(blockedPhase)}.`
|
|
334
|
+
: `Run is blocked during ${formatPublicPhase(blockedPhase)} for ${scopeReference}.`
|
|
335
|
+
: `Run is blocked during ${formatPublicPhase(blockedPhase)}.`,
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
if (state.status === 'done') {
|
|
340
|
+
return [
|
|
341
|
+
{
|
|
342
|
+
signature: `lifecycle:${runId}:done:${state.topLevelMode}:${publicStatus}`,
|
|
343
|
+
line: state.topLevelMode === 'plan' ? `Plan refinement complete.` : `Implementation complete.`,
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
}
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
function renderScopeIntroEvent(state, scopeDescriptor, sequence) {
|
|
350
|
+
const scopeContextKey = getScopeContextKey(scopeDescriptor) ?? scopeDescriptor.display;
|
|
351
|
+
return {
|
|
352
|
+
signature: `scope-intro:${basename(state.runDir)}:${sequence}:${scopeContextKey}`,
|
|
353
|
+
line: capitalizeLeadingScope(scopeDescriptor.display),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function shouldRenderCurrentScopeIntro(state, scopeDescriptor, currentIntroAlreadyPrinted) {
|
|
357
|
+
return Boolean(scopeDescriptor && !currentIntroAlreadyPrinted && hasCurrentScopeNarrativeEvent(state));
|
|
358
|
+
}
|
|
359
|
+
function hasCurrentScopeNarrativeEvent(state) {
|
|
360
|
+
if (state.topLevelMode !== 'execute') {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
const displayStatus = getRunDisplayStatus(state);
|
|
364
|
+
if (displayStatus.waitingForOperatorGuidance || displayStatus.pendingOperatorGuidance) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (isScopeActionPhase(state.phase)) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
if (state.phase === 'interactive_blocked_recovery') {
|
|
371
|
+
const recovery = getInteractiveRecoveryView(state);
|
|
372
|
+
return formatScopeRelatedPhase(recovery?.sourcePhase ?? state.blockedFromPhase ?? state.phase);
|
|
373
|
+
}
|
|
374
|
+
const finalCompletion = getFinalCompletionView(state);
|
|
375
|
+
if (finalCompletion?.hasResolvedAction && finalCompletion.effectiveAction === 'continue_execution') {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
if (state.status === 'paused') {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
return state.status === 'blocked' && formatScopeRelatedPhase(state.blockedFromPhase ?? state.phase);
|
|
382
|
+
}
|
|
383
|
+
function formatScopeReference(scopeLabel, descriptor, options = {}) {
|
|
384
|
+
const reference = descriptor?.scopeLabel === scopeLabel ? descriptor.display : `scope ${scopeLabel}`;
|
|
385
|
+
if (!options.capitalized) {
|
|
386
|
+
return reference;
|
|
387
|
+
}
|
|
388
|
+
return reference[0]?.toUpperCase() + reference.slice(1);
|
|
389
|
+
}
|
|
390
|
+
function formatCompletedScopeLine(scopeLabel, result, descriptor, options) {
|
|
391
|
+
if (options.scopeIntroEstablished) {
|
|
392
|
+
return result === 'accepted' ? `Scope accepted.` : `Scope blocked.`;
|
|
393
|
+
}
|
|
394
|
+
if (descriptor?.scopeLabel !== scopeLabel || !descriptor.title) {
|
|
395
|
+
return `Scope ${scopeLabel} finalized and ${result}.`;
|
|
396
|
+
}
|
|
397
|
+
const scopeReference = formatScopeReference(scopeLabel, descriptor, { capitalized: true });
|
|
398
|
+
return `${scopeReference.replace(`: ${descriptor.title}`, '')} finalized and ${result}: ${descriptor.title}.`;
|
|
399
|
+
}
|
|
400
|
+
function getScopeContextKey(descriptor) {
|
|
401
|
+
if (!descriptor) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return JSON.stringify([
|
|
405
|
+
descriptor.planPath,
|
|
406
|
+
descriptor.scopeLabel,
|
|
407
|
+
descriptor.planScopeNumber,
|
|
408
|
+
formatScopeCountKey(descriptor.scopeCount),
|
|
409
|
+
descriptor.title,
|
|
410
|
+
descriptor.display,
|
|
411
|
+
]);
|
|
412
|
+
}
|
|
413
|
+
function formatScopeCountKey(scopeCount) {
|
|
414
|
+
return scopeCount.kind === 'known' ? `${scopeCount.kind}:${scopeCount.total}` : scopeCount.kind;
|
|
415
|
+
}
|
|
416
|
+
function capitalizeLeadingScope(value) {
|
|
417
|
+
if (value === '') {
|
|
418
|
+
return value;
|
|
419
|
+
}
|
|
420
|
+
return value[0].toUpperCase() + value.slice(1);
|
|
421
|
+
}
|
|
422
|
+
function isScopeActionPhase(phase) {
|
|
423
|
+
return (phase === 'coder_scope' ||
|
|
424
|
+
phase === 'reviewer_scope' ||
|
|
425
|
+
phase === 'coder_response' ||
|
|
426
|
+
phase === 'coder_optional_response' ||
|
|
427
|
+
phase === EXECUTE_FINALIZATION_PHASE);
|
|
428
|
+
}
|
|
429
|
+
function formatScopeRelatedPhase(phase) {
|
|
430
|
+
return (phase === 'coder_scope' ||
|
|
431
|
+
phase === 'reviewer_scope' ||
|
|
432
|
+
phase === 'coder_response' ||
|
|
433
|
+
phase === 'coder_optional_response' ||
|
|
434
|
+
phase === EXECUTE_FINALIZATION_PHASE);
|
|
435
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
function stringValue(value) {
|
|
2
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
3
|
+
}
|
|
4
|
+
function numberValue(value) {
|
|
5
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
6
|
+
}
|
|
7
|
+
export function isVerificationCommand(command) {
|
|
8
|
+
return /\b(test|tests|typecheck|tsc|lint|build|verify|validation|check|pytest|vitest|jest)\b/i.test(command);
|
|
9
|
+
}
|
|
10
|
+
function isCommandExecutionEvent(event) {
|
|
11
|
+
return (event.type === 'provider.command_completed' ||
|
|
12
|
+
event.type === 'coder.command_execution' ||
|
|
13
|
+
event.type === 'codex.command_execution');
|
|
14
|
+
}
|
|
15
|
+
export function extractCommandResults(events) {
|
|
16
|
+
return events
|
|
17
|
+
.filter(isCommandExecutionEvent)
|
|
18
|
+
.map((event) => {
|
|
19
|
+
const command = stringValue(event.data?.command);
|
|
20
|
+
if (!command) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
command,
|
|
25
|
+
provider: stringValue(event.data?.provider),
|
|
26
|
+
status: stringValue(event.data?.status),
|
|
27
|
+
exitCode: numberValue(event.data?.exitCode),
|
|
28
|
+
cwd: stringValue(event.data?.cwd),
|
|
29
|
+
gitHead: stringValue(event.data?.gitHead),
|
|
30
|
+
completedAt: stringValue(event.ts),
|
|
31
|
+
itemId: stringValue(event.data?.itemId),
|
|
32
|
+
outputLength: numberValue(event.data?.outputLength),
|
|
33
|
+
};
|
|
34
|
+
})
|
|
35
|
+
.filter((result) => result !== null);
|
|
36
|
+
}
|
|
37
|
+
export function extractVerificationCommandResults(events) {
|
|
38
|
+
return extractCommandResults(events).filter((result) => isVerificationCommand(result.command));
|
|
39
|
+
}
|
|
40
|
+
export function latestCommandResultPerCommand(results) {
|
|
41
|
+
const byCommand = new Map();
|
|
42
|
+
for (const result of results) {
|
|
43
|
+
byCommand.set(result.command, result);
|
|
44
|
+
}
|
|
45
|
+
return [...byCommand.values()];
|
|
46
|
+
}
|
|
47
|
+
function renderCommandStatus(result) {
|
|
48
|
+
if (result.exitCode === 0) {
|
|
49
|
+
return 'passed';
|
|
50
|
+
}
|
|
51
|
+
if (typeof result.exitCode === 'number') {
|
|
52
|
+
return 'failed';
|
|
53
|
+
}
|
|
54
|
+
if (result.status === 'failed') {
|
|
55
|
+
return 'failed';
|
|
56
|
+
}
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
59
|
+
export function summarizeVerificationCommandResults(results) {
|
|
60
|
+
if (results.length === 0) {
|
|
61
|
+
return 'No verification commands were recorded in events.ndjson.';
|
|
62
|
+
}
|
|
63
|
+
const latestResults = latestCommandResultPerCommand(results);
|
|
64
|
+
const lines = ['Recorded latest verification command results for this run:'];
|
|
65
|
+
let unknownExitStatusCount = 0;
|
|
66
|
+
for (const result of latestResults) {
|
|
67
|
+
const renderedStatus = renderCommandStatus(result);
|
|
68
|
+
const exit = typeof result.exitCode === 'number' ? `exit ${result.exitCode}` : 'exit unknown';
|
|
69
|
+
const gitHead = result.gitHead ? `, git ${result.gitHead.slice(0, 12)}` : '';
|
|
70
|
+
const cwd = result.cwd ? `, cwd ${result.cwd}` : '';
|
|
71
|
+
const completedAt = result.completedAt ? `, at ${result.completedAt}` : '';
|
|
72
|
+
lines.push(`- ${renderedStatus} (${exit}${gitHead}${cwd}${completedAt}): ${result.command}`);
|
|
73
|
+
if (typeof result.exitCode !== 'number') {
|
|
74
|
+
unknownExitStatusCount += 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (unknownExitStatusCount > 0) {
|
|
78
|
+
lines.push(`- ${unknownExitStatusCount} latest verification command(s) have unknown exit status, usually from older events recorded before structured command results were available.`);
|
|
79
|
+
}
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
let cachedBuildMetadata = null;
|
|
8
|
+
export function getAppVersion() {
|
|
9
|
+
const packageJsonPath = new URL('../../package.json', import.meta.url);
|
|
10
|
+
const metadata = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
11
|
+
if (typeof metadata.version !== 'string' || metadata.version.trim() === '') {
|
|
12
|
+
throw new Error('package.json is missing a valid version');
|
|
13
|
+
}
|
|
14
|
+
return metadata.version;
|
|
15
|
+
}
|
|
16
|
+
export async function getNealBuildMetadata() {
|
|
17
|
+
cachedBuildMetadata ??= readNealBuildMetadata();
|
|
18
|
+
return cachedBuildMetadata;
|
|
19
|
+
}
|
|
20
|
+
async function readNealBuildMetadata() {
|
|
21
|
+
return {
|
|
22
|
+
packageVersion: getAppVersion(),
|
|
23
|
+
nodeVersion: process.version,
|
|
24
|
+
sourceGitSha: await readSourceGitSha(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async function readSourceGitSha() {
|
|
28
|
+
const packageRoot = dirname(fileURLToPath(new URL('../../package.json', import.meta.url)));
|
|
29
|
+
try {
|
|
30
|
+
const { stdout } = await execFileAsync('git', ['-C', packageRoot, 'rev-parse', 'HEAD']);
|
|
31
|
+
const sha = stdout.trim();
|
|
32
|
+
return /^[0-9a-f]{40}$/i.test(sha) ? sha : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|