@longtable/cli 0.1.50 → 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
@@ -2,16 +2,9 @@
2
2
 
3
3
  Researcher-facing CLI for LongTable.
4
4
 
5
- LongTable is an npm-first, provider-neutral research harness. It keeps the core
6
- product contract in project files and shared packages, while Codex skills,
7
- Claude skills, and future MCP surfaces remain generated adapter artifacts.
8
-
9
- The basic contract is:
10
-
11
- 1. approve provider/runtime support once
12
- 2. start each project inside the provider with `$longtable-interview`
13
- 3. create or resume a workspace from that interview
14
- 4. preserve decisions, tensions, and evidence as durable project state
5
+ LongTable keeps scholarly project state in `.longtable/` and exposes generated
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.
15
8
 
16
9
  ## Install
17
10
 
@@ -19,55 +12,35 @@ The basic contract is:
19
12
  npm install -g @longtable/cli
20
13
  ```
21
14
 
22
- The npm install only installs the CLI. It does not write Codex skills, MCP
23
- config, hooks, or provider runtime files without explicit setup approval.
24
-
25
15
  ## Primary Flow
26
16
 
27
- Start Codex from the research folder. The provider uses the shell working
28
- directory at process start as the session workspace.
29
-
30
17
  ```bash
31
18
  longtable setup --provider codex
32
19
  cd "<research-folder>"
33
20
  codex
34
21
  ```
35
22
 
36
- You can use `codex -C "<research-folder>"` instead of `cd` plus `codex`.
37
- Changing directories after Codex is already running does not change that
38
- session's workspace root or rerun LongTable's `SessionStart` hook.
39
-
40
- Examples:
23
+ Then invoke:
41
24
 
42
- ```bash
43
- # macOS / Linux
44
- codex -C "/Users/yourname/Research/My-Research-Project"
45
- ```
46
-
47
- ```powershell
48
- # Windows PowerShell
49
- codex -C "C:\Users\YourName\Documents\Research\My-Research-Project"
25
+ ```text
26
+ $longtable-start
50
27
  ```
51
28
 
52
- Then invoke `$longtable-interview` inside Codex.
29
+ `$longtable-start` creates or resumes the workspace, asks open research-start
30
+ questions, and stores a Research Specification when there is enough material.
53
31
 
54
- `longtable setup --provider codex` is the permission-first setup route. It asks
55
- where LongTable may install support, which runtime surfaces it may enable, how
56
- strongly it may interrupt research decisions, and whether to show the
57
- provider-native interview launch steps. `longtable init` remains only as a
58
- deprecated compatibility alias.
32
+ After a Research Specification exists, use:
59
33
 
60
- Return later:
61
-
62
- ```bash
63
- cd "<project-path>"
64
- codex
34
+ ```text
35
+ $longtable-interview
65
36
  ```
66
37
 
67
- Run `longtable resume` inside the project folder when you want a terminal
68
- summary without starting a provider session.
38
+ `$longtable-interview` is post-start. It uses option-first follow-up choices for
39
+ spec revisions, checkpoint resolution, evidence boundaries, coding rules, method
40
+ choices, and protected decisions. If no usable Research Specification exists, it
41
+ must route to `$longtable-start`.
69
42
 
70
- ## What `$longtable-interview` Creates
43
+ ## Workspace Artifacts
71
44
 
72
45
  ```text
73
46
  <project>/
@@ -80,221 +53,51 @@ summary without starting a provider session.
80
53
  sessions/
81
54
  ```
82
55
 
83
- ## Artifact Contract
84
-
85
- - `AGENTS.md`: runtime guidance for Codex
86
- - `CURRENT.md`: human-facing current view regenerated from state
87
- - `.longtable/project.json`: stable project identity
88
- - `.longtable/current-session.json`: current session cursor
89
- - `.longtable/state.json`: layered memory state, including First Research
90
- Shape and Research Specification when the interview has produced them
91
- - `.longtable/sessions/`: historical snapshots
92
-
93
- `$longtable-interview` first stabilizes a short First Research Shape. When the
94
- conversation is substantive enough, it should also preserve a Research
95
- Specification covering scope, construct ontology, theory framing,
96
- measurement/coding, method options, evidence/access requirements, epistemic
97
- alignment, protected decisions, open questions, and next actions. `CURRENT.md`
98
- renders that specification so later agents do not need to reconstruct the full
99
- interview from memory.
100
-
101
- ## Why This Shape
102
-
103
- The CLI tries to keep the root simple for novice researchers while preserving enough structure for power users and downstream tooling.
56
+ - `CURRENT.md`: human-readable current project state
57
+ - `.longtable/state.json`: durable memory, questions, decisions, interview
58
+ turns, evidence records, First Research Shape, and Research Specification
59
+ - `QuestionRecord -> DecisionRecord`: the durable checkpoint lifecycle
104
60
 
105
- The memory model distinguishes:
106
-
107
- - explicit state
108
- - working state
109
- - inferred hypotheses
110
- - open tensions
111
- - narrative traces
112
-
113
- This is how LongTable avoids turning tacit knowledge into fake certainty.
61
+ Provider UI is transport. LongTable supports MCP/native structured elicitation,
62
+ interactive TTY selector surfaces, and numbered/plain-text fallback. Tmux is not
63
+ required for LongTable core behavior.
114
64
 
115
65
  ## Commands
116
66
 
117
67
  ```bash
