@longtable/cli 0.1.51 → 0.1.53

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
@@ -4,7 +4,8 @@ Researcher-facing CLI for LongTable.
4
4
 
5
5
  LongTable keeps scholarly project state in `.longtable/` and exposes generated
6
6
  provider skills for Codex and Claude Code. The CLI installs setup, state,
7
- checkpoint, search, and diagnostic tooling. It does not replace the provider.
7
+ checkpoint, search, panel, and diagnostic tooling. It does not replace the
8
+ provider.
8
9
 
9
10
  ## Install
10
11
 
@@ -74,14 +75,53 @@ longtable question --prompt "<decision context>"
74
75
  longtable decide --question <id> --answer <value>
75
76
  longtable spec read --cwd "<project-path>"
76
77
  longtable search --query "<topic>"
78
+ longtable panel --prompt "review this measurement plan" --json
77
79
  ```
78
80
 
79
81
  `longtable start` remains available for scripted workspace creation with
80
82
  `--no-interview --json`, but it is not the primary research-start surface.
81
83
 
84
+ ## Panel Orchestration
85
+
86
+ Panel orchestration is for moments where disagreement matters: methods risk,
87
+ measurement validity, theory fit, literature positioning, and claims that need
88
+ challenge before they become project memory.
89
+
90
+ The CLI creates a provider-neutral `PanelPlan` and returns a planned
91
+ `PanelResult`. When native subagents are unavailable, LongTable uses a stable
92
+ sequential fallback prompt. That keeps the same research semantics available in
93
+ Codex and Claude Code without making either provider's native question or agent
94
+ tool the source of truth.
95
+
96
+ ```bash
97
+ longtable panel --prompt "Review this measurement plan." --role editor,measurement_auditor --json
98
+ longtable panel --visibility always_visible --prompt "Keep unresolved disagreement visible." --json
99
+ longtable ask --prompt "lt debate: Review this design before I commit it." --json
100
+ ```
101
+
102
+ Team-style requests route through panel. Explicit debate-language requests write
103
+ panel debate records under `.longtable/panel/`; LongTable team execution is
104
+ disabled for new work.
105
+
82
106
  ## Development
83
107
 
84
108
  ```bash
85
109
  npm run build --workspace @longtable/cli
86
110
  npm run typecheck --workspace @longtable/cli
