sequant 2.2.0 → 2.3.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 +73 -0
- package/dist/bin/cli.js +94 -9
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- package/dist/src/commands/locks.d.ts +67 -0
- package/dist/src/commands/locks.js +290 -0
- package/dist/src/commands/merge.js +11 -0
- package/dist/src/commands/prompt.d.ts +39 -0
- package/dist/src/commands/prompt.js +179 -0
- package/dist/src/commands/run-display.d.ts +11 -2
- package/dist/src/commands/run-display.js +62 -28
- package/dist/src/commands/run-progress.d.ts +32 -0
- package/dist/src/commands/run-progress.js +76 -0
- package/dist/src/commands/run.js +80 -18
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +12 -0
- package/dist/src/commands/watch.d.ts +16 -0
- package/dist/src/commands/watch.js +147 -0
- package/dist/src/lib/ac-linter.d.ts +1 -1
- package/dist/src/lib/ac-linter.js +81 -0
- package/dist/src/lib/assess-collision-detect.d.ts +91 -0
- package/dist/src/lib/assess-collision-detect.js +217 -0
- package/dist/src/lib/assess-comment-parser.d.ts +59 -1
- package/dist/src/lib/assess-comment-parser.js +124 -2
- package/dist/src/lib/cli-ui/format.d.ts +19 -0
- package/dist/src/lib/cli-ui/format.js +34 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
- package/dist/src/lib/locks/index.d.ts +7 -0
- package/dist/src/lib/locks/index.js +5 -0
- package/dist/src/lib/locks/lock-manager.d.ts +168 -0
- package/dist/src/lib/locks/lock-manager.js +433 -0
- package/dist/src/lib/locks/types.d.ts +59 -0
- package/dist/src/lib/locks/types.js +31 -0
- package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
- package/dist/src/lib/qa/markdown-only-ci.js +74 -0
- package/dist/src/lib/relay/activation.d.ts +60 -0
- package/dist/src/lib/relay/activation.js +122 -0
- package/dist/src/lib/relay/archive.d.ts +34 -0
- package/dist/src/lib/relay/archive.js +106 -0
- package/dist/src/lib/relay/frame.d.ts +20 -0
- package/dist/src/lib/relay/frame.js +76 -0
- package/dist/src/lib/relay/index.d.ts +13 -0
- package/dist/src/lib/relay/index.js +13 -0
- package/dist/src/lib/relay/paths.d.ts +43 -0
- package/dist/src/lib/relay/paths.js +59 -0
- package/dist/src/lib/relay/pid.d.ts +34 -0
- package/dist/src/lib/relay/pid.js +72 -0
- package/dist/src/lib/relay/reader.d.ts +35 -0
- package/dist/src/lib/relay/reader.js +115 -0
- package/dist/src/lib/relay/types.d.ts +68 -0
- package/dist/src/lib/relay/types.js +76 -0
- package/dist/src/lib/relay/writer.d.ts +48 -0
- package/dist/src/lib/relay/writer.js +113 -0
- package/dist/src/lib/settings.d.ts +31 -1
- package/dist/src/lib/settings.js +18 -3
- package/dist/src/lib/version-check.d.ts +60 -5
- package/dist/src/lib/version-check.js +97 -9
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +248 -175
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
- package/dist/src/lib/workflow/heartbeat.js +194 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +62 -8
- package/dist/src/lib/workflow/phase-executor.js +157 -16
- package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
- package/dist/src/lib/workflow/phase-mapper.js +17 -20
- package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
- package/dist/src/lib/workflow/platforms/github.js +20 -3
- package/dist/src/lib/workflow/pr-status.d.ts +18 -2
- package/dist/src/lib/workflow/pr-status.js +41 -9
- package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
- package/dist/src/lib/workflow/qa-stagnation.js +179 -0
- package/dist/src/lib/workflow/run-orchestrator.d.ts +39 -0
- package/dist/src/lib/workflow/run-orchestrator.js +340 -15
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/run-state.d.ts +71 -0
- package/dist/src/lib/workflow/run-state.js +14 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
- package/dist/src/lib/workflow/state-cleanup.js +17 -5
- package/dist/src/lib/workflow/state-manager.d.ts +12 -1
- package/dist/src/lib/workflow/state-manager.js +37 -0
- package/dist/src/lib/workflow/state-schema.d.ts +62 -0
- package/dist/src/lib/workflow/state-schema.js +35 -1
- package/dist/src/lib/workflow/types.d.ts +74 -1
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +15 -6
- package/dist/src/mcp/tools/run.d.ts +44 -0
- package/dist/src/mcp/tools/run.js +104 -13
- package/dist/src/ui/tui/App.d.ts +14 -0
- package/dist/src/ui/tui/App.js +41 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
- package/dist/src/ui/tui/ElapsedTimer.js +31 -0
- package/dist/src/ui/tui/Header.d.ts +6 -0
- package/dist/src/ui/tui/Header.js +15 -0
- package/dist/src/ui/tui/IssueBox.d.ts +16 -0
- package/dist/src/ui/tui/IssueBox.js +68 -0
- package/dist/src/ui/tui/Spinner.d.ts +9 -0
- package/dist/src/ui/tui/Spinner.js +18 -0
- package/dist/src/ui/tui/index.d.ts +15 -0
- package/dist/src/ui/tui/index.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +29 -0
- package/dist/src/ui/tui/theme.js +52 -0
- package/dist/src/ui/tui/truncate.d.ts +11 -0
- package/dist/src/ui/tui/truncate.js +31 -0
- package/package.json +10 -3
- package/templates/agents/sequant-explorer.md +1 -0
- package/templates/agents/sequant-qa-checker.md +2 -1
- package/templates/agents/sequant-testgen.md +1 -0
- package/templates/hooks/post-tool.sh +11 -0
- package/templates/hooks/pre-tool.sh +18 -9
- package/templates/hooks/relay-check.sh +107 -0
- package/templates/relay/frame.txt +11 -0
- package/templates/scripts/cleanup-worktree.sh +25 -3
- package/templates/scripts/new-feature.sh +6 -0
- package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/templates/skills/_shared/references/subagent-types.md +21 -8
- package/templates/skills/assess/SKILL.md +103 -49
- package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
- package/templates/skills/docs/SKILL.md +141 -22
- package/templates/skills/exec/SKILL.md +10 -8
- package/templates/skills/fullsolve/SKILL.md +79 -5
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +727 -8
- package/templates/skills/setup/SKILL.md +6 -0
- package/templates/skills/spec/SKILL.md +52 -0
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- package/templates/skills/testgen/SKILL.md +24 -17
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown-only CI relaxation helpers used by the `/qa` verdict algorithm.
|
|
3
|
+
*
|
|
4
|
+
* When a diff touches only documentation/markdown files (no source, no
|
|
5
|
+
* configuration that affects builds), pending CI checks like the build matrix
|
|
6
|
+
* cannot meaningfully fail. `/qa` uses these helpers to detect such diffs and
|
|
7
|
+
* partition pending CI checks into a "relaxed" bucket (informational, does not
|
|
8
|
+
* gate the verdict) and a "gating" bucket (still gates `READY_FOR_MERGE`).
|
|
9
|
+
*
|
|
10
|
+
* Failed CI checks are NOT relaxed — they always gate regardless of diff type.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Predicate: does the given changed-file list qualify as a markdown-only diff?
|
|
14
|
+
*
|
|
15
|
+
* A file qualifies only if it ends in `.md` (case-insensitive) AND is not
|
|
16
|
+
* inside `.github/workflows/`. Any non-`.md` file (including `package.json`,
|
|
17
|
+
* `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `tsconfig*.json`, and
|
|
18
|
+
* `*.config.{js,ts,mjs,cjs}`) automatically disqualifies the diff because it
|
|
19
|
+
* does not end in `.md`.
|
|
20
|
+
*
|
|
21
|
+
* @param files - Paths from `git diff --name-only`. Forward slashes; relative to repo root.
|
|
22
|
+
* @returns `true` if every changed file is markdown and none are workflow files.
|
|
23
|
+
*/
|
|
24
|
+
export declare function detectMarkdownOnlyDiff(files: string[]): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Result of partitioning pending CI checks against the safe-pattern allowlist.
|
|
27
|
+
*/
|
|
28
|
+
export interface RelaxedPendingResult {
|
|
29
|
+
/** Pending check names that match a safe pattern — informational only. */
|
|
30
|
+
relaxed: string[];
|
|
31
|
+
/** Pending check names that do NOT match a safe pattern — still gate the verdict. */
|
|
32
|
+
gating: string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Partition a list of pending CI check names into "relaxed" (allowlisted) and
|
|
36
|
+
* "gating" (everything else).
|
|
37
|
+
*
|
|
38
|
+
* Patterns support a single `*` wildcard which matches any sequence of
|
|
39
|
+
* characters (greedy). Special regex characters are otherwise escaped, so
|
|
40
|
+
* patterns like `build (*)` or `Plugin Structure Validation` work as written.
|
|
41
|
+
*
|
|
42
|
+
* @param pendingCheckNames - Names of CI checks currently in a pending state.
|
|
43
|
+
* @param safePatterns - Glob-like patterns for safe-to-ignore checks.
|
|
44
|
+
* @returns Buckets for relaxed and gating checks.
|
|
45
|
+
*/
|
|
46
|
+
export declare function filterRelaxablePending(pendingCheckNames: string[], safePatterns: string[]): RelaxedPendingResult;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown-only CI relaxation helpers used by the `/qa` verdict algorithm.
|
|
3
|
+
*
|
|
4
|
+
* When a diff touches only documentation/markdown files (no source, no
|
|
5
|
+
* configuration that affects builds), pending CI checks like the build matrix
|
|
6
|
+
* cannot meaningfully fail. `/qa` uses these helpers to detect such diffs and
|
|
7
|
+
* partition pending CI checks into a "relaxed" bucket (informational, does not
|
|
8
|
+
* gate the verdict) and a "gating" bucket (still gates `READY_FOR_MERGE`).
|
|
9
|
+
*
|
|
10
|
+
* Failed CI checks are NOT relaxed — they always gate regardless of diff type.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Predicate: does the given changed-file list qualify as a markdown-only diff?
|
|
14
|
+
*
|
|
15
|
+
* A file qualifies only if it ends in `.md` (case-insensitive) AND is not
|
|
16
|
+
* inside `.github/workflows/`. Any non-`.md` file (including `package.json`,
|
|
17
|
+
* `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `tsconfig*.json`, and
|
|
18
|
+
* `*.config.{js,ts,mjs,cjs}`) automatically disqualifies the diff because it
|
|
19
|
+
* does not end in `.md`.
|
|
20
|
+
*
|
|
21
|
+
* @param files - Paths from `git diff --name-only`. Forward slashes; relative to repo root.
|
|
22
|
+
* @returns `true` if every changed file is markdown and none are workflow files.
|
|
23
|
+
*/
|
|
24
|
+
export function detectMarkdownOnlyDiff(files) {
|
|
25
|
+
if (files.length === 0)
|
|
26
|
+
return false;
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
if (!file.toLowerCase().endsWith(".md"))
|
|
29
|
+
return false;
|
|
30
|
+
if (file.startsWith(".github/workflows/"))
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Partition a list of pending CI check names into "relaxed" (allowlisted) and
|
|
37
|
+
* "gating" (everything else).
|
|
38
|
+
*
|
|
39
|
+
* Patterns support a single `*` wildcard which matches any sequence of
|
|
40
|
+
* characters (greedy). Special regex characters are otherwise escaped, so
|
|
41
|
+
* patterns like `build (*)` or `Plugin Structure Validation` work as written.
|
|
42
|
+
*
|
|
43
|
+
* @param pendingCheckNames - Names of CI checks currently in a pending state.
|
|
44
|
+
* @param safePatterns - Glob-like patterns for safe-to-ignore checks.
|
|
45
|
+
* @returns Buckets for relaxed and gating checks.
|
|
46
|
+
*/
|
|
47
|
+
export function filterRelaxablePending(pendingCheckNames, safePatterns) {
|
|
48
|
+
if (safePatterns.length === 0) {
|
|
49
|
+
return { relaxed: [], gating: [...pendingCheckNames] };
|
|
50
|
+
}
|
|
51
|
+
const matchers = safePatterns.map(globToRegex);
|
|
52
|
+
const relaxed = [];
|
|
53
|
+
const gating = [];
|
|
54
|
+
for (const name of pendingCheckNames) {
|
|
55
|
+
if (matchers.some((re) => re.test(name))) {
|
|
56
|
+
relaxed.push(name);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
gating.push(name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { relaxed, gating };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Translate a single-`*`-wildcard glob into an anchored RegExp.
|
|
66
|
+
*
|
|
67
|
+
* `build (*)` → `/^build \(.*\)$/`
|
|
68
|
+
* `Plugin Structure Validation` → `/^Plugin Structure Validation$/`
|
|
69
|
+
*/
|
|
70
|
+
function globToRegex(pattern) {
|
|
71
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
72
|
+
const wildcarded = escaped.replace(/\*/g, ".*");
|
|
73
|
+
return new RegExp(`^${wildcarded}$`);
|
|
74
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay activation and deactivation lifecycle (#383).
|
|
3
|
+
*
|
|
4
|
+
* Activation creates the relay dir, writes the per-issue PID file, and updates
|
|
5
|
+
* IssueState.relay. Deactivation archives the relay dir (preserving inbox /
|
|
6
|
+
* outbox transcripts in `.sequant/logs/relay/`) and clears the runtime files.
|
|
7
|
+
*
|
|
8
|
+
* Both operations swallow errors when relay is disabled or filesystem is
|
|
9
|
+
* read-only — relay must never block the underlying `sequant run` flow.
|
|
10
|
+
*/
|
|
11
|
+
import type { StateManager } from "../workflow/state-manager.js";
|
|
12
|
+
export interface ActivationOptions {
|
|
13
|
+
/** Worktree path; falls back to `cwd` for spec-phase / main-repo relay. */
|
|
14
|
+
worktreePath?: string;
|
|
15
|
+
/** Main repo cwd (used for the PID file location). */
|
|
16
|
+
cwd?: string;
|
|
17
|
+
/** Optional state manager — updates IssueState.relay when provided. */
|
|
18
|
+
stateManager?: StateManager | null;
|
|
19
|
+
/** PID to record (defaults to current process). */
|
|
20
|
+
pid?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ActivationResult {
|
|
23
|
+
/** Was activation successful? */
|
|
24
|
+
activated: boolean;
|
|
25
|
+
/** Absolute path to the relay directory. */
|
|
26
|
+
relayDir: string;
|
|
27
|
+
/** Absolute path to the PID file. */
|
|
28
|
+
pidPath: string | null;
|
|
29
|
+
/** When activation occurred (ISO 8601). */
|
|
30
|
+
startedAt: string;
|
|
31
|
+
/** Error, if activation partially failed. Relay still considered active. */
|
|
32
|
+
warning: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the absolute path of `templates/relay/frame.txt` inside the
|
|
36
|
+
* installed sequant package. Used by phase-executor to set the
|
|
37
|
+
* SEQUANT_RELAY_FRAME env var so the bash hook can locate the template.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveBundledFramePath(): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Activate the relay for `issue`. Creates `<worktree>/.sequant/relay/`,
|
|
42
|
+
* writes `.sequant/pids/<issue>.pid` and (optionally) updates state.
|
|
43
|
+
*/
|
|
44
|
+
export declare function activateRelay(issue: number, options?: ActivationOptions): Promise<ActivationResult>;
|
|
45
|
+
export interface DeactivationOptions extends ActivationOptions {
|
|
46
|
+
/** Phase whose work just ended — encoded in the archive dir name. */
|
|
47
|
+
phase: string;
|
|
48
|
+
/** When the relay was activated (echoed into archive meta.json). */
|
|
49
|
+
startedAt: string;
|
|
50
|
+
}
|
|
51
|
+
export interface DeactivationResult {
|
|
52
|
+
archived: boolean;
|
|
53
|
+
archivePath: string | null;
|
|
54
|
+
warning: string | null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Deactivate the relay for `issue`: archive inbox/outbox, remove the pidfile,
|
|
58
|
+
* and clear IssueState.relay. Always returns — never throws.
|
|
59
|
+
*/
|
|
60
|
+
export declare function deactivateRelay(issue: number, options: DeactivationOptions): Promise<DeactivationResult>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay activation and deactivation lifecycle (#383).
|
|
3
|
+
*
|
|
4
|
+
* Activation creates the relay dir, writes the per-issue PID file, and updates
|
|
5
|
+
* IssueState.relay. Deactivation archives the relay dir (preserving inbox /
|
|
6
|
+
* outbox transcripts in `.sequant/logs/relay/`) and clears the runtime files.
|
|
7
|
+
*
|
|
8
|
+
* Both operations swallow errors when relay is disabled or filesystem is
|
|
9
|
+
* read-only — relay must never block the underlying `sequant run` flow.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync } from "fs";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { dirname, resolve } from "path";
|
|
14
|
+
import { archiveRelayDir, tallyMessageCount } from "./archive.js";
|
|
15
|
+
import { relayDirFor } from "./paths.js";
|
|
16
|
+
import { writePidFile, removePidFile } from "./pid.js";
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the absolute path of `templates/relay/frame.txt` inside the
|
|
19
|
+
* installed sequant package. Used by phase-executor to set the
|
|
20
|
+
* SEQUANT_RELAY_FRAME env var so the bash hook can locate the template.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveBundledFramePath() {
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
let dir = __dirname;
|
|
25
|
+
for (let i = 0; i < 6; i++) {
|
|
26
|
+
const candidate = resolve(dir, "templates", "relay", "frame.txt");
|
|
27
|
+
if (existsSync(candidate))
|
|
28
|
+
return candidate;
|
|
29
|
+
dir = dirname(dir);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Activate the relay for `issue`. Creates `<worktree>/.sequant/relay/`,
|
|
35
|
+
* writes `.sequant/pids/<issue>.pid` and (optionally) updates state.
|
|
36
|
+
*/
|
|
37
|
+
export async function activateRelay(issue, options = {}) {
|
|
38
|
+
const startedAt = new Date().toISOString();
|
|
39
|
+
const pid = options.pid ?? process.pid;
|
|
40
|
+
const relayDir = relayDirFor(issue, {
|
|
41
|
+
worktreePath: options.worktreePath,
|
|
42
|
+
cwd: options.cwd,
|
|
43
|
+
});
|
|
44
|
+
let warning = null;
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(relayDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
warning = `Failed to create relay dir: ${err instanceof Error ? err.message : String(err)}`;
|
|
50
|
+
}
|
|
51
|
+
let pidPath = null;
|
|
52
|
+
try {
|
|
53
|
+
pidPath = writePidFile(issue, pid, options.cwd ?? process.cwd());
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
warning =
|
|
57
|
+
(warning ? warning + "; " : "") +
|
|
58
|
+
`Failed to write pid file: ${err instanceof Error ? err.message : String(err)}`;
|
|
59
|
+
}
|
|
60
|
+
if (options.stateManager) {
|
|
61
|
+
try {
|
|
62
|
+
await options.stateManager.setRelayState(issue, {
|
|
63
|
+
enabled: true,
|
|
64
|
+
pid,
|
|
65
|
+
startedAt,
|
|
66
|
+
messageCount: 0,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Issue may not be in state yet (race with initializeIssue) — non-fatal.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
activated: warning === null,
|
|
75
|
+
relayDir,
|
|
76
|
+
pidPath,
|
|
77
|
+
startedAt,
|
|
78
|
+
warning,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Deactivate the relay for `issue`: archive inbox/outbox, remove the pidfile,
|
|
83
|
+
* and clear IssueState.relay. Always returns — never throws.
|
|
84
|
+
*/
|
|
85
|
+
export async function deactivateRelay(issue, options) {
|
|
86
|
+
let warning = null;
|
|
87
|
+
const cwd = options.cwd ?? process.cwd();
|
|
88
|
+
const messageCount = tallyMessageCount(issue, {
|
|
89
|
+
worktreePath: options.worktreePath,
|
|
90
|
+
cwd,
|
|
91
|
+
});
|
|
92
|
+
const archive = archiveRelayDir(issue, {
|
|
93
|
+
phase: options.phase,
|
|
94
|
+
startedAt: options.startedAt,
|
|
95
|
+
messageCount,
|
|
96
|
+
worktreePath: options.worktreePath,
|
|
97
|
+
cwd,
|
|
98
|
+
archiveCwd: cwd,
|
|
99
|
+
});
|
|
100
|
+
if (archive.error) {
|
|
101
|
+
warning = archive.error;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
removePidFile(issue, cwd);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* swallow */
|
|
108
|
+
}
|
|
109
|
+
if (options.stateManager) {
|
|
110
|
+
try {
|
|
111
|
+
await options.stateManager.setRelayState(issue, null);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* swallow */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
archived: archive.archived,
|
|
119
|
+
archivePath: archive.archivePath,
|
|
120
|
+
warning,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archive the working relay directory to `.sequant/logs/relay/<issue>-<phase>-<ts>/`
|
|
3
|
+
* at phase end so transcripts survive worktree teardown (AC-6, AC-D2, AC-D4).
|
|
4
|
+
*
|
|
5
|
+
* The archive copies inbox.jsonl + outbox.jsonl + meta.json, then clears the
|
|
6
|
+
* working dir. Failures are non-fatal — teardown must still proceed.
|
|
7
|
+
*/
|
|
8
|
+
import { type RelayPathOptions } from "./paths.js";
|
|
9
|
+
export interface ArchiveOptions extends RelayPathOptions {
|
|
10
|
+
/** Phase whose work just finished — used in the archive dir name. */
|
|
11
|
+
phase: string;
|
|
12
|
+
/** When the relay was activated. */
|
|
13
|
+
startedAt: string;
|
|
14
|
+
/** Total messages exchanged during the run. */
|
|
15
|
+
messageCount: number;
|
|
16
|
+
/** Override timestamp (test seam). */
|
|
17
|
+
endedAt?: string;
|
|
18
|
+
/** Main repo cwd for archive root (`.sequant/logs/relay/`). */
|
|
19
|
+
archiveCwd?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ArchiveResult {
|
|
22
|
+
archived: boolean;
|
|
23
|
+
archivePath: string | null;
|
|
24
|
+
error: string | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Archive the relay directory for `issue` and clear it. Idempotent; if the
|
|
28
|
+
* dir doesn't exist, returns `{ archived: false }` without error.
|
|
29
|
+
*/
|
|
30
|
+
export declare function archiveRelayDir(issue: number, options: ArchiveOptions): ArchiveResult;
|
|
31
|
+
/** Count messages currently in the inbox + outbox of a relay dir. */
|
|
32
|
+
export declare function tallyMessageCount(issue: number, options?: RelayPathOptions): number;
|
|
33
|
+
/** List archived relay directories for an issue (sorted newest first). */
|
|
34
|
+
export declare function listArchives(issue: number, archiveCwd?: string): string[];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archive the working relay directory to `.sequant/logs/relay/<issue>-<phase>-<ts>/`
|
|
3
|
+
* at phase end so transcripts survive worktree teardown (AC-6, AC-D2, AC-D4).
|
|
4
|
+
*
|
|
5
|
+
* The archive copies inbox.jsonl + outbox.jsonl + meta.json, then clears the
|
|
6
|
+
* working dir. Failures are non-fatal — teardown must still proceed.
|
|
7
|
+
*/
|
|
8
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { archiveDirFor, relayDirFor, RELAY_CURSOR, RELAY_INBOX, RELAY_OUTBOX, } from "./paths.js";
|
|
11
|
+
import { RelayArchiveMetaSchema } from "./types.js";
|
|
12
|
+
function countLines(path) {
|
|
13
|
+
if (!existsSync(path))
|
|
14
|
+
return 0;
|
|
15
|
+
try {
|
|
16
|
+
const st = statSync(path);
|
|
17
|
+
if (st.size === 0)
|
|
18
|
+
return 0;
|
|
19
|
+
const text = readFileSync(path, "utf-8");
|
|
20
|
+
return text.split("\n").filter((l) => l.trim() !== "").length;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Archive the relay directory for `issue` and clear it. Idempotent; if the
|
|
28
|
+
* dir doesn't exist, returns `{ archived: false }` without error.
|
|
29
|
+
*/
|
|
30
|
+
export function archiveRelayDir(issue, options) {
|
|
31
|
+
const srcDir = relayDirFor(issue, options);
|
|
32
|
+
if (!existsSync(srcDir)) {
|
|
33
|
+
return { archived: false, archivePath: null, error: null };
|
|
34
|
+
}
|
|
35
|
+
// Idempotent: if there are no transcripts to preserve, skip creating an
|
|
36
|
+
// empty archive dir. Lets callers safely deactivate twice.
|
|
37
|
+
const hasInbox = existsSync(join(srcDir, RELAY_INBOX));
|
|
38
|
+
const hasOutbox = existsSync(join(srcDir, RELAY_OUTBOX));
|
|
39
|
+
if (!hasInbox && !hasOutbox) {
|
|
40
|
+
return { archived: false, archivePath: null, error: null };
|
|
41
|
+
}
|
|
42
|
+
const endedAt = options.endedAt ?? new Date().toISOString();
|
|
43
|
+
let destDir = archiveDirFor(issue, options.phase, endedAt, options.archiveCwd ?? process.cwd());
|
|
44
|
+
// If the dest exists (clock collision on same-second), append a suffix.
|
|
45
|
+
if (existsSync(destDir)) {
|
|
46
|
+
let n = 2;
|
|
47
|
+
while (existsSync(`${destDir}.${n}`))
|
|
48
|
+
n++;
|
|
49
|
+
destDir = `${destDir}.${n}`;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(destDir, { recursive: true });
|
|
53
|
+
// Copy inbox/outbox if present.
|
|
54
|
+
for (const name of [RELAY_INBOX, RELAY_OUTBOX]) {
|
|
55
|
+
const src = join(srcDir, name);
|
|
56
|
+
if (existsSync(src)) {
|
|
57
|
+
copyFileSync(src, join(destDir, name));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Write meta.json.
|
|
61
|
+
const meta = RelayArchiveMetaSchema.parse({
|
|
62
|
+
issue,
|
|
63
|
+
phase: options.phase,
|
|
64
|
+
startedAt: options.startedAt,
|
|
65
|
+
endedAt,
|
|
66
|
+
messageCount: options.messageCount,
|
|
67
|
+
});
|
|
68
|
+
writeFileSync(join(destDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
|
|
69
|
+
// Clear the working relay dir (inbox/outbox/cursor).
|
|
70
|
+
for (const name of [RELAY_INBOX, RELAY_OUTBOX, RELAY_CURSOR]) {
|
|
71
|
+
const p = join(srcDir, name);
|
|
72
|
+
if (existsSync(p))
|
|
73
|
+
rmSync(p, { force: true });
|
|
74
|
+
}
|
|
75
|
+
return { archived: true, archivePath: destDir, error: null };
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
79
|
+
return { archived: false, archivePath: null, error: message };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Count messages currently in the inbox + outbox of a relay dir. */
|
|
83
|
+
export function tallyMessageCount(issue, options = {}) {
|
|
84
|
+
const dir = relayDirFor(issue, options);
|
|
85
|
+
if (!existsSync(dir))
|
|
86
|
+
return 0;
|
|
87
|
+
const inbox = countLines(join(dir, RELAY_INBOX));
|
|
88
|
+
const outbox = countLines(join(dir, RELAY_OUTBOX));
|
|
89
|
+
return inbox + outbox;
|
|
90
|
+
}
|
|
91
|
+
/** List archived relay directories for an issue (sorted newest first). */
|
|
92
|
+
export function listArchives(issue, archiveCwd = process.cwd()) {
|
|
93
|
+
const root = join(archiveCwd, ".sequant", "logs", "relay");
|
|
94
|
+
if (!existsSync(root))
|
|
95
|
+
return [];
|
|
96
|
+
try {
|
|
97
|
+
return readdirSync(root)
|
|
98
|
+
.filter((n) => n.startsWith(`${issue}-`))
|
|
99
|
+
.sort()
|
|
100
|
+
.reverse()
|
|
101
|
+
.map((n) => join(root, n));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the relay framing prompt that wraps inbox messages before they are
|
|
3
|
+
* fed back to Claude via the PostToolUse hook (AC-9, AC-14, AC-15).
|
|
4
|
+
*
|
|
5
|
+
* The template lives at `templates/relay/frame.txt` (single source of truth
|
|
6
|
+
* per AC-15) and is interpolated with the per-invocation messages.
|
|
7
|
+
*/
|
|
8
|
+
import type { RelayMessage } from "./types.js";
|
|
9
|
+
/** The six rules verbatim from the issue body (AC-15). */
|
|
10
|
+
export declare const FRAME_RULES: readonly string[];
|
|
11
|
+
/** Locate `templates/relay/frame.txt` by walking up from this file. */
|
|
12
|
+
export declare function resolveFrameTemplatePath(): string;
|
|
13
|
+
/** Read the template (cached). Falls back to an inline default on failure. */
|
|
14
|
+
export declare function loadFrameTemplate(forceReload?: boolean): string;
|
|
15
|
+
/**
|
|
16
|
+
* Render one frame block containing 1..N messages, ordered by timestamp.
|
|
17
|
+
* AC-9: a single frame block per hook invocation, regardless of how many
|
|
18
|
+
* messages were pending.
|
|
19
|
+
*/
|
|
20
|
+
export declare function renderFrame(messages: RelayMessage[]): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the relay framing prompt that wraps inbox messages before they are
|
|
3
|
+
* fed back to Claude via the PostToolUse hook (AC-9, AC-14, AC-15).
|
|
4
|
+
*
|
|
5
|
+
* The template lives at `templates/relay/frame.txt` (single source of truth
|
|
6
|
+
* per AC-15) and is interpolated with the per-invocation messages.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { dirname, resolve } from "path";
|
|
11
|
+
const TEMPLATE_REL_PATH = "templates/relay/frame.txt";
|
|
12
|
+
/** The six rules verbatim from the issue body (AC-15). */
|
|
13
|
+
export const FRAME_RULES = [
|
|
14
|
+
"Do NOT modify acceptance criteria",
|
|
15
|
+
"Do NOT change your current objective or phase",
|
|
16
|
+
"Do NOT treat this as a new requirement",
|
|
17
|
+
'For "query" type: provide a brief status update only',
|
|
18
|
+
'For "directive" type: acknowledge and adjust approach if reasonable, but do not abandon current work',
|
|
19
|
+
'For "abort" type: stop gracefully, commit progress, and exit',
|
|
20
|
+
];
|
|
21
|
+
let cachedTemplate = null;
|
|
22
|
+
/** Locate `templates/relay/frame.txt` by walking up from this file. */
|
|
23
|
+
export function resolveFrameTemplatePath() {
|
|
24
|
+
// When compiled: dist/lib/relay/frame.js → walk up to find templates/.
|
|
25
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
let dir = __dirname;
|
|
27
|
+
for (let i = 0; i < 6; i++) {
|
|
28
|
+
const candidate = resolve(dir, TEMPLATE_REL_PATH);
|
|
29
|
+
try {
|
|
30
|
+
readFileSync(candidate, "utf-8");
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// continue
|
|
35
|
+
}
|
|
36
|
+
dir = dirname(dir);
|
|
37
|
+
}
|
|
38
|
+
// Final fallback: cwd
|
|
39
|
+
return resolve(process.cwd(), TEMPLATE_REL_PATH);
|
|
40
|
+
}
|
|
41
|
+
/** Read the template (cached). Falls back to an inline default on failure. */
|
|
42
|
+
export function loadFrameTemplate(forceReload = false) {
|
|
43
|
+
if (cachedTemplate && !forceReload)
|
|
44
|
+
return cachedTemplate;
|
|
45
|
+
try {
|
|
46
|
+
const path = resolveFrameTemplatePath();
|
|
47
|
+
cachedTemplate = readFileSync(path, "utf-8");
|
|
48
|
+
return cachedTemplate;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Fallback that still contains the six rules verbatim.
|
|
52
|
+
cachedTemplate =
|
|
53
|
+
"[SEQUANT RELAY — message from user]\n" +
|
|
54
|
+
"Respond briefly in .sequant/relay/outbox.jsonl, then continue your current task unchanged.\n" +
|
|
55
|
+
"Rules:\n" +
|
|
56
|
+
FRAME_RULES.map((r) => `- ${r}`).join("\n") +
|
|
57
|
+
"\n\n{{MESSAGES}}\n";
|
|
58
|
+
return cachedTemplate;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function formatSingleMessage(m) {
|
|
62
|
+
const body = m.type === "abort" && !m.message ? "" : (m.message ?? "");
|
|
63
|
+
return `Type: ${m.type}\nMessage: ${JSON.stringify(body)}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render one frame block containing 1..N messages, ordered by timestamp.
|
|
67
|
+
* AC-9: a single frame block per hook invocation, regardless of how many
|
|
68
|
+
* messages were pending.
|
|
69
|
+
*/
|
|
70
|
+
export function renderFrame(messages) {
|
|
71
|
+
if (messages.length === 0)
|
|
72
|
+
return "";
|
|
73
|
+
const sorted = [...messages].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
74
|
+
const block = sorted.map(formatSingleMessage).join("\n---\n");
|
|
75
|
+
return loadFrameTemplate().replace("{{MESSAGES}}", block);
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive relay (#383): file-based IPC between the user terminal and a
|
|
3
|
+
* headless `sequant run` session. See `src/lib/relay/types.ts` for the wire
|
|
4
|
+
* format and `templates/relay/frame.txt` for the framing prompt.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./types.js";
|
|
7
|
+
export * from "./paths.js";
|
|
8
|
+
export * from "./writer.js";
|
|
9
|
+
export * from "./reader.js";
|
|
10
|
+
export * from "./frame.js";
|
|
11
|
+
export * from "./pid.js";
|
|
12
|
+
export * from "./archive.js";
|
|
13
|
+
export * from "./activation.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive relay (#383): file-based IPC between the user terminal and a
|
|
3
|
+
* headless `sequant run` session. See `src/lib/relay/types.ts` for the wire
|
|
4
|
+
* format and `templates/relay/frame.txt` for the framing prompt.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./types.js";
|
|
7
|
+
export * from "./paths.js";
|
|
8
|
+
export * from "./writer.js";
|
|
9
|
+
export * from "./reader.js";
|
|
10
|
+
export * from "./frame.js";
|
|
11
|
+
export * from "./pid.js";
|
|
12
|
+
export * from "./archive.js";
|
|
13
|
+
export * from "./activation.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution for relay directories.
|
|
3
|
+
*
|
|
4
|
+
* The relay lives inside a per-issue worktree at `<worktree>/.sequant/relay/`.
|
|
5
|
+
* During the `spec` phase the worktree doesn't exist yet, so we fall back to
|
|
6
|
+
* the main repo at `.sequant/relay/<issue>/`. The phase-executor sets
|
|
7
|
+
* `SEQUANT_WORKTREE` whenever an isolated worktree is active.
|
|
8
|
+
*/
|
|
9
|
+
export declare const RELAY_INBOX = "inbox.jsonl";
|
|
10
|
+
export declare const RELAY_OUTBOX = "outbox.jsonl";
|
|
11
|
+
export declare const RELAY_CURSOR = ".cursor";
|
|
12
|
+
export declare const RELAY_PIDS_DIR = ".sequant/pids";
|
|
13
|
+
export interface RelayPathOptions {
|
|
14
|
+
/** Optional override; if omitted, reads SEQUANT_WORKTREE then falls back. */
|
|
15
|
+
worktreePath?: string;
|
|
16
|
+
/** Optional override for the main repo cwd (test seam). */
|
|
17
|
+
cwd?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the absolute relay directory for an issue.
|
|
21
|
+
*
|
|
22
|
+
* - With a worktree: `<worktree>/.sequant/relay/`
|
|
23
|
+
* - Without (spec phase or CLI from main repo): `<cwd>/.sequant/relay/<issue>/`
|
|
24
|
+
*/
|
|
25
|
+
export declare function relayDirFor(issue: number, options?: RelayPathOptions): string;
|
|
26
|
+
/** Path to the inbox JSONL file. */
|
|
27
|
+
export declare function inboxPathFor(issue: number, options?: RelayPathOptions): string;
|
|
28
|
+
/** Path to the outbox JSONL file. */
|
|
29
|
+
export declare function outboxPathFor(issue: number, options?: RelayPathOptions): string;
|
|
30
|
+
/** Path to the reader cursor file. */
|
|
31
|
+
export declare function cursorPathFor(issue: number, options?: RelayPathOptions): string;
|
|
32
|
+
/** Path to the per-issue PID file. */
|
|
33
|
+
export declare function pidPathFor(issue: number, cwd?: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the archive root directory for the relay logs:
|
|
36
|
+
* `<cwd>/.sequant/logs/relay/`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function archiveRootDir(cwd?: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the archive directory for a particular phase end:
|
|
41
|
+
* `<archiveRoot>/<issue>-<phase>-<timestamp>/`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function archiveDirFor(issue: number, phase: string, timestamp: string, cwd?: string): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution for relay directories.
|
|
3
|
+
*
|
|
4
|
+
* The relay lives inside a per-issue worktree at `<worktree>/.sequant/relay/`.
|
|
5
|
+
* During the `spec` phase the worktree doesn't exist yet, so we fall back to
|
|
6
|
+
* the main repo at `.sequant/relay/<issue>/`. The phase-executor sets
|
|
7
|
+
* `SEQUANT_WORKTREE` whenever an isolated worktree is active.
|
|
8
|
+
*/
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
|
+
export const RELAY_INBOX = "inbox.jsonl";
|
|
11
|
+
export const RELAY_OUTBOX = "outbox.jsonl";
|
|
12
|
+
export const RELAY_CURSOR = ".cursor";
|
|
13
|
+
export const RELAY_PIDS_DIR = ".sequant/pids";
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the absolute relay directory for an issue.
|
|
16
|
+
*
|
|
17
|
+
* - With a worktree: `<worktree>/.sequant/relay/`
|
|
18
|
+
* - Without (spec phase or CLI from main repo): `<cwd>/.sequant/relay/<issue>/`
|
|
19
|
+
*/
|
|
20
|
+
export function relayDirFor(issue, options = {}) {
|
|
21
|
+
const worktree = options.worktreePath ?? process.env.SEQUANT_WORKTREE;
|
|
22
|
+
const cwd = options.cwd ?? process.cwd();
|
|
23
|
+
if (worktree && worktree.trim() !== "") {
|
|
24
|
+
return resolve(worktree, ".sequant", "relay");
|
|
25
|
+
}
|
|
26
|
+
return resolve(cwd, ".sequant", "relay", String(issue));
|
|
27
|
+
}
|
|
28
|
+
/** Path to the inbox JSONL file. */
|
|
29
|
+
export function inboxPathFor(issue, options = {}) {
|
|
30
|
+
return join(relayDirFor(issue, options), RELAY_INBOX);
|
|
31
|
+
}
|
|
32
|
+
/** Path to the outbox JSONL file. */
|
|
33
|
+
export function outboxPathFor(issue, options = {}) {
|
|
34
|
+
return join(relayDirFor(issue, options), RELAY_OUTBOX);
|
|
35
|
+
}
|
|
36
|
+
/** Path to the reader cursor file. */
|
|
37
|
+
export function cursorPathFor(issue, options = {}) {
|
|
38
|
+
return join(relayDirFor(issue, options), RELAY_CURSOR);
|
|
39
|
+
}
|
|
40
|
+
/** Path to the per-issue PID file. */
|
|
41
|
+
export function pidPathFor(issue, cwd = process.cwd()) {
|
|
42
|
+
return resolve(cwd, RELAY_PIDS_DIR, `${issue}.pid`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the archive root directory for the relay logs:
|
|
46
|
+
* `<cwd>/.sequant/logs/relay/`.
|
|
47
|
+
*/
|
|
48
|
+
export function archiveRootDir(cwd = process.cwd()) {
|
|
49
|
+
return resolve(cwd, ".sequant", "logs", "relay");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the archive directory for a particular phase end:
|
|
53
|
+
* `<archiveRoot>/<issue>-<phase>-<timestamp>/`.
|
|
54
|
+
*/
|
|
55
|
+
export function archiveDirFor(issue, phase, timestamp, cwd = process.cwd()) {
|
|
56
|
+
// Sanitize timestamp for filesystem use (colons are illegal on Windows).
|
|
57
|
+
const safeTs = timestamp.replace(/[:.]/g, "-");
|
|
58
|
+
return join(archiveRootDir(cwd), `${issue}-${phase}-${safeTs}`);
|
|
59
|
+
}
|