118
- longtable setup
68
+ longtable setup --provider codex
69
+ longtable doctor
70
+ longtable status --cwd "<project-path>"
119
71
  longtable resume --cwd "<project-path>"
120
72
  longtable roles
121
- longtable ask --cwd "<project-path>" --prompt "..."
122
- longtable panel --prompt "..."
123
- longtable sentinel --prompt "Should I define a new measurement construct?"
124
- longtable team --prompt "Review this measurement plan." --role editor,measurement_auditor --json
125
- longtable team --debate --prompt "Review this measurement plan." --role editor,measurement_auditor --json
126
- longtable codex install-skills
127
- longtable claude install-skills
73
+ longtable question --prompt "<decision context>"
74
+ longtable decide --question <id> --answer <value>
75
+ longtable spec read --cwd "<project-path>"
76
+ longtable search --query "<topic>"
128
77
  ```
129
78
 
130
- Useful structured routes for scripts and debugging:
79
+ `longtable start` remains available for scripted workspace creation with
80
+ `--no-interview --json`, but it is not the primary research-start surface.
131
81
 
132
- ```bash
133
- longtable panel --prompt "review this methods section" --json
134
- longtable review --role methods_critic,measurement_auditor --panel --prompt "review this methods section" --json
135
- longtable ask --prompt "lt panel: show the disagreement before I commit" --json
136
- ```
137
-
138
- ## Inside Codex
139
-
140
- Natural language should be the default.
141
-
142
- Codex UI Researcher Checkpoints are a core LongTable feature when enabled:
82
+ ## Development
143
83
 
144
84
  ```bash
145
- longtable setup --provider codex --surfaces skills_mcp --checkpoint-ui strong
85
+ npm run build --workspace @longtable/cli
86
+ npm run typecheck --workspace @longtable/cli
146
87
  ```
147
88
 
148
- That setup writes the MCP configuration and Codex elicitation approval needed
149
- for form-style checkpoint prompts. Without it, LongTable keeps the same
150
- `QuestionRecord` pending and falls back to numbered text.
151
-
152
- ## Runtime Boundary
153
-
154
- LongTable is not a replacement wrapper for Codex. Markdown docs and generated
155
- skills are soft policy; hooks, MCP elicitation, CLI gates, and `.longtable/`
156
- state are the enforcement layers.
157
-
158
- LongTable should ask and stop before acting when the next step would change or
159
- settle one of four high-risk research commitments:
160
-
161
- 1. Research question or scope
162
- 2. Theory frame or construct map
163
- 3. Measurement, coding, or extraction standard
164
- 4. Method design or analysis strategy
165
-
166
- Low-risk reversible work should continue with visible assumptions instead of a
167
- hook interruption. If human knowledge, AI inference, and durable project state
168
- conflict, LongTable should prefer the most explicit durable state; if that state
169
- is not explicit enough, it should ask the researcher for clarity.
170
-
171
- Explicit short forms are available when needed:
172
-
173
- ```text
174
- lt explore: Where should I narrow the question first?
175
- lt review: What is weak in this claim?
176
- lt panel: Show me the disagreement before I commit.
177
- lt methods: Where is the design vulnerable?
178
- ```
89
+ ## Codex hard-stop diagnostics
179
90
 
180
- Provider-native surfaces are available when installed:
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:
181
94
 
182
95
  ```bash
