sequant 2.9.0 → 2.11.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +20 -5
- package/dist/bin/cli.js +143 -18
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +56 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/src/commands/assess-render.d.ts +23 -0
- package/dist/src/commands/assess-render.js +60 -0
- package/dist/src/commands/doctor.js +8 -13
- package/dist/src/commands/init.js +13 -1
- package/dist/src/commands/locks.d.ts +20 -1
- package/dist/src/commands/locks.js +208 -4
- package/dist/src/commands/logs.js +14 -5
- package/dist/src/commands/merge.js +154 -2
- package/dist/src/commands/ready-tui-adapter.js +6 -1
- package/dist/src/commands/ready.d.ts +31 -3
- package/dist/src/commands/ready.js +53 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.js +38 -4
- package/dist/src/commands/run-flags.d.ts +42 -1
- package/dist/src/commands/run-flags.js +53 -1
- package/dist/src/commands/run-progress.js +26 -1
- package/dist/src/commands/run.js +11 -10
- package/dist/src/commands/state.js +7 -0
- package/dist/src/commands/status.d.ts +9 -0
- package/dist/src/commands/status.js +24 -1
- package/dist/src/commands/sync.js +90 -19
- package/dist/src/commands/update.js +28 -5
- package/dist/src/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- package/dist/src/lib/ac-parser.d.ts +14 -0
- package/dist/src/lib/ac-parser.js +99 -5
- package/dist/src/lib/assess/index.d.ts +10 -0
- package/dist/src/lib/assess/index.js +9 -0
- package/dist/src/lib/assess/renderer.d.ts +64 -0
- package/dist/src/lib/assess/renderer.js +481 -0
- package/dist/src/lib/assess/types.d.ts +224 -0
- package/dist/src/lib/assess/types.js +241 -0
- package/dist/src/lib/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +56 -0
- package/dist/src/lib/cli-flags.js +89 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +69 -1
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +7 -4
- package/dist/src/lib/locks/index.js +5 -3
- package/dist/src/lib/locks/lock-manager.d.ts +101 -3
- package/dist/src/lib/locks/lock-manager.js +277 -8
- package/dist/src/lib/locks/types.d.ts +106 -3
- package/dist/src/lib/locks/types.js +44 -0
- package/dist/src/lib/mcp-config.d.ts +45 -0
- package/dist/src/lib/mcp-config.js +77 -1
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
- package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
- package/dist/src/lib/merge-check/command-result.d.ts +36 -0
- package/dist/src/lib/merge-check/command-result.js +56 -0
- package/dist/src/lib/merge-check/index.d.ts +4 -0
- package/dist/src/lib/merge-check/index.js +1 -1
- package/dist/src/lib/merge-check/report.js +5 -1
- package/dist/src/lib/merge-check/types.d.ts +9 -0
- package/dist/src/lib/merge-check/watch.d.ts +121 -0
- package/dist/src/lib/merge-check/watch.js +215 -0
- package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
- package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
- package/dist/src/lib/settings.d.ts +87 -0
- package/dist/src/lib/settings.js +106 -8
- package/dist/src/lib/shutdown.d.ts +42 -2
- package/dist/src/lib/shutdown.js +75 -5
- package/dist/src/lib/skills-check.d.ts +26 -0
- package/dist/src/lib/skills-check.js +31 -0
- package/dist/src/lib/stacks.d.ts +110 -1
- package/dist/src/lib/stacks.js +244 -5
- package/dist/src/lib/templates.d.ts +74 -0
- package/dist/src/lib/templates.js +171 -16
- package/dist/src/lib/test-tautology-detector.d.ts +10 -2
- package/dist/src/lib/test-tautology-detector.js +213 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +45 -2
- package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
- package/dist/src/lib/workflow/batch-executor.js +552 -60
- package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
- package/dist/src/lib/workflow/chain-resume.js +24 -20
- package/dist/src/lib/workflow/completed-status.d.ts +52 -0
- package/dist/src/lib/workflow/completed-status.js +57 -0
- package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
- package/dist/src/lib/workflow/config-resolver.js +143 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
- package/dist/src/lib/workflow/drivers/aider.js +2 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
- package/dist/src/lib/workflow/git-diff-utils.js +77 -4
- package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
- package/dist/src/lib/workflow/heartbeat.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +54 -2
- package/dist/src/lib/workflow/log-writer.js +95 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +467 -52
- package/dist/src/lib/workflow/phase-registry.js +2 -1
- package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
- package/dist/src/lib/workflow/platforms/github.js +152 -0
- package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
- package/dist/src/lib/workflow/qa-cache.js +22 -9
- package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
- package/dist/src/lib/workflow/ready-gate.js +49 -12
- package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
- package/dist/src/lib/workflow/run-log-schema.js +78 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +168 -20
- package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/skills-preflight.js +112 -0
- package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -0
- package/dist/src/lib/workflow/state-cleanup.js +9 -2
- package/dist/src/lib/workflow/state-manager.d.ts +33 -0
- package/dist/src/lib/workflow/state-manager.js +83 -0
- package/dist/src/lib/workflow/state-schema.d.ts +26 -0
- package/dist/src/lib/workflow/state-schema.js +47 -0
- package/dist/src/lib/workflow/types.d.ts +190 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
- package/dist/src/lib/workflow/worktree-manager.js +111 -24
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- package/dist/src/mcp/tools/run.d.ts +9 -1
- package/dist/src/mcp/tools/run.js +20 -1
- package/dist/src/mcp/tools/status.js +6 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
- package/dist/src/ui/tui/ElapsedTimer.js +12 -10
- package/dist/src/ui/tui/IssueBox.js +10 -2
- package/dist/src/ui/tui/row-cap.js +2 -1
- package/dist/src/ui/tui/theme.d.ts +7 -0
- package/dist/src/ui/tui/theme.js +9 -0
- package/package.json +10 -8
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +303 -10
- package/templates/scripts/cleanup-worktree.sh +217 -33
- package/templates/scripts/new-feature.sh +289 -35
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +220 -391
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +87 -13
- package/templates/skills/fullsolve/SKILL.md +130 -22
- package/templates/skills/loop/SKILL.md +94 -13
- package/templates/skills/merger/SKILL.md +100 -12
- package/templates/skills/qa/SKILL.md +396 -63
- package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
- package/templates/skills/qa/references/call-site-review.md +2 -2
- package/templates/skills/qa/references/code-review-checklist.md +2 -2
- package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/templates/skills/qa/references/quality-gates.md +21 -7
- package/templates/skills/qa/references/testing-requirements.md +1 -1
- package/templates/skills/qa/scripts/quality-checks.sh +11 -11
- package/templates/skills/release/SKILL.md +80 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +33 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- package/dist/src/lib/phase-spinner.d.ts +0 -146
- package/dist/src/lib/phase-spinner.js +0 -255
- package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
- package/dist/src/lib/workflow/pr-operations.js +0 -326
- package/dist/src/lib/workflow/run-summary.d.ts +0 -36
- package/dist/src/lib/workflow/run-summary.js +0 -142
- package/templates/agents/sequant-explorer.md +0 -24
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence-based effort escalation on quality-loop retries (#915).
|
|
3
|
+
*
|
|
4
|
+
* Sequant already detects several "this attempt is a retry" moments: the
|
|
5
|
+
* outer quality-loop re-entering a phase (`batch-executor.ts`) and the
|
|
6
|
+
* `sequant ready` QA-pass loop re-running `qa`/`loop` (`ready-gate.ts`).
|
|
7
|
+
* Escalation raises the phase's reasoning effort one tier for exactly that
|
|
8
|
+
* retried execution when the workflow observed a prior attempt fail — never
|
|
9
|
+
* speculatively, and never more than one tier per retry (see AC-6).
|
|
10
|
+
*
|
|
11
|
+
* Deliberately its own module rather than living beside `resolvePhasePolicies`
|
|
12
|
+
* in `config-resolver.ts`: `config-resolver.ts` imports `getEnvConfig` from
|
|
13
|
+
* `batch-executor.ts`, and `batch-executor.ts` is one of this module's
|
|
14
|
+
* dispatch-time callers, so co-locating here avoids introducing that cycle.
|
|
15
|
+
*/
|
|
16
|
+
import { EFFORT_LEVELS } from "../settings.js";
|
|
17
|
+
import type { ExecutionConfig, Phase } from "./types.js";
|
|
18
|
+
/**
|
|
19
|
+
* Base effort assumed for a phase with no configured `effort` override, when
|
|
20
|
+
* escalation needs a starting point to step up from (AC-5). `phase-executor.ts`
|
|
21
|
+
* omits the `effort` key entirely in that case (#914) so the Agent SDK's own
|
|
22
|
+
* default applies — #914 deliberately never encoded what that default is,
|
|
23
|
+
* since "omitted" is not the same claim as "equals X".
|
|
24
|
+
*
|
|
25
|
+
* Verified against `@anthropic-ai/claude-agent-sdk`'s own `query()` Options
|
|
26
|
+
* type (`sdk.d.ts`): `effort?: EffortLevel` is documented inline as
|
|
27
|
+
* `'high' — Deep reasoning (default)`. That is the SDK's default for the
|
|
28
|
+
* exact call sequant makes (raw `query()`, not the Claude Code CLI product —
|
|
29
|
+
* whose own `xhigh` default is a caller choice on top of this SDK, not the
|
|
30
|
+
* SDK's own default), so this constant is not a guess.
|
|
31
|
+
*/
|
|
32
|
+
export declare const DEFAULT_ESCALATION_BASE: (typeof EFFORT_LEVELS)[number];
|
|
33
|
+
/**
|
|
34
|
+
* Pure ladder step: one tier above `base` on `EFFORT_LEVELS`, capped at the
|
|
35
|
+
* top (`max`). Returns `base` unchanged whenever `enabled` is false or this
|
|
36
|
+
* isn't a retry — the disabled/first-attempt path must be indistinguishable
|
|
37
|
+
* from #914 with escalation never having existed (AC-2).
|
|
38
|
+
*
|
|
39
|
+
* Always escalates from the phase's CONFIGURED base, never from a previously
|
|
40
|
+
* escalated value — callers must not accumulate escalation across iterations
|
|
41
|
+
* (AC-6): base `high` on the 3rd loop iteration is `xhigh`, not `max`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveEscalatedEffort(base: string | undefined, isRetry: boolean, enabled: boolean): string | undefined;
|
|
44
|
+
/** One escalated execution, for observability (run metrics + verbose output). */
|
|
45
|
+
export interface EscalationRecord {
|
|
46
|
+
phase: Phase;
|
|
47
|
+
base: string;
|
|
48
|
+
escalated: string;
|
|
49
|
+
}
|
|
50
|
+
export interface EscalationOutcome {
|
|
51
|
+
/**
|
|
52
|
+
* The config to dispatch with. Identical by reference to the input `config`
|
|
53
|
+
* whenever nothing escalated — so a shared `ExecutionConfig` object is never
|
|
54
|
+
* mutated and an escalation never leaks into a phase execution it wasn't
|
|
55
|
+
* computed for (AC-7).
|
|
56
|
+
*/
|
|
57
|
+
config: ExecutionConfig;
|
|
58
|
+
/** Present only when this dispatch actually escalated. */
|
|
59
|
+
record?: EscalationRecord;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Apply escalation to ONE phase's execution, for THIS dispatch only.
|
|
63
|
+
*
|
|
64
|
+
* This is deliberately a per-execution decision made at the dispatch site,
|
|
65
|
+
* not a value baked into `ExecutionConfig` at build time: `buildExecutionConfig`
|
|
66
|
+
* / `buildPhaseConfig` run once per run/gate, not once per phase execution, so
|
|
67
|
+
* a static escalated value would leak across every phase in the chain and
|
|
68
|
+
* violate AC-7. The three retry-dispatch sites (batch-executor.ts's quality
|
|
69
|
+
* loop, ready-gate.ts's QA-pass loop `qa`/`loop` dispatch) call this function
|
|
70
|
+
* — and only this function — so they cannot drift on the cap/one-tier rules
|
|
71
|
+
* in AC-6 (see resolveEscalatedEffort's doc comment).
|
|
72
|
+
*/
|
|
73
|
+
export declare function withEscalatedEffort(config: ExecutionConfig, phase: Phase, isRetry: boolean): EscalationOutcome;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence-based effort escalation on quality-loop retries (#915).
|
|
3
|
+
*
|
|
4
|
+
* Sequant already detects several "this attempt is a retry" moments: the
|
|
5
|
+
* outer quality-loop re-entering a phase (`batch-executor.ts`) and the
|
|
6
|
+
* `sequant ready` QA-pass loop re-running `qa`/`loop` (`ready-gate.ts`).
|
|
7
|
+
* Escalation raises the phase's reasoning effort one tier for exactly that
|
|
8
|
+
* retried execution when the workflow observed a prior attempt fail — never
|
|
9
|
+
* speculatively, and never more than one tier per retry (see AC-6).
|
|
10
|
+
*
|
|
11
|
+
* Deliberately its own module rather than living beside `resolvePhasePolicies`
|
|
12
|
+
* in `config-resolver.ts`: `config-resolver.ts` imports `getEnvConfig` from
|
|
13
|
+
* `batch-executor.ts`, and `batch-executor.ts` is one of this module's
|
|
14
|
+
* dispatch-time callers, so co-locating here avoids introducing that cycle.
|
|
15
|
+
*/
|
|
16
|
+
import { EFFORT_LEVELS } from "../settings.js";
|
|
17
|
+
/**
|
|
18
|
+
* Base effort assumed for a phase with no configured `effort` override, when
|
|
19
|
+
* escalation needs a starting point to step up from (AC-5). `phase-executor.ts`
|
|
20
|
+
* omits the `effort` key entirely in that case (#914) so the Agent SDK's own
|
|
21
|
+
* default applies — #914 deliberately never encoded what that default is,
|
|
22
|
+
* since "omitted" is not the same claim as "equals X".
|
|
23
|
+
*
|
|
24
|
+
* Verified against `@anthropic-ai/claude-agent-sdk`'s own `query()` Options
|
|
25
|
+
* type (`sdk.d.ts`): `effort?: EffortLevel` is documented inline as
|
|
26
|
+
* `'high' — Deep reasoning (default)`. That is the SDK's default for the
|
|
27
|
+
* exact call sequant makes (raw `query()`, not the Claude Code CLI product —
|
|
28
|
+
* whose own `xhigh` default is a caller choice on top of this SDK, not the
|
|
29
|
+
* SDK's own default), so this constant is not a guess.
|
|
30
|
+
*/
|
|
31
|
+
export const DEFAULT_ESCALATION_BASE = "high";
|
|
32
|
+
/**
|
|
33
|
+
* Pure ladder step: one tier above `base` on `EFFORT_LEVELS`, capped at the
|
|
34
|
+
* top (`max`). Returns `base` unchanged whenever `enabled` is false or this
|
|
35
|
+
* isn't a retry — the disabled/first-attempt path must be indistinguishable
|
|
36
|
+
* from #914 with escalation never having existed (AC-2).
|
|
37
|
+
*
|
|
38
|
+
* Always escalates from the phase's CONFIGURED base, never from a previously
|
|
39
|
+
* escalated value — callers must not accumulate escalation across iterations
|
|
40
|
+
* (AC-6): base `high` on the 3rd loop iteration is `xhigh`, not `max`.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveEscalatedEffort(base, isRetry, enabled) {
|
|
43
|
+
if (!enabled || !isRetry)
|
|
44
|
+
return base;
|
|
45
|
+
const effectiveBase = (base ??
|
|
46
|
+
DEFAULT_ESCALATION_BASE);
|
|
47
|
+
const baseIdx = EFFORT_LEVELS.indexOf(effectiveBase);
|
|
48
|
+
const resolvedIdx = baseIdx === -1 ? EFFORT_LEVELS.indexOf(DEFAULT_ESCALATION_BASE) : baseIdx;
|
|
49
|
+
const nextIdx = Math.min(resolvedIdx + 1, EFFORT_LEVELS.length - 1);
|
|
50
|
+
return EFFORT_LEVELS[nextIdx];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Apply escalation to ONE phase's execution, for THIS dispatch only.
|
|
54
|
+
*
|
|
55
|
+
* This is deliberately a per-execution decision made at the dispatch site,
|
|
56
|
+
* not a value baked into `ExecutionConfig` at build time: `buildExecutionConfig`
|
|
57
|
+
* / `buildPhaseConfig` run once per run/gate, not once per phase execution, so
|
|
58
|
+
* a static escalated value would leak across every phase in the chain and
|
|
59
|
+
* violate AC-7. The three retry-dispatch sites (batch-executor.ts's quality
|
|
60
|
+
* loop, ready-gate.ts's QA-pass loop `qa`/`loop` dispatch) call this function
|
|
61
|
+
* — and only this function — so they cannot drift on the cap/one-tier rules
|
|
62
|
+
* in AC-6 (see resolveEscalatedEffort's doc comment).
|
|
63
|
+
*/
|
|
64
|
+
export function withEscalatedEffort(config, phase, isRetry) {
|
|
65
|
+
if (!config.effortEscalation || !isRetry)
|
|
66
|
+
return { config };
|
|
67
|
+
const currentPolicy = config.phasePolicies?.[phase];
|
|
68
|
+
const base = currentPolicy?.effort;
|
|
69
|
+
const escalated = resolveEscalatedEffort(base, isRetry, true);
|
|
70
|
+
if (!escalated || escalated === base)
|
|
71
|
+
return { config };
|
|
72
|
+
return {
|
|
73
|
+
config: {
|
|
74
|
+
...config,
|
|
75
|
+
phasePolicies: {
|
|
76
|
+
...config.phasePolicies,
|
|
77
|
+
[phase]: { ...currentPolicy, effort: escalated },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
record: { phase, base: base ?? DEFAULT_ESCALATION_BASE, escalated },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -11,9 +11,12 @@ import { SequantError } from "../errors.js";
|
|
|
11
11
|
* `rate_limit` / `billing` (#761 AC-6) only arise from the driver's structured
|
|
12
12
|
* errors — `classifyError` never produces them, since stderr text cannot
|
|
13
13
|
* distinguish a window-exhausted limit from a transient 429 (`api_error`).
|
|
14
|
+
* `pr_creation` (#920) is assigned only at the `deriveFailureCategory` call
|
|
15
|
+
* site, not by `classifyError` — a failed `createPR` has no failing phase to
|
|
16
|
+
* classify, so it would otherwise leave `failureCategory` `undefined`.
|
|
14
17
|
* Keep in sync with the inline category enum in `run-log-schema.ts`.
|
|
15
18
|
*/
|
|
16
|
-
export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "rate_limit", "billing", "unknown"];
|
|
19
|
+
export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "rate_limit", "billing", "pr_creation", "unknown"];
|
|
17
20
|
export type ErrorCategory = (typeof ERROR_CATEGORIES)[number];
|
|
18
21
|
/**
|
|
19
22
|
* Map from error type name to legacy category string.
|
|
@@ -11,6 +11,9 @@ import { ContextOverflowError, ApiError, HookFailureError, BuildError, TimeoutEr
|
|
|
11
11
|
* `rate_limit` / `billing` (#761 AC-6) only arise from the driver's structured
|
|
12
12
|
* errors — `classifyError` never produces them, since stderr text cannot
|
|
13
13
|
* distinguish a window-exhausted limit from a transient 429 (`api_error`).
|
|
14
|
+
* `pr_creation` (#920) is assigned only at the `deriveFailureCategory` call
|
|
15
|
+
* site, not by `classifyError` — a failed `createPR` has no failing phase to
|
|
16
|
+
* classify, so it would otherwise leave `failureCategory` `undefined`.
|
|
14
17
|
* Keep in sync with the inline category enum in `run-log-schema.ts`.
|
|
15
18
|
*/
|
|
16
19
|
export const ERROR_CATEGORIES = [
|
|
@@ -21,6 +24,7 @@ export const ERROR_CATEGORIES = [
|
|
|
21
24
|
"timeout",
|
|
22
25
|
"rate_limit",
|
|
23
26
|
"billing",
|
|
27
|
+
"pr_creation",
|
|
24
28
|
"unknown",
|
|
25
29
|
];
|
|
26
30
|
/**
|
|
@@ -18,18 +18,50 @@ export interface GitDiffStatsResult {
|
|
|
18
18
|
/** Total lines deleted across all files */
|
|
19
19
|
totalDeletions: number;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the ref diff stats should compare against (#878).
|
|
23
|
+
*
|
|
24
|
+
* Worktrees are created from `origin/<base>` (worktree-manager), but callers
|
|
25
|
+
* historically passed the bare branch name and the diff ran against the
|
|
26
|
+
* *local* ref. When local `<base>` lags the remote — routine, since nothing
|
|
27
|
+
* in the run path updates it — `<base>...HEAD` attributes commits the run
|
|
28
|
+
* never made (phantom filesModified).
|
|
29
|
+
*
|
|
30
|
+
* Candidates are the origin-qualified and bare forms of `baseBranch`
|
|
31
|
+
* (already-remote-qualified input keeps a single candidate). Among the
|
|
32
|
+
* candidates that resolve to a commit, pick the one nearest to HEAD
|
|
33
|
+
* (smallest `rev-list --count <cand>..HEAD`), preferring the
|
|
34
|
+
* origin-qualified form on a tie. Nearest-wins matches the worktree's
|
|
35
|
+
* actual creation point in both staleness directions: a stale local
|
|
36
|
+
* default branch (this issue) and a chain-mode worktree branched from a
|
|
37
|
+
* local base that is ahead of its pushed counterpart. Falls back to
|
|
38
|
+
* `baseBranch` verbatim when no candidate resolves (e.g. remote-less repo
|
|
39
|
+
* with a missing branch) — the diff then fails gracefully to empty, the
|
|
40
|
+
* pre-#878 behavior.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveDiffBase(worktreePath: string, baseBranch: string): string;
|
|
21
43
|
/**
|
|
22
44
|
* Get git commit SHA for a worktree (AC-2)
|
|
23
45
|
*
|
|
46
|
+
* When `baseRef` is provided, returns undefined if HEAD has no commits
|
|
47
|
+
* unique to it (#878) — a branch that never moved off its base would
|
|
48
|
+
* otherwise log the base tip as if it were the phase's commit. Callers
|
|
49
|
+
* recording plain "where is HEAD" markers (run start/end) omit `baseRef`.
|
|
50
|
+
*
|
|
24
51
|
* @param worktreePath - Path to the git worktree
|
|
25
|
-
* @
|
|
52
|
+
* @param baseRef - Optional resolved base ref (see resolveDiffBase); when
|
|
53
|
+
* given, a HEAD with zero commits past it yields undefined
|
|
54
|
+
* @returns The current HEAD commit SHA, or undefined on error / no unique
|
|
55
|
+
* commits
|
|
26
56
|
*/
|
|
27
|
-
export declare function getCommitHash(worktreePath: string): string | undefined;
|
|
57
|
+
export declare function getCommitHash(worktreePath: string, baseRef?: string): string | undefined;
|
|
28
58
|
/**
|
|
29
59
|
* Get git diff statistics for a worktree (AC-1, AC-3, AC-4)
|
|
30
60
|
*
|
|
31
61
|
* Efficiently captures both filesModified and fileDiffStats using
|
|
32
|
-
* minimal git commands.
|
|
62
|
+
* minimal git commands. The base is resolved via resolveDiffBase (#878) so
|
|
63
|
+
* the comparison targets the ref the worktree was actually created from
|
|
64
|
+
* (origin/<base> in the common case) rather than a possibly-stale local ref.
|
|
33
65
|
*
|
|
34
66
|
* @param worktreePath - Path to the git worktree
|
|
35
67
|
* @param baseBranch - Branch to compare against (default: "main")
|
|
@@ -73,13 +73,73 @@ function parseNameStatus(output) {
|
|
|
73
73
|
}
|
|
74
74
|
return result;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the ref diff stats should compare against (#878).
|
|
78
|
+
*
|
|
79
|
+
* Worktrees are created from `origin/<base>` (worktree-manager), but callers
|
|
80
|
+
* historically passed the bare branch name and the diff ran against the
|
|
81
|
+
* *local* ref. When local `<base>` lags the remote — routine, since nothing
|
|
82
|
+
* in the run path updates it — `<base>...HEAD` attributes commits the run
|
|
83
|
+
* never made (phantom filesModified).
|
|
84
|
+
*
|
|
85
|
+
* Candidates are the origin-qualified and bare forms of `baseBranch`
|
|
86
|
+
* (already-remote-qualified input keeps a single candidate). Among the
|
|
87
|
+
* candidates that resolve to a commit, pick the one nearest to HEAD
|
|
88
|
+
* (smallest `rev-list --count <cand>..HEAD`), preferring the
|
|
89
|
+
* origin-qualified form on a tie. Nearest-wins matches the worktree's
|
|
90
|
+
* actual creation point in both staleness directions: a stale local
|
|
91
|
+
* default branch (this issue) and a chain-mode worktree branched from a
|
|
92
|
+
* local base that is ahead of its pushed counterpart. Falls back to
|
|
93
|
+
* `baseBranch` verbatim when no candidate resolves (e.g. remote-less repo
|
|
94
|
+
* with a missing branch) — the diff then fails gracefully to empty, the
|
|
95
|
+
* pre-#878 behavior.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveDiffBase(worktreePath, baseBranch) {
|
|
98
|
+
const candidates = baseBranch.startsWith("origin/")
|
|
99
|
+
? [baseBranch]
|
|
100
|
+
: [`origin/${baseBranch}`, baseBranch];
|
|
101
|
+
let best;
|
|
102
|
+
let bestCount = Number.POSITIVE_INFINITY;
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
const verify = spawnSync("git", [
|
|
105
|
+
"-C",
|
|
106
|
+
worktreePath,
|
|
107
|
+
"rev-parse",
|
|
108
|
+
"--verify",
|
|
109
|
+
"--quiet",
|
|
110
|
+
`${candidate}^{commit}`,
|
|
111
|
+
], { stdio: "pipe", encoding: "utf-8" });
|
|
112
|
+
if (verify.status !== 0)
|
|
113
|
+
continue;
|
|
114
|
+
const count = spawnSync("git", ["-C", worktreePath, "rev-list", "--count", `${candidate}..HEAD`], { stdio: "pipe", encoding: "utf-8" });
|
|
115
|
+
if (count.status !== 0)
|
|
116
|
+
continue;
|
|
117
|
+
const n = Number.parseInt(count.stdout.trim(), 10);
|
|
118
|
+
if (Number.isNaN(n))
|
|
119
|
+
continue;
|
|
120
|
+
// Strict < keeps the earlier (origin-qualified) candidate on a tie.
|
|
121
|
+
if (n < bestCount) {
|
|
122
|
+
best = candidate;
|
|
123
|
+
bestCount = n;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return best ?? baseBranch;
|
|
127
|
+
}
|
|
76
128
|
/**
|
|
77
129
|
* Get git commit SHA for a worktree (AC-2)
|
|
78
130
|
*
|
|
131
|
+
* When `baseRef` is provided, returns undefined if HEAD has no commits
|
|
132
|
+
* unique to it (#878) — a branch that never moved off its base would
|
|
133
|
+
* otherwise log the base tip as if it were the phase's commit. Callers
|
|
134
|
+
* recording plain "where is HEAD" markers (run start/end) omit `baseRef`.
|
|
135
|
+
*
|
|
79
136
|
* @param worktreePath - Path to the git worktree
|
|
80
|
-
* @
|
|
137
|
+
* @param baseRef - Optional resolved base ref (see resolveDiffBase); when
|
|
138
|
+
* given, a HEAD with zero commits past it yields undefined
|
|
139
|
+
* @returns The current HEAD commit SHA, or undefined on error / no unique
|
|
140
|
+
* commits
|
|
81
141
|
*/
|
|
82
|
-
export function getCommitHash(worktreePath) {
|
|
142
|
+
export function getCommitHash(worktreePath, baseRef) {
|
|
83
143
|
const result = spawnSync("git", ["-C", worktreePath, "rev-parse", "HEAD"], {
|
|
84
144
|
stdio: "pipe",
|
|
85
145
|
encoding: "utf-8",
|
|
@@ -87,20 +147,33 @@ export function getCommitHash(worktreePath) {
|
|
|
87
147
|
if (result.status !== 0) {
|
|
88
148
|
return undefined;
|
|
89
149
|
}
|
|
150
|
+
if (baseRef !== undefined) {
|
|
151
|
+
const count = spawnSync("git", ["-C", worktreePath, "rev-list", "--count", `${baseRef}..HEAD`], { stdio: "pipe", encoding: "utf-8" });
|
|
152
|
+
// Fail open on git errors: a transient failure should not erase a real
|
|
153
|
+
// commit hash from the log — only a confirmed zero suppresses it.
|
|
154
|
+
if (count.status === 0) {
|
|
155
|
+
const n = Number.parseInt(count.stdout.trim(), 10);
|
|
156
|
+
if (n === 0) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
90
161
|
return result.stdout.trim();
|
|
91
162
|
}
|
|
92
163
|
/**
|
|
93
164
|
* Get git diff statistics for a worktree (AC-1, AC-3, AC-4)
|
|
94
165
|
*
|
|
95
166
|
* Efficiently captures both filesModified and fileDiffStats using
|
|
96
|
-
* minimal git commands.
|
|
167
|
+
* minimal git commands. The base is resolved via resolveDiffBase (#878) so
|
|
168
|
+
* the comparison targets the ref the worktree was actually created from
|
|
169
|
+
* (origin/<base> in the common case) rather than a possibly-stale local ref.
|
|
97
170
|
*
|
|
98
171
|
* @param worktreePath - Path to the git worktree
|
|
99
172
|
* @param baseBranch - Branch to compare against (default: "main")
|
|
100
173
|
* @returns GitDiffStatsResult with files, stats, and totals
|
|
101
174
|
*/
|
|
102
175
|
export function getGitDiffStats(worktreePath, baseBranch = "main") {
|
|
103
|
-
const diffRef = `${baseBranch}...HEAD`;
|
|
176
|
+
const diffRef = `${resolveDiffBase(worktreePath, baseBranch)}...HEAD`;
|
|
104
177
|
// Get numstat for additions/deletions
|
|
105
178
|
const numstatResult = spawnSync("git", ["-C", worktreePath, "diff", "--numstat", diffRef], { stdio: "pipe", encoding: "utf-8" });
|
|
106
179
|
// Get name-status for file status (added/modified/deleted/renamed)
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { ShutdownManager } from "../shutdown.js";
|
|
2
|
+
/**
|
|
3
|
+
* Cadence of the non-TTY auto-wait notice (#860 AC-6). Non-TTY output is
|
|
4
|
+
* append-only (no `\r` rewrite), so the TTY heartbeat's every-tick refresh
|
|
5
|
+
* would log ~600 lines over a five-hour wait. Five minutes keeps an unattended
|
|
6
|
+
* log legibly alive (~60 lines per five-hour window) without drowning it.
|
|
7
|
+
*
|
|
8
|
+
* @internal Exported for testing only
|
|
9
|
+
*/
|
|
10
|
+
export declare const NON_TTY_WAIT_NOTICE_INTERVAL_MS: number;
|
|
2
11
|
export interface LivenessHeartbeatOptions {
|
|
3
12
|
/** Polling cadence for heartbeat ticks. Default: 30_000ms */
|
|
4
13
|
pollIntervalMs?: number;
|
|
@@ -60,10 +69,39 @@ export declare class LivenessHeartbeat {
|
|
|
60
69
|
* Dispose all tracked phases and clear the timer. Idempotent.
|
|
61
70
|
*/
|
|
62
71
|
dispose(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Mark a phase as auto-waiting for a rate-limit window to reopen (#804 AC-7).
|
|
74
|
+
*
|
|
75
|
+
* This is not cosmetic. The stall detector's activity proxy is the mtime of
|
|
76
|
+
* `.sequant/state.json`, and an auto-wait produces no writes by definition —
|
|
77
|
+
* so a multi-hour wait would otherwise trip the "no log activity" warning
|
|
78
|
+
* within minutes and report an entirely expected pause as a stall. Marking
|
|
79
|
+
* the phase both relabels the heartbeat line with the wake time and
|
|
80
|
+
* suppresses that false warning.
|
|
81
|
+
*
|
|
82
|
+
* No-op for an untracked phase, so an out-of-order or late notice cannot
|
|
83
|
+
* resurrect a stopped entry.
|
|
84
|
+
*/
|
|
85
|
+
pauseForWait(key: PhaseKey, wakeAtMs: number): void;
|
|
86
|
+
/** Clear the auto-wait marker set by {@link pauseForWait} (#804). */
|
|
87
|
+
resumeFromWait(key: PhaseKey): void;
|
|
63
88
|
/** Test hook: drive a poll synchronously without waiting on real timers. */
|
|
64
89
|
tickNow(): void;
|
|
65
90
|
private tick;
|
|
66
91
|
private writeHeartbeat;
|
|
92
|
+
/**
|
|
93
|
+
* Heartbeat line for a phase paused on an auto-wait (#804). Names the wake
|
|
94
|
+
* time and the remaining wait so a multi-hour pause is legibly deliberate
|
|
95
|
+
* rather than indistinguishable from a hang.
|
|
96
|
+
*/
|
|
97
|
+
private writeWaitHeartbeat;
|
|
98
|
+
/**
|
|
99
|
+
* Append-only auto-wait notice for non-TTY runs (#860 AC-6). Same facts as
|
|
100
|
+
* {@link writeWaitHeartbeat} — wake time and remaining wait — but a plain
|
|
101
|
+
* `\n`-terminated line with no cursor control, throttled by the caller to
|
|
102
|
+
* {@link NON_TTY_WAIT_NOTICE_INTERVAL_MS} so an unattended log stays legible.
|
|
103
|
+
*/
|
|
104
|
+
private writeNonTtyWaitNotice;
|
|
67
105
|
private writeStallWarning;
|
|
68
106
|
}
|
|
69
107
|
/** Convenience factory mirroring `phaseSpinner()`. */
|
|
@@ -12,10 +12,20 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from "fs";
|
|
14
14
|
import { formatElapsedTime } from "../cli-ui/format.js";
|
|
15
|
+
import { formatResetTime } from "../errors.js";
|
|
15
16
|
const DEFAULT_POLL_INTERVAL_MS = 30_000;
|
|
16
17
|
const DEFAULT_STALL_THRESHOLD_MS = 5 * 60_000;
|
|
17
18
|
const DEFAULT_LIVENESS_FILE = ".sequant/state.json";
|
|
18
19
|
const CLEANUP_NAME = "liveness-heartbeat";
|
|
20
|
+
/**
|
|
21
|
+
* Cadence of the non-TTY auto-wait notice (#860 AC-6). Non-TTY output is
|
|
22
|
+
* append-only (no `\r` rewrite), so the TTY heartbeat's every-tick refresh
|
|
23
|
+
* would log ~600 lines over a five-hour wait. Five minutes keeps an unattended
|
|
24
|
+
* log legibly alive (~60 lines per five-hour window) without drowning it.
|
|
25
|
+
*
|
|
26
|
+
* @internal Exported for testing only
|
|
27
|
+
*/
|
|
28
|
+
export const NON_TTY_WAIT_NOTICE_INTERVAL_MS = 5 * 60_000;
|
|
19
29
|
function keyFor(k) {
|
|
20
30
|
return `${k.issueNumber}:${k.phase}`;
|
|
21
31
|
}
|
|
@@ -118,6 +128,39 @@ export class LivenessHeartbeat {
|
|
|
118
128
|
this.cleanupRegistered = false;
|
|
119
129
|
}
|
|
120
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Mark a phase as auto-waiting for a rate-limit window to reopen (#804 AC-7).
|
|
133
|
+
*
|
|
134
|
+
* This is not cosmetic. The stall detector's activity proxy is the mtime of
|
|
135
|
+
* `.sequant/state.json`, and an auto-wait produces no writes by definition —
|
|
136
|
+
* so a multi-hour wait would otherwise trip the "no log activity" warning
|
|
137
|
+
* within minutes and report an entirely expected pause as a stall. Marking
|
|
138
|
+
* the phase both relabels the heartbeat line with the wake time and
|
|
139
|
+
* suppresses that false warning.
|
|
140
|
+
*
|
|
141
|
+
* No-op for an untracked phase, so an out-of-order or late notice cannot
|
|
142
|
+
* resurrect a stopped entry.
|
|
143
|
+
*/
|
|
144
|
+
pauseForWait(key, wakeAtMs) {
|
|
145
|
+
const entry = this.phases.get(keyFor(key));
|
|
146
|
+
if (!entry)
|
|
147
|
+
return;
|
|
148
|
+
entry.waitingUntil = wakeAtMs;
|
|
149
|
+
// Clear any warning already fired so a genuine stall after the wait can
|
|
150
|
+
// still warn once.
|
|
151
|
+
entry.warningFired = false;
|
|
152
|
+
// #860 AC-6: a fresh wait announces itself on the next non-TTY tick.
|
|
153
|
+
entry.lastWaitNoticeAt = undefined;
|
|
154
|
+
}
|
|
155
|
+
/** Clear the auto-wait marker set by {@link pauseForWait} (#804). */
|
|
156
|
+
resumeFromWait(key) {
|
|
157
|
+
const entry = this.phases.get(keyFor(key));
|
|
158
|
+
if (!entry)
|
|
159
|
+
return;
|
|
160
|
+
entry.waitingUntil = undefined;
|
|
161
|
+
entry.warningFired = false;
|
|
162
|
+
entry.lastWaitNoticeAt = undefined;
|
|
163
|
+
}
|
|
121
164
|
/** Test hook: drive a poll synchronously without waiting on real timers. */
|
|
122
165
|
tickNow() {
|
|
123
166
|
this.tick();
|
|
@@ -127,6 +170,23 @@ export class LivenessHeartbeat {
|
|
|
127
170
|
return;
|
|
128
171
|
const now = this.now();
|
|
129
172
|
for (const entry of this.phases.values()) {
|
|
173
|
+
// #804: an auto-waiting phase has its own liveness story. Handled before
|
|
174
|
+
// the mtime read because the wait produces no file activity at all — the
|
|
175
|
+
// stall detector's proxy is meaningless here, and the `mtimeMs === null`
|
|
176
|
+
// early-continue below would otherwise leave the wait completely silent.
|
|
177
|
+
if (entry.waitingUntil !== undefined) {
|
|
178
|
+
if (this.tty) {
|
|
179
|
+
this.writeWaitHeartbeat(entry, Math.max(0, now - entry.startedAt));
|
|
180
|
+
}
|
|
181
|
+
else if (entry.lastWaitNoticeAt === undefined ||
|
|
182
|
+
now - entry.lastWaitNoticeAt >= NON_TTY_WAIT_NOTICE_INTERVAL_MS) {
|
|
183
|
+
// #860 AC-6: without this, a non-TTY wait emits nothing for hours —
|
|
184
|
+
// indistinguishable from the #856 hang. Append-only line, throttled.
|
|
185
|
+
this.writeNonTtyWaitNotice(entry);
|
|
186
|
+
entry.lastWaitNoticeAt = now;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
130
190
|
let mtimeMs;
|
|
131
191
|
try {
|
|
132
192
|
const stat = fs.statSync(this.livenessFile);
|
|
@@ -168,6 +228,36 @@ export class LivenessHeartbeat {
|
|
|
168
228
|
const line = `\r ▸ #${entry.issueNumber} ${entry.phase} (${elapsed} elapsed, last log update ${sinceActivity} ago)[K`;
|
|
169
229
|
this.stdoutWrite(line);
|
|
170
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Heartbeat line for a phase paused on an auto-wait (#804). Names the wake
|
|
233
|
+
* time and the remaining wait so a multi-hour pause is legibly deliberate
|
|
234
|
+
* rather than indistinguishable from a hang.
|
|
235
|
+
*/
|
|
236
|
+
writeWaitHeartbeat(entry, elapsedSinceStartMs) {
|
|
237
|
+
if (this.stopped || entry.waitingUntil === undefined)
|
|
238
|
+
return;
|
|
239
|
+
const elapsed = formatElapsedTime(Math.floor(elapsedSinceStartMs / 1000));
|
|
240
|
+
const remainingMs = Math.max(0, entry.waitingUntil - this.now());
|
|
241
|
+
const remaining = formatElapsedTime(Math.floor(remainingMs / 1000));
|
|
242
|
+
const wake = formatResetTime(entry.waitingUntil);
|
|
243
|
+
const line = `\r ⏸ #${entry.issueNumber} ${entry.phase} (${elapsed} elapsed, rate-limit window — resuming at ${wake}, ${remaining} left)\x1b[K`;
|
|
244
|
+
this.stdoutWrite(line);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Append-only auto-wait notice for non-TTY runs (#860 AC-6). Same facts as
|
|
248
|
+
* {@link writeWaitHeartbeat} — wake time and remaining wait — but a plain
|
|
249
|
+
* `\n`-terminated line with no cursor control, throttled by the caller to
|
|
250
|
+
* {@link NON_TTY_WAIT_NOTICE_INTERVAL_MS} so an unattended log stays legible.
|
|
251
|
+
*/
|
|
252
|
+
writeNonTtyWaitNotice(entry) {
|
|
253
|
+
if (this.stopped || entry.waitingUntil === undefined)
|
|
254
|
+
return;
|
|
255
|
+
const remainingMs = Math.max(0, entry.waitingUntil - this.now());
|
|
256
|
+
const remaining = formatElapsedTime(Math.floor(remainingMs / 1000));
|
|
257
|
+
const wake = formatResetTime(entry.waitingUntil);
|
|
258
|
+
const line = ` ⏸ #${entry.issueNumber} ${entry.phase} rate-limit window — resuming at ${wake} (${remaining} left)\n`;
|
|
259
|
+
this.stdoutWrite(line);
|
|
260
|
+
}
|
|
171
261
|
writeStallWarning(entry, sinceActivityMs, elapsedSinceStartMs) {
|
|
172
262
|
if (this.stopped)
|
|
173
263
|
return;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* await writer.finalize();
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
|
-
import { type RunLog, type RunConfig, type PhaseLog, type Phase } from "./run-log-schema.js";
|
|
16
|
+
import { type RunLog, type RunConfig, type PhaseLog, type Phase, type SpecRecommendation } from "./run-log-schema.js";
|
|
17
17
|
import { type RotationSettings } from "./log-rotation.js";
|
|
18
18
|
export interface LogWriterOptions {
|
|
19
19
|
/** Path to log directory (default: .sequant/logs in current directory) */
|
|
@@ -26,6 +26,13 @@ export interface LogWriterOptions {
|
|
|
26
26
|
rotation?: RotationSettings;
|
|
27
27
|
/** Git commit SHA at run start (AC-2) */
|
|
28
28
|
startCommit?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Run start timestamp (#867). When provided, the log's `startTime` uses this
|
|
31
|
+
* origin instead of self-stamping `new Date()` at initialize(), so the log's
|
|
32
|
+
* stored wall clock and the orchestrator's summary derive from one shared
|
|
33
|
+
* start. Defaults to now when omitted.
|
|
34
|
+
*/
|
|
35
|
+
startTime?: Date;
|
|
29
36
|
}
|
|
30
37
|
/**
|
|
31
38
|
* Manages writing structured run logs to disk
|
|
@@ -36,11 +43,20 @@ export declare class LogWriter {
|
|
|
36
43
|
private activeIssues;
|
|
37
44
|
/** @deprecated Single-issue slot for backwards compatibility — use activeIssues */
|
|
38
45
|
private currentIssue;
|
|
46
|
+
/**
|
|
47
|
+
* Issue slots forced to `failure` by {@link markIssueFailed} (#879). Keyed on
|
|
48
|
+
* the slot object so it covers both the numbered and legacy `currentIssue`
|
|
49
|
+
* paths. `completeIssue` re-derives status from phases (#856), so a seeded
|
|
50
|
+
* `issue.status = "failure"` alone would be overwritten back to `success`;
|
|
51
|
+
* this marker survives that re-derivation.
|
|
52
|
+
*/
|
|
53
|
+
private forcedFailures;
|
|
39
54
|
private logPath;
|
|
40
55
|
private writeToUserLogs;
|
|
41
56
|
private verbose;
|
|
42
57
|
private rotation;
|
|
43
58
|
private startCommit?;
|
|
59
|
+
private startTime?;
|
|
44
60
|
constructor(options?: LogWriterOptions);
|
|
45
61
|
/**
|
|
46
62
|
* Initialize a new run log
|
|
@@ -66,10 +82,37 @@ export declare class LogWriter {
|
|
|
66
82
|
* Set PR info on the current issue (call before completeIssue)
|
|
67
83
|
*/
|
|
68
84
|
setPRInfo(prNumber: number, prUrl: string, issueNumber?: number): void;
|
|
85
|
+
/**
|
|
86
|
+
* Record how the spec→run phase recommendation was resolved (#921 AC-4).
|
|
87
|
+
*
|
|
88
|
+
* Called right after `resolveSpecRecommendation` runs, before the next
|
|
89
|
+
* phase starts — mirrors {@link setPRInfo}'s post-hoc-setter shape because
|
|
90
|
+
* the spec `PhaseLog` is already written by the time resolution (which does
|
|
91
|
+
* its own comment-fetch I/O) completes.
|
|
92
|
+
*/
|
|
93
|
+
setSpecRecommendation(recommendation: SpecRecommendation, issueNumber?: number): void;
|
|
94
|
+
/**
|
|
95
|
+
* Force the in-flight issue's status to `failure` (#879).
|
|
96
|
+
*
|
|
97
|
+
* `deriveIssueLogStatus` runs at phase-log time, so an issue whose phases all
|
|
98
|
+
* passed but whose PR creation then failed would otherwise be recorded as
|
|
99
|
+
* `success`. Call this after the last phase is logged and before
|
|
100
|
+
* {@link completeIssue} to count it under `failed`. No-op if the issue is not
|
|
101
|
+
* active. Reuses the existing `failure` enum — no schema change.
|
|
102
|
+
*/
|
|
103
|
+
markIssueFailed(issueNumber?: number): void;
|
|
69
104
|
/**
|
|
70
105
|
* Complete the current issue and add it to the run log
|
|
106
|
+
*
|
|
107
|
+
* @param issueNumber - Issue to complete (defaults to the legacy single slot)
|
|
108
|
+
* @param abort - Set when the run is being torn down by an external signal
|
|
109
|
+
* (#856). Marks the issue aborted with its cause instead of persisting
|
|
110
|
+
* whatever verdict the incomplete phase list happens to imply.
|
|
71
111
|
*/
|
|
72
|
-
completeIssue(issueNumber?: number
|
|
112
|
+
completeIssue(issueNumber?: number, abort?: {
|
|
113
|
+
signal: string;
|
|
114
|
+
reason: string;
|
|
115
|
+
}): void;
|
|
73
116
|
/**
|
|
74
117
|
* Finalize the run log and write to disk
|
|
75
118
|
*
|
|
@@ -77,10 +120,19 @@ export declare class LogWriter {
|
|
|
77
120
|
*
|
|
78
121
|
* @param options - Optional finalization options
|
|
79
122
|
* @param options.endCommit - Git commit SHA at run end (AC-2)
|
|
123
|
+
* @param options.aborted - Set when finalizing because the run was
|
|
124
|
+
* terminated by an external signal (#856). Every still-in-flight issue is
|
|
125
|
+
* recorded as an abort naming its cause, and the run log carries
|
|
126
|
+
* `abortedBy`. Without it, a killed run's log is indistinguishable from a
|
|
127
|
+
* clean one that happened to do nothing.
|
|
80
128
|
* @returns Path to the written log file
|
|
81
129
|
*/
|
|
82
130
|
finalize(options?: {
|
|
83
131
|
endCommit?: string;
|
|
132
|
+
aborted?: {
|
|
133
|
+
signal: string;
|
|
134
|
+
reason: string;
|
|
135
|
+
};
|
|
84
136
|
}): Promise<string>;
|
|
85
137
|
/**
|
|
86
138
|
* Get the current run log (for inspection)
|