sequant 2.6.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +4 -0
  4. package/dist/bin/cli.js +28 -4
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +3 -0
  7. package/dist/marketplace/external_plugins/sequant/skills/clean/SKILL.md +3 -0
  8. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +3 -0
  9. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +3 -0
  10. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -0
  11. package/dist/marketplace/external_plugins/sequant/skills/improve/SKILL.md +4 -1
  12. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +3 -0
  13. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +3 -0
  14. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +3 -0
  15. package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +3 -0
  16. package/dist/marketplace/external_plugins/sequant/skills/security-review/SKILL.md +3 -0
  17. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +3 -0
  18. package/dist/marketplace/external_plugins/sequant/skills/solve/SKILL.md +3 -0
  19. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +3 -0
  20. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +3 -0
  21. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +3 -0
  22. package/dist/marketplace/external_plugins/sequant/skills/verify/SKILL.md +3 -0
  23. package/dist/src/commands/ready.js +1 -1
  24. package/dist/src/commands/run.js +1 -1
  25. package/dist/src/commands/sync.d.ts +44 -5
  26. package/dist/src/commands/sync.js +244 -18
  27. package/dist/src/commands/update.d.ts +1 -0
  28. package/dist/src/commands/update.js +80 -68
  29. package/dist/src/lib/templates.d.ts +50 -0
  30. package/dist/src/lib/templates.js +134 -15
  31. package/dist/src/ui/tui/App.js +24 -2
  32. package/dist/src/ui/tui/IssueBox.js +4 -4
  33. package/dist/src/ui/tui/load.d.ts +25 -0
  34. package/dist/src/ui/tui/load.js +41 -0
  35. package/dist/src/ui/tui/theme.d.ts +21 -3
  36. package/dist/src/ui/tui/theme.js +22 -4
  37. package/package.json +1 -1
  38. package/templates/skills/assess/SKILL.md +3 -0
  39. package/templates/skills/clean/SKILL.md +3 -0
  40. package/templates/skills/docs/SKILL.md +3 -0
  41. package/templates/skills/exec/SKILL.md +3 -0
  42. package/templates/skills/fullsolve/SKILL.md +3 -0
  43. package/templates/skills/improve/SKILL.md +4 -1
  44. package/templates/skills/loop/SKILL.md +3 -0
  45. package/templates/skills/merger/SKILL.md +3 -0
  46. package/templates/skills/qa/SKILL.md +3 -0
  47. package/templates/skills/reflect/SKILL.md +3 -0
  48. package/templates/skills/security-review/SKILL.md +3 -0
  49. package/templates/skills/setup/SKILL.md +3 -0
  50. package/templates/skills/solve/SKILL.md +3 -0
  51. package/templates/skills/spec/SKILL.md +3 -0
  52. package/templates/skills/test/SKILL.md +3 -0
  53. package/templates/skills/testgen/SKILL.md +3 -0
  54. package/templates/skills/verify/SKILL.md +3 -0
@@ -4,6 +4,7 @@
4
4
  import { readdir, chmod } from "fs/promises";
5
5
  import { join, dirname, relative, isAbsolute } from "path";
6
6
  import { fileURLToPath } from "url";
7
+ import { diffLines } from "diff";
7
8
  import { readFile, writeFile, ensureDir, fileExists, isSymlink, createSymlink, removeFileOrSymlink, } from "./fs.js";
8
9
  import { getPackageVersion } from "./manifest.js";
9
10
  const SKILLS_VERSION_PATH = ".claude/skills/.sequant-version";
@@ -12,6 +13,11 @@ import { isNativeWindows } from "./system.js";
12
13
  import { getProjectName } from "./project-name.js";
13
14
  // Get the package templates directory