183
- longtable codex install-skills
184
- longtable claude install-skills
96
+ longtable codex hook-doctor --json
97
+ longtable codex status --json
98
+ longtable doctor --json
185
99
  ```
186
100
 
187
- By default, provider skills use the compact surface: `longtable`,
188
- `longtable-interview`, and five short role shortcuts: `longtable-methods`,
189
- `longtable-measure`, `longtable-theory`, `longtable-reviewer`, and
190
- `longtable-voice`. `$longtable` remains the general router and can still invoke
191
- editor, ethics, venue, panel, explore, or review behavior when the request calls
192
- for it.
193
-
194
- Power users can install the legacy full surface explicitly:
195
-
196
- ```bash
197
- longtable codex install-skills --surface full
198
- longtable claude install-skills --surface full
199
- ```
200
-
201
- Do not depend on `/prompts`; current Codex builds may reject it.
202
-
203
- ## Panel Orchestration
204
-
205
- Panel orchestration is for moments where disagreement matters: methods risk,
206
- measurement validity, theory fit, literature positioning, and claims that need
207
- challenge before they become project memory.
208
-
209
- The CLI creates a provider-neutral `PanelPlan` and returns a planned
210
- `PanelResult`. When native subagents are unavailable, LongTable uses a stable
211
- sequential fallback prompt. That keeps the same research semantics available in
212
- Codex and Claude Code without making either provider's native question or agent
213
- tool the source of truth.
214
-
215
- Inside a LongTable project workspace, panel planning also appends an
216
- `InvocationRecord` to `.longtable/state.json`, creates a pending follow-up
217
- `QuestionRecord`, and refreshes `CURRENT.md`.
218
-
219
- Panel output should remain inspectable. A panel or debate result is expected to
220
- show the consulted roles, each role's main claim or objection, the disagreement
221
- map, decision options, a defensible recommendation when one exists, and the
222
- exact researcher-facing question before a high-stakes decision is treated as
223
- settled.
224
-
225
- Default panel roles include:
226
-
227
- - `reviewer`
228
- - `methods_critic`
229
- - `measurement_auditor`
230
- - `theory_critic`
231
-
232
- Use `--role` to constrain the panel when the research problem is already clear.
233
-
234
- ## Sentinel And Agent Team
235
-
236
- `longtable sentinel` is an explicit gap/tacit check for prompts that may contain
237
- measurement, theory, method, evidence, authorship, or tacit-assumption risks.
238
- Use `--record` inside a LongTable workspace to store the finding as an
239
- unconfirmed inferred hypothesis.
240
-
241
- The Codex hook stays quiet for advisory-only questions. Required hook context is
242
- reserved for durable Researcher Checkpoints, especially when a prompt would
243
- change the research question/scope, theory frame, measurement/coding standard,
244
- method design, or analysis strategy. Low-risk reversible work should proceed
245
- with visible assumptions rather than a noisy hook interruption.
246
-
247
- `longtable team` creates a file-backed agent-team review under
248
- `.longtable/team/<id>/`: independent review, cross-review, and
249
- synthesis/checkpoint. Use it when roles should inspect each other's concerns
250
- before LongTable proposes a researcher decision.
251
-
252
- `longtable team --debate` creates a fixed five-round debate record under
253
- `.longtable/team/<id>/`: independent review, cross-review, rebuttal,
254
- convergence, and synthesis/checkpoint. The file-backed artifact directory is
255
- the source of truth.
256
-
257
- See `docs/AGENT-TEAM-README.md` in the repository for a user-facing guide to
258
- panel, team, and debate surfaces.
259
-
260
- ## Evidence And Search Direction
261
-
262
- LongTable should not behave like a generic web scraper. Research search should
263
- start from scholarly routes when the user needs literature discovery, citation
264
- verification, publication metadata, or evidence-backed research decisions.
265
-
266
- `longtable search` routes research queries through arXiv, Crossref, OpenAlex,
267
- Semantic Scholar, PubMed/NCBI, ERIC, DOAJ, and Unpaywall, then normalizes,
268
- deduplicates, ranks, and labels results as evidence cards. Some sources work
269
- without keys, some require a contact email, and some need API keys for reliable
270
- use.
271
-
272
- Scholarly access is configured separately through `longtable access setup`.
273
- It records readiness for metadata, OA full text, institutional access,
274
- publisher API/TDM credentials, and manual PDFs without storing secrets.
275
- Publisher probes cover Elsevier, Springer Nature, Wiley, and Taylor & Francis.
276
-
277
- Citation support should be checked explicitly. A reference can be useful as
278
- background while still failing to support the specific claim attached to it.
279
-
280
- ```bash
281
- longtable access setup
282
- longtable access probe --doi "10.1016/example" --publisher elsevier
283
- longtable search --query "trust calibration measurement" --intent measurement
284
- longtable search --query "trust calibration measurement" --publisher-access --json
285
- longtable search --query "trust calibration citation support" --intent citation --record
286
- ```
287
-
288
- See:
289
-
290
- - [Research Search](https://github.com/HosungYou/LongTable/blob/main/docs/RESEARCH-SEARCH.md)
291
- - [Evidence Policy](https://github.com/HosungYou/LongTable/blob/main/docs/EVIDENCE-POLICY.md)
292
- - [LongTable Command Surface](https://github.com/HosungYou/LongTable/blob/main/docs/LONGTABLE-COMMAND-SURFACE.md)
293
-
294
- ## Validation
295
-
296
- ```bash
297
- npm install
298
- npm run typecheck
299
- npm run build
300
- ```
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";
@@ -112,16 +113,16 @@ function renderBrandBanner(title, subtitle) {
112
113
  }
113
114
  function renderInterviewLaunchSteps(provider) {
114
115
  const command = provider === "codex" ? "codex" : "claude";
115
- return renderSectionCard("LongTable Interview", [
116
+ return renderSectionCard("LongTable Start", [
116
117
  "Setup is permission and runtime calibration, not the research interview.",
117
118
  "The first research conversation now happens inside the provider so the model can listen, reflect, and ask one natural-language follow-up at a time.",
118
119
  "",
119
120
  "Next:",
120
121
  "1. cd \"<research-folder>\"",
121
122
  `2. run \`${command}\``,
