@longtable/cli 0.1.51 → 0.1.52

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/README.md CHANGED
@@ -85,3 +85,19 @@ longtable search --query "<topic>"
85
85
  npm run build --workspace @longtable/cli
86
86
  npm run typecheck --workspace @longtable/cli
87
87
  ```
88
+
89
+ ## Codex hard-stop diagnostics
90
+
91
+ Codex `Stop` blocks only active LongTable hard-stop blockers: unresolved
92
+ Research Specification question, scope, construct, method, evidence, or protected
93
+ decision commitments. Use:
94
+
95
+ ```bash
96
+ longtable codex hook-doctor --json
97
+ longtable codex status --json
98
+ longtable doctor --json
99
+ ```
100
+
101
+ to inspect hook coverage/trust plus `stopWouldBlock`, `activeBlockers`, stale
102
+ pending-question counts, and next actions. Tmux remains an optional terminal
103
+ transport; LongTable state and hooks own the behavior.
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { stdin as input, stdout as output, cwd, env, exit } from "node:process";
8
8
  import { dirname, join, resolve } from "node:path";
9
9
  import { homedir } from "node:os";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { collectHardStopBlockers } from "@longtable/core";
11
12
  import { classifyCheckpointTrigger } from "@longtable/checkpoints";
12
13
  import { assessSearchSourceCapabilities, buildResearchSearchIntent, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, SEARCH_SOURCES, summarizeConfiguredPublisherAccess } from "./search/index.js";
13
14
  import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupOutput, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
@@ -169,6 +170,7 @@ function usage() {
169
170
  " longtable codex install-hooks [--codex-config <path>] [--hooks-path <path>] [--json]",
170
171
  " longtable codex remove-hooks [--codex-config <path>] [--hooks-path <path>] [--json]",
171
172
  " longtable codex status [--surface compact|full] [--dir <path>] [--codex-config <path>] [--hooks-path <path>] [--json]",
173
+ " longtable codex hook-doctor [--cwd <path>] [--codex-config <path>] [--hooks-path <path>] [--json]",
172
174
  " longtable claude install-skills [--surface compact|full] [--dir <path>]",
173
175
  " longtable claude remove-skills [--dir <path>]",
174
176
  " longtable claude status [--surface compact|full] [--dir <path>] [--json]",
@@ -1527,6 +1529,22 @@ function setupForProvider(setup, provider) {
1527
1529
  }
1528
1530
  };
1529
1531
  }
1532
+ async function collectHardStopDiagnostics(startPath) {
1533
+ const empty = {
1534
+ stopWouldBlock: false,
1535
+ activeBlockers: [],
1536
+ staleOrUnrelatedPendingQuestionCount: 0,
1537
+ stalePendingQuestionCount: 0,
1538
+ stalePendingObligationCount: 0,
1539
+ nextActions: []
1540
+ };
1541
+ const context = await loadProjectContextFromDirectory(startPath);
1542
+ if (!context) {
1543
+ return empty;
1544
+ }
1545
+ const state = await loadWorkspaceState(context);
1546
+ return collectHardStopBlockers(state);
1547
+ }
1530
1548
  async function collectDoctorStatus(args) {
1531
1549
  const roles = listRoleDefinitions();
1532
1550
  const skillSurface = parseSkillSurface(args);
@@ -1579,11 +1597,13 @@ async function collectDoctorStatus(args) {
1579
1597
  : [];
1580
1598
  const expectedCodexSkills = buildCodexSkillSpecs(roles, skillSurface).map((skill) => skill.name);
1581
1599
  const expectedClaudeSkills = buildClaudeSkillSpecs(roles, skillSurface).map((skill) => skill.name);
1582
- const [codexSkills, claudeSkills, codexAliases, workspace] = await Promise.all([
1600
+ const workspacePath = typeof args.cwd === "string" ? args.cwd : cwd();
1601
+ const [codexSkills, claudeSkills, codexAliases, workspace, hardStop] = await Promise.all([
1583
1602
  listInstalledCodexSkills(roles, codexDir, skillSurface),
1584
1603
  listInstalledClaudeSkills(roles, claudeDir, skillSurface),
1585
1604
  listInstalledCodexPromptAliases(codexPromptsDir),
1586
- inspectProjectWorkspace(typeof args.cwd === "string" ? args.cwd : cwd())
1605
+ inspectProjectWorkspace(workspacePath),
1606
+ collectHardStopDiagnostics(workspacePath)
1587
1607
  ]);
1588
1608
  const installedCodexSkills = codexSkills.map((skill) => skill.name);
1589
1609
  const installedClaudeSkills = claudeSkills.map((skill) => skill.name);
@@ -1614,7 +1634,12 @@ async function collectDoctorStatus(args) {
1614
1634
  hooksExists: existsSync(codexHooksPath),
1615
1635
  codexHooksEnabled: codexHooksEnabled(codexMcpConfig),
1616
1636
  missingManagedHookEvents,
1617
- missingManagedHookTrustState
1637
+ missingManagedHookTrustState,
1638
+ stopWouldBlock: hardStop.stopWouldBlock,
1639
+ activeBlockers: hardStop.activeBlockers,
1640
+ stalePendingQuestionCount: hardStop.stalePendingQuestionCount,
1641
+ stalePendingObligationCount: hardStop.stalePendingObligationCount,
1642
+ nextActions: hardStop.nextActions
1618
1643
  },
1619
1644
  claude: {
1620
1645
  command: "claude",
@@ -1627,7 +1652,8 @@ async function collectDoctorStatus(args) {
1627
1652
  missingSkills: missingNames(expectedClaudeSkills, installedClaudeSkills)
1628
1653
  }
1629
1654
  },
1630
- workspace
1655
+ workspace,
1656
+ hardStop
1631
1657
  };
1632
1658
  }
1633
1659
  function renderProviderDoctorBlock(label, provider) {
@@ -1665,6 +1691,9 @@ function renderDoctorStatus(status) {
1665
1691
  `- hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