87
111
  ```
112
+
113
+ ## Codex hard-stop diagnostics
114
+
115
+ Codex `Stop` blocks only active LongTable hard-stop blockers: unresolved
116
+ Research Specification question, scope, construct, method, evidence, or protected
117
+ decision commitments. Use:
118
+
119
+ ```bash
120
+ longtable codex hook-doctor --json
121
+ longtable codex status --json
122
+ longtable doctor --json
123
+ ```
124
+
125
+ to inspect hook coverage/trust plus `stopWouldBlock`, `activeBlockers`, stale
126
+ pending-question counts, and next actions. Tmux remains an optional terminal
127
+ 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";
@@ -19,7 +20,7 @@ import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
19
20
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
20
21
  import { LONGTABLE_MANAGED_HOOK_EVENTS, codexHooksEnabled, enableCodexHooksFeature, getMissingManagedCodexHookEvents, getMissingManagedCodexHookTrustState, mergeCodexHookTrustState, mergeManagedCodexHooksConfig, removeCodexHookTrustState, removeManagedCodexHooks } from "./codex-hooks.js";
21
22
  import { appendInvocationRecordToWorkspace, applyResearchSpecificationPatch, assertWorkspaceNotBlocked, answerWorkspaceQuestion, buildQuestionOpportunitySpecs, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, diffResearchSpecifications, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, findUnincorporatedResearchEvidence, proposeResearchSpecificationPatch, pruneWorkspaceQuestions, readResearchSpecificationHistory, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
22
- import { buildTeamDebate, buildTeamReview, renderTeamDebateSummary } from "./debate.js";
23
+ import { buildTeamDebate } from "./debate.js";
23
24
  import { createPromptRenderer } from "./prompt-renderer.js";
24
25
  const VALID_MODES = new Set([
25
26
  "explore",
@@ -153,7 +154,6 @@ function usage() {
153
154
  " longtable access probe --doi <doi> [--publisher auto|elsevier|springer_nature|wiley|taylor_francis] [--json]",
154
155
  " longtable search --query <text> [--intent literature|theory|measurement|citation|metadata|venue] [--field <text>] [--source all|crossref,arxiv,openalex,semantic_scholar,pubmed,eric,doaj,unpaywall] [--must <term[,term]>] [--exclude <term[,term]>] [--limit <n>] [--allow-partial] [--publisher-access] [--record] [--cwd <path>] [--json]",
155
156
  " longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
156
- " longtable team --prompt <text> [--role <role[,role]>] [--debate] [--rounds 3|5] [--cwd <path>] [--json]",
157
157
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
158
158
  " longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
159
159
  " longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
@@ -169,6 +169,7 @@ function usage() {
169
169
  " longtable codex install-hooks [--codex-config <path>] [--hooks-path <path>] [--json]",
170
170
  " longtable codex remove-hooks [--codex-config <path>] [--hooks-path <path>] [--json]",
171
171
  " longtable codex status [--surface compact|full] [--dir <path>] [--codex-config <path>] [--hooks-path <path>] [--json]",
172
+ " longtable codex hook-doctor [--cwd <path>] [--codex-config <path>] [--hooks-path <path>] [--json]",
172
173
  " longtable claude install-skills [--surface compact|full] [--dir <path>]",
173
174
  " longtable claude remove-skills [--dir <path>]",
174
175
  " longtable claude status [--surface compact|full] [--dir <path>] [--json]",
@@ -1527,6 +1528,22 @@ function setupForProvider(setup, provider) {
1527
1528
  }
1528
1529
  };
1529
1530
  }
1531
+ async function collectHardStopDiagnostics(startPath) {
1532
+ const empty = {
1533
+ stopWouldBlock: false,
1534
+ activeBlockers: [],
1535
+ staleOrUnrelatedPendingQuestionCount: 0,
1536
+ stalePendingQuestionCount: 0,
1537
+ stalePendingObligationCount: 0,
1538
+ nextActions: []
1539
+ };
1540
+ const context = await loadProjectContextFromDirectory(startPath);
1541
+ if (!context) {
1542
+ return empty;
1543
+ }
1544
+ const state = await loadWorkspaceState(context);
1545
+ return collectHardStopBlockers(state);
1546
+ }
1530
1547
  async function collectDoctorStatus(args) {
1531
1548
  const roles = listRoleDefinitions();
1532
1549
  const skillSurface = parseSkillSurface(args);
@@ -1579,11 +1596,13 @@ async function collectDoctorStatus(args) {
1579
1596
  : [];
1580
1597
  const expectedCodexSkills = buildCodexSkillSpecs(roles, skillSurface).map((skill) => skill.name);
1581
1598
  const expectedClaudeSkills = buildClaudeSkillSpecs(roles, skillSurface).map((skill) => skill.name);
1582
- const [codexSkills, claudeSkills, codexAliases, workspace] = await Promise.all([
1599
+ const workspacePath = typeof args.cwd === "string" ? args.cwd : cwd();
1600
+ const [codexSkills, claudeSkills, codexAliases, workspace, hardStop] = await Promise.all([
1583
1601
  listInstalledCodexSkills(roles, codexDir, skillSurface),
1584
1602
  listInstalledClaudeSkills(roles, claudeDir, skillSurface),
1585
1603
  listInstalledCodexPromptAliases(codexPromptsDir),
1586
- inspectProjectWorkspace(typeof args.cwd === "string" ? args.cwd : cwd())
1604
+ inspectProjectWorkspace(workspacePath),
1605
+ collectHardStopDiagnostics(workspacePath)
1587
1606
  ]);
1588
1607
  const installedCodexSkills = codexSkills.map((skill) => skill.name);
1589
1608
  const installedClaudeSkills = claudeSkills.map((skill) => skill.name);
@@ -1614,7 +1633,12 @@ async function collectDoctorStatus(args) {
1614
1633
  hooksExists: existsSync(codexHooksPath),
1615
1634
  codexHooksEnabled: codexHooksEnabled(codexMcpConfig),
1616
1635
  missingManagedHookEvents,
1617
- missingManagedHookTrustState
1636
+ missingManagedHookTrustState,
1637
+ stopWouldBlock: hardStop.stopWouldBlock,
1638
+ activeBlockers: hardStop.activeBlockers,
1639
+ stalePendingQuestionCount: hardStop.stalePendingQuestionCount,
1640
+ stalePendingObligationCount: hardStop.stalePendingObligationCount,
1641
+ nextActions: hardStop.nextActions
1618
1642
  },
1619
1643
  claude: {
1620
1644
  command: "claude",
@@ -1627,7 +1651,8 @@ async function collectDoctorStatus(args) {
1627
1651
  missingSkills: missingNames(expectedClaudeSkills, installedClaudeSkills)
1628
1652
  }
1629
1653
  },
1630
- workspace
1654
+ workspace,
1655
+ hardStop
1631
1656
  };
1632
1657
  }
1633
1658
  function renderProviderDoctorBlock(label, provider) {
@@ -1665,6 +1690,9 @@ function renderDoctorStatus(status) {
1665
1690
  `- hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
1666
1691
  `- managed hook coverage: ${status.providers.codex.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.providers.codex.missingManagedHookEvents.join(", ")}`}`,
1667
1692
  `- managed hook trust: ${status.providers.codex.missingManagedHookTrustState.length === 0 ? "current" : `missing/stale ${status.providers.codex.missingManagedHookTrustState.length}`}`,
1693
+ `- Stop would block now: ${status.hardStop.stopWouldBlock ? "yes" : "no"}`,
1694
+ `- active hard-stop blockers: ${status.hardStop.activeBlockers.length}`,
1695
+ `- stale/unrelated pending questions: ${status.hardStop.staleOrUnrelatedPendingQuestionCount}`,
1668
1696
  "",
1669
1697
  ...renderProviderDoctorBlock("Claude", status.providers.claude),
1670
1698
  "",
@@ -1675,7 +1703,7 @@ function renderDoctorStatus(status) {
1675
1703
  }
1676
1704
  else {
1677
1705
  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}`);