122
- "3. invoke `$longtable-interview`",
123
+ "3. invoke `$longtable-start`",
123
124
  "",
124
- "The interview will create or resume `.longtable/`, may store a short First Research Shape handle, and uses option UI for the final Research Specification confirmation."
125
+ "The start interview will create or resume `.longtable/`, may store a short First Research Shape handle, and uses option UI for the final Research Specification confirmation."
125
126
  ]);
126
127
  }
127
128
  function renderProgressBar(current, total) {
@@ -133,7 +134,7 @@ function usage() {
133
134
  return [
134
135
  "Usage:",
135
136
  " Run `longtable ...` in your terminal, not inside the Codex chat box.",
136
- " LongTable research starts inside Codex or Claude with `$longtable-interview` after setup.",
137
+ " LongTable research starts inside Codex or Claude with `$longtable-start` after setup.",
137
138
  "",
138
139
  " longtable setup [--provider codex|claude] [--install-scope user|project|none] [--surfaces cli_only|skills|skills_mcp|skills_mcp_sentinel] [--intervention advisory|balanced|strong] [--checkpoint-ui off|interactive|strong] [--workspace create|later] [--project-dir <path>] [--json] [--dir <path>] [--skills-dir <path>] [--runtime-path <file>] [--setup-path <file>]",
139
140
  " longtable init [deprecated alias for setup; full legacy flags still supported for automation]",
@@ -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]",
@@ -178,7 +180,7 @@ function usage() {
178
180
  "Examples:",
179
181
  " longtable setup --provider codex",
180
182
  " cd \"<research-folder>\" && codex",
181
- " $longtable-interview",
183
+ " $longtable-start",
182
184
  " longtable start --no-interview --path ~/Research/My-Project --name \"AI Adoption Meta-Analysis\" --goal \"Narrow the review question\"",
183
185
  " longtable doctor",
184
186
  " longtable roles",
@@ -525,12 +527,12 @@ function buildPermissionSetupChoices() {
525
527
  {
526
528
  id: "create",
527
529
  label: "Show interview launch steps",
528
- description: "Why: research should start inside the provider. What you get: setup finishes with Codex/Claude + $longtable-interview steps. Tradeoff: workspace creation waits for the in-provider interview."
530
+ description: "Why: research should start inside the provider. What you get: setup finishes with Codex/Claude + $longtable-start steps. Tradeoff: workspace creation waits for the in-provider interview."
529
531
  },
530
532
  {
531
533
  id: "later",
532
534
  label: "No, prepare runtime only",
533
- description: "Why: keeps setup short. What you get: runtime support without project state. Tradeoff: no durable research memory until `$longtable-interview` creates or resumes a workspace."
535
+ description: "Why: keeps setup short. What you get: runtime support without project state. Tradeoff: no durable research memory until `$longtable-start` creates or resumes a workspace."
534
536
  }
535
537
  ]
536
538
  };
@@ -652,7 +654,7 @@ async function runSetup(args) {
652
654
  interventionPosture: effectiveIntervention,
653
655
  checkpointUiMode: checkpointUi,
654
656
  workspaceCreationPreference: workspacePreference,
655
- officialStartSurface: "$longtable-interview",
657
+ officialStartSurface: "$longtable-start",
656
658
  setupPosture: "permission_first",
657
659
  teamMode: "panel"
658
660
  };
@@ -698,9 +700,9 @@ async function runSetup(args) {
698
700
  mcpInstall,
699
701
  workspacePreference,
700
702
  nextStep: {
701
- surface: "$longtable-interview",
703
+ surface: "$longtable-start",
702
704
  command: provider === "codex" ? "codex" : "claude",
703
- description: "Open the provider in the research folder and invoke `$longtable-interview`."
705
+ description: "Open the provider in the research folder and invoke `$longtable-start`."
704
706
  }
705
707
  }, null, 2));