1666
1692
  `- managed hook coverage: ${status.providers.codex.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.providers.codex.missingManagedHookEvents.join(", ")}`}`,
1667
1693
  `- managed hook trust: ${status.providers.codex.missingManagedHookTrustState.length === 0 ? "current" : `missing/stale ${status.providers.codex.missingManagedHookTrustState.length}`}`,
1694
+ `- Stop would block now: ${status.hardStop.stopWouldBlock ? "yes" : "no"}`,
1695
+ `- active hard-stop blockers: ${status.hardStop.activeBlockers.length}`,
1696
+ `- stale/unrelated pending questions: ${status.hardStop.staleOrUnrelatedPendingQuestionCount}`,
1668
1697
  "",
1669
1698
  ...renderProviderDoctorBlock("Claude", status.providers.claude),
1670
1699
  "",
@@ -1675,7 +1704,7 @@ function renderDoctorStatus(status) {
1675
1704
  }
1676
1705
  else {
1677
1706
  const workspace = status.workspace;
1678
- lines.push(`- project: ${workspace.project?.name ?? "unknown"}`, `- root: ${workspace.rootPath ?? "unknown"}`, `- goal: ${workspace.session?.currentGoal ?? "unknown"}`, `- invocations: ${workspace.counts?.invocations ?? 0}`, `- questions: ${workspace.counts?.questions ?? 0} (${workspace.counts?.pendingQuestions ?? 0} pending, ${workspace.counts?.answeredQuestions ?? 0} answered)`, `- obligations: ${workspace.counts?.pendingObligations ?? 0} pending`, `- decisions: ${workspace.counts?.decisions ?? 0}`);
1707
+ lines.push(`- project: ${workspace.project?.name ?? "unknown"}`, `- root: ${workspace.rootPath ?? "unknown"}`, `- goal: ${workspace.session?.currentGoal ?? "unknown"}`, `- invocations: ${workspace.counts?.invocations ?? 0}`, `- questions: ${workspace.counts?.questions ?? 0} (${workspace.counts?.pendingQuestions ?? 0} pending, ${workspace.counts?.answeredQuestions ?? 0} answered)`, `- obligations: ${workspace.counts?.pendingObligations ?? 0} pending`, `- Stop hard-stop: ${workspace.hardStop?.stopWouldBlock ? "would block" : "clear"}`, `- stale/unrelated pending: ${workspace.hardStop?.stalePendingQuestionCount ?? 0} questions, ${workspace.hardStop?.stalePendingObligationCount ?? 0} obligations`, `- decisions: ${workspace.counts?.decisions ?? 0}`);
1679
1708
  if ((workspace.recentInvocations ?? []).length > 0) {
1680
1709
  lines.push("- recent invocations:");
1681
1710
  for (const invocation of workspace.recentInvocations ?? []) {
@@ -1689,12 +1718,25 @@ function renderDoctorStatus(status) {
1689
1718
  lines.push(` - ${question.id}: ${question.question} (${question.options.join("/")})`);
1690
1719
  }
1691
1720
  }
1721
+ if (status.hardStop.activeBlockers.length > 0) {
1722
+ lines.push("- active hard-stop blockers:");
1723
+ for (const blocker of status.hardStop.activeBlockers) {
1724
+ lines.push(` - ${blocker.id}: ${blocker.scope} (${blocker.type})`);
1725
+ }
1726
+ }
1692
1727
  if ((workspace.pendingObligations ?? []).length > 0) {
1693
1728
  lines.push("- pending obligations:");
1694
1729
  for (const obligation of workspace.pendingObligations ?? []) {
1695
1730
  lines.push(` - ${obligation.id}: ${obligation.prompt}`);
1696
1731
  }
1697
1732
  }
1733
+ if ((workspace.hardStop?.activeBlockers ?? []).length > 0) {
1734
+ lines.push("- hard-stop blockers:");
1735
+ for (const blocker of workspace.hardStop?.activeBlockers ?? []) {
1736
+ lines.push(` - ${blocker.id} [${blocker.scope}]: ${blocker.prompt}`);
1737
+ lines.push(` next: ${blocker.commandHint}`);
1738
+ }
1739
+ }
1698
1740
  if ((workspace.answerWarnings ?? []).length > 0) {
1699
1741
  lines.push("- answer warnings:");
1700
1742
  for (const warning of workspace.answerWarnings ?? []) {
@@ -1736,10 +1778,16 @@ function renderDoctorStatus(status) {
1736
1778
  if (!status.workspace.found) {
1737
1779
  nextActions.push("longtable start");
1738
1780
  }
1781
+ nextActions.push(...status.hardStop.nextActions);
1739
1782
  const firstQuestion = status.workspace.pendingQuestions?.[0];
1740
- if (firstQuestion) {
1783
+ if (firstQuestion && status.hardStop.nextActions.length === 0) {
1741
1784
  nextActions.push(`longtable decide --question ${firstQuestion.id} --answer <value>`);
1742
1785
  }
1786
+ for (const action of status.workspace.hardStop?.nextActions ?? []) {
1787
+ if (!nextActions.includes(action)) {
1788
+ nextActions.push(action);
1789
+ }
1790
+ }
1743
1791
  if (nextActions.length > 0) {
1744
1792
  lines.push("", "Next actions:");
1745
1793
  for (const action of nextActions) {
@@ -2813,7 +2861,7 @@ async function runAccess(subcommand, args) {
2813
2861
  await runAccessSetup(args);
2814
2862
  return;
2815
2863
  }
2816
- if (subcommand === "status") {
2864
+ if (subcommand === "status" || subcommand === "hook-doctor") {
2817
2865
  await runAccessStatus(args);
2818
2866
  return;
2819
2867
  }
@@ -3768,7 +3816,7 @@ async function runCodexSubcommand(subcommand, args) {
3768
3816
  console.log(renderCodexHookInstallSummary(result));
3769
3817
  return;
3770
3818
  }
3771
- if (subcommand === "status") {
3819
+ if (subcommand === "status" || subcommand === "hook-doctor") {
3772
3820
  const aliases = await listInstalledCodexPromptAliases(customDir);
3773
3821
  const skills = await listInstalledCodexSkills(roles, customDir, skillSurface);
3774
3822
  const setupPath = resolveDefaultSetupPath(typeof args.path === "string" ? args.path : undefined).path;
@@ -3777,6 +3825,7 @@ async function runCodexSubcommand(subcommand, args) {
3777
3825
  const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
3778
3826
  const hooksPath = resolveCodexHooksPath(args);
3779
3827
  const hooksContent = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
3828
+ const workspace = await inspectProjectWorkspace(typeof args.cwd === "string" ? args.cwd : cwd());
3780
3829
  const status = {
3781
3830
  setupPath,
3782
3831
  setupExists: existsSync(setupPath),
@@ -3796,13 +3845,21 @@ async function runCodexSubcommand(subcommand, args) {
3796
3845
  : [...LONGTABLE_MANAGED_HOOK_EVENTS],
3797
3846
  missingManagedHookTrustState: hooksContent
3798
3847
  ? getMissingManagedCodexHookTrustState(configContent, hooksPath, hooksContent)
3799
- : []
3848
+ : [],
3849
+ workspaceHardStop: workspace.hardStop ?? {
3850
+ stopWouldBlock: false,
3851
+ activeBlockers: [],
3852
+ staleOrUnrelatedPendingQuestionCount: 0,
3853
+ stalePendingQuestionCount: 0,
3854
+ stalePendingObligationCount: 0,
3855
+ nextActions: []
3856
+ }
3800
3857
  };
3801
3858
  if (args.json === true) {
3802
3859
  console.log(JSON.stringify(status, null, 2));
3803
3860
  return;
3804
3861
  }
3805
- console.log("LongTable Codex status");
3862
+ console.log(subcommand === "hook-doctor" ? "LongTable Codex hook doctor" : "LongTable Codex status");
3806
3863
  console.log(`- setup: ${status.setupExists ? "present" : "missing"} (${setupPath})`);
3807
3864
  console.log(`- codex runtime artifact: ${status.runtimeExists ? "present" : "missing"} (${runtimePath})`);
3808
3865
  console.log(`- skills dir: ${status.skillsDir}`);
@@ -3832,6 +3889,9 @@ async function runCodexSubcommand(subcommand, args) {
3832
3889
  console.log(`- hooks file: ${status.hooksExists ? "present" : "missing"} (${status.hooksPath})`);
3833
3890
  console.log(`- managed hook coverage: ${status.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.missingManagedHookEvents.join(", ")}`}`);
