sequant 2.6.0 → 2.6.2

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 +5 -0
  4. package/dist/bin/cli.js +27 -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 +43 -5
  26. package/dist/src/commands/sync.js +188 -17
  27. package/dist/src/commands/update.d.ts +1 -0
  28. package/dist/src/commands/update.js +73 -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
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "sequant",
10
10
  "description": "AI coding agent orchestrator for Claude Code — resolve GitHub issues end-to-end with isolated git worktrees, quality gates, and an MCP server. Includes 17 skills, workflow MCP tools, and pre/post-tool hooks.",
11
- "version": "2.6.0",
11
+ "version": "2.6.2",
12
12
  "author": {
13
13
  "name": "sequant-io",
14
14
  "email": "hello@sequant.io"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sequant",
3
3
  "description": "AI coding agent orchestrator for Claude Code — resolve GitHub issues end-to-end with isolated git worktrees and quality gates, through spec → exec → qa phases.",
4
- "version": "2.6.0",
4
+ "version": "2.6.2",
5
5
  "author": {
6
6
  "name": "sequant-io",
7
7
  "email": "hello@sequant.io"
package/README.md CHANGED
@@ -16,6 +16,11 @@ AI coding agents write code well, but leave you to run the workflow around it
16
16
 
17
17
  See the [CHANGELOG](CHANGELOG.md) for release notes, or the [migration guide](CHANGELOG.md#migration-from-v1x) if upgrading from v1.x.
18
18
 
19
+ ### What's new in 2.6
20
+
21
+ - **Boxed Ink TUI is the default for `sequant run`** — on a TTY, `run` now renders the boxed dashboard by default (matching `sequant ready`). Opt out with `--no-tui` (line renderer) or `-s`/`--quiet` (heartbeat-only); non-TTY output auto-degrades.
22
+ - **Flag change:** `--quiet` moved from `-q` to **`-s`** (silent). `-q` is now an alias for `-Q, --quality-loop`, so `sequant run … -q` enables the quality loop as intended. (`--experimental-tui` is kept as a hidden no-op alias.)
23
+
19
24
  ### What's new in 2.5
20
25
 
21
26
  - **`sequant ready <issue>`** — a post-resolve A+ QA gate that drives a resolved issue through a full-weight `qa → loop → qa` pass and **stops at the human merge gate — it never merges**.
package/dist/bin/cli.js CHANGED
@@ -44,7 +44,7 @@ import { logsCommand } from "../src/commands/logs.js";
44
44
  import { statsCommand } from "../src/commands/stats.js";
45
45
  import { dashboardCommand } from "../src/commands/dashboard.js";
46
46
  import { stateInitCommand, stateRebuildCommand, stateCleanCommand, } from "../src/commands/state.js";
47
- import { syncCommand, areSkillsOutdated } from "../src/commands/sync.js";
47
+ import { syncCommand, areSkillsOutdated, checkAndWarnSkillsOutdated, } from "../src/commands/sync.js";
48
48
  import { mergeCommand } from "../src/commands/merge.js";
49
49
  import { readyCommand, } from "../src/commands/ready.js";
50
50
  import { conventionsCommand } from "../src/commands/conventions.js";
@@ -128,6 +128,7 @@ program
128
128
  .description("Update templates from the Sequant package")
129
129
  .option("-d, --dry-run", "Show what would be updated without making changes")
130
130
  .option("-f, --force", "Overwrite local modifications")
131
+ .option("-y, --yes", "Apply updates without prompting (for CI/non-interactive shells)")
131
132
  .action(updateCommand);
132
133
  program
133
134
  .command("sync")
@@ -399,14 +400,36 @@ locksCmd
399
400
  // Projects that manage skills manually (no marker) are not affected.
400
401
  program.hook("preAction", async (thisCommand) => {
401
402
  const cmd = thisCommand.name();
402
- if (cmd === "init" || cmd === "sync")
403
+ // `update` is excluded alongside `init`/`sync`: it is itself the command that
404
+ // resolves drift, so the warn-only "run sync/update" pre-flight would be a
405
+ // circular nag right before it does exactly that.
406
+ if (cmd === "init" || cmd === "sync" || cmd === "update")
403
407
  return;
404
408
  const manifest = await getManifest();
405
409
  if (!manifest)
406
410
  return;
407
- const { outdated, currentVersion } = await areSkillsOutdated();
408
- if (outdated && currentVersion !== null) {
411
+ // `cache: true` opts the per-command pre-flight into the stat-only drift
412
+ // fingerprint cache: the full template scan runs only when something that
413
+ // affects drift changed, keeping latency off the hot path (AC-5).
414
+ const status = await areSkillsOutdated({ cache: true });
415
+ const { outdated, currentVersion, contentDrift } = status;
416
+ // No version marker → the project manages skills manually; stay silent and
417
+ // do nothing (unchanged behavior — see the header comment above the hook).
418
+ if (currentVersion === null)
419
+ return;
420
+ if (outdated) {
421
+ // Version-marker mismatch → stale install: auto-sync (copy) as before.
409
422
  await syncCommand({ quiet: true });
423
+ return;
424
+ }
425
+ // Version-current but bundled content drifted in place (#708/#713). AC-3
426
+ // decision: auto-sync (copy) stays gated on version bumps ONLY — we do NOT
427
+ // copy here, because that would clobber in-place customizations (#711).
428
+ // Content-only drift is surfaced as a non-destructive, warn-only signal,
429
+ // leaving the fix to the user (`sequant sync`/`update`). The helper never
430
+ // touches process.exitCode, so the command still exits normally.
431
+ if (contentDrift > 0) {
432
+ await checkAndWarnSkillsOutdated(status);
410
433
  }
411
434
  });
412
435
  // Parse and execute
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sequant",
3
3
  "description": "AI coding agent orchestrator for Claude Code — resolve GitHub issues end-to-end with isolated git worktrees and quality gates, through spec → exec → qa phases.",
4
- "version": "2.6.0",
4
+ "version": "2.6.2",
5
5
  "author": {
6
6
  "name": "sequant-io",
7
7
  "email": "hello@sequant.io"
@@ -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.
@@ -130,7 +130,7 @@ export async function readyCommand(issueArg, options) {
130
130
  const branch = listWorktrees().find((w) => w.issue === issueNumber)?.branch ?? "";
131
131
  adapter = new ReadySnapshotAdapter({ issueNumber, title, branch });
132
132
  onProgress = adapter.onProgress;
133
- const { renderTui } = await import("../ui/tui/index.js");
133
+ const { renderTui } = await (await import("../ui/tui/load.js")).loadTui();
134
134
  tuiHandle = renderTui(adapter);
135
135
  }
136
136
  else if (!options.json) {
@@ -92,7 +92,7 @@ export async function runCommand(issues, options) {
92
92
  maxLoopIterations: resolved.config.maxIterations,
93
93
  });
94
94
  if (tuiEnabled) {
95
- const { renderTui } = await import("../ui/tui/index.js");
95
+ const { renderTui } = await (await import("../ui/tui/load.js")).loadTui();
96
96
  let tuiHandle = null;
97
97
  // Unmount the TUI before ShutdownManager writes its shutdown banner so
98
98
  // the two don't race on stdout / leave the terminal in alt-screen buffer.
@@ -13,16 +13,54 @@ interface SyncOptions {
13
13
  */
14
14
  export declare function getSkillsVersion(): Promise<string | null>;
15
15
  /**
16
- * Check if skills are outdated compared to package version
16
+ * Skills status relative to the bundled package, as seen by the pre-flight path.
17
17
  */
18
- export declare function areSkillsOutdated(): Promise<{
18
+ export interface SkillsOutdatedStatus {
19
+ /** Version-marker mismatch (`.sequant-version` ≠ package). Cheap fast-path. */
19
20
  outdated: boolean;
20
21
  currentVersion: string | null;
21
22
  packageVersion: string;
22
- }>;
23
+ /**
24
+ * Count of bundled files that are `new` or `modified` in place at a *matching*
25
+ * version (the #708 blind spot the version marker can't see). Only computed
26
+ * when versions match; `0` otherwise (a mismatch already means stale). Excludes
27
+ * `local-override`/`unchanged` so customized files (e.g. constitution, #711)
28
+ * don't register as drift.
29
+ */
30
+ contentDrift: number;
31
+ }
32
+ /**
33
+ * Check if skills are outdated compared to package version.
34
+ *
35
+ * The version marker is only a cheap hint: a tree at the matching version can
36
+ * still have drifted bundled content in place (the #708 root cause). So when the
37
+ * marker matches we run the same content diff `sync` uses (`computeTemplateChanges`,
38
+ * the single source of truth from #708/#710) and surface a `contentDrift` count.
39
+ * On a version *mismatch* we skip the diff entirely — the install is already stale
40
+ * and the copy path handles it — keeping the per-command pre-flight cheap (AC-5).
41
+ *
42
+ * `options.cache` opts into a stat-only fingerprint cache for the content scan,
43
+ * so the hot pre-flight path (which runs before most commands, including batched
44
+ * `/assess` dashboard calls) pays the full ~15ms scan only when something that
45
+ * affects drift actually changed. Off by default so diagnostic callers (`doctor`)
46
+ * always see fresh truth.
47
+ */
48
+ export declare function areSkillsOutdated(options?: {
49
+ cache?: boolean;
50
+ }): Promise<SkillsOutdatedStatus>;
23
51
  export declare function syncCommand(options?: SyncOptions): Promise<void>;
24
52
  /**
25
- * Check and warn if skills are outdated (for use by other commands)
53
+ * Check and warn if skills are outdated (for use by other commands).
54
+ *
55
+ * Warns on either signal: a version-marker mismatch, or in-place content drift at
56
+ * a matching version (#708/#713). The content-drift path is warn-only by design —
57
+ * it never mutates files and never sets `process.exitCode` (this is a pre-flight,
58
+ * not the command itself), so customized installs (#711) are left intact.
59
+ *
60
+ * Callers that have already computed the status (e.g. the `preAction` hook) can
61
+ * pass it in to avoid a second template scan on the hot path (AC-5).
62
+ *
63
+ * @returns `true` if a warning was emitted, `false` if up to date.
26
64
  */
27
- export declare function checkAndWarnSkillsOutdated(): Promise<boolean>;
65
+ export declare function checkAndWarnSkillsOutdated(status?: SkillsOutdatedStatus): Promise<boolean>;
28
66
  export {};
@@ -5,14 +5,23 @@
5
5
  * Designed for plugin users who need to update after upgrading sequant.
6
6
  */
7
7
  import chalk from "chalk";
8
+ import { join } from "path";
9
+ import { createHash } from "crypto";
8
10
  import { getManifest, updateManifest, getPackageVersion, } from "../lib/manifest.js";
9
- import { copyTemplates } from "../lib/templates.js";
11
+ import { copyTemplates, computeTemplateChanges, listTemplateFiles, getTemplatesDir, } from "../lib/templates.js";
10
12
  import { getConfig } from "../lib/config.js";
11
- import { writeFile, readFile, fileExists } from "../lib/fs.js";
13
+ import { writeFile, readFile, fileExists, getFileStats } from "../lib/fs.js";
12
14
  import { generateAgentsMd, writeAgentsMd, AGENTS_MD_PATH, } from "../lib/agents-md.js";
13
15
  import { getProjectName } from "../lib/project-name.js";
14
16
  import { getStackConfig } from "../lib/stacks.js";
15
17
  const SKILLS_VERSION_PATH = ".claude/skills/.sequant-version";
18
+ // Where the cheap drift-fingerprint cache lives (gitignored via `**/.sequant/`).
19
+ const DRIFT_CACHE_PATH = ".claude/.sequant/.skills-drift-cache.json";
20
+ // Mirrors config.ts / manifest.ts (those constants are module-private). These
21
+ // install paths are stable; we stat them only to invalidate the drift cache
22
+ // when the project's config tokens or manifest stack change.
23
+ const CONFIG_FILE_PATH = ".claude/.sequant/config.json";
24
+ const MANIFEST_FILE_PATH = ".sequant-manifest.json";
16
25
  /**
17
26
  * Get the version of skills currently installed
18
27
  */
@@ -29,16 +38,141 @@ export async function getSkillsVersion() {
29
38
  }
30
39
  }
31
40
  /**
32
- * Check if skills are outdated compared to package version
41
+ * Cheap stat-only fingerprint of every input that can change the content-drift
42
+ * result: package version plus the mtime (or absence) of each bundled template,
43
+ * its installed counterpart, any `.claude/.local/` override, and the config and
44
+ * manifest. A full read+render+diff scan is ~15ms per command; this fingerprint
45
+ * is ~2-5ms, so the per-command pre-flight can skip the scan when nothing that
46
+ * affects drift has changed (AC-5). A per-file hash (not a max-mtime) is used so
47
+ * editing an *older* file — whose new mtime may still trail another file's —
48
+ * still changes the fingerprint and forces a rescan (no missed warnings).
49
+ *
50
+ * Returns `null` if it cannot be computed; the caller then scans uncached.
51
+ */
52
+ async function computeDriftFingerprint(packageVersion) {
53
+ try {
54
+ const templateFiles = await listTemplateFiles();
55
+ const templatesDir = getTemplatesDir();
56
+ const lines = [`v=${packageVersion}`];
57
+ const addPath = async (fsPath, key) => {
58
+ try {
59
+ const stats = await getFileStats(fsPath);
60
+ lines.push(`${key}:${Math.round(stats.mtimeMs)}`);
61
+ }
62
+ catch {
63
+ // Missing file is itself signal: a `.local` override or installed file
64
+ // appearing/disappearing flips this line and invalidates the cache.
65
+ lines.push(`${key}:absent`);
66
+ }
67
+ };
68
+ for (const templatePath of templateFiles) {
69
+ const normalized = templatePath.replace(/\\/g, "/");
70
+ const localPath = normalized.replace("templates/", ".claude/");
71
+ if (localPath.includes(".local/"))
72
+ continue;
73
+ const templateFsPath = join(templatesDir, normalized.replace("templates/", ""));
74
+ const overridePath = localPath.replace(".claude/", ".claude/.local/");
75
+ await addPath(templateFsPath, `t:${normalized}`);
76
+ await addPath(localPath, `l:${localPath}`);
77
+ await addPath(overridePath, `o:${overridePath}`);
78
+ }
79
+ await addPath(CONFIG_FILE_PATH, "config");
80
+ await addPath(MANIFEST_FILE_PATH, "manifest");
81
+ lines.sort();
82
+ return createHash("sha1").update(lines.join("\n")).digest("hex");
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ async function readDriftCache() {
89
+ try {
90
+ if (!(await fileExists(DRIFT_CACHE_PATH)))
91
+ return null;
92
+ const parsed = JSON.parse(await readFile(DRIFT_CACHE_PATH));
93
+ if (typeof parsed?.fingerprint === "string" &&
94
+ typeof parsed?.contentDrift === "number") {
95
+ return parsed;
96
+ }
97
+ return null;
98
+ }
99
+ catch {
100
+ // Corrupt/unreadable cache → treat as a miss; the scan path rebuilds it.
101
+ return null;
102
+ }
103
+ }
104
+ async function writeDriftCache(cache) {
105
+ try {
106
+ await writeFile(DRIFT_CACHE_PATH, JSON.stringify(cache));
107
+ }
108
+ catch {
109
+ // The cache is a pure optimization — never fail a command over a write miss
110
+ // (e.g. the `.claude/.sequant/` dir not existing yet).
111
+ }
112
+ }
113
+ /**
114
+ * Run the content-drift scan (the source-of-truth `computeTemplateChanges` diff),
115
+ * returning the count of `new`+`modified` files. When `useCache` is true (the
116
+ * per-command pre-flight), a stat-only fingerprint short-circuits the scan if no
117
+ * drift-affecting input changed since the last run. Callers that need fresh
118
+ * truth (`doctor`, and `sync` itself) leave caching off — the default.
33
119
  */
34
- export async function areSkillsOutdated() {
120
+ async function computeContentDrift(packageVersion, useCache) {
121
+ let fingerprint = null;
122
+ if (useCache) {
123
+ fingerprint = await computeDriftFingerprint(packageVersion);
124
+ if (fingerprint) {
125
+ const cached = await readDriftCache();
126
+ if (cached && cached.fingerprint === fingerprint) {
127
+ return cached.contentDrift;
128
+ }
129
+ }
130
+ }
131
+ try {
132
+ const manifest = await getManifest();
133
+ if (!manifest)
134
+ return 0;
135
+ const config = await getConfig();
136
+ const tokens = config?.tokens || {};
137
+ const changes = await computeTemplateChanges(manifest.stack, tokens);
138
+ const contentDrift = changes.filter((c) => c.status === "new" || c.status === "modified").length;
139
+ if (useCache && fingerprint) {
140
+ await writeDriftCache({ fingerprint, contentDrift });
141
+ }
142
+ return contentDrift;
143
+ }
144
+ catch {
145
+ // The pre-flight must never break the actual command. If the content diff
146
+ // fails (missing templates, read error), treat it as "no detectable drift"
147
+ // and let the command proceed.
148
+ return 0;
149
+ }
150
+ }
151
+ /**
152
+ * Check if skills are outdated compared to package version.
153
+ *
154
+ * The version marker is only a cheap hint: a tree at the matching version can
155
+ * still have drifted bundled content in place (the #708 root cause). So when the
156
+ * marker matches we run the same content diff `sync` uses (`computeTemplateChanges`,
157
+ * the single source of truth from #708/#710) and surface a `contentDrift` count.
158
+ * On a version *mismatch* we skip the diff entirely — the install is already stale
159
+ * and the copy path handles it — keeping the per-command pre-flight cheap (AC-5).
160
+ *
161
+ * `options.cache` opts into a stat-only fingerprint cache for the content scan,
162
+ * so the hot pre-flight path (which runs before most commands, including batched
163
+ * `/assess` dashboard calls) pays the full ~15ms scan only when something that
164
+ * affects drift actually changed. Off by default so diagnostic callers (`doctor`)
165
+ * always see fresh truth.
166
+ */
167
+ export async function areSkillsOutdated(options = {}) {
35
168
  const currentVersion = await getSkillsVersion();
36
169
  const packageVersion = getPackageVersion();
37
- return {
38
- outdated: currentVersion !== packageVersion,
39
- currentVersion,
40
- packageVersion,
41
- };
170
+ const outdated = currentVersion !== packageVersion;
171
+ let contentDrift = 0;
172
+ if (!outdated) {
173
+ contentDrift = await computeContentDrift(packageVersion, options.cache === true);
174
+ }
175
+ return { outdated, currentVersion, packageVersion, contentDrift };
42
176
  }
43
177
  /**
44
178
  * Update the skills version marker
@@ -68,16 +202,35 @@ export async function syncCommand(options = {}) {
68
202
  console.log(chalk.gray(`Package version: ${packageVersion}`));
69
203
  console.log(chalk.gray(`Stack: ${manifest.stack}\n`));
70
204
  }
71
- // Check if sync is needed
205
+ // Get config tokens for template processing
206
+ const config = await getConfig();
207
+ const tokens = config?.tokens || {};
208
+ // The version marker is only a fast-path hint — verify actual content before
209
+ // claiming "up to date". On a version match we still diff bundled templates
210
+ // against installed content (rendered with the same variables) so we never
211
+ // declare success while real drift sits in place (#708).
72
212
  if (!force && skillsVersion === packageVersion) {
213
+ const changes = await computeTemplateChanges(manifest.stack, tokens);
214
+ const drifted = changes.filter((c) => c.status === "new" || c.status === "modified");
215
+ if (drifted.length === 0) {
216
+ // Truthful no-op: content is actually identical.
217
+ if (!quiet) {
218
+ console.log(chalk.green("✔ Skills are already up to date!"));
219
+ }
220
+ return;
221
+ }
222
+ // Version current but content differs — report, don't mutate (report-only
223
+ // keeps the fast path from silently overwriting in-place customizations).
73
224
  if (!quiet) {
74
- console.log(chalk.green("✔ Skills are already up to date!"));
225
+ console.log(chalk.yellow(`! Version current, but ${drifted.length} file(s) differ — run \`update\` or \`sync --force\``));
75
226
  }
227
+ // Signal drift with a non-zero exit code even under --quiet. The exit code
228
+ // is the machine signal the (suppressible) message is not, so the
229
+ // non-interactive / CI path we recommend can't treat a drifted tree as
230
+ // success — the original failure mode in #708.
231
+ process.exitCode = 1;
76
232
  return;
77
233
  }
78
- // Get config tokens for template processing
79
- const config = await getConfig();
80
- const tokens = config?.tokens || {};
81
234
  // Copy templates with force to overwrite existing files
82
235
  const copyOptions = {
83
236
  force: true, // Always overwrite when syncing
@@ -118,14 +271,32 @@ export async function syncCommand(options = {}) {
118
271
  }
119
272
  }
120
273
  /**
121
- * Check and warn if skills are outdated (for use by other commands)
274
+ * Check and warn if skills are outdated (for use by other commands).
275
+ *
276
+ * Warns on either signal: a version-marker mismatch, or in-place content drift at
277
+ * a matching version (#708/#713). The content-drift path is warn-only by design —
278
+ * it never mutates files and never sets `process.exitCode` (this is a pre-flight,
279
+ * not the command itself), so customized installs (#711) are left intact.
280
+ *
281
+ * Callers that have already computed the status (e.g. the `preAction` hook) can
282
+ * pass it in to avoid a second template scan on the hot path (AC-5).
283
+ *
284
+ * @returns `true` if a warning was emitted, `false` if up to date.
122
285
  */
123
- export async function checkAndWarnSkillsOutdated() {
124
- const { outdated, currentVersion, packageVersion } = await areSkillsOutdated();
286
+ export async function checkAndWarnSkillsOutdated(status) {
287
+ const { outdated, currentVersion, packageVersion, contentDrift } = status ?? (await areSkillsOutdated());
125
288
  if (outdated) {
126
289
  console.log(chalk.yellow(`\n! Skills are outdated (${currentVersion || "unknown"} → ${packageVersion})`));
127
290
  console.log(chalk.yellow(" Run: npx sequant sync\n"));
128
291
  return true;
129
292
  }
293
+ if (contentDrift > 0) {
294
+ // Mirror syncCommand's own drift remediation: a bare `sync` at a matching
295
+ // version is report-only (it won't copy), so point at the commands that
296
+ // actually resolve in-place drift — `sync --force` or `update`.
297
+ console.log(chalk.yellow(`\n! Version current, but ${contentDrift} file(s) differ from bundled content`));
298
+ console.log(chalk.yellow(" Run: npx sequant sync --force (or npx sequant update)\n"));
299
+ return true;
300
+ }
130
301
  return false;
131
302
  }
@@ -4,6 +4,7 @@
4
4
  interface UpdateOptions {
5
5
  dryRun?: boolean;
6
6
  force?: boolean;
7
+ yes?: boolean;
7
8
  }
8
9
  export declare function updateCommand(options: UpdateOptions): Promise<void>;
9
10
  export {};