14
15
  export function getTemplatesDir() {
16
+ // Allow overriding the templates source (used by tests; also lets the dir be
17
+ // relocated without relying on the compiled-output layout below).
18
+ if (process.env.SEQUANT_TEMPLATES_DIR) {
19
+ return process.env.SEQUANT_TEMPLATES_DIR;
20
+ }
15
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
22
  // Compiled structure: dist/src/lib/templates.js
17
23
  // So we need ../../../templates to reach project root templates/
@@ -64,6 +70,132 @@ export async function getTemplateContent(templatePath) {
64
70
  const fullPath = join(templatesDir, relativePath);
65
71
  return readFile(fullPath);
66
72
  }
73
+ /**
74
+ * Files that are meant to be edited in place per project (e.g. the
75
+ * constitution). When one of these diverges from the rendered template
76
+ * without a parallel `.claude/.local/` file, it is treated as a protected
77
+ * local override rather than a stale "modified" file — so the default
78
+ * (non-`--force`) update/sync path never silently overwrites it.
79
+ */
80
+ export const CUSTOMIZABLE_FILES = [".claude/memory/constitution.md"];
81
+ /**
82
+ * Whether a local path is a customizable file edited in place per project.
83
+ */
84
+ export function isCustomizableFile(localPath) {
85
+ // Normalize OS path separators so the allow-list match holds on Windows,
86
+ // where template paths are assembled with backslashes (#708).
87
+ return CUSTOMIZABLE_FILES.includes(localPath.replace(/\\/g, "/"));
88
+ }
89
+ /**
90
+ * Build the full set of template variables used when rendering templates.
91
+ *
92
+ * This is the single source of truth shared by `copyTemplates` (write time)
93
+ * and `computeTemplateChanges` (diff time) so the two can never drift — a
94
+ * mismatch here is what caused `constitution.md` to read as "modified" on
95
+ * every project (the diff used a different/incomplete variable set than the
96
+ * write). See #708.
97
+ */
98
+ export async function buildTemplateVariables(stack, tokens, options = {}) {
99
+ const stackConfig = getStackConfig(stack);
100
+ // Detect project name from available sources (package.json, Cargo.toml, etc.)
101
+ const projectName = await getProjectName();
102
+ // Get stack-specific notes for constitution template
103
+ // Use multi-stack notes if additional stacks are provided
104
+ const stackNotes = options.additionalStacks && options.additionalStacks.length > 0
105
+ ? getMultiStackNotes(stack, options.additionalStacks)
106
+ : getStackNotes(stack);
107
+ return {
108
+ ...stackConfig.variables,
109
+ ...tokens,
110
+ PROJECT_NAME: projectName,
111
+ STACK: stack,
112
+ STACK_NOTES: stackNotes,
113
+ };
114
+ }
115
+ /**
116
+ * Compare bundled template content against what's installed under `.claude/`.
117
+ *
118
+ * Templates are rendered with the project's variables *before* comparison, so
119
+ * an unmodified file (e.g. a constitution with `{{PROJECT_NAME}}` expanded)
120
+ * reads as `unchanged` rather than `modified`. A file that diverges in place is
121
+ * `local-override` (skip-by-default) when it has a parallel `.claude/.local/`
122
+ * file or is in the customizable allow-list; otherwise it is `modified`.
123
+ */
124
+ export async function computeTemplateChanges(stack, tokens, options = {}) {
125
+ const variables = await buildTemplateVariables(stack, tokens, options);
126
+ const templateFiles = await listTemplateFiles();
127
+ const changes = [];
128
+ for (const templatePath of templateFiles) {
129
+ // Normalize separators first: listTemplateFiles builds paths with the OS
130
+ // separator (backslashes on Windows), but the prefix swap and the .local/
131
+ // and customizable-file checks below all assume forward slashes (#708).
132
+ const localPath = templatePath
133
+ .replace(/\\/g, "/")
134
+ .replace("templates/", ".claude/");
135
+ // Skip .local files (user customizations are never overwritten)
136
+ if (localPath.includes(".local/")) {
137
+ continue;
138
+ }
139
+ const rendered = processTemplate(await getTemplateContent(templatePath), variables);
140
+ const exists = await fileExists(localPath);
141
+ if (!exists) {
142
+ changes.push({ path: localPath, templatePath, status: "new", rendered });
143
+ continue;
144
+ }
145
+ const localContent = await readFile(localPath);
146
+ if (localContent === rendered) {
147
+ changes.push({
148
+ path: localPath,
149
+ templatePath,
150
+ status: "unchanged",
151
+ rendered,
152
+ });
153
+ continue;
154
+ }
155
+ // Content differs after rendering. Protect in-place customizations:
156
+ // a parallel `.claude/.local/` override, or a known customizable file.
157
+ //
158
+ // Note: this protects a managed file that was *edited in place* (e.g. the
159
+ // constitution) when a parallel `.claude/.local/` twin exists. It is NOT a
160
+ // skill-loading mechanism — the harness never loads `.claude/.local/skills/
161
+ // <name>/SKILL.md`, so a full-file SKILL.md shadow does nothing at runtime
162
+ // (#711). Skills are instead customized via a runtime overlay: each managed
163
+ // SKILL.md opens (before its first heading) with a directive to honor
164
+ // `.claude/.local/skills/<name>/overrides.md`, and that overrides file is
165
+ // auto-skipped above because it lives under `.local/`. The directive sits at
166
+ // the top, not end-of-file, so it fires reliably even in 3000-line skills.
167
+ // See docs/guides/customization.md.
168
+ const localOverridePath = localPath.replace(".claude/", ".claude/.local/");
169
+ const hasLocalOverride = await fileExists(localOverridePath);
170
+ if (hasLocalOverride || isCustomizableFile(localPath)) {
171
+ changes.push({
172
+ path: localPath,
173
+ templatePath,
174
+ status: "local-override",
175
+ rendered,
176
+ });
177
+ continue;
178
+ }
179
+ const diff = diffLines(localContent, rendered)
180
+ .map((part) => {
181
+ const prefix = part.added ? "+" : part.removed ? "-" : " ";
182
+ return part.value
183
+ .split("\n")
184
+ .filter((l) => l)
185
+ .map((l) => `${prefix} ${l}`)
186
+ .join("\n");
187
+ })
188
+ .join("\n");
189
+ changes.push({
190
+ path: localPath,
191
+ templatePath,
192
+ status: "modified",
193
+ rendered,
194
+ diff,
195
+ });
196
+ }
197
+ return changes;
198
+ }
67
199
  /**
68
200
  * Create symlinks for files in a directory, with fallback to copy
69
201
  * @param srcDir Source directory containing template files
@@ -159,21 +291,8 @@ export async function symlinkDir(srcDir, destDir, options = {}) {
159
291
  */
160
292
  export async function copyTemplates(stack, tokens, options = {}) {
161
293
  const templatesDir = getTemplatesDir();
162
- const stackConfig = getStackConfig(stack);
163
- // Detect project name from available sources (package.json, Cargo.toml, etc.)
164
- const projectName = await getProjectName();
165
- // Get stack-specific notes for constitution template
166
- // Use multi-stack notes if additional stacks are provided
167
- const stackNotes = options.additionalStacks && options.additionalStacks.length > 0
168
- ? getMultiStackNotes(stack, options.additionalStacks)
169
- : getStackNotes(stack);
170
- const variables = {
171
- ...stackConfig.variables,
172
- ...tokens,
173
- PROJECT_NAME: projectName,
174
- STACK: stack,
175
- STACK_NOTES: stackNotes,
176
- };
294
+ // Single source of truth for template variables (shared with the diff path)
295
+ const variables = await buildTemplateVariables(stack, tokens, options);
177
296
  async function copyDir(srcDir, destDir) {
178
297
  try {
179
298
  const entries = await readdir(srcDir, { withFileTypes: true });
@@ -19,6 +19,7 @@ export function App({ getSnapshot, onDone, }) {
19
19
  const [now, setNow] = useState(() => Date.now());
20
20
  const doneFired = useRef(false);
21
21
  const { stdout } = useStdout();
22
+ const [columns, setColumns] = useState(() => stdout?.columns ?? 80);
22
23
  // Snapshot poller (drives all state transitions).
23
24
  useEffect(() => {
24
25
  const id = setInterval(() => {
@@ -37,8 +38,29 @@ export function App({ getSnapshot, onDone, }) {
37
38
  const id = setInterval(() => setNow(Date.now()), 1000);
38
39
  return () => clearInterval(id);
39
40
  }, []);
40
- const columns = stdout?.columns ?? 80;
41
- const boxWidth = Math.min(columns - 2, 100);
41
+ // Track the terminal width reactively. ink's own resize handler re-renders
42
+ // the existing React tree but does NOT re-run this component, so a width read
43
+ // imperatively in render goes stale until the next poll. In that window ink
44
+ // repaints boxes at the old (now too-wide) width and the lines wrap, which
45
+ // misaligns the box borders into the duplicate/garbled frames. Updating
46
+ // `columns` from the resize event forces an immediate re-layout at the new
47
+ // width. A 1 Hz fallback poll covers terminals that don't emit `resize`.
48
+ useEffect(() => {
49
+ if (!stdout)
50
+ return;
51
+ const sync = () => setColumns(stdout.columns ?? 80);
52
+ stdout.on("resize", sync);
53
+ sync();
54
+ const id = setInterval(sync, 1000);
55
+ return () => {
56
+ stdout.off("resize", sync);
57
+ clearInterval(id);
58
+ };
59
+ }, [stdout]);
60
+ // Clamp each box to the current terminal width (minus a 2-col safety margin)
61
+ // so a box line can never equal or exceed the terminal width and wrap.
62
+ const safeColumns = columns > 0 ? columns : 80;
63
+ const boxWidth = Math.max(20, Math.min(safeColumns - 2, 100));
42
64
  // #699 AC-4: clamp the number of boxes to the terminal height so a large
43
65
  // batch on a short terminal can't overflow the frame (parity with the plain
44
66
  // renderer's #624 row cap). Older completed issues collapse into `✔ N done`.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { DIVIDER_COLOR, PHASE_GLYPHS, borderColorForIssue, phaseStatusColor, } from "./theme.js";
3
+ import { ACTIVE_PHASE_COLOR, DIVIDER_COLOR, PHASE_GLYPHS, borderColorForIssue, phaseStatusColor, } from "./theme.js";
4
4
  import { Spinner } from "./Spinner.js";
5
5
  import { ElapsedTimer, formatSinceActivity } from "./ElapsedTimer.js";
6
6
  import { truncateToWidth } from "./truncate.js";
@@ -20,7 +20,7 @@ export function IssueBox({ state, slot, width, now, }) {
20
20
  const displayPhaseN = activePhaseIndex >= 0 ? activePhaseIndex + 1 : doneCount;
21
21
  const total = state.phases.length;
22
22
  const headerTitle = truncateToWidth(`#${state.number} ${state.title}`, Math.max(10, innerWidth - 20));
23
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: border, paddingX: 1, marginBottom: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: border, children: headerTitle }), _jsxs(Text, { color: DIVIDER_COLOR, children: ["phase ", displayPhaseN, "/", total, " \u2022", " ", _jsx(ElapsedTimer, { startedAt: state.startedAt })] })] }), _jsx(Divider, { width: innerWidth }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "branch " }), _jsx(Text, { children: truncateToWidth(state.branch, innerWidth - 8) })] }), _jsx(PhaseProgression, { phases: state.phases, borderColor: border }), state.currentPhase?.logPath ? (_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "log " }), _jsx(Text, { children: truncateToWidth(state.currentPhase.logPath, innerWidth - 8) })] })) : null] }), _jsx(Divider, { width: innerWidth }), _jsx(Box, { flexDirection: "column", children: state.currentPhase ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "now " }), _jsx(Spinner, { color: border }), _jsxs(Text, { children: [" ", truncateToWidth(state.currentPhase.nowLine, innerWidth - 12)] })] }), _jsx(Box, { children: _jsxs(Text, { color: DIVIDER_COLOR, children: [" └ last activity ", formatSinceActivity(now, state.currentPhase.lastActivityAt)] }) })] })) : (_jsx(Text, { color: DIVIDER_COLOR, children: statusLine(state) })) })] }));
23
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: border, paddingX: 1, marginBottom: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: border, children: headerTitle }), _jsxs(Text, { color: DIVIDER_COLOR, children: ["phase ", displayPhaseN, "/", total, " \u2022", " ", _jsx(ElapsedTimer, { startedAt: state.startedAt })] })] }), _jsx(Divider, { width: innerWidth }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "branch " }), _jsx(Text, { children: truncateToWidth(state.branch, innerWidth - 8) })] }), _jsx(PhaseProgression, { phases: state.phases, activeColor: ACTIVE_PHASE_COLOR }), state.currentPhase?.logPath ? (_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "log " }), _jsx(Text, { children: truncateToWidth(state.currentPhase.logPath, innerWidth - 8) })] })) : null] }), _jsx(Divider, { width: innerWidth }), _jsx(Box, { flexDirection: "column", children: state.currentPhase ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "now " }), _jsx(Spinner, { color: ACTIVE_PHASE_COLOR }), _jsxs(Text, { children: [" ", truncateToWidth(state.currentPhase.nowLine, innerWidth - 12)] })] }), _jsx(Box, { children: _jsxs(Text, { color: DIVIDER_COLOR, children: [" └ last activity ", formatSinceActivity(now, state.currentPhase.lastActivityAt)] }) })] })) : (_jsx(Text, { color: DIVIDER_COLOR, children: statusLine(state) })) })] }));
24
24
  }