3834
3891
  console.log(`- managed hook trust: ${status.missingManagedHookTrustState.length === 0 ? "current" : `missing/stale ${status.missingManagedHookTrustState.length}`}`);
3892
+ console.log(`- Stop hard-stop: ${status.workspaceHardStop.stopWouldBlock ? "would block" : "clear"}`);
3893
+ console.log(`- active hard-stop blockers: ${status.workspaceHardStop.activeBlockers.length}`);
3894
+ console.log(`- stale/unrelated pending: ${status.workspaceHardStop.stalePendingQuestionCount} questions, ${status.workspaceHardStop.stalePendingObligationCount} obligations`);
3835
3895
  return;
3836
3896
  }
3837
3897
  throw new Error("Unknown codex subcommand.");
@@ -48,14 +48,12 @@ export function buildManagedCodexHooksConfig(packageRoot) {
48
48
  ],
49
49
  PreToolUse: [
50
50
  buildCommandHook(command, {
51
- matcher: "Bash",
52
- statusMessage: "Running LongTable checkpoint guard"
51
+ matcher: "Bash"
53
52
  })
54
53
  ],
55
54
  PostToolUse: [
56
55
  buildCommandHook(command, {
57
- matcher: "Bash",
58
- statusMessage: "Reviewing LongTable post-tool state"
56
+ matcher: "Bash"
59
57
  })
60
58
  ],