706
708
  return;
@@ -734,7 +736,7 @@ async function runSetup(args) {
734
736
  console.log(renderInterviewLaunchSteps(provider));
735
737
  if (workspacePreference === "create") {
736
738
  console.log("");
737
- console.log("Workspace launch requested. Open the provider in your research folder and run `$longtable-interview`; the interview will create `.longtable/` there.");
739
+ console.log("Workspace launch requested. Open the provider in your research folder and run `$longtable-start`; the interview will create `.longtable/` there.");
738
740
  }
739
741
  }
740
742
  function perspectiveChoices() {
@@ -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) {
@@ -2009,6 +2057,7 @@ function buildRoleAuditEntry(provider, spec) {
2009
2057
  function runRoleAudit() {
2010
2058
  const baseSkillNames = new Set([
2011
2059
  "longtable",
2060
+ "longtable-start",
2012
2061
  "longtable-interview",
2013
2062
  "longtable-panel",
2014
2063
  "longtable-explore",
@@ -2812,7 +2861,7 @@ async function runAccess(subcommand, args) {
2812
2861
  await runAccessSetup(args);
2813
2862
  return;
2814
2863
  }
2815
- if (subcommand === "status") {
2864
+ if (subcommand === "status" || subcommand === "hook-doctor") {
2816
2865
  await runAccessStatus(args);
2817
2866
  return;
2818
2867
  }
@@ -3624,7 +3673,7 @@ async function runStart(args) {
3624
3673
  "1. longtable setup --provider codex",
3625
3674
  "2. cd \"<research-folder>\"",
3626
3675
  "3. codex",
3627
- "4. $longtable-interview",
3676
+ "4. $longtable-start",
3628
3677
  "",
3629
3678
  "For automation, pass `--no-interview --json` with `--name`, `--path`, and `--goal`."
3630
3679
  ]));
@@ -3767,7 +3816,7 @@ async function runCodexSubcommand(subcommand, args) {
3767
3816
  console.log(renderCodexHookInstallSummary(result));
3768
3817
  return;
3769
3818
  }
3770
- if (subcommand === "status") {
3819
+ if (subcommand === "status" || subcommand === "hook-doctor") {
3771
3820
  const aliases = await listInstalledCodexPromptAliases(customDir);
3772
3821
  const skills = await listInstalledCodexSkills(roles, customDir, skillSurface);
3773
3822
  const setupPath = resolveDefaultSetupPath(typeof args.path === "string" ? args.path : undefined).path;
@@ -3776,6 +3825,7 @@ async function runCodexSubcommand(subcommand, args) {
3776
3825
  const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
3777
3826
  const hooksPath = resolveCodexHooksPath(args);
3778
3827
  const hooksContent = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
3828
+ const workspace = await inspectProjectWorkspace(typeof args.cwd === "string" ? args.cwd : cwd());
3779
3829
  const status = {
3780
3830
  setupPath,
3781
3831
  setupExists: existsSync(setupPath),
@@ -3795,13 +3845,21 @@ async function runCodexSubcommand(subcommand, args) {
3795
3845
  : [...LONGTABLE_MANAGED_HOOK_EVENTS],
3796
3846
  missingManagedHookTrustState: hooksContent
3797
3847
  ? getMissingManagedCodexHookTrustState(configContent, hooksPath, hooksContent)
3798
- : []
3848
+ : [],
3849
+ workspaceHardStop: workspace.hardStop ?? {
3850
+ stopWouldBlock: false,
3851
+ activeBlockers: [],
3852
+ staleOrUnrelatedPendingQuestionCount: 0,
3853
+ stalePendingQuestionCount: 0,
3854
+ stalePendingObligationCount: 0,
3855
+ nextActions: []
3856
+ }
3799
3857
  };
3800
3858
  if (args.json === true) {
3801
3859
  console.log(JSON.stringify(status, null, 2));
3802
3860
  return;
3803
3861
  }
3804
- console.log("LongTable Codex status");
3862
+ console.log(subcommand === "hook-doctor" ? "LongTable Codex hook doctor" : "LongTable Codex status");
3805
3863
  console.log(`- setup: ${status.setupExists ? "present" : "missing"} (${setupPath})`);
3806
3864
  console.log(`- codex runtime artifact: ${status.runtimeExists ? "present" : "missing"} (${runtimePath})`);
3807
3865
  console.log(`- skills dir: ${status.skillsDir}`);
@@ -3831,6 +3889,9 @@ async function runCodexSubcommand(subcommand, args) {
3831
3889
  console.log(`- hooks file: ${status.hooksExists ? "present" : "missing"} (${status.hooksPath})`);
3832
3890
  console.log(`- managed hook coverage: ${status.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.missingManagedHookEvents.join(", ")}`}`);
3833
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`);
3834
3895
  return;
3835
3896
  }
3836
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
  }
@@ -168,15 +172,15 @@ function looksLikeExplicitInterviewPrompt(prompt) {
168
172
  if (!normalized) {
169
173
  return false;
170
174
  }
171
- if (/\$longtable-interview\b/i.test(normalized)) {
175
+ if (/\$longtable-(?:start|interview)\b/i.test(normalized)) {
172
176
  return true;
173
177
  }
174
178
  if (looksLikeLongTableProductOrToolingPrompt(normalized)) {
175
179
  return false;
176
180
  }
177
- return /\bLongTable\b.*\binterview\b/i.test(normalized)
181
+ return /\bLongTable\b.*\b(?:start|interview)\b/i.test(normalized)
178
182
  || /\bfirst research shape\b/i.test(normalized)
179
- || /롱테이블.*인터뷰|LongTable.*인터뷰|First Research Shape/i.test(normalized);
183
+ || /롱테이블.*(?:시작|인터뷰)|LongTable.*인터뷰|First Research Shape/i.test(normalized);
180
184
  }
181
185
  function looksLikeResearchStateConfirmationPrompt(prompt) {
182
186
  if (looksLikeLongTableProductOrToolingPrompt(prompt) && !looksLikeExplicitInterviewPrompt(prompt)) {
@@ -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"];
@@ -339,7 +340,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
339
340
  "",
340
341
  "## 빠른 시작",
341
342
  "- 이 디렉토리에서 `codex`를 엽니다.",
342
- `- 첫 메시지는 보통 \`${session.firstResearchShape ? suggestedPrompt : "$longtable-interview"}\` 정도면 충분합니다.`,
343
+ `- 첫 메시지는 보통 \`${session.firstResearchShape ? suggestedPrompt : "$longtable-start"}\` 정도면 충분합니다.`,
343
344
  "",
344
345
  "## 증거 규칙",
345
346
  "- 외부 사실이나 현재 정보는 source를 붙이거나 inference로 낮춥니다."
@@ -416,7 +417,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
416
417
  "",
417
418
  "## Quick Start",
418
419
  "- Open `codex` in this directory.",
419
- `- A good first message is usually \`${session.firstResearchShape ? suggestedPrompt : "$longtable-interview"}\`.`,
420
+ `- A good first message is usually \`${session.firstResearchShape ? suggestedPrompt : "$longtable-start"}\`.`,
420
421
  "",
421
422
  "## Evidence Rule",
422
423
  "- External or current claims should carry a source link or be labeled as inference."
@@ -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) => ({
@@ -878,7 +887,8 @@ function buildProjectAgentsMd(project, session) {
878
887
  "- Treat `AGENTS.md` as runtime guidance, not as the researcher-facing resume artifact.",
879
888
  "",
880
889
  "## Invocation Rules",
881
- "- If the user message starts with `$longtable-interview`, run the LongTable interview flow before generic research advice.",
890
+ "- If the user message starts with `$longtable-start`, run the LongTable research-start flow before generic research advice.",
891
+ "- If the user message starts with `$longtable-interview`, use post-start structured interview only when a usable Research Specification exists; otherwise route to `$longtable-start`.",
882
892
  "- If the user message starts with `lt `, `longtable `, `long table `, or `롱테이블 ` followed by a directive and `:`, treat it as an explicit LongTable invocation.",
883
893
  "- Supported explicit directives are: interview, explore, review, critique, draft, commit, panel, status, editor, reviewer, methods, theory, measurement, ethics, voice, venue.",
884
894
  "- For explicit LongTable invocations, do not begin by scanning the workspace. Use the current session files first and answer as LongTable immediately.",
@@ -886,13 +896,14 @@ function buildProjectAgentsMd(project, session) {
886
896
  "",
887
897
  "## Research Behavior",
888
898
  "- Begin exploratory work with clarifying or tension questions before recommending a direction.",
889
- "- For `$longtable-interview`, ask one natural-language question at a time, reflect with `LongTable hears: ...`, record turns when MCP is available, and avoid early reader/reviewer or theory/method/measurement classification.",
890
- "- Do not summarize `$longtable-interview` because a fixed number of turns has passed; wait for content-based readiness around research object, focal uncertainty, boundary, evidence/material, protected decision, and next action.",
899
+ "- For `$longtable-start`, ask one natural-language question at a time, reflect with `LongTable hears: ...`, record turns when MCP is available, and avoid early reader/reviewer or theory/method/measurement classification.",
900
+ "- Do not summarize `$longtable-start` because a fixed number of turns has passed; wait for content-based readiness around research object, focal uncertainty, boundary, evidence/material, protected decision, and next action.",
891
901
  "- First Research Shape is a short handle/resume index, not the default closure point.",
892
902
  "- After the First Research Shape, create a Research Specification when the interview has enough detail to preserve scope, construct ontology, theory framing, coding rules, method options, evidence/access requirements, epistemic alignment, protected decisions, open questions, and next actions.",
893
903
  "- If a confirmed First Research Shape exists without a Research Specification, continue directly into the next Research Specification question instead of asking shape-level continue/revise/restart questions.",
894
904
  "- If the researcher chooses `ask_one_more` or `revise_section` at Research Specification confirmation, answer that gap and return to the Research Specification Preview before ending the interview.",
895
- "- Do not let unrelated pending Researcher Checkpoints interrupt `$longtable-interview`; mention them only as separate unresolved checkpoints unless the researcher is confirming, saving, or recording a research decision.",
905
+ "- Do not let unrelated pending Researcher Checkpoints interrupt `$longtable-start`; mention them only as separate unresolved checkpoints unless the researcher is confirming, saving, or recording a research decision.",
906
+ "- For `$longtable-interview` after a Research Specification exists, use option-first follow-up choices with Other/free-text or one open-question escape hatch.",
896
907
  "- Use structured options at the final Research Specification confirmation, at explicit short-handle stop points, or at true checkpoint boundaries.",
897
908
  "- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
898
909
  "- Keep one accountable synthesis, but do not hide meaningful disagreement.",
@@ -1195,7 +1206,7 @@ export async function beginLongTableInterview(options) {
1195
1206
  turns: [],
1196
1207
  qualityNotes: [],
1197
1208
  rationale: [
1198
- "Official LongTable research start surface is provider-native `$longtable-interview`, not the CLI start questionnaire.",
1209
+ "Official LongTable research start surface is provider-native `$longtable-start`, not the CLI start questionnaire.",
1199
1210
  "The hook keeps early research ambiguity open until a first research handle can be summarized."
1200
1211
  ]
1201
1212
  };
@@ -1203,7 +1214,7 @@ export async function beginLongTableInterview(options) {
1203
1214
  updated.workingState = {
1204
1215
  ...updated.workingState,
1205
1216
  activeInterviewHookId: hook.id,
1206
- interviewSurface: "$longtable-interview",
1217
+ interviewSurface: "$longtable-start",
1207
1218
  ...(options.openingQuestion ? { interviewOpeningQuestion: options.openingQuestion } : {}),
1208
1219
  ...(options.seedAnswer ? { interviewSeedAnswer: options.seedAnswer } : {})
1209
1220
  };
@@ -1335,7 +1346,7 @@ export async function summarizeLongTableInterview(options) {
1335
1346
  updated.narrativeTraces.push({
1336
1347
  id: createId("narrative_trace"),
1337
1348
  timestamp,
1338
- source: "$longtable-interview",
1349
+ source: "$longtable-start",
1339
1350
  traceType: "judgment",
1340
1351
  summary: `First Research Shape: ${shape.handle}.`,
1341
1352
  visibility: "explicit",
@@ -1457,7 +1468,7 @@ export async function summarizeLongTableResearchSpecification(options) {
1457
1468
  updated.narrativeTraces.push({
1458
1469
  id: createId("narrative_trace"),
1459
1470
  timestamp,
1460
- source: "$longtable-interview",
1471
+ source: "$longtable-start",
1461
1472
  traceType: "judgment",
1462
1473
  summary: `Research Specification draft: ${specification.title}.`,
1463
1474
  visibility: "explicit",
@@ -2303,12 +2314,45 @@ function inferEpistemicBasis(input) {
2303
2314
  return "mixed";
2304
2315
  return unique[0];
2305
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
+ }
2306
2348
  function resolveQuestionRecordMetadata(input) {
2307
2349
  const commitmentFamily = input.commitmentFamily ?? inferCommitmentFamily(input);
2308
2350
  const epistemicBasis = input.epistemicBasis ?? inferEpistemicBasis(input);
2351
+ const hardStopScope = inferHardStopScope(input, commitmentFamily);
2309
2352
  return {
2310
2353
  ...(commitmentFamily ? { commitmentFamily } : {}),
2311
- ...(epistemicBasis ? { epistemicBasis } : {})
2354
+ ...(epistemicBasis ? { epistemicBasis } : {}),
2355
+ ...(hardStopScope ? { hardStop: true, hardStopScope } : {})
2312
2356
  };
2313
2357
  }
2314
2358
  function hasFollowUpPrompt(record, prompt) {
@@ -2369,6 +2413,8 @@ export async function createWorkspaceFollowUpQuestions(options) {
2369
2413
  updatedAt: createdAt,
2370
2414
  status: "pending",
2371
2415
  ...metadata,
2416
+ ...(typeof options.hardStop === "boolean" ? { hardStop: options.hardStop } : {}),
2417
+ ...(options.hardStopScope ? { hardStopScope: options.hardStopScope } : {}),
2372
2418
  prompt: {
2373
2419
  id: createId("question_prompt"),
2374
2420
  checkpointKey,
@@ -2423,6 +2469,8 @@ export async function createWorkspaceQuestion(options) {
2423
2469
  updatedAt: createdAt,
2424
2470
  status: "pending",
2425
2471
  ...metadata,
2472
+ ...(typeof options.hardStop === "boolean" ? { hardStop: options.hardStop } : {}),
2473
+ ...(options.hardStopScope ? { hardStopScope: options.hardStopScope } : {}),
2426
2474
  prompt: {
2427
2475
  id: createId("question_prompt"),
2428
2476
  checkpointKey,
@@ -29,7 +29,7 @@ function promptSpec() {
29
29
  argumentHint: "[project context or current uncertainty]",
30
30
  body: [
31
31
  "You are LongTable setup guidance inside Codex.",
32
- "Treat `longtable init` as deprecated. Prefer `longtable setup --provider codex` for runtime permissions, then `$longtable-interview` inside Codex for the project interview.",
32
+ "Treat `longtable init` as deprecated. Prefer `longtable setup --provider codex` for runtime permissions, then `$longtable-start` inside Codex for the project interview.",
33
33
  "Ask exactly one setup question at a time.",
34
34
  "Use numbered choices and include a concise Why / What you get / Tradeoff for each option.",
35
35
  "Do not move to the next question until the researcher answers the current one.",
@@ -37,10 +37,10 @@ function promptSpec() {
37
37
  "Do not ask for field, career stage, experience level, authorship signal, weakest domain, or panel preference during runtime setup.",
38
38
  "Project interview covers: the first research handle, early uncertainty, first inspectable material, and final structured confirmation.",
39
39
  "After collecting runtime answers, summarize the proposed setup and output the exact `longtable setup --provider codex ...` command.",
40
- "If the researcher is ready to create a project workspace, tell them to open Codex in the research folder and invoke `$longtable-interview`.",
40
+ "If the researcher is ready to create a project workspace, tell them to open Codex in the research folder and invoke `$longtable-start`.",
41
41
  "If the researcher asks you to stay inside Codex, keep the conversation in numbered form and do not prematurely close.",
42
42
  "Frame setup as permission and intervention calibration, not a researcher profile interview.",
43
- "Do not pretend setup is the full project-start interview. The project-start interview happens in `$longtable-interview`.",
43
+ "Do not pretend setup is the full project-start interview. The project-start interview happens in `$longtable-start`.",
44
44
  "Treat any slash-command arguments as context for why setup is being done now."
45
45
  ]
46
46
  },
@@ -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.50",
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.50",
33
- "@longtable/core": "0.1.50",
34
- "@longtable/memory": "0.1.50",
35
- "@longtable/provider-claude": "0.1.50",
36
- "@longtable/provider-codex": "0.1.50",
37
- "@longtable/setup": "0.1.50"
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",