@longtable/cli 0.1.51 → 0.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/cli.js +70 -10
- package/dist/codex-hooks.js +2 -4
- package/dist/hard-stop.d.ts +2 -0
- package/dist/hard-stop.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/longtable-codex-native-hook.js +45 -17
- package/dist/project-session.d.ts +13 -1
- package/dist/project-session.js +47 -1
- package/dist/question-obligations.js +2 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -85,3 +85,19 @@ longtable search --query "<topic>"
|
|
|
85
85
|
npm run build --workspace @longtable/cli
|
|
86
86
|
npm run typecheck --workspace @longtable/cli
|
|
87
87
|
```
|
|
88
|
+
|
|
89
|
+
## Codex hard-stop diagnostics
|
|
90
|
+
|
|
91
|
+
Codex `Stop` blocks only active LongTable hard-stop blockers: unresolved
|
|
92
|
+
Research Specification question, scope, construct, method, evidence, or protected
|
|
93
|
+
decision commitments. Use:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
longtable codex hook-doctor --json
|
|
97
|
+
longtable codex status --json
|
|
98
|
+
longtable doctor --json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
to inspect hook coverage/trust plus `stopWouldBlock`, `activeBlockers`, stale
|
|
102
|
+
pending-question counts, and next actions. Tmux remains an optional terminal
|
|
103
|
+
transport; LongTable state and hooks own the behavior.
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { stdin as input, stdout as output, cwd, env, exit } from "node:process";
|
|
|
8
8
|
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { collectHardStopBlockers } from "@longtable/core";
|
|
11
12
|
import { classifyCheckpointTrigger } from "@longtable/checkpoints";
|
|
12
13
|
import { assessSearchSourceCapabilities, buildResearchSearchIntent, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, SEARCH_SOURCES, summarizeConfiguredPublisherAccess } from "./search/index.js";
|
|
13
14
|
import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupOutput, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
|
|
@@ -169,6 +170,7 @@ function usage() {
|
|
|
169
170
|
" longtable codex install-hooks [--codex-config <path>] [--hooks-path <path>] [--json]",
|
|
170
171
|
" longtable codex remove-hooks [--codex-config <path>] [--hooks-path <path>] [--json]",
|
|
171
172
|
" longtable codex status [--surface compact|full] [--dir <path>] [--codex-config <path>] [--hooks-path <path>] [--json]",
|
|
173
|
+
" longtable codex hook-doctor [--cwd <path>] [--codex-config <path>] [--hooks-path <path>] [--json]",
|
|
172
174
|
" longtable claude install-skills [--surface compact|full] [--dir <path>]",
|
|
173
175
|
" longtable claude remove-skills [--dir <path>]",
|
|
174
176
|
" longtable claude status [--surface compact|full] [--dir <path>] [--json]",
|
|
@@ -1527,6 +1529,22 @@ function setupForProvider(setup, provider) {
|
|
|
1527
1529
|
}
|
|
1528
1530
|
};
|
|
1529
1531
|
}
|
|
1532
|
+
async function collectHardStopDiagnostics(startPath) {
|
|
1533
|
+
const empty = {
|
|
1534
|
+
stopWouldBlock: false,
|
|
1535
|
+
activeBlockers: [],
|
|
1536
|
+
staleOrUnrelatedPendingQuestionCount: 0,
|
|
1537
|
+
stalePendingQuestionCount: 0,
|
|
1538
|
+
stalePendingObligationCount: 0,
|
|
1539
|
+
nextActions: []
|
|
1540
|
+
};
|
|
1541
|
+
const context = await loadProjectContextFromDirectory(startPath);
|
|
1542
|
+
if (!context) {
|
|
1543
|
+
return empty;
|
|
1544
|
+
}
|
|
1545
|
+
const state = await loadWorkspaceState(context);
|
|
1546
|
+
return collectHardStopBlockers(state);
|
|
1547
|
+
}
|
|
1530
1548
|
async function collectDoctorStatus(args) {
|
|
1531
1549
|
const roles = listRoleDefinitions();
|
|
1532
1550
|
const skillSurface = parseSkillSurface(args);
|
|
@@ -1579,11 +1597,13 @@ async function collectDoctorStatus(args) {
|
|
|
1579
1597
|
: [];
|
|
1580
1598
|
const expectedCodexSkills = buildCodexSkillSpecs(roles, skillSurface).map((skill) => skill.name);
|
|
1581
1599
|
const expectedClaudeSkills = buildClaudeSkillSpecs(roles, skillSurface).map((skill) => skill.name);
|
|
1582
|
-
const
|
|
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(
|
|
1605
|
+
inspectProjectWorkspace(workspacePath),
|
|
1606
|
+
collectHardStopDiagnostics(workspacePath)
|
|
1587
1607
|
]);
|
|
1588
1608
|
const installedCodexSkills = codexSkills.map((skill) => skill.name);
|
|
1589
1609
|
const installedClaudeSkills = claudeSkills.map((skill) => skill.name);
|
|
@@ -1614,7 +1634,12 @@ async function collectDoctorStatus(args) {
|
|
|
1614
1634
|
hooksExists: existsSync(codexHooksPath),
|
|
1615
1635
|
codexHooksEnabled: codexHooksEnabled(codexMcpConfig),
|
|
1616
1636
|
missingManagedHookEvents,
|
|
1617
|
-
missingManagedHookTrustState
|
|
1637
|
+
missingManagedHookTrustState,
|
|
1638
|
+
stopWouldBlock: hardStop.stopWouldBlock,
|
|
1639
|
+
activeBlockers: hardStop.activeBlockers,
|
|
1640
|
+
stalePendingQuestionCount: hardStop.stalePendingQuestionCount,
|
|
1641
|
+
stalePendingObligationCount: hardStop.stalePendingObligationCount,
|
|
1642
|
+
nextActions: hardStop.nextActions
|
|
1618
1643
|
},
|
|
1619
1644
|
claude: {
|
|
1620
1645
|
command: "claude",
|
|
@@ -1627,7 +1652,8 @@ async function collectDoctorStatus(args) {
|
|
|
1627
1652
|
missingSkills: missingNames(expectedClaudeSkills, installedClaudeSkills)
|
|
1628
1653
|
}
|
|
1629
1654
|
},
|
|
1630
|
-
workspace
|
|
1655
|
+
workspace,
|
|
1656
|
+
hardStop
|
|
1631
1657
|
};
|
|
1632
1658
|
}
|
|
1633
1659
|
function renderProviderDoctorBlock(label, provider) {
|
|
@@ -1665,6 +1691,9 @@ function renderDoctorStatus(status) {
|
|
|
1665
1691
|
`- hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
|
|
1666
1692
|
`- managed hook coverage: ${status.providers.codex.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.providers.codex.missingManagedHookEvents.join(", ")}`}`,
|
|
1667
1693
|
`- managed hook trust: ${status.providers.codex.missingManagedHookTrustState.length === 0 ? "current" : `missing/stale ${status.providers.codex.missingManagedHookTrustState.length}`}`,
|
|
1694
|
+
`- Stop would block now: ${status.hardStop.stopWouldBlock ? "yes" : "no"}`,
|
|
1695
|
+
`- active hard-stop blockers: ${status.hardStop.activeBlockers.length}`,
|
|
1696
|
+
`- stale/unrelated pending questions: ${status.hardStop.staleOrUnrelatedPendingQuestionCount}`,
|
|
1668
1697
|
"",
|
|
1669
1698
|
...renderProviderDoctorBlock("Claude", status.providers.claude),
|
|
1670
1699
|
"",
|
|
@@ -1675,7 +1704,7 @@ function renderDoctorStatus(status) {
|
|
|
1675
1704
|
}
|
|
1676
1705
|
else {
|
|
1677
1706
|
const workspace = status.workspace;
|
|
1678
|
-
lines.push(`- project: ${workspace.project?.name ?? "unknown"}`, `- root: ${workspace.rootPath ?? "unknown"}`, `- goal: ${workspace.session?.currentGoal ?? "unknown"}`, `- invocations: ${workspace.counts?.invocations ?? 0}`, `- questions: ${workspace.counts?.questions ?? 0} (${workspace.counts?.pendingQuestions ?? 0} pending, ${workspace.counts?.answeredQuestions ?? 0} answered)`, `- obligations: ${workspace.counts?.pendingObligations ?? 0} pending`, `- decisions: ${workspace.counts?.decisions ?? 0}`);
|
|
1707
|
+
lines.push(`- project: ${workspace.project?.name ?? "unknown"}`, `- root: ${workspace.rootPath ?? "unknown"}`, `- goal: ${workspace.session?.currentGoal ?? "unknown"}`, `- invocations: ${workspace.counts?.invocations ?? 0}`, `- questions: ${workspace.counts?.questions ?? 0} (${workspace.counts?.pendingQuestions ?? 0} pending, ${workspace.counts?.answeredQuestions ?? 0} answered)`, `- obligations: ${workspace.counts?.pendingObligations ?? 0} pending`, `- Stop hard-stop: ${workspace.hardStop?.stopWouldBlock ? "would block" : "clear"}`, `- stale/unrelated pending: ${workspace.hardStop?.stalePendingQuestionCount ?? 0} questions, ${workspace.hardStop?.stalePendingObligationCount ?? 0} obligations`, `- decisions: ${workspace.counts?.decisions ?? 0}`);
|
|
1679
1708
|
if ((workspace.recentInvocations ?? []).length > 0) {
|
|
1680
1709
|
lines.push("- recent invocations:");
|
|
1681
1710
|
for (const invocation of workspace.recentInvocations ?? []) {
|
|
@@ -1689,12 +1718,25 @@ function renderDoctorStatus(status) {
|
|
|
1689
1718
|
lines.push(` - ${question.id}: ${question.question} (${question.options.join("/")})`);
|
|
1690
1719
|
}
|
|
1691
1720
|
}
|
|
1721
|
+
if (status.hardStop.activeBlockers.length > 0) {
|
|
1722
|
+
lines.push("- active hard-stop blockers:");
|
|
1723
|
+
for (const blocker of status.hardStop.activeBlockers) {
|
|
1724
|
+
lines.push(` - ${blocker.id}: ${blocker.scope} (${blocker.type})`);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1692
1727
|
if ((workspace.pendingObligations ?? []).length > 0) {
|
|
1693
1728
|
lines.push("- pending obligations:");
|
|
1694
1729
|
for (const obligation of workspace.pendingObligations ?? []) {
|
|
1695
1730
|
lines.push(` - ${obligation.id}: ${obligation.prompt}`);
|
|
1696
1731
|
}
|
|
1697
1732
|
}
|
|
1733
|
+
if ((workspace.hardStop?.activeBlockers ?? []).length > 0) {
|
|
1734
|
+
lines.push("- hard-stop blockers:");
|
|
1735
|
+
for (const blocker of workspace.hardStop?.activeBlockers ?? []) {
|
|
1736
|
+
lines.push(` - ${blocker.id} [${blocker.scope}]: ${blocker.prompt}`);
|
|
1737
|
+
lines.push(` next: ${blocker.commandHint}`);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1698
1740
|
if ((workspace.answerWarnings ?? []).length > 0) {
|
|
1699
1741
|
lines.push("- answer warnings:");
|
|
1700
1742
|
for (const warning of workspace.answerWarnings ?? []) {
|
|
@@ -1736,10 +1778,16 @@ function renderDoctorStatus(status) {
|
|
|
1736
1778
|
if (!status.workspace.found) {
|
|
1737
1779
|
nextActions.push("longtable start");
|
|
1738
1780
|
}
|
|
1781
|
+
nextActions.push(...status.hardStop.nextActions);
|
|
1739
1782
|
const firstQuestion = status.workspace.pendingQuestions?.[0];
|
|
1740
|
-
if (firstQuestion) {
|
|
1783
|
+
if (firstQuestion && status.hardStop.nextActions.length === 0) {
|
|
1741
1784
|
nextActions.push(`longtable decide --question ${firstQuestion.id} --answer <value>`);
|
|
1742
1785
|
}
|
|
1786
|
+
for (const action of status.workspace.hardStop?.nextActions ?? []) {
|
|
1787
|
+
if (!nextActions.includes(action)) {
|
|
1788
|
+
nextActions.push(action);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1743
1791
|
if (nextActions.length > 0) {
|
|
1744
1792
|
lines.push("", "Next actions:");
|
|
1745
1793
|
for (const action of nextActions) {
|
|
@@ -2813,7 +2861,7 @@ async function runAccess(subcommand, args) {
|
|
|
2813
2861
|
await runAccessSetup(args);
|
|
2814
2862
|
return;
|
|
2815
2863
|
}
|
|
2816
|
-
if (subcommand === "status") {
|
|
2864
|
+
if (subcommand === "status" || subcommand === "hook-doctor") {
|
|
2817
2865
|
await runAccessStatus(args);
|
|
2818
2866
|
return;
|
|
2819
2867
|
}
|
|
@@ -3768,7 +3816,7 @@ async function runCodexSubcommand(subcommand, args) {
|
|
|
3768
3816
|
console.log(renderCodexHookInstallSummary(result));
|
|
3769
3817
|
return;
|
|
3770
3818
|
}
|
|
3771
|
-
if (subcommand === "status") {
|
|
3819
|
+
if (subcommand === "status" || subcommand === "hook-doctor") {
|
|
3772
3820
|
const aliases = await listInstalledCodexPromptAliases(customDir);
|
|
3773
3821
|
const skills = await listInstalledCodexSkills(roles, customDir, skillSurface);
|
|
3774
3822
|
const setupPath = resolveDefaultSetupPath(typeof args.path === "string" ? args.path : undefined).path;
|
|
@@ -3777,6 +3825,7 @@ async function runCodexSubcommand(subcommand, args) {
|
|
|
3777
3825
|
const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
|
|
3778
3826
|
const hooksPath = resolveCodexHooksPath(args);
|
|
3779
3827
|
const hooksContent = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
|
|
3828
|
+
const workspace = await inspectProjectWorkspace(typeof args.cwd === "string" ? args.cwd : cwd());
|
|
3780
3829
|
const status = {
|
|
3781
3830
|
setupPath,
|
|
3782
3831
|
setupExists: existsSync(setupPath),
|
|
@@ -3796,13 +3845,21 @@ async function runCodexSubcommand(subcommand, args) {
|
|
|
3796
3845
|
: [...LONGTABLE_MANAGED_HOOK_EVENTS],
|
|
3797
3846
|
missingManagedHookTrustState: hooksContent
|
|
3798
3847
|
? getMissingManagedCodexHookTrustState(configContent, hooksPath, hooksContent)
|
|
3799
|
-
: []
|
|
3848
|
+
: [],
|
|
3849
|
+
workspaceHardStop: workspace.hardStop ?? {
|
|
3850
|
+
stopWouldBlock: false,
|
|
3851
|
+
activeBlockers: [],
|
|
3852
|
+
staleOrUnrelatedPendingQuestionCount: 0,
|
|
3853
|
+
stalePendingQuestionCount: 0,
|
|
3854
|
+
stalePendingObligationCount: 0,
|
|
3855
|
+
nextActions: []
|
|
3856
|
+
}
|
|
3800
3857
|
};
|
|
3801
3858
|
if (args.json === true) {
|
|
3802
3859
|
console.log(JSON.stringify(status, null, 2));
|
|
3803
3860
|
return;
|
|
3804
3861
|
}
|
|
3805
|
-
console.log("LongTable Codex status");
|
|
3862
|
+
console.log(subcommand === "hook-doctor" ? "LongTable Codex hook doctor" : "LongTable Codex status");
|
|
3806
3863
|
console.log(`- setup: ${status.setupExists ? "present" : "missing"} (${setupPath})`);
|
|
3807
3864
|
console.log(`- codex runtime artifact: ${status.runtimeExists ? "present" : "missing"} (${runtimePath})`);
|
|
3808
3865
|
console.log(`- skills dir: ${status.skillsDir}`);
|
|
@@ -3832,6 +3889,9 @@ async function runCodexSubcommand(subcommand, args) {
|
|
|
3832
3889
|
console.log(`- hooks file: ${status.hooksExists ? "present" : "missing"} (${status.hooksPath})`);
|
|
3833
3890
|
console.log(`- managed hook coverage: ${status.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.missingManagedHookEvents.join(", ")}`}`);
|
|
3834
3891
|
console.log(`- managed hook trust: ${status.missingManagedHookTrustState.length === 0 ? "current" : `missing/stale ${status.missingManagedHookTrustState.length}`}`);
|
|
3892
|
+
console.log(`- Stop hard-stop: ${status.workspaceHardStop.stopWouldBlock ? "would block" : "clear"}`);
|
|
3893
|
+
console.log(`- active hard-stop blockers: ${status.workspaceHardStop.activeBlockers.length}`);
|
|
3894
|
+
console.log(`- stale/unrelated pending: ${status.workspaceHardStop.stalePendingQuestionCount} questions, ${status.workspaceHardStop.stalePendingObligationCount} obligations`);
|
|
3835
3895
|
return;
|
|
3836
3896
|
}
|
|
3837
3897
|
throw new Error("Unknown codex subcommand.");
|
package/dist/codex-hooks.js
CHANGED
|
@@ -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 @@
|
|
|
1
|
+
export { collectHardStopBlockers } from "@longtable/core";
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { collectHardStopBlockers } from "@longtable/core";
|
|
2
3
|
import { createWorkspaceFollowUpQuestions, loadProjectContextFromDirectory, loadWorkspaceState, pendingQuestionObligations } from "./index.js";
|
|
3
4
|
function safeString(value) {
|
|
4
5
|
return typeof value === "string" ? value : "";
|
|
@@ -66,6 +67,9 @@ function formatQuestionOptions(question) {
|
|
|
66
67
|
function pendingRequiredQuestions(state) {
|
|
67
68
|
return (state.questionLog ?? []).filter((question) => question.status === "pending" && question.prompt.required);
|
|
68
69
|
}
|
|
70
|
+
function hardStopBlockers(state) {
|
|
71
|
+
return collectHardStopBlockers(state).activeBlockers;
|
|
72
|
+
}
|
|
69
73
|
function pendingObligations(state) {
|
|
70
74
|
return pendingQuestionObligations(state);
|
|
71
75
|
}
|
|
@@ -283,6 +287,22 @@ function buildGeneratedQuestionsContext(questions, created) {
|
|
|
283
287
|
lines.push("Do not choose or record answers for these checkpoints unless the researcher explicitly provides the selections.");
|
|
284
288
|
return lines.join("\n");
|
|
285
289
|
}
|
|
290
|
+
function buildHardStopBlockerContext(blocker) {
|
|
291
|
+
return [
|
|
292
|
+
`LongTable hard-stop blocker ${blocker.id} affects ${blocker.scope.replace(/_/g, " ")}.`,
|
|
293
|
+
blocker.prompt,
|
|
294
|
+
blocker.reason,
|
|
295
|
+
`Next action: ${blocker.commandHints[0] ?? "decide, clear, or defer with rationale"}`
|
|
296
|
+
].join("\n");
|
|
297
|
+
}
|
|
298
|
+
function buildStopBlockerReason(blocker, count) {
|
|
299
|
+
const suffix = count > 1 ? ` (${count} active blockers total)` : "";
|
|
300
|
+
return [
|
|
301
|
+
`LongTable hard-stop ${blocker.id}${suffix}: ${blocker.scope.replace(/_/g, " ")}.`,
|
|
302
|
+
compactContextValue(blocker.prompt, 120),
|
|
303
|
+
`Required next action: ${blocker.commandHints[0] ?? "decide, clear, or defer with rationale"}.`
|
|
304
|
+
].join(" ");
|
|
305
|
+
}
|
|
286
306
|
function buildPendingObligationContext(obligation) {
|
|
287
307
|
return [
|
|
288
308
|
`Pending LongTable research obligation: ${obligation.prompt}`,
|
|
@@ -298,6 +318,21 @@ function buildSeparatePendingObligationNotice(obligation) {
|
|
|
298
318
|
"This is not part of the active interview. Keep it visible only when the researcher is settling or saving the research direction."
|
|
299
319
|
].join("\n");
|
|
300
320
|
}
|
|
321
|
+
function buildHardStopContext(runtime) {
|
|
322
|
+
const verdict = collectHardStopBlockers(runtime.state);
|
|
323
|
+
const blocker = verdict.activeBlockers[0];
|
|
324
|
+
if (!blocker) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return [
|
|
328
|
+
`Hard-stop Researcher Checkpoint is still pending: ${blocker.id}`,
|
|
329
|
+
`Affected Research Specification area: ${blocker.scope}`,
|
|
330
|
+
`Question/obligation: ${blocker.prompt}`,
|
|
331
|
+
`Reason: ${blocker.reason}`,
|
|
332
|
+
`Required next action: ${blocker.commandHint}; or clear/defer it with an explicit rationale.`,
|
|
333
|
+
verdict.activeBlockers.length > 1 ? `Additional hard-stop blockers: ${verdict.activeBlockers.length - 1}` : ""
|
|
334
|
+
].filter(Boolean).join("\n");
|
|
335
|
+
}
|
|
301
336
|
function buildActiveInterviewContext(hook) {
|
|
302
337
|
const turnCount = hook.turns?.length ?? 0;
|
|
303
338
|
return [
|
|
@@ -421,13 +456,9 @@ function preToolUseOutput(runtime, payload) {
|
|
|
421
456
|
if (!stateChangingCommand) {
|
|
422
457
|
return null;
|
|
423
458
|
}
|
|
424
|
-
const
|
|
425
|
-
if (
|
|
426
|
-
return buildBlockOutput("PreToolUse", "A
|
|
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
|
|
442
|
-
|
|
443
|
-
|
|
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", "
|
|
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
|
-
|
|
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<{
|
package/dist/project-session.js
CHANGED
|
@@ -4,6 +4,7 @@ import { execSync } from "node:child_process";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { appendDecisionRecord as appendDecisionToResearchState, appendInvocationRecord as appendInvocationToResearchState, appendQuestionRecords, createEmptyResearchState } from "@longtable/memory";
|
|
6
6
|
import { classifyCheckpointTrigger } from "@longtable/checkpoints";
|
|
7
|
+
import { collectHardStopBlockers } from "@longtable/core";
|
|
7
8
|
import { ensureRequiredQuestionObligation, pendingQuestionObligations, resolveQuestionObligationByQuestionId } from "./question-obligations.js";
|
|
8
9
|
const CURRENT_FILE_NAME = "CURRENT.md";
|
|
9
10
|
const LEGACY_ROOT_FILES = ["LONGTABLE.md", "START-HERE.md", "NEXT-STEPS.md", "SESSION-SNAPSHOT.md"];
|
|
@@ -765,6 +766,7 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
765
766
|
const pendingQuestions = questions.filter((record) => record.status === "pending");
|
|
766
767
|
const answeredQuestions = questions.filter((record) => record.status === "answered");
|
|
767
768
|
const pendingObligations = visiblePendingObligations(state);
|
|
769
|
+
const hardStop = collectHardStopBlockers(state);
|
|
768
770
|
return {
|
|
769
771
|
found: true,
|
|
770
772
|
rootPath: context.project.projectPath,
|
|
@@ -803,6 +805,8 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
803
805
|
questions: questions.length,
|
|
804
806
|
pendingQuestions: pendingQuestions.length,
|
|
805
807
|
pendingObligations: pendingObligations.length,
|
|
808
|
+
stalePendingQuestions: hardStop.stalePendingQuestionCount,
|
|
809
|
+
stalePendingObligations: hardStop.stalePendingObligationCount,
|
|
806
810
|
answeredQuestions: answeredQuestions.length,
|
|
807
811
|
decisions: (state.decisionLog ?? []).length,
|
|
808
812
|
interviewTurns: (state.interviewTurns ?? []).length,
|
|
@@ -810,6 +814,7 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
810
814
|
specPatches: (state.specPatches ?? []).length,
|
|
811
815
|
specRevisions: (state.specRevisions ?? []).length
|
|
812
816
|
},
|
|
817
|
+
hardStop,
|
|
813
818
|
recentInvocations: recentInvocationRecords(state, 5).map((record) => ({
|
|
814
819
|
id: record.id,
|
|
815
820
|
kind: record.intent.kind,
|
|
@@ -826,6 +831,8 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
826
831
|
question: record.prompt.question,
|
|
827
832
|
...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
|
|
828
833
|
...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
|
|
834
|
+
...(typeof record.hardStop === "boolean" ? { hardStop: record.hardStop } : {}),
|
|
835
|
+
...(record.hardStopScope ? { hardStopScope: record.hardStopScope } : {}),
|
|
829
836
|
options: formatQuestionOptionValues(record),
|
|
830
837
|
required: record.prompt.required
|
|
831
838
|
})),
|
|
@@ -834,6 +841,8 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
834
841
|
kind: obligation.kind,
|
|
835
842
|
prompt: obligation.prompt,
|
|
836
843
|
reason: obligation.reason,
|
|
844
|
+
...(typeof obligation.hardStop === "boolean" ? { hardStop: obligation.hardStop } : {}),
|
|
845
|
+
...(obligation.hardStopScope ? { hardStopScope: obligation.hardStopScope } : {}),
|
|
837
846
|
...(obligation.questionId ? { questionId: obligation.questionId } : {})
|
|
838
847
|
})),
|
|
839
848
|
recentDecisions: (state.decisionLog ?? []).slice(-5).reverse().map((record) => ({
|
|
@@ -2305,12 +2314,45 @@ function inferEpistemicBasis(input) {
|
|
|
2305
2314
|
return "mixed";
|
|
2306
2315
|
return unique[0];
|
|
2307
2316
|
}
|
|
2317
|
+
function inferHardStopScope(input, commitmentFamily) {
|
|
2318
|
+
if (commitmentFamily === "product_policy")
|
|
2319
|
+
return undefined;
|
|
2320
|
+
if (commitmentFamily === "scope")
|
|
2321
|
+
return "scope";
|
|
2322
|
+
if (commitmentFamily === "construct" || commitmentFamily === "coding")
|
|
2323
|
+
return "construct";
|
|
2324
|
+
if (commitmentFamily === "method")
|
|
2325
|
+
return "method";
|
|
2326
|
+
if (commitmentFamily === "evidence")
|
|
2327
|
+
return "evidence";
|
|
2328
|
+
if (commitmentFamily === "epistemic_authority")
|
|
2329
|
+
return "protected_decision";
|
|
2330
|
+
const text = compactMetadataText([input.checkpointKey, input.title, input.question, input.prompt, input.rationale]);
|
|
2331
|
+
if (textMatchesAny(text, [/product_runtime|checkpoint policy|hook ux|setup|install|cli|npm|release|git|github|docs?|readme|package|workflow/])) {
|
|
2332
|
+
return undefined;
|
|
2333
|
+
}
|
|
2334
|
+
if (textMatchesAny(text, [/protected_decision|closure/]))
|
|
2335
|
+
return "protected_decision";
|
|
2336
|
+
if (textMatchesAny(text, [/research_question|research direction|question_freeze/]))
|
|
2337
|
+
return "research_question";
|
|
2338
|
+
if (textMatchesAny(text, [/scope|boundary|inclusion|exclusion/]))
|
|
2339
|
+
return "scope";
|
|
2340
|
+
if (textMatchesAny(text, [/construct|theory|frame|ontology|measurement|coding|validity/]))
|
|
2341
|
+
return "construct";
|
|
2342
|
+
if (textMatchesAny(text, [/method|design|sample|analysis|strategy|model/]))
|
|
2343
|
+
return "method";
|
|
2344
|
+
if (textMatchesAny(text, [/evidence|access|source|corpus|pdf|full[-_ ]?text|scholarly/]))
|
|
2345
|
+
return "evidence";
|
|
2346
|
+
return undefined;
|
|
2347
|
+
}
|
|
2308
2348
|
function resolveQuestionRecordMetadata(input) {
|
|
2309
2349
|
const commitmentFamily = input.commitmentFamily ?? inferCommitmentFamily(input);
|
|
2310
2350
|
const epistemicBasis = input.epistemicBasis ?? inferEpistemicBasis(input);
|
|
2351
|
+
const hardStopScope = inferHardStopScope(input, commitmentFamily);
|
|
2311
2352
|
return {
|
|
2312
2353
|
...(commitmentFamily ? { commitmentFamily } : {}),
|
|
2313
|
-
...(epistemicBasis ? { epistemicBasis } : {})
|
|
2354
|
+
...(epistemicBasis ? { epistemicBasis } : {}),
|
|
2355
|
+
...(hardStopScope ? { hardStop: true, hardStopScope } : {})
|
|
2314
2356
|
};
|
|
2315
2357
|
}
|
|
2316
2358
|
function hasFollowUpPrompt(record, prompt) {
|
|
@@ -2371,6 +2413,8 @@ export async function createWorkspaceFollowUpQuestions(options) {
|
|
|
2371
2413
|
updatedAt: createdAt,
|
|
2372
2414
|
status: "pending",
|
|
2373
2415
|
...metadata,
|
|
2416
|
+
...(typeof options.hardStop === "boolean" ? { hardStop: options.hardStop } : {}),
|
|
2417
|
+
...(options.hardStopScope ? { hardStopScope: options.hardStopScope } : {}),
|
|
2374
2418
|
prompt: {
|
|
2375
2419
|
id: createId("question_prompt"),
|
|
2376
2420
|
checkpointKey,
|
|
@@ -2425,6 +2469,8 @@ export async function createWorkspaceQuestion(options) {
|
|
|
2425
2469
|
updatedAt: createdAt,
|
|
2426
2470
|
status: "pending",
|
|
2427
2471
|
...metadata,
|
|
2472
|
+
...(typeof options.hardStop === "boolean" ? { hardStop: options.hardStop } : {}),
|
|
2473
|
+
...(options.hardStopScope ? { hardStopScope: options.hardStopScope } : {}),
|
|
2428
2474
|
prompt: {
|
|
2429
2475
|
id: createId("question_prompt"),
|
|
2430
2476
|
checkpointKey,
|
|
@@ -17,6 +17,8 @@ export function createRequiredQuestionObligation(question) {
|
|
|
17
17
|
updatedAt: timestamp,
|
|
18
18
|
prompt: question.prompt.question,
|
|
19
19
|
reason: question.prompt.displayReason ?? "A required LongTable checkpoint is pending.",
|
|
20
|
+
...(question.hardStop !== undefined ? { hardStop: question.hardStop } : {}),
|
|
21
|
+
...(question.hardStopScope ? { hardStopScope: question.hardStopScope } : {}),
|
|
20
22
|
questionId: question.id
|
|
21
23
|
};
|
|
22
24
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/cli",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
33
|
-
"@longtable/core": "0.1.
|
|
34
|
-
"@longtable/memory": "0.1.
|
|
35
|
-
"@longtable/provider-claude": "0.1.
|
|
36
|
-
"@longtable/provider-codex": "0.1.
|
|
37
|
-
"@longtable/setup": "0.1.
|
|
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",
|