25
25
  function statusLine(state) {
26
26
  switch (state.status) {
@@ -37,10 +37,10 @@ function statusLine(state) {
37
37
  function Divider({ width }) {
38
38
  return _jsx(Text, { color: DIVIDER_COLOR, children: "─".repeat(Math.max(0, width)) });
39
39
  }
40
- function PhaseProgression({ phases, borderColor, }) {
40
+ function PhaseProgression({ phases, activeColor, }) {
41
41
  return (_jsxs(Box, { flexWrap: "wrap", children: [_jsx(Text, { color: DIVIDER_COLOR, children: "phases " }), phases.map((p, i) => {
42
42
  const isLast = i === phases.length - 1;
43
- return (_jsxs(Box, { children: [_jsx(PhaseGlyph, { status: p.status, label: p.name, activeColor: borderColor, elapsedMs: p.elapsedMs }), !isLast ? (_jsxs(Text, { color: DIVIDER_COLOR, children: [" ", PHASE_GLYPHS.separator, " "] })) : null] }, `${p.name}-${i}`));
43
+ return (_jsxs(Box, { children: [_jsx(PhaseGlyph, { status: p.status, label: p.name, activeColor: activeColor, elapsedMs: p.elapsedMs }), !isLast ? (_jsxs(Text, { color: DIVIDER_COLOR, children: [" ", PHASE_GLYPHS.separator, " "] })) : null] }, `${p.name}-${i}`));
44
44
  })] }));
45
45
  }
46
46
  function PhaseGlyph({ status, label, activeColor, elapsedMs, }) {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Loader for the Ink TUI module that forces React into its production build.
3
+ *
4
+ * `react-reconciler` (pulled in by `ink`) selects its dev-vs-prod bundle from
5
+ * `process.env.NODE_ENV` **at module-evaluation time**. The development bundle
6
+ * emits a `performance.measure()` per component render and never calls
7
+ * `performance.clearMeasures()`. Driven by the TUI's 10 Hz poll over a long
8
+ * `sequant run`, those entries accumulate in Node's global performance buffer
9
+ * until it overflows its ~1,000,000-entry cap and prints
10
+ * `MaxPerformanceEntryBufferExceededWarning` to stderr — a memory leak that
11
+ * also corrupts the dashboard's in-place redraw (the stderr write scrolls the
12
+ * terminal between log-update frames; see #647/#664).
13
+ *
14
+ * The TUI module is the *only* importer of `react`/`ink`/`react-reconciler`,
15
+ * and it is always reached through a dynamic `import()`, so bracketing that one
16
+ * import with `NODE_ENV=production` caches the production reconciler (which has
17
+ * zero `performance.measure` calls). We restore `NODE_ENV` immediately after so
18
+ * spawned child processes (claude phases, `npm install`, build steps) do NOT
19
+ * inherit `NODE_ENV=production` — which would, e.g., make `npm install` skip
20
+ * devDependencies.
21
+ *
22
+ * Only overrides when `NODE_ENV` is unset/empty: an explicit `development` or
23
+ * `test` (the test runner) is respected so dev warnings remain available there.
24
+ */
25
+ export declare function loadTui(): Promise<typeof import("./index.js")>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Loader for the Ink TUI module that forces React into its production build.
3
+ *
4
+ * `react-reconciler` (pulled in by `ink`) selects its dev-vs-prod bundle from
5
+ * `process.env.NODE_ENV` **at module-evaluation time**. The development bundle
6
+ * emits a `performance.measure()` per component render and never calls
7
+ * `performance.clearMeasures()`. Driven by the TUI's 10 Hz poll over a long
8
+ * `sequant run`, those entries accumulate in Node's global performance buffer
9
+ * until it overflows its ~1,000,000-entry cap and prints
10
+ * `MaxPerformanceEntryBufferExceededWarning` to stderr — a memory leak that
11
+ * also corrupts the dashboard's in-place redraw (the stderr write scrolls the
12
+ * terminal between log-update frames; see #647/#664).
13
+ *
14
+ * The TUI module is the *only* importer of `react`/`ink`/`react-reconciler`,
15
+ * and it is always reached through a dynamic `import()`, so bracketing that one
16
+ * import with `NODE_ENV=production` caches the production reconciler (which has
17
+ * zero `performance.measure` calls). We restore `NODE_ENV` immediately after so
18
+ * spawned child processes (claude phases, `npm install`, build steps) do NOT
19
+ * inherit `NODE_ENV=production` — which would, e.g., make `npm install` skip
20
+ * devDependencies.
21
+ *
22
+ * Only overrides when `NODE_ENV` is unset/empty: an explicit `development` or
23
+ * `test` (the test runner) is respected so dev warnings remain available there.
24
+ */
25
+ export async function loadTui() {
26
+ const prev = process.env.NODE_ENV;
27
+ const override = prev === undefined || prev === "";
28
+ if (override)
29
+ process.env.NODE_ENV = "production";
30
+ try {
31
+ return await import("./index.js");
32
+ }
33
+ finally {
34
+ if (override) {
35
+ if (prev === undefined)
36
+ delete process.env.NODE_ENV;
37
+ else
38
+ process.env.NODE_ENV = prev;
39
+ }
40
+ }
41
+ }
@@ -6,14 +6,32 @@
6
6
  * Respects `NO_COLOR` automatically via `ink`/`chalk`.
7
7
  */
8
8
  import type { IssueStatus, PhaseStatus } from "../../lib/workflow/run-state.js";
9
+ /**
10
+ * Sequant brand accents, sourced from sequant-landing `src/styles/tokens.css`:
11
+ * - `BRAND_ORANGE` is the primary brand color (`--color-primary` dark mode).
12
+ * - `BRAND_GREEN` is the accent/success green (`--color-accent`).
13
+ *
14
+ * Used to brand the two color signals that matter most at a glance — the
15
+ * live/active phase and success — while issue-distinction (border rotation),
16
+ * failure (red), and dividers (gray) stay on robust named ANSI colors.
17
+ *
18
+ * Ink/chalk auto-downsamples hex to the nearest ANSI color on terminals
19
+ * without truecolor, and `NO_COLOR` still strips all color, so these degrade
20
+ * gracefully without a manual capability check.
21
+ */
22
+ export declare const BRAND_ORANGE: "#FF8012";
23
+ export declare const BRAND_GREEN: "#10b981";
9
24
  /** Border-color palette rotated by issue start order. */
10
25
  export declare const BORDER_ROTATION: readonly ["cyan", "magenta", "blue", "yellow"];
11
- export type BorderColor = (typeof BORDER_ROTATION)[number] | "green" | "red" | "gray";
26
+ export type BorderColor = (typeof BORDER_ROTATION)[number] | typeof BRAND_GREEN | typeof BRAND_ORANGE | "red" | "gray";
12
27
  /** Gray used for horizontal dividers inside each box. */
13
28
  export declare const DIVIDER_COLOR: "gray";
14
- /** Green used for the rolled-up `✔ N done` summary line (#699, parity with the
29
+ /** Brand orange for the live/active phase spinner the one element the eye
30
+ * tracks. Border rotation still distinguishes concurrent issues. */
31
+ export declare const ACTIVE_PHASE_COLOR: "#FF8012";
32
+ /** Brand green for the rolled-up `✔ N done` summary line (#699, parity with the
15
33
  * plain renderer's #624 rollup). */
16
- export declare const ROLLUP_COLOR: "green";
34
+ export declare const ROLLUP_COLOR: "#10b981";
17
35
  /**
18
36
  * Pick the border color for an issue.
19
37
  * Failed / passed states win over rotation; otherwise rotate by slot.
@@ -5,13 +5,31 @@
5
5
  * status (failed / passed) overrides the rotation color where applicable.
6
6
  * Respects `NO_COLOR` automatically via `ink`/`chalk`.
7
7
  */
8
+ /**
9
+ * Sequant brand accents, sourced from sequant-landing `src/styles/tokens.css`:
10
+ * - `BRAND_ORANGE` is the primary brand color (`--color-primary` dark mode).
11
+ * - `BRAND_GREEN` is the accent/success green (`--color-accent`).
12
+ *
13
+ * Used to brand the two color signals that matter most at a glance — the
14
+ * live/active phase and success — while issue-distinction (border rotation),
15
+ * failure (red), and dividers (gray) stay on robust named ANSI colors.
16
+ *
17
+ * Ink/chalk auto-downsamples hex to the nearest ANSI color on terminals
18
+ * without truecolor, and `NO_COLOR` still strips all color, so these degrade
19
+ * gracefully without a manual capability check.
20
+ */
21
+ export const BRAND_ORANGE = "#FF8012";
22
+ export const BRAND_GREEN = "#10b981";
8
23
  /** Border-color palette rotated by issue start order. */
9
24
  export const BORDER_ROTATION = ["cyan", "magenta", "blue", "yellow"];
10
25
  /** Gray used for horizontal dividers inside each box. */
11
26
  export const DIVIDER_COLOR = "gray";
12
- /** Green used for the rolled-up `✔ N done` summary line (#699, parity with the
27
+ /** Brand orange for the live/active phase spinner the one element the eye
28
+ * tracks. Border rotation still distinguishes concurrent issues. */
29
+ export const ACTIVE_PHASE_COLOR = BRAND_ORANGE;
30
+ /** Brand green for the rolled-up `✔ N done` summary line (#699, parity with the
13
31
  * plain renderer's #624 rollup). */
14
- export const ROLLUP_COLOR = "green";
32
+ export const ROLLUP_COLOR = BRAND_GREEN;
15
33
  /**
16
34
  * Pick the border color for an issue.
17
35
  * Failed / passed states win over rotation; otherwise rotate by slot.
@@ -20,7 +38,7 @@ export function borderColorForIssue(status, slot) {
20
38
  if (status === "failed")
21
39
  return "red";
22
40
  if (status === "passed")
23
- return "green";
41
+ return BRAND_GREEN;
24
42
  const idx = ((slot % BORDER_ROTATION.length) + BORDER_ROTATION.length) %
25
43
  BORDER_ROTATION.length;
26
44
  return BORDER_ROTATION[idx];
@@ -48,7 +66,7 @@ export const SPINNER_FRAMES = [
48
66
  /** Color for a phase glyph based on its status. Active phase uses border color. */
49
67
  export function phaseStatusColor(status) {
50
68
  if (status === "done")
51
- return "green";
69
+ return BRAND_GREEN;
52
70
  if (status === "failed")
53
71
  return "red";
54
72
  return "gray";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "description": "AI coding agent orchestrator — resolve GitHub issues end-to-end with isolated git worktrees, quality gates, and an MCP server. Works with Claude Code or Aider.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,9 @@ allowed-tools:
13
13
  - Bash(gh *)
14
14
  ---
15
15
 
16
+ <!-- sequant:local-override -->
17
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/assess/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/assess` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
18
+
16
19
  # Unified Issue Assessment & Triage
17
20
 
18
21
  You are the "Assessment Agent" for the current repository.
@@ -27,6 +27,9 @@ allowed-tools:
27
27
  - Grep
28
28
  ---
29
29
 
30
+ <!-- sequant:local-override -->
31
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/clean/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/clean` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
32
+
30
33
  # Repository Cleanup Command
31
34
 
32
35
  Comprehensive, safe repository cleanup that archives stale files, removes artifacts, and commits changes.
@@ -18,6 +18,9 @@ allowed-tools:
18
18
  - Bash(gh pr diff:*)
19
19
  ---
20
20
 
21
+ <!-- sequant:local-override -->
22
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/docs/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/docs` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
23
+
21
24
  # Documentation Generator
22
25
 
23
26
  You are the Phase 4 "Documentation Agent" for the current repository.
@@ -43,6 +43,9 @@ allowed-tools:
43
43
  - TodoWrite
44
44
  ---
45
45
 
46
+ <!-- sequant:local-override -->
47
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/exec/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/exec` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
48
+
46
49
  # Implementation Command
47
50
 
48
51
  You are the Phase 2 "Implementation Agent" for the current repository.
@@ -39,6 +39,9 @@ allowed-tools:
39
39
  - Bash(./scripts/list-worktrees.sh:*)
40
40
  ---
41
41
 
42
+ <!-- sequant:local-override -->
43
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/fullsolve/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/fullsolve` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
44
+
42
45
  # Full Solve Command
43
46
 
44
47
  You are the "Full Solve Agent" for the current repository.
@@ -16,6 +16,9 @@ allowed-tools:
16
16
  - AskUserQuestion
17
17
  ---
18
18
 
19
+ <!-- sequant:local-override -->
20
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/improve/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/improve` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
21
+
19
22
  # Improve Command
20
23
 
21
24
  You are the "Improvement Agent" for the current repository.
@@ -665,4 +668,4 @@ Error: Path `src/nonexistent/` not found.
665
668
  - [ ] **Recommendations** - Tips for running quick wins vs larger refactors
666
669
 
667
670
  **DO NOT proceed to issue creation without user selection.**
668
- **DO NOT respond until all items are verified.**
671
+ **DO NOT respond until all items are verified.**
@@ -25,6 +25,9 @@ allowed-tools:
25
25
  - Bash(git status:*)
26
26
  ---
27
27
 
28
+ <!-- sequant:local-override -->
29
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/loop/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/loop` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
30
+
28
31
  # Quality Loop Command
29
32
 
30
33
  You are the "Quality Loop Agent" for the current repository.
@@ -16,6 +16,9 @@ allowed-tools:
16
16
  - Glob
17
17
  ---
18
18
 
19
+ <!-- sequant:local-override -->
20
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/merger/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/merger` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
21
+
19
22
  # Merger Skill
20
23
 
21
24
  You are the "Merger Agent" for handling post-QA integration of completed worktrees.
@@ -24,6 +24,9 @@ allowed-tools:
24
24
  - AgentOutputTool
25
25
  ---
26
26
 
27
+ <!-- sequant:local-override -->
28
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/qa/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/qa` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
29
+
27
30
  # QA & Code Review
28
31
 
29
32
  You are the Phase 3 "QA & Code Review Agent" for the current repository.
@@ -12,6 +12,9 @@ allowed-tools:
12
12
  - Grep
13
13
  ---
14
14
 
15
+ <!-- sequant:local-override -->
16
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/reflect/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/reflect` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
17
+
15
18
  # Reflection Agent
16
19
 
17
20
  You are the "Reflection Agent" for the current repository.
@@ -17,6 +17,9 @@ allowed-tools:
17
17
  - Bash(gh issue comment:*)
18
18
  ---
19
19
 
20
+ <!-- sequant:local-override -->
21
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/security-review/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/security-review` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
22
+
20
23
  # Security Review Command
21
24
 
22
25
  You are the Security Review Agent for the current repository.
@@ -30,6 +30,9 @@ allowed-tools:
30
30
  - Bash(curl:*)
31
31
  ---
32
32
 
33
+ <!-- sequant:local-override -->
34
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/setup/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/setup` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
35
+
33
36
  # Sequant Setup
34
37
 
35
38
  Initialize Sequant workflow system in your current project.
@@ -13,6 +13,9 @@ allowed-tools:
13
13
  - Bash(gh *)
14
14
  ---
15
15
 
16
+ <!-- sequant:local-override -->
17
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/solve/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/solve` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
18
+
16
19
  # /solve — Deprecated Alias for /assess
17
20
 
18
21
  **This command has been merged into `/assess`.** Use `/assess` instead.
@@ -17,6 +17,9 @@ allowed-tools:
17
17
  - AgentOutputTool
18
18
  ---
19
19
 
20
+ <!-- sequant:local-override -->
21
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/spec/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/spec` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
22
+
20
23
  # Planning Agent
21
24
 
22
25
  Phase 1 "Planning Agent." Understands the issue and AC, reviews or synthesizes a plan, identifies gaps and risks, and drafts a GitHub issue comment.
@@ -19,6 +19,9 @@ allowed-tools:
19
19
  - Bash(npx tsx:*)
20
20
  ---
21
21
 
22
+ <!-- sequant:local-override -->
23
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/test/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/test` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
24
+
22
25
  # Browser Testing Command
23
26
 
24
27
  You are the "Testing Agent" for the current repository.
@@ -20,6 +20,9 @@ allowed-tools:
20
20
  - Agent(sequant-testgen)
21
21
  ---
22
22
 
23
+ <!-- sequant:local-override -->
24
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/testgen/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/testgen` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
25
+
23
26
  # Test Generation Command
24
27
 
25
28
  You are the "Test Generation Agent" for the current repository.
@@ -15,6 +15,9 @@ allowed-tools:
15
15
  - Bash(gh issue comment:*)
16
16
  ---
17
17
 
18
+ <!-- sequant:local-override -->
19
+ > **Local overrides (read this first).** Before following any instruction below, check whether `.claude/.local/skills/verify/overrides.md` exists. If it does, read it and treat its contents as authoritative: its instructions take precedence over anything in this skill they conflict with. This is the supported way to tailor `/verify` without forking it — `overrides.md` lives under `.claude/.local/`, which `sequant update` and `sync` never overwrite.
20
+
18
21
  # Execution Verification
19
22
 
20
23
  You are the "Execution Verification Agent" for the current repository.