1706
+ 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
1707
  if ((workspace.recentInvocations ?? []).length > 0) {
1680
1708
  lines.push("- recent invocations:");
1681
1709
  for (const invocation of workspace.recentInvocations ?? []) {
@@ -1689,12 +1717,25 @@ function renderDoctorStatus(status) {
1689
1717
  lines.push(` - ${question.id}: ${question.question} (${question.options.join("/")})`);
1690
1718
  }
1691
1719
  }
1720
+ if (status.hardStop.activeBlockers.length > 0) {
1721
+ lines.push("- active hard-stop blockers:");
1722
+ for (const blocker of status.hardStop.activeBlockers) {
1723
+ lines.push(` - ${blocker.id}: ${blocker.scope} (${blocker.type})`);
1724
+ }
1725
+ }
1692
1726
  if ((workspace.pendingObligations ?? []).length > 0) {
1693
1727
  lines.push("- pending obligations:");
1694
1728
  for (const obligation of workspace.pendingObligations ?? []) {
1695
1729
  lines.push(` - ${obligation.id}: ${obligation.prompt}`);
1696
1730
  }
1697
1731
  }
1732
+ if ((workspace.hardStop?.activeBlockers ?? []).length > 0) {
1733
+ lines.push("- hard-stop blockers:");
1734
+ for (const blocker of workspace.hardStop?.activeBlockers ?? []) {
1735
+ lines.push(` - ${blocker.id} [${blocker.scope}]: ${blocker.prompt}`);
1736
+ lines.push(` next: ${blocker.commandHint}`);
1737
+ }
1738
+ }
1698
1739
  if ((workspace.answerWarnings ?? []).length > 0) {
1699
1740
  lines.push("- answer warnings:");
1700
1741
  for (const warning of workspace.answerWarnings ?? []) {
@@ -1736,10 +1777,16 @@ function renderDoctorStatus(status) {
1736
1777
  if (!status.workspace.found) {
1737
1778
  nextActions.push("longtable start");
1738
1779
  }
1780
+ nextActions.push(...status.hardStop.nextActions);
1739
1781
  const firstQuestion = status.workspace.pendingQuestions?.[0];
1740
- if (firstQuestion) {
1782
+ if (firstQuestion && status.hardStop.nextActions.length === 0) {
1741
1783
  nextActions.push(`longtable decide --question ${firstQuestion.id} --answer <value>`);
1742
1784
  }
1785
+ for (const action of status.workspace.hardStop?.nextActions ?? []) {
1786
+ if (!nextActions.includes(action)) {
1787
+ nextActions.push(action);
1788
+ }
1789
+ }
1743
1790
  if (nextActions.length > 0) {
1744
1791
  lines.push("", "Next actions:");
1745
1792
  for (const action of nextActions) {
@@ -2216,10 +2263,10 @@ function inferCollaborationRoute(prompt) {
2216
2263
  if (explicitDebate) {
2217
2264
  return "debate";
2218
2265
  }
2219
- const explicitTeam = /\bagent team\b|\bresearch team\b|\bteam review\b|\bteam-style\b|\buse a team\b/i.test(prompt) ||
2266
+ const explicitPanelTeam = /\bagent team\b|\bresearch team\b|\bteam review\b|\bteam-style\b|\buse a team\b/i.test(prompt) ||
2220
2267
  /에이전트\s*팀|연구\s*팀|팀\s*(리뷰|검토)|팀으로/.test(prompt);
2221
- if (explicitTeam) {
2222
- return "team";
2268
+ if (explicitPanelTeam) {
2269
+ return "panel";
2223
2270
  }
2224
2271
  const panelCue = /\bpanel\b|\bmulti[- ]?role\b|\bmultiple perspectives\b|\brole disagreement\b|\bdisagreement\b|\bconflict\b/i.test(prompt) ||
2225
2272
  /패널|여러\s*관점|복수\s*관점|역할.*불일치|불일치|충돌/.test(prompt);
@@ -2234,10 +2281,10 @@ function inferCollaborationRoute(prompt) {
2234
2281
  trigger.signal.artifactStakes === "study_protocol" ||
2235
2282
  trigger.requiresQuestionBeforeClosure;
2236
2283
  if (panelCue && trigger.signal.artifactStakes === "external_submission") {
2237
- return "debate";
2284
+ return "panel";
2238
2285
  }
2239
2286
  if (highStakes) {
2240
- return "team";
2287
+ return "panel";
2241
2288
  }
2242
2289
  return "panel";
2243
2290
  }
@@ -2813,7 +2860,7 @@ async function runAccess(subcommand, args) {
2813
2860
  await runAccessSetup(args);
2814
2861
  return;
2815
2862
  }
2816
- if (subcommand === "status") {
2863
+ if (subcommand === "status" || subcommand === "hook-doctor") {
2817
2864
  await runAccessStatus(args);
2818
2865
  return;
2819
2866
  }
@@ -3338,10 +3385,10 @@ async function runAsk(args) {
3338
3385
  (directive.panel || delegatedArgs.panel === true
3339
3386
  ? "panel"
3340
3387
  : inferCollaborationRoute(effectivePrompt) ?? (inferred === "panel" ? "panel" : null));
3341
- if (collaborationRoute === "team" || collaborationRoute === "debate") {
3342
- await runTeam({
3388
+ if (collaborationRoute === "debate") {
3389
+ await runPanelDebateCommand({
3343
3390
  ...delegatedArgs,
3344
- debate: collaborationRoute === "debate"
3391
+ debate: true
3345
3392
  });
3346
3393
  return;
3347
3394
  }
@@ -3360,21 +3407,21 @@ function localId(prefix) {
3360
3407
  async function writeJsonFile(path, value) {
3361
3408
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
3362
3409
  }
3363
- async function writeTeamDebateArtifacts(bundle, teamDir, prompt) {
3364
- await mkdir(teamDir, { recursive: true });
3365
- await writeFile(join(teamDir, "prompt.txt"), prompt, "utf8");
3366
- await writeJsonFile(join(teamDir, "plan.json"), bundle.plan);
3367
- await writeJsonFile(join(teamDir, "run.json"), bundle.run);
3410
+ async function writePanelDebateArtifacts(bundle, panelDir, prompt) {
3411
+ await mkdir(panelDir, { recursive: true });
3412
+ await writeFile(join(panelDir, "prompt.txt"), prompt, "utf8");
3413
+ await writeJsonFile(join(panelDir, "plan.json"), bundle.plan);
3414
+ await writeJsonFile(join(panelDir, "run.json"), bundle.run);
3368
3415
  for (const round of bundle.run.rounds) {
3369
3416
  await mkdir(round.artifactDir, { recursive: true });
3370
3417
  await writeJsonFile(join(round.artifactDir, "round.json"), round);
3371
3418
  for (const contribution of round.contributions) {
3372
- await writeJsonFile(join(teamDir, contribution.artifactPath), contribution);
3419
+ await writeJsonFile(join(panelDir, contribution.artifactPath), contribution);
3373
3420
  }
3374
3421
  }
3375
- await writeJsonFile(join(teamDir, "synthesis.json"), bundle.run.synthesis);
3376
- await writeJsonFile(join(teamDir, "checkpoint.json"), bundle.questionRecord);
3377
- await writeJsonFile(join(teamDir, "invocation.json"), bundle.invocationRecord);
3422
+ await writeJsonFile(join(panelDir, "synthesis.json"), bundle.run.synthesis);
3423
+ await writeJsonFile(join(panelDir, "checkpoint.json"), bundle.questionRecord);
3424
+ await writeJsonFile(join(panelDir, "invocation.json"), bundle.invocationRecord);
3378
3425
  }
3379
3426
  function sentinelSummary(prompt, workingDirectory) {
3380
3427
  const trigger = classifyCheckpointTrigger(prompt, {
@@ -3453,19 +3500,15 @@ async function runSentinel(args) {
3453
3500
  console.log(context ? `- recorded in: ${context.stateFilePath}` : "- record skipped: no LongTable workspace found");
3454
3501
  }
3455
3502
  }
3456
- async function runTeam(args) {
3503
+ async function runPanelDebateCommand(args) {
3457
3504
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
3458
3505
  const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
3459
3506
  if (!prompt) {
3460
3507
  throw new Error("A prompt is required.");
3461
3508
  }
3462
- const isDebate = args.debate === true;
3463
- const expectedRounds = isDebate ? 5 : 3;
3464
- const rounds = typeof args.rounds === "string" ? Number(args.rounds) : expectedRounds;
3465
- if (!Number.isInteger(rounds) || rounds !== expectedRounds) {
3466
- throw new Error(isDebate
3467
- ? "LongTable team debate v1 supports `--rounds 5` only."
3468
- : "LongTable team v1 supports `--rounds 3` cross-review only.");
3509
+ const rounds = typeof args.rounds === "string" ? Number(args.rounds) : 5;
3510
+ if (!Number.isInteger(rounds) || rounds !== 5) {
3511
+ throw new Error("LongTable panel debate v1 supports `--rounds 5` only.");
3469
3512
  }
3470
3513
  const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
3471
3514
  const projectContext = await loadProjectContextFromDirectory(workingDirectory);
@@ -3473,78 +3516,47 @@ async function runTeam(args) {
3473
3516
  await assertWorkspaceNotBlocked(projectContext);
3474
3517
  }
3475
3518
  const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
3476
- const teamId = localId("team");
3477
- const teamDir = join(workingDirectory, ".longtable", "team", teamId);
3478
- if (isDebate) {
3479
- const debate = buildTeamDebate({
3480
- teamId,
3481
- teamDir,
3482
- prompt: projectAware.prompt,
3483
- roleFlag: typeof args.role === "string" ? args.role : undefined,
3484
- provider: setup?.providerSelection.provider,
3485
- visibility: "always_visible",
3486
- roundCount: rounds
3487
- });
3488
- await writeTeamDebateArtifacts(debate, teamDir, prompt);
3489
- const canRecordWorkspace = projectAware.projectContextFound && projectContext && existsSync(projectContext.stateFilePath);
3490
- if (canRecordWorkspace) {
3491
- await appendInvocationRecordToWorkspace(projectContext, debate.invocationRecord, [debate.questionRecord]);
3492
- }
3493
- if (args.json === true) {
3494
- console.log(JSON.stringify({
3495
- teamId,
3496
- teamDir,
3497
- plan: debate.plan,
3498
- run: debate.run,
3499
- questionRecord: debate.questionRecord,
3500
- invocationRecord: debate.invocationRecord,
3501
- execution: {
3502
- status: "completed",
3503
- surface: debate.run.surface,
3504
- projectContextFound: projectAware.projectContextFound,
3505
- invocationLogged: canRecordWorkspace
3506
- }
3507
- }, null, 2));
3508
- return;
3509
- }
3510
- console.log(renderTeamDebateSummary(debate.run));
3511
- console.log(`- checkpoint: ${debate.questionRecord.id}`);
3512
- return;
3513
- }
3514
- const team = buildTeamReview({
3515
- teamId,
3516
- teamDir,
3519
+ const panelId = localId("panel_debate");
3520
+ const panelDir = join(workingDirectory, ".longtable", "panel", panelId);
3521
+ const debate = buildTeamDebate({
3522
+ teamId: panelId,
3523
+ teamDir: panelDir,
3517
3524
  prompt: projectAware.prompt,
3518
3525
  roleFlag: typeof args.role === "string" ? args.role : undefined,
3519
3526
  provider: setup?.providerSelection.provider,
3520
3527
  visibility: "always_visible",
3521
3528
  roundCount: rounds
3522
3529
  });
3523
- await writeTeamDebateArtifacts(team, teamDir, prompt);
3530
+ await writePanelDebateArtifacts(debate, panelDir, prompt);
3524
3531
  const canRecordWorkspace = projectAware.projectContextFound && projectContext && existsSync(projectContext.stateFilePath);
3525
3532
  if (canRecordWorkspace) {
3526
- await appendInvocationRecordToWorkspace(projectContext, team.invocationRecord, [team.questionRecord]);
3533
+ await appendInvocationRecordToWorkspace(projectContext, debate.invocationRecord, [debate.questionRecord]);
3527
3534
  }
3528
3535
  if (args.json === true) {
3529
3536
  console.log(JSON.stringify({
3530
- teamId,
3531
- teamDir,
3532
- plan: team.plan,
3533
- run: team.run,
3534
- questionRecord: team.questionRecord,
3535
- invocationRecord: team.invocationRecord,
3537
+ panelId,
3538
+ panelDir,
3539
+ plan: debate.plan,
3540
+ run: debate.run,
3541
+ questionRecord: debate.questionRecord,
3542
+ invocationRecord: debate.invocationRecord,
3536
3543
  execution: {
3537
3544
  status: "completed",
3538
- surface: team.run.surface,
3539
- interactionDepth: team.run.interactionDepth,
3545
+ surface: debate.run.surface,
3540
3546
  projectContextFound: projectAware.projectContextFound,
3541
3547
  invocationLogged: canRecordWorkspace
3542
3548
  }
3543
3549
  }, null, 2));
3544
3550
  return;
3545
3551
  }
3546
- console.log(renderTeamDebateSummary(team.run));
3547
- console.log(`- checkpoint: ${team.questionRecord.id}`);
3552
+ console.log("LongTable Panel Debate");
3553
+ console.log(`- panel: ${panelId}`);
3554
+ console.log(`- interaction depth: ${debate.run.interactionDepth}`);
3555
+ console.log(`- rounds: ${debate.run.roundCount}`);
3556
+ console.log(`- checkpoint: ${debate.questionRecord.id}`);
3557
+ }
3558
+ function disabledTeamCommandError() {
3559
+ return new Error("`longtable team` is disabled. Use `longtable panel --prompt <text>` for visible multi-role review, or `longtable ask --prompt \"lt debate: <text>\"` when debate is explicitly requested.");
3548
3560
  }
3549
3561
  async function runDecide(args) {
3550
3562
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
@@ -3768,7 +3780,7 @@ async function runCodexSubcommand(subcommand, args) {
3768
3780
  console.log(renderCodexHookInstallSummary(result));
3769
3781
  return;
3770
3782
  }
3771
- if (subcommand === "status") {
3783
+ if (subcommand === "status" || subcommand === "hook-doctor") {
3772
3784
  const aliases = await listInstalledCodexPromptAliases(customDir);
3773
3785
  const skills = await listInstalledCodexSkills(roles, customDir, skillSurface);
3774
3786
  const setupPath = resolveDefaultSetupPath(typeof args.path === "string" ? args.path : undefined).path;
@@ -3777,6 +3789,7 @@ async function runCodexSubcommand(subcommand, args) {
3777
3789
  const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
3778
3790
  const hooksPath = resolveCodexHooksPath(args);
3779
3791
  const hooksContent = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
3792
+ const workspace = await inspectProjectWorkspace(typeof args.cwd === "string" ? args.cwd : cwd());
3780
3793
  const status = {
3781
3794
  setupPath,
3782
3795
  setupExists: existsSync(setupPath),
@@ -3796,13 +3809,21 @@ async function runCodexSubcommand(subcommand, args) {
3796
3809
  : [...LONGTABLE_MANAGED_HOOK_EVENTS],
3797
3810
  missingManagedHookTrustState: hooksContent
3798
3811
  ? getMissingManagedCodexHookTrustState(configContent, hooksPath, hooksContent)
3799
- : []
3812
+ : [],
3813
+ workspaceHardStop: workspace.hardStop ?? {
3814
+ stopWouldBlock: false,
3815
+ activeBlockers: [],
3816
+ staleOrUnrelatedPendingQuestionCount: 0,
3817
+ stalePendingQuestionCount: 0,
3818
+ stalePendingObligationCount: 0,
3819
+ nextActions: []
3820
+ }
3800
3821
  };
3801
3822
  if (args.json === true) {
3802
3823
  console.log(JSON.stringify(status, null, 2));
3803
3824
  return;
3804
3825
  }
3805
- console.log("LongTable Codex status");
3826
+ console.log(subcommand === "hook-doctor" ? "LongTable Codex hook doctor" : "LongTable Codex status");
3806
3827
  console.log(`- setup: ${status.setupExists ? "present" : "missing"} (${setupPath})`);
3807
3828
  console.log(`- codex runtime artifact: ${status.runtimeExists ? "present" : "missing"} (${runtimePath})`);
3808
3829
  console.log(`- skills dir: ${status.skillsDir}`);
@@ -3832,6 +3853,9 @@ async function runCodexSubcommand(subcommand, args) {
3832
3853
  console.log(`- hooks file: ${status.hooksExists ? "present" : "missing"} (${status.hooksPath})`);
3833
3854
  console.log(`- managed hook coverage: ${status.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.missingManagedHookEvents.join(", ")}`}`);
3834
3855
  console.log(`- managed hook trust: ${status.missingManagedHookTrustState.length === 0 ? "current" : `missing/stale ${status.missingManagedHookTrustState.length}`}`);
3856
+ console.log(`- Stop hard-stop: ${status.workspaceHardStop.stopWouldBlock ? "would block" : "clear"}`);
3857
+ console.log(`- active hard-stop blockers: ${status.workspaceHardStop.activeBlockers.length}`);
3858
+ console.log(`- stale/unrelated pending: ${status.workspaceHardStop.stalePendingQuestionCount} questions, ${status.workspaceHardStop.stalePendingObligationCount} obligations`);
3835
3859
  return;
3836
3860
  }
3837
3861
  throw new Error("Unknown codex subcommand.");
@@ -3977,8 +4001,7 @@ async function main() {
3977
4001
  return;
3978
4002
  }
3979
4003
  if (command === "team") {
3980
- await runTeam(values);
3981
- return;
4004
+ throw disabledTeamCommandError();
3982
4005
  }
3983
4006
  if (command === "decide") {
3984
4007
  await runDecide(values);
@@ -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: [
package/dist/debate.js CHANGED
@@ -166,7 +166,7 @@ function convergenceContribution(roundId, plan, role, label, artifactPath) {
166
166
  function buildSynthesis(plan, artifactPath, kind) {
167
167
  const labels = plan.members.map((member) => member.label);
168
168
  const highSensitivity = plan.checkpointSensitivity === "high";
169
- const runLabel = kind === "debate" ? "debate" : "team review";
169
+ const runLabel = kind === "debate" ? "panel debate" : "panel review";
170
170
  return {
171
171
  artifactPath,
172
172
  summary: `The ${runLabel} completed across ${labels.join(", ")}. It should slow closure by turning role disagreement into an explicit researcher decision.`,
@@ -201,8 +201,8 @@ export function createTeamDebateQuestionRecord(run, provider) {
201
201
  status: "pending",
202
202
  prompt: {
203
203
  id: createId("question_prompt"),
204
- checkpointKey: "team_debate_next_decision",
205
- title: isDebate ? "Team debate follow-up decision" : "Agent team follow-up decision",
204
+ checkpointKey: "panel_debate_next_decision",
205
+ title: isDebate ? "Panel debate follow-up decision" : "Panel review follow-up decision",
206
206
  question: run.synthesis.recommendedCheckpoint,
207
207
  type: "single_choice",
208
208
  options: [
@@ -238,11 +238,11 @@ export function createTeamDebateQuestionRecord(run, provider) {
238
238
  ? "Role rebuttals and convergence should connect to an explicit researcher decision."
239
239
  : "Cross-review created role disagreement that should connect to an explicit researcher decision.",
240
240
  rationale: [
241
- "Agent team orchestration is a research harness surface, not a substitute for researcher judgment.",
241
+ "LongTable panel orchestration is a research harness surface, not a substitute for researcher judgment.",
242
242
  isDebate
243
243
  ? "The fixed debate rounds created disagreement that should connect to an explicit researcher decision."
244
244
  : "The cross-review round created disagreement that should connect to an explicit researcher decision.",
245
- `Agent team run: ${run.id}.`
245
+ `LongTable panel run: ${run.id}.`
246
246
  ],
247
247
  preferredSurfaces: provider === "claude"
248
248
  ? ["native_structured", "numbered"]
@@ -255,8 +255,8 @@ function buildTeamBundle(options, kind) {
255
255
  const expectedRounds = kind === "debate" ? 5 : 3;
256
256
  if (roundCount !== expectedRounds) {
257
257
  throw new Error(kind === "debate"
258
- ? "LongTable debate v1 supports fixed 5-round debate only."
259
- : "LongTable team v1 supports fixed 3-round cross-review only.");
258
+ ? "LongTable panel debate v1 supports fixed 5-round debate only."
259
+ : "LongTable panel review v1 supports fixed 3-round cross-review only.");
260
260
  }
261
261
  const createdAt = nowIso();
262
262
  const plan = buildPanelPlan({
@@ -267,7 +267,7 @@ function buildTeamBundle(options, kind) {
267
267
  visibility: options.visibility ?? "always_visible"
268
268
  });
269
269
  const rounds = [];
270
- const round1Id = createId("team_round");
270
+ const round1Id = createId("panel_round");
271
271
  const independentContributions = plan.members.map((member) => independentContribution(round1Id, plan, member.role, member.label, join("round-1-independent", `${member.role}.json`)));
272
272
  rounds.push({
273
273
  id: round1Id,
@@ -278,7 +278,7 @@ function buildTeamBundle(options, kind) {
278
278
  artifactDir: join(options.teamDir, "round-1-independent"),
279
279
  contributions: independentContributions
280
280
  });
281
- const round2Id = createId("team_round");
281
+ const round2Id = createId("panel_round");
282
282
  const crossContributions = plan.members.flatMap((member) => plan.members
283
283
  .filter((target) => target.role !== member.role)
284
284
  .map((target) => crossReviewContribution(round2Id, plan, member.role, member.label, target.role, target.label, independentContributions.find((contribution) => contribution.role === target.role), join("round-2-cross-review", `${member.role}-on-${target.role}.json`))));
@@ -292,7 +292,7 @@ function buildTeamBundle(options, kind) {
292
292
  contributions: crossContributions
293
293
  });
294
294
  if (kind === "debate") {
295
- const round3Id = createId("team_round");
295
+ const round3Id = createId("panel_round");
296
296
  rounds.push({
297
297
  id: round3Id,
298
298
  index: 3,
@@ -302,7 +302,7 @@ function buildTeamBundle(options, kind) {
302
302
  artifactDir: join(options.teamDir, "round-3-rebuttal"),
303
303
  contributions: plan.members.map((member) => rebuttalContribution(round3Id, member.role, member.label, join("round-3-rebuttal", `${member.role}.json`)))
304
304
  });
305
- const round4Id = createId("team_round");
305
+ const round4Id = createId("panel_round");
306
306
  rounds.push({
307
307
  id: round4Id,
308
308
  index: 4,
@@ -315,22 +315,22 @@ function buildTeamBundle(options, kind) {
315
315
  }
316
316
  const synthesis = buildSynthesis(plan, "synthesis.json", kind);
317
317
  const run = {
318
- id: createId("team_debate_run"),
318
+ id: createId("panel_debate_run"),
319
319
  teamId: options.teamId,
320
320
  createdAt,
321
321
  updatedAt: createdAt,
322
322
  prompt: options.prompt,
323
323
  roles: plan.members,
324
324
  status: "completed",
325
- surface: "file_backed_debate",
325
+ surface: "file_backed_panel_debate",
326
326
  interactionDepth: kind === "debate" ? "debated" : "cross_reviewed",
327
- roundPolicy: kind === "debate" ? "fixed" : "team_cross_review",
327
+ roundPolicy: kind === "debate" ? "fixed" : "panel_cross_review",
328
328
  roundCount,
329
329
  artifactRoot: options.teamDir,
330
330
  rounds: [
331
331
  ...rounds,
332
332
  {
333
- id: createId("team_round"),
333
+ id: createId("panel_round"),
334
334
  index: roundCount,
335
335
  kind: "synthesis",
336
336
  title: "Coordinator synthesis and checkpoint",
@@ -353,12 +353,12 @@ function buildTeamBundle(options, kind) {
353
353
  checkpointSensitivity: plan.checkpointSensitivity,
354
354
  rationale: [
355
355
  kind === "debate"
356
- ? "Autonomous debate requested through LongTable team orchestration."
357
- : "Agent team cross-review requested through LongTable team orchestration.",
356
+ ? "Autonomous debate requested through LongTable panel orchestration."
357
+ : "Cross-role review requested through LongTable panel orchestration.",
358
358
  "File-backed rounds keep disagreement inspectable before researcher closure."
359
359
  ]
360
360
  });
361
- intent.kind = kind === "debate" ? "team_debate" : "team";
361
+ intent.kind = kind === "debate" ? "panel_debate" : "panel";
362
362
  intent.requestedSurface = run.surface;
363
363
  const invocationRecord = {
364
364
  id: createId("invocation_record"),
@@ -371,7 +371,7 @@ function buildTeamBundle(options, kind) {
371
371
  interactionDepth: run.interactionDepth,
372
372
  panelPlan: plan,
373
373
  teamDebateRun: run,
374
- degradationReason: "File-backed team artifacts are the canonical execution record."
374
+ degradationReason: "File-backed panel artifacts are the canonical execution record."
375
375
  };
376
376
  return {
377
377
  plan,
@@ -389,8 +389,8 @@ export function buildTeamDebate(options) {
389
389
  }
390
390
  export function renderTeamDebateSummary(run) {
391
391
  return [
392
- "LongTable Team Debate",
393
- `- team: ${run.teamId}`,
392
+ "LongTable Panel Debate",
393
+ `- panel: ${run.teamId}`,
394
394
  `- surface: ${run.surface}`,
395
395
  `- interaction depth: ${run.interactionDepth}`,
396
396
  `- rounds: ${run.roundCount} ${run.roundPolicy}`,
@@ -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
  }
@@ -221,7 +225,7 @@ function mutatesLongTableResearchState(command) {
221
225
  return false;
222
226
  }
223
227
  return /\.longtable(?:\/|\b)|\bCURRENT\.md\b/.test(normalized)
224
- || /\blongtable\s+(?:start|question|clear-question|prune-questions|ask|clarify|panel|team)\b/.test(normalized);
228
+ || /\blongtable\s+(?:start|question|clear-question|prune-questions|ask|clarify|panel)\b/.test(normalized);
225
229
  }
226
230
  async function loadLongTableRuntime(startPath) {
227
231
  const context = await loadProjectContextFromDirectory(startPath);
@@ -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);
package/dist/panel.js CHANGED
@@ -183,7 +183,7 @@ export function createPlannedInvocationRecord(options) {
183
183
  interactionDepth: "independent",
184
184
  panelPlan: options.plan,
185
185
  panelResult: options.result,
186
- degradationReason: "Native provider team execution is optional; sequential fallback is the stable LongTable surface."
186
+ degradationReason: "Sequential fallback is the stable LongTable panel surface."
187
187
  };
188
188
  }
189
189
  function roleInstruction(member) {
@@ -12,7 +12,7 @@ export interface LongTableInvocationDirective {
12
12
  explicit: boolean;
13
13
  cleanedPrompt: string;
14
14
  mode?: InteractionMode | "panel" | "status";
15
- collaboration?: "panel" | "team" | "debate";
15
+ collaboration?: "panel" | "debate";
16
16
  roles: CanonicalPersona[];
17
17
  panel: boolean;
18
18
  showConflicts: boolean;
@@ -8,8 +8,8 @@ const DIRECTIVE_MAP = [
8
8
  { key: "draft", mode: "draft" },
9
9
  { key: "commit", mode: "commit" },
10
10
  { key: "panel", mode: "panel", collaboration: "panel", panel: true, showConflicts: true },
11
- { key: "team", mode: "review", collaboration: "team", panel: true, showConflicts: true },
12
- { key: "debate", mode: "review", collaboration: "debate", panel: true, showConflicts: true },
11
+ { key: "team", mode: "panel", collaboration: "panel", panel: true, showConflicts: true },
12
+ { key: "debate", mode: "panel", collaboration: "debate", panel: true, showConflicts: true },
13
13
  { key: "status", mode: "status" },
14
14
  { key: "editor", mode: "review", roles: ["editor"] },
15
15
  { key: "reviewer", mode: "review", roles: ["reviewer"] },
@@ -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) => ({
@@ -2266,7 +2275,7 @@ const COMMITMENT_FAMILY_BY_CHECKPOINT = [
2266
2275
  [/research_question|research_direction|scope|boundary|inclusion|exclusion/, "scope"],
2267
2276
  [/theory|construct|conceptual/, "construct"],
2268
2277
  [/measurement|coding|codebook|extraction/, "coding"],
2269
- [/method|analysis|panel_disagreement|team_debate|review/, "method"],
2278
+ [/method|analysis|panel_disagreement|panel_debate|team_debate|review/, "method"],
2270
2279
  [/evidence|scholarly_access|source_authority/, "evidence"],
2271
2280
  [/knowledge_gap|tacit_assumption|epistemic/, "epistemic_authority"]
2272
2281
  ];
@@ -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.53",
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.53",
33
+ "@longtable/core": "0.1.53",
34
+ "@longtable/memory": "0.1.53",
35
+ "@longtable/provider-claude": "0.1.53",
36
+ "@longtable/provider-codex": "0.1.53",
37
+ "@longtable/setup": "0.1.53"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",