61
59
  UserPromptSubmit: [
@@ -0,0 +1,2 @@
1
+ export { collectHardStopBlockers } from "@longtable/core";
2
+ export type { HardStopBlocker, HardStopBlockerType, HardStopVerdict } from "@longtable/core";
@@ -0,0 +1 @@
1
+ export { collectHardStopBlockers } from "@longtable/core";
package/dist/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export * from "./persona-router.js";
5
5
  export * from "./panel.js";
6
6
  export * from "./project-session.js";
7
7
  export * from "./question-obligations.js";
8
+ export * from "./hard-stop.js";
package/dist/index.js CHANGED
@@ -5,3 +5,4 @@ export * from "./persona-router.js";
5
5
  export * from "./panel.js";
6
6
  export * from "./project-session.js";
7
7
  export * from "./question-obligations.js";
8
+ export * from "./hard-stop.js";
@@ -1,4 +1,5 @@
1
1
  import { pathToFileURL } from "node:url";
2
+ import { collectHardStopBlockers } from "@longtable/core";
2
3
  import { createWorkspaceFollowUpQuestions, loadProjectContextFromDirectory, loadWorkspaceState, pendingQuestionObligations } from "./index.js";
3
4
  function safeString(value) {
4
5
  return typeof value === "string" ? value : "";
@@ -66,6 +67,9 @@ function formatQuestionOptions(question) {
66
67
  function pendingRequiredQuestions(state) {
67
68
  return (state.questionLog ?? []).filter((question) => question.status === "pending" && question.prompt.required);
68
69
  }
70
+ function hardStopBlockers(state) {
71
+ return collectHardStopBlockers(state).activeBlockers;
72
+ }
69
73
  function pendingObligations(state) {
70
74
  return pendingQuestionObligations(state);
71
75
  }
@@ -283,6 +287,22 @@ function buildGeneratedQuestionsContext(questions, created) {
283
287
  lines.push("Do not choose or record answers for these checkpoints unless the researcher explicitly provides the selections.");
284
288
  return lines.join("\n");
285
289
  }
290
+ function buildHardStopBlockerContext(blocker) {
291
+ return [
292
+ `LongTable hard-stop blocker ${blocker.id} affects ${blocker.scope.replace(/_/g, " ")}.`,
293
+ blocker.prompt,
294
+ blocker.reason,
295
+ `Next action: ${blocker.commandHints[0] ?? "decide, clear, or defer with rationale"}`
296
+ ].join("\n");
297
+ }
298
+ function buildStopBlockerReason(blocker, count) {
299
+ const suffix = count > 1 ? ` (${count} active blockers total)` : "";
300
+ return [
301
+ `LongTable hard-stop ${blocker.id}${suffix}: ${blocker.scope.replace(/_/g, " ")}.`,
302
+ compactContextValue(blocker.prompt, 120),
303
+ `Required next action: ${blocker.commandHints[0] ?? "decide, clear, or defer with rationale"}.`
304
+ ].join(" ");
305
+ }
286
306
  function buildPendingObligationContext(obligation) {
287
307
  return [
288
308
  `Pending LongTable research obligation: ${obligation.prompt}`,
@@ -298,6 +318,21 @@ function buildSeparatePendingObligationNotice(obligation) {
298
318
  "This is not part of the active interview. Keep it visible only when the researcher is settling or saving the research direction."
299
319
  ].join("\n");
300
320
  }
321
+ function buildHardStopContext(runtime) {
322
+ const verdict = collectHardStopBlockers(runtime.state);
323
+ const blocker = verdict.activeBlockers[0];
324
+ if (!blocker) {
325
+ return null;
326
+ }
327
+ return [
328
+ `Hard-stop Researcher Checkpoint is still pending: ${blocker.id}`,
329
+ `Affected Research Specification area: ${blocker.scope}`,
330
+ `Question/obligation: ${blocker.prompt}`,
331
+ `Reason: ${blocker.reason}`,
332
+ `Required next action: ${blocker.commandHint}; or clear/defer it with an explicit rationale.`,
333
+ verdict.activeBlockers.length > 1 ? `Additional hard-stop blockers: ${verdict.activeBlockers.length - 1}` : ""
334
+ ].filter(Boolean).join("\n");
335
+ }
301
336
  function buildActiveInterviewContext(hook) {
302
337
  const turnCount = hook.turns?.length ?? 0;
303
338
  return [
@@ -421,13 +456,9 @@ function preToolUseOutput(runtime, payload) {
421
456
  if (!stateChangingCommand) {
422
457
  return null;
423
458
  }
424
- const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
425
- if (blockingQuestion && mutatesLongTableResearchState(command)) {
426
- return buildBlockOutput("PreToolUse", "A required LongTable checkpoint is still pending before a research-state Bash command.", buildPendingQuestionContext(blockingQuestion));
427
- }
428
- const blockingObligation = pendingObligations(runtime.state)[0];
429
- if (blockingObligation && mutatesLongTableResearchState(command)) {
430
- return buildBlockOutput("PreToolUse", "A LongTable research obligation is still pending before a research-state Bash command.", buildPendingObligationContext(blockingObligation));
459
+ const hardStopContext = buildHardStopContext(runtime);
460
+ if (hardStopContext && mutatesLongTableResearchState(command)) {
461
+ return buildBlockOutput("PreToolUse", "A LongTable hard-stop is pending before a research-state Bash command.", hardStopContext);
431
462
  }
432
463
  return null;
433
464
  }
@@ -438,21 +469,18 @@ function postToolUseOutput(runtime, payload) {
438
469
  const command = readCommandText(payload);
439
470
  const exitCode = readExitCode(payload);
440
471
  const output = readCombinedOutput(payload);
441
- const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
442
- const blockingObligation = pendingObligations(runtime.state)[0];
443
- if ((blockingQuestion || blockingObligation) && mutatesLongTableResearchState(command)) {
444
- return buildBlockOutput("PostToolUse", "A research-state Bash command completed while LongTable still had an unresolved checkpoint or obligation.", blockingQuestion
445
- ? buildPendingQuestionContext(blockingQuestion)
446
- : buildPendingObligationContext(blockingObligation));
472
+ const hardStopContext = buildHardStopContext(runtime);
473
+ if (hardStopContext && mutatesLongTableResearchState(command)) {
474
+ return buildBlockOutput("PostToolUse", "A research-state Bash command completed while LongTable still had an unresolved hard-stop.", hardStopContext);
447
475
  }
448
- if (exitCode !== null && exitCode !== 0 && output) {
449
- return buildBlockOutput("PostToolUse", "The Bash command returned a non-zero exit code and should be reviewed before LongTable continues.", "Review the command output and explain what failed before retrying or continuing.");
476
+ if (exitCode !== null && exitCode !== 0 && output && mutatesLongTableResearchState(command)) {
477
+ return buildBlockOutput("PostToolUse", "A LongTable-relevant Bash command returned a non-zero exit code and should be reviewed before LongTable continues.", "Review the command output and explain what failed before retrying or continuing.");
450
478
  }
451
479
  return null;
452
480
  }
453
481
  function stopOutput(runtime) {
454
- void runtime;
455
- return null;
482
+ const hardStopContext = buildHardStopContext(runtime);
483
+ return hardStopContext ? buildStopBlockOutput(hardStopContext) : null;
456
484
  }
457
485
  export async function dispatchCodexHook(payload, cwdOverride) {
458
486
  const hookEventName = readHookEventName(payload);
@@ -1,5 +1,6 @@
1
- import type { DecisionRecord, EvidenceRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionCommitmentFamily, QuestionEpistemicBasis, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionPromptType, QuestionRecord, ResearchSpecificationChange, ResearchSpecificationPatch, ResearchSpecificationPatchSource, ResearchSpecificationRevision, ResearchState } from "@longtable/core";
1
+ import type { DecisionRecord, EvidenceRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, HardStopScope, QuestionCommitmentFamily, QuestionEpistemicBasis, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionPromptType, QuestionRecord, ResearchSpecificationChange, ResearchSpecificationPatch, ResearchSpecificationPatchSource, ResearchSpecificationRevision, ResearchState } from "@longtable/core";
2
2
  import type { SetupPersistedOutput } from "@longtable/setup";
3
+ import { type HardStopVerdict } from "@longtable/core";
3
4
  export type ProjectDisagreementPreference = "synthesis_only" | "show_on_conflict" | "always_visible";
4
5
  export type StartInterviewSignal = "phenomenon" | "audience" | "artifact" | "evidence" | "assumption" | "decision_risk" | "voice";
5
6
  export interface StartInterviewTurn {
@@ -211,6 +212,8 @@ export interface LongTableWorkspaceInspection {
211
212
  questions: number;
212
213
  pendingQuestions: number;
213
214
  pendingObligations: number;
215
+ stalePendingQuestions?: number;
216
+ stalePendingObligations?: number;
214
217
  answeredQuestions: number;
215
218
  decisions: number;
216
219
  interviewTurns?: number;
@@ -218,6 +221,7 @@ export interface LongTableWorkspaceInspection {
218
221
  specPatches?: number;
219
222
  specRevisions?: number;
220
223
  };
224
+ hardStop?: HardStopVerdict;
221
225
  recentInvocations?: Array<{
222
226
  id: string;
223
227
  kind: string;
@@ -234,6 +238,8 @@ export interface LongTableWorkspaceInspection {
234
238
  question: string;
235
239
  commitmentFamily?: QuestionCommitmentFamily;
236
240
  epistemicBasis?: QuestionEpistemicBasis;
241
+ hardStop?: boolean;
242
+ hardStopScope?: string;
237
243
  options: string[];
238
244
  required: boolean;
239
245
  }>;
@@ -242,6 +248,8 @@ export interface LongTableWorkspaceInspection {
242
248
  kind: string;
243
249
  prompt: string;
244
250
  reason: string;
251
+ hardStop?: boolean;
252
+ hardStopScope?: string;
245
253
  questionId?: string;
246
254
  }>;
247
255
  recentDecisions?: Array<{
@@ -379,6 +387,8 @@ export declare function createWorkspaceFollowUpQuestions(options: {
379
387
  prompt: string;
380
388
  provider?: ProviderKind;
381
389
  required?: boolean;
390
+ hardStop?: boolean;
391
+ hardStopScope?: HardStopScope;
382
392
  force?: boolean;
383
393
  auto?: boolean;
384
394
  requiredOnly?: boolean;
@@ -401,6 +411,8 @@ export declare function createWorkspaceQuestion(options: {
401
411
  displayReason?: string;
402
412
  provider?: ProviderKind;
403
413
  required?: boolean;
414
+ hardStop?: boolean;
415
+ hardStopScope?: HardStopScope;
404
416
  commitmentFamily?: QuestionCommitmentFamily;
405
417
  epistemicBasis?: QuestionEpistemicBasis;
406
418
  }): Promise<{
@@ -4,6 +4,7 @@ import { execSync } from "node:child_process";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { appendDecisionRecord as appendDecisionToResearchState, appendInvocationRecord as appendInvocationToResearchState, appendQuestionRecords, createEmptyResearchState } from "@longtable/memory";
6
6
  import { classifyCheckpointTrigger } from "@longtable/checkpoints";
7
+ import { collectHardStopBlockers } from "@longtable/core";
7
8
  import { ensureRequiredQuestionObligation, pendingQuestionObligations, resolveQuestionObligationByQuestionId } from "./question-obligations.js";
8
9
  const CURRENT_FILE_NAME = "CURRENT.md";
9
10
  const LEGACY_ROOT_FILES = ["LONGTABLE.md", "START-HERE.md", "NEXT-STEPS.md", "SESSION-SNAPSHOT.md"];
@@ -765,6 +766,7 @@ function summarizeWorkspaceInspection(context, state) {
765
766
  const pendingQuestions = questions.filter((record) => record.status === "pending");
766
767
  const answeredQuestions = questions.filter((record) => record.status === "answered");
767
768
  const pendingObligations = visiblePendingObligations(state);
769
+ const hardStop = collectHardStopBlockers(state);
768
770
  return {
769
771
  found: true,
770
772
  rootPath: context.project.projectPath,
@@ -803,6 +805,8 @@ function summarizeWorkspaceInspection(context, state) {
803
805
  questions: questions.length,
804
806
  pendingQuestions: pendingQuestions.length,
805
807
  pendingObligations: pendingObligations.length,
808
+ stalePendingQuestions: hardStop.stalePendingQuestionCount,
809
+ stalePendingObligations: hardStop.stalePendingObligationCount,
806
810
  answeredQuestions: answeredQuestions.length,
807
811
  decisions: (state.decisionLog ?? []).length,
808
812
  interviewTurns: (state.interviewTurns ?? []).length,
@@ -810,6 +814,7 @@ function summarizeWorkspaceInspection(context, state) {
810
814
  specPatches: (state.specPatches ?? []).length,
811
815
  specRevisions: (state.specRevisions ?? []).length
812
816
  },
817
+ hardStop,
813
818
  recentInvocations: recentInvocationRecords(state, 5).map((record) => ({
814
819
  id: record.id,
815
820
  kind: record.intent.kind,
@@ -826,6 +831,8 @@ function summarizeWorkspaceInspection(context, state) {
826
831
  question: record.prompt.question,
827
832
  ...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
828
833
  ...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
834
+ ...(typeof record.hardStop === "boolean" ? { hardStop: record.hardStop } : {}),
835
+ ...(record.hardStopScope ? { hardStopScope: record.hardStopScope } : {}),
829
836
  options: formatQuestionOptionValues(record),
830
837
  required: record.prompt.required
831
838
  })),
@@ -834,6 +841,8 @@ function summarizeWorkspaceInspection(context, state) {
834
841
  kind: obligation.kind,
835
842
  prompt: obligation.prompt,
836
843
  reason: obligation.reason,
844
+ ...(typeof obligation.hardStop === "boolean" ? { hardStop: obligation.hardStop } : {}),
845
+ ...(obligation.hardStopScope ? { hardStopScope: obligation.hardStopScope } : {}),
837
846
  ...(obligation.questionId ? { questionId: obligation.questionId } : {})
838
847
  })),
839
848
  recentDecisions: (state.decisionLog ?? []).slice(-5).reverse().map((record) => ({
@@ -2305,12 +2314,45 @@ function inferEpistemicBasis(input) {
2305
2314
  return "mixed";
2306
2315
  return unique[0];
2307
2316
  }
2317
+ function inferHardStopScope(input, commitmentFamily) {
2318
+ if (commitmentFamily === "product_policy")
2319
+ return undefined;
2320
+ if (commitmentFamily === "scope")
2321
+ return "scope";
2322
+ if (commitmentFamily === "construct" || commitmentFamily === "coding")
2323
+ return "construct";
2324
+ if (commitmentFamily === "method")
2325
+ return "method";
2326
+ if (commitmentFamily === "evidence")
2327
+ return "evidence";
2328
+ if (commitmentFamily === "epistemic_authority")
2329
+ return "protected_decision";
2330
+ const text = compactMetadataText([input.checkpointKey, input.title, input.question, input.prompt, input.rationale]);
2331
+ if (textMatchesAny(text, [/product_runtime|checkpoint policy|hook ux|setup|install|cli|npm|release|git|github|docs?|readme|package|workflow/])) {
2332
+ return undefined;
2333
+ }
2334
+ if (textMatchesAny(text, [/protected_decision|closure/]))
2335
+ return "protected_decision";
2336
+ if (textMatchesAny(text, [/research_question|research direction|question_freeze/]))
2337
+ return "research_question";
2338
+ if (textMatchesAny(text, [/scope|boundary|inclusion|exclusion/]))
2339
+ return "scope";
2340
+ if (textMatchesAny(text, [/construct|theory|frame|ontology|measurement|coding|validity/]))
2341
+ return "construct";
2342
+ if (textMatchesAny(text, [/method|design|sample|analysis|strategy|model/]))
2343
+ return "method";
2344
+ if (textMatchesAny(text, [/evidence|access|source|corpus|pdf|full[-_ ]?text|scholarly/]))
2345
+ return "evidence";
2346
+ return undefined;
2347
+ }
2308
2348
  function resolveQuestionRecordMetadata(input) {
2309
2349
  const commitmentFamily = input.commitmentFamily ?? inferCommitmentFamily(input);
2310
2350
  const epistemicBasis = input.epistemicBasis ?? inferEpistemicBasis(input);
2351
+ const hardStopScope = inferHardStopScope(input, commitmentFamily);
2311
2352
  return {
2312
2353
  ...(commitmentFamily ? { commitmentFamily } : {}),
2313
- ...(epistemicBasis ? { epistemicBasis } : {})
2354
+ ...(epistemicBasis ? { epistemicBasis } : {}),
2355
+ ...(hardStopScope ? { hardStop: true, hardStopScope } : {})
2314
2356
  };
2315
2357
  }
2316
2358
  function hasFollowUpPrompt(record, prompt) {
@@ -2371,6 +2413,8 @@ export async function createWorkspaceFollowUpQuestions(options) {
2371
2413
  updatedAt: createdAt,
2372
2414
  status: "pending",
2373
2415
  ...metadata,
2416
+ ...(typeof options.hardStop === "boolean" ? { hardStop: options.hardStop } : {}),
2417
+ ...(options.hardStopScope ? { hardStopScope: options.hardStopScope } : {}),
2374
2418
  prompt: {
2375
2419
  id: createId("question_prompt"),
2376
2420
  checkpointKey,
@@ -2425,6 +2469,8 @@ export async function createWorkspaceQuestion(options) {
2425
2469
  updatedAt: createdAt,
2426
2470
  status: "pending",
2427
2471
  ...metadata,
2472
+ ...(typeof options.hardStop === "boolean" ? { hardStop: options.hardStop } : {}),
2473
+ ...(options.hardStopScope ? { hardStopScope: options.hardStopScope } : {}),
2428
2474
  prompt: {
2429
2475
  id: createId("question_prompt"),
2430
2476
  checkpointKey,
@@ -17,6 +17,8 @@ export function createRequiredQuestionObligation(question) {
17
17
  updatedAt: timestamp,
18
18
  prompt: question.prompt.question,
19
19
  reason: question.prompt.displayReason ?? "A required LongTable checkpoint is pending.",
20
+ ...(question.hardStop !== undefined ? { hardStop: question.hardStop } : {}),
21
+ ...(question.hardStopScope ? { hardStopScope: question.hardStopScope } : {}),
20
22
  questionId: question.id
21
23
  };
22
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.51",
3
+ "version": "0.1.52",
4
4
  "private": false,
5
5
  "description": "Researcher-facing LongTable CLI",
6
6
  "type": "module",
@@ -29,12 +29,12 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^1.2.0",
32
- "@longtable/checkpoints": "0.1.51",
33
- "@longtable/core": "0.1.51",
34
- "@longtable/memory": "0.1.51",
35
- "@longtable/provider-claude": "0.1.51",
36
- "@longtable/provider-codex": "0.1.51",
37
- "@longtable/setup": "0.1.51"
32
+ "@longtable/checkpoints": "0.1.52",
33
+ "@longtable/core": "0.1.52",
34
+ "@longtable/memory": "0.1.52",
35
+ "@longtable/provider-claude": "0.1.52",
36
+ "@longtable/provider-codex": "0.1.52",
37
+ "@longtable/setup": "0.1.52"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",