sneakoscope 7.3.0 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +117 -5
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +2 -1
  6. package/dist/cli/install-helpers-codex-lb-selftest.js +2 -1
  7. package/dist/cli/install-helpers-install-support.js +6 -16
  8. package/dist/cli/install-helpers.js +59 -50
  9. package/dist/cli/router.js +9 -1
  10. package/dist/config/skills-manifest.json +50 -71
  11. package/dist/core/codex-app.js +5 -5
  12. package/dist/core/codex-native/core-skill-manifest.js +9 -6
  13. package/dist/core/commands/image-ux-review-command.js +243 -50
  14. package/dist/core/commands/naruto-command.js +245 -8
  15. package/dist/core/commands/ppt-command.js +2 -2
  16. package/dist/core/commands/telegram-command.js +123 -10
  17. package/dist/core/commands/triwiki-command.js +11 -1
  18. package/dist/core/commands/triwiki-graph-command.js +160 -0
  19. package/dist/core/commands/wiki-command.js +100 -20
  20. package/dist/core/errors/message.js +3 -0
  21. package/dist/core/feature-registry.js +2 -2
  22. package/dist/core/fsx.js +7 -3
  23. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  24. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  25. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  26. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  27. package/dist/core/hooks-runtime.js +28 -86
  28. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  29. package/dist/core/image-ux-review/imagegen-adapter.js +54 -18
  30. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  31. package/dist/core/image-ux-review.js +182 -7
  32. package/dist/core/imagegen/codex-lb-imagegen-target.js +22 -7
  33. package/dist/core/imagegen/imagegen-capability.js +11 -4
  34. package/dist/core/imagegen/require-imagegen.js +29 -5
  35. package/dist/core/init/skills.js +22 -10
  36. package/dist/core/install/installed-package-smoke.js +157 -4
  37. package/dist/core/managed-path-safety.js +8 -10
  38. package/dist/core/mcp-config/backup.js +2 -4
  39. package/dist/core/mcp-config/config-reader.js +2 -4
  40. package/dist/core/mcp-config/scope.js +3 -5
  41. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  42. package/dist/core/naruto/context-graph-advisor.js +0 -0
  43. package/dist/core/pipeline-internals/runtime-core.js +44 -0
  44. package/dist/core/policy/latest-version-guidance.js +113 -0
  45. package/dist/core/ppt.js +8 -8
  46. package/dist/core/release/gate-manifest.js +32 -1
  47. package/dist/core/release/npm-stage-contract.js +21 -0
  48. package/dist/core/release/package-size-budget.js +2 -2
  49. package/dist/core/release/release-gate-contract.js +5 -0
  50. package/dist/core/release/stage-publish.js +27 -0
  51. package/dist/core/responses-stream.js +65 -19
  52. package/dist/core/routes/design-policy.js +7 -2
  53. package/dist/core/routes/dollar-manifest-lite.js +0 -4
  54. package/dist/core/routes/evidence.js +1 -1
  55. package/dist/core/routes.js +12 -8
  56. package/dist/core/safety/command-contract/contracts.js +2 -0
  57. package/dist/core/safety/command-contract/types.js +1 -1
  58. package/dist/core/search/context-graph-seeds.js +205 -0
  59. package/dist/core/search/context.js +0 -0
  60. package/dist/core/search/provider.js +2 -2
  61. package/dist/core/subagents/naruto-help-contract.js +2 -1
  62. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  63. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  64. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  65. package/dist/core/subagents/official-subagent-runner.js +26 -8
  66. package/dist/core/subagents/triwiki-attention.js +135 -123
  67. package/dist/core/telegram/bot-api.js +59 -4
  68. package/dist/core/telegram/config.js +3 -2
  69. package/dist/core/telegram/hub.js +3 -2
  70. package/dist/core/telegram/owner-lock.js +4 -6
  71. package/dist/core/telegram/setup.js +223 -73
  72. package/dist/core/triwiki/code-pack.js +12 -120
  73. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  74. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  75. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  76. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  77. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  78. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  79. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  80. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  81. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  82. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  83. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  84. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  85. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  86. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  87. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  88. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  89. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  90. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  91. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  92. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  93. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  94. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  95. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  96. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  97. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  98. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  99. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  100. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  101. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  102. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  103. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  104. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  105. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  106. package/dist/core/triwiki/context-graph/ids.js +0 -0
  107. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  108. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  109. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  110. package/dist/core/triwiki/context-graph/paths.js +116 -0
  111. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  112. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  113. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  114. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  115. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  116. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  117. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  118. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  119. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  120. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  121. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  122. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  123. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  124. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  125. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  126. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  127. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  128. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  129. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  130. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  131. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  132. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  133. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  134. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  135. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  136. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  137. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  138. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  139. package/dist/core/verification/context-graph-affected.js +271 -0
  140. package/dist/core/version.js +1 -1
  141. package/dist/scripts/context-graph-check.js +191 -0
  142. package/dist/scripts/helper-dedup-check.js +0 -1
  143. package/dist/scripts/installed-package-smoke-check.js +4 -1
  144. package/dist/scripts/latest-version-guidance-check.js +74 -0
  145. package/dist/scripts/release-metadata-check.js +20 -3
  146. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  147. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  148. package/package.json +5 -3
  149. package/release-gates.v2.json +174 -0
  150. package/schemas/triwiki/context-graph.schema.json +119 -0
  151. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -1,10 +1,13 @@
1
1
  import path from 'node:path';
2
+ import { isDeepStrictEqual } from 'node:util';
2
3
  import { ui as cliUi } from '../../cli/cli-theme.js';
3
4
  import { createMission, findLatestMission, getOrCreateExplicitNarutoMission, getOrCreateSessionMission, loadStateForSession, loadMission, sessionStateKey, setCurrent, updateCurrentIfMissionAndRun } from '../mission.js';
4
5
  import { closeWorkOrderLedgerForRouteResult, createAndWriteWorkOrderLedgerForPrompt } from '../work-order-ledger.js';
5
6
  import { appendJsonl, exists, nowIso, readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
6
- import { SUBAGENT_EVENT_LOG_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME, bindTrustworthySubagentParentSummaryToRun, persistOrReuseTrustworthySubagentParentSummary, readSubagentEvents, writeSubagentEvidence } from '../subagents/subagent-evidence.js';
7
+ import { SUBAGENT_EVENT_LOG_FILENAME, SUBAGENT_EVIDENCE_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME, bindTrustworthySubagentParentSummaryToRun, normalizeSubagentParentSummary, persistOrReuseTrustworthySubagentParentSummary, readSubagentEvents, writeSubagentEvidence } from '../subagents/subagent-evidence.js';
7
8
  import { buildNarutoHelpResult } from '../subagents/naruto-help-contract.js';
9
+ import { resolveNarutoCredentialPolicy } from '../subagents/naruto-host-credentials.js';
10
+ import { DEFAULT_SUBAGENT_EFFORT, DEFAULT_SUBAGENT_MODEL, NARUTO_PARENT_EFFORT, NARUTO_PARENT_MODEL } from '../subagents/model-policy.js';
8
11
  import { buildNarutoProofProjection } from '../subagents/naruto-proof-projection.js';
9
12
  import { withFileLock } from '../locks/file-lock.js';
10
13
  import { codexAppSessionKey, detectCodexAppSession, runOfficialSubagentWorkflow } from '../subagents/official-subagent-runner.js';
@@ -14,7 +17,9 @@ import { effectiveSubagentTarget, refreshSubagentWaveLifecycle } from '../subage
14
17
  import { HOST_CAPABILITY_HOOK_RUNTIME_FILENAME, bindParentSummaryToHostCapabilityEvidence, createHostCapabilityHookRuntimeBinding, resolveHostCapabilityHookRuntimeBinding } from '../agent-bridge/host-capability-runtime.js';
15
18
  import { renderHostCapabilityBlockedLines } from '../agent-bridge/host-capability-policy.js';
16
19
  import { uniqueStrings } from '../text/strings.js';
20
+ import { completeNarutoTerminalBundle, refreshOfficialSubagentCompletionArtifacts } from '../hooks-runtime/official-subagent-lifecycle.js';
17
21
  export { buildNarutoGateResult } from '../subagents/official-subagent-preparation.js';
22
+ const MAX_PARENT_SUMMARY_STDIN_BYTES = 1024 * 1024;
18
23
  let nextNarutoPreparationFailureInjectionForTest = null;
19
24
  export function injectNextNarutoPreparationFailureForTest(value) {
20
25
  nextNarutoPreparationFailureInjectionForTest = value;
@@ -24,6 +29,23 @@ export async function narutoCommand(commandOrArgs = 'naruto', maybeArgs = []) {
24
29
  if (args.some((arg) => arg === '--glm' || arg.startsWith('--glm=')))
25
30
  return blockGlmOverride(args.includes('--json'));
26
31
  const parsed = parseNarutoArgs(args);
32
+ const credentialPolicy = parsed.credentialPolicy;
33
+ if (credentialPolicy.blockers.length) {
34
+ const blocked = {
35
+ schema: 'sks.naruto-credential-policy.v1',
36
+ ok: false,
37
+ blockers: credentialPolicy.blockers,
38
+ hint: 'sks naruto run --auth-mode=host --model-provider=<config.toml provider block> --provider-env-key=<ENV NAME>'
39
+ };
40
+ if (args.includes('--json'))
41
+ console.log(JSON.stringify(blocked, null, 2));
42
+ else {
43
+ console.error(`Naruto credential policy blocked: ${credentialPolicy.blockers.join(', ')}`);
44
+ console.error(blocked.hint);
45
+ }
46
+ process.exitCode = 2;
47
+ return null;
48
+ }
27
49
  if (parsed.argumentErrors.length) {
28
50
  const result = argumentBlock(parsed.argumentErrors);
29
51
  return emit(parsed, result, () => {
@@ -41,8 +63,185 @@ export async function narutoCommand(commandOrArgs = 'naruto', maybeArgs = []) {
41
63
  return narutoSubagents(parsed);
42
64
  if (parsed.action === 'proof')
43
65
  return narutoProof(parsed);
66
+ if (parsed.action === 'parent-summary')
67
+ return narutoParentSummary(parsed);
44
68
  return narutoRun(parsed);
45
69
  }
70
+ async function narutoParentSummary(parsed) {
71
+ const root = await sksRoot();
72
+ const appSession = detectCodexAppSession();
73
+ const sessionKey = appSession ? codexAppSessionKey() : null;
74
+ if (!appSession || !sessionKey) {
75
+ return blockedParentSummary(parsed, ['naruto_parent_summary_app_session_required']);
76
+ }
77
+ return withFileLock({
78
+ lockPath: path.join(root, '.sneakoscope', 'state', `naruto-session-${sessionStateKey(sessionKey)}.lock`),
79
+ timeoutMs: 20_000,
80
+ staleMs: 120_000
81
+ }, () => narutoParentSummaryTransaction(parsed, root, sessionKey));
82
+ }
83
+ async function narutoParentSummaryTransaction(parsed, root, sessionKey) {
84
+ const state = await loadStateForSession(root, sessionKey).catch(() => null);
85
+ const route = String(state?.route || state?.route_command || state?.mode || '')
86
+ .replace(/^\$/, '')
87
+ .replace(/[-_]/g, '')
88
+ .toUpperCase();
89
+ const stateBlockers = uniqueStrings([
90
+ ...(state?._session_key === sessionStateKey(sessionKey) ? [] : ['naruto_parent_summary_session_state_mismatch']),
91
+ ...(state?.session_scope === sessionKey ? [] : ['naruto_parent_summary_session_scope_mismatch']),
92
+ ...(state?.mission_id === parsed.missionId ? [] : ['naruto_parent_summary_active_mission_mismatch']),
93
+ ...(route === 'NARUTO' ? [] : ['naruto_parent_summary_active_route_mismatch']),
94
+ ...(state?.route_closed === true ? ['naruto_parent_summary_route_closed'] : [])
95
+ ]);
96
+ if (stateBlockers.length > 0)
97
+ return blockedParentSummary(parsed, stateBlockers);
98
+ const loaded = await loadMission(root, parsed.missionId).catch(() => null);
99
+ if (!loaded)
100
+ return blockedParentSummary(parsed, [`naruto_parent_summary_mission_not_found:${parsed.missionId}`]);
101
+ const plan = await readJson(path.join(loaded.dir, SUBAGENT_PLAN_FILENAME), null).catch(() => null);
102
+ const workflowRunId = String(plan?.workflow_run_id || '').trim();
103
+ const planBlockers = uniqueStrings([
104
+ ...(plan?.schema === 'sks.subagent-plan.v1' ? [] : ['naruto_parent_summary_plan_schema_invalid']),
105
+ ...(plan?.workflow === 'official_codex_subagent' ? [] : ['naruto_parent_summary_plan_workflow_invalid']),
106
+ ...(plan?.mission_id === parsed.missionId ? [] : ['naruto_parent_summary_plan_mission_mismatch']),
107
+ ...(plan?.session_scope === sessionKey ? [] : ['naruto_parent_summary_plan_session_scope_mismatch']),
108
+ ...(workflowRunId ? [] : ['naruto_parent_summary_plan_run_id_missing']),
109
+ ...(workflowRunId && state?.official_subagent_run_id === workflowRunId
110
+ ? []
111
+ : ['naruto_parent_summary_active_run_mismatch'])
112
+ ]);
113
+ if (planBlockers.length > 0)
114
+ return blockedParentSummary(parsed, planBlockers);
115
+ const stdin = await readBoundedParentSummaryStdin();
116
+ if (!stdin.ok)
117
+ return blockedParentSummary(parsed, [stdin.blocker]);
118
+ const raw = stdin.value;
119
+ let submitted;
120
+ try {
121
+ submitted = JSON.parse(raw);
122
+ }
123
+ catch {
124
+ return blockedParentSummary(parsed, ['naruto_parent_summary_stdin_json_invalid']);
125
+ }
126
+ if (!submitted || typeof submitted !== 'object' || Array.isArray(submitted)) {
127
+ return blockedParentSummary(parsed, ['naruto_parent_summary_stdin_object_required']);
128
+ }
129
+ const normalized = normalizeSubagentParentSummary(submitted);
130
+ const summaryBlockers = uniqueStrings([
131
+ ...(normalized.trustworthy && normalized.raw ? [] : normalized.blockers.length
132
+ ? normalized.blockers.map((blocker) => `naruto_parent_summary_schema:${blocker}`)
133
+ : ['naruto_parent_summary_schema_invalid']),
134
+ ...(!normalized.run_id
135
+ ? ['naruto_parent_summary_run_id_missing']
136
+ : normalized.run_id === workflowRunId
137
+ ? []
138
+ : ['naruto_parent_summary_run_id_mismatch'])
139
+ ]);
140
+ if (summaryBlockers.length > 0)
141
+ return blockedParentSummary(parsed, summaryBlockers);
142
+ const submittedSummary = normalized.raw;
143
+ if (!submittedSummary)
144
+ return blockedParentSummary(parsed, ['naruto_parent_summary_schema_invalid']);
145
+ const existing = await readJson(path.join(loaded.dir, SUBAGENT_PARENT_SUMMARY_FILENAME), null).catch(() => null);
146
+ if (existing !== null) {
147
+ const normalizedExisting = normalizeSubagentParentSummary(existing);
148
+ const [existingGate, existingSummary, existingEvidence] = await Promise.all([
149
+ readJson(path.join(loaded.dir, NARUTO_GATE_FILENAME), null).catch(() => null),
150
+ readJson(path.join(loaded.dir, NARUTO_SUMMARY_FILENAME), null).catch(() => null),
151
+ readJson(path.join(loaded.dir, SUBAGENT_EVIDENCE_FILENAME), null).catch(() => null)
152
+ ]);
153
+ const terminal = completeNarutoTerminalBundle({
154
+ workflowRunId,
155
+ gate: existingGate,
156
+ summary: existingSummary,
157
+ evidence: existingEvidence,
158
+ parentSummary: existing
159
+ });
160
+ if (terminal) {
161
+ if (!normalizedExisting.trustworthy || !normalizedExisting.raw) {
162
+ return blockedParentSummary(parsed, ['naruto_parent_summary_existing_canonical_invalid']);
163
+ }
164
+ if (normalizedExisting.run_id !== workflowRunId) {
165
+ return blockedParentSummary(parsed, ['naruto_parent_summary_existing_run_id_mismatch']);
166
+ }
167
+ if (!sameParentAuthoredSummary(normalizedExisting.raw, submittedSummary)) {
168
+ return blockedParentSummary(parsed, ['naruto_parent_summary_conflicts_with_canonical']);
169
+ }
170
+ }
171
+ }
172
+ const evidence = await refreshOfficialSubagentCompletionArtifacts(root, state, submittedSummary, sessionKey);
173
+ if (!evidence)
174
+ return blockedParentSummary(parsed, ['naruto_parent_summary_lifecycle_commit_rejected']);
175
+ const [persisted, summary, gate] = await Promise.all([
176
+ readJson(path.join(loaded.dir, SUBAGENT_PARENT_SUMMARY_FILENAME), null).catch(() => null),
177
+ readJson(path.join(loaded.dir, NARUTO_SUMMARY_FILENAME), null).catch(() => null),
178
+ readJson(path.join(loaded.dir, NARUTO_GATE_FILENAME), null).catch(() => null)
179
+ ]);
180
+ const normalizedPersisted = normalizeSubagentParentSummary(persisted);
181
+ if (!normalizedPersisted.trustworthy
182
+ || normalizedPersisted.run_id !== workflowRunId
183
+ || !sameParentAuthoredSummary(normalizedPersisted.raw, submittedSummary)) {
184
+ return blockedParentSummary(parsed, ['naruto_parent_summary_canonical_commit_mismatch']);
185
+ }
186
+ const hardBlocked = normalizedPersisted.status === 'failed';
187
+ const accepted = gate?.passed === true || hardBlocked;
188
+ const result = {
189
+ schema: NARUTO_RESULT_SCHEMA,
190
+ action: 'parent-summary',
191
+ ok: gate?.passed === true,
192
+ accepted,
193
+ status: summary?.status || evidence?.status || normalizedPersisted.status || 'incomplete',
194
+ mission_id: parsed.missionId,
195
+ workflow_run_id: workflowRunId,
196
+ parent_summary_status: normalizedPersisted.status,
197
+ completion_evidence: gate?.passed === true,
198
+ blockers: Array.isArray(gate?.blockers) ? gate.blockers : [],
199
+ artifacts: narutoArtifactLinks(evidence)
200
+ };
201
+ return emit(parsed, result, () => renderParentSummaryResult(result), result.ok !== true);
202
+ }
203
+ async function readBoundedParentSummaryStdin() {
204
+ let value = '';
205
+ let bytes = 0;
206
+ process.stdin.setEncoding('utf8');
207
+ for await (const chunk of process.stdin) {
208
+ const text = String(chunk);
209
+ bytes += Buffer.byteLength(text, 'utf8');
210
+ if (bytes > MAX_PARENT_SUMMARY_STDIN_BYTES) {
211
+ return { ok: false, blocker: `naruto_parent_summary_stdin_too_large:${MAX_PARENT_SUMMARY_STDIN_BYTES}` };
212
+ }
213
+ value += text;
214
+ }
215
+ if (!value.trim())
216
+ return { ok: false, blocker: 'naruto_parent_summary_stdin_empty' };
217
+ return { ok: true, value };
218
+ }
219
+ function sameParentAuthoredSummary(left, right) {
220
+ if (!left || typeof left !== 'object' || Array.isArray(left))
221
+ return false;
222
+ if (!right || typeof right !== 'object' || Array.isArray(right))
223
+ return false;
224
+ const omitHostReceipts = (value) => {
225
+ const { artifacts: _artifacts, capabilities_used: _capabilitiesUsed, ...authored } = value;
226
+ return authored;
227
+ };
228
+ return isDeepStrictEqual(omitHostReceipts(left), omitHostReceipts(right));
229
+ }
230
+ function blockedParentSummary(parsed, blockers) {
231
+ const result = {
232
+ schema: NARUTO_RESULT_SCHEMA,
233
+ action: 'parent-summary',
234
+ ok: false,
235
+ accepted: false,
236
+ status: 'blocked',
237
+ mission_id: parsed.missionId || null,
238
+ blockers: uniqueStrings(blockers)
239
+ };
240
+ return emit(parsed, result, () => {
241
+ for (const line of renderNarutoBlockedLines(result.blockers))
242
+ console.error(line);
243
+ }, true);
244
+ }
46
245
  async function narutoRun(parsed) {
47
246
  const root = await sksRoot();
48
247
  const appSession = detectCodexAppSession();
@@ -152,6 +351,7 @@ async function narutoRunTransaction(parsed, root, appSession, sessionKey, resolv
152
351
  missionId: id,
153
352
  workflowRunId,
154
353
  sessionKey,
354
+ credentialPolicy: parsed.credentialPolicy,
155
355
  ...(missionLease ? { onChildSpawn: missionLease.protectChildPid } : {})
156
356
  });
157
357
  const result = await withOfficialSubagentLifecycleLock(dir, async () => {
@@ -482,6 +682,7 @@ function narutoHelp(parsed) {
482
682
  console.log(`Nesting: max_depth=${result.max_depth}; Naruto children must not spawn children`);
483
683
  console.log('Context: bounded TriWiki attention.use_first anchors with on-demand source hydration');
484
684
  console.log('Evidence: SubagentStop is lifecycle-only; completion requires subagent-parent-summary.json with one structured outcome per Naruto child thread.');
685
+ console.log('Codex App finalization: submit the structured object with parent-summary --mission <id> --stdin, then return localized Markdown without exposing JSON.');
485
686
  });
486
687
  }
487
688
  async function resolveRunMission(root, parsed, sessionKey = null) {
@@ -541,7 +742,7 @@ export function parseNarutoArgs(args) {
541
742
  : args;
542
743
  const first = normalized[0] && !normalized[0].startsWith('-') ? normalized[0] : '';
543
744
  const actionName = first;
544
- const actions = new Set(['run', 'status', 'subagents', 'proof', 'help']);
745
+ const actions = new Set(['run', 'status', 'subagents', 'proof', 'parent-summary', 'help']);
545
746
  const action = (actions.has(actionName) ? actionName : 'run');
546
747
  const explicitAction = actions.has(actionName);
547
748
  const rest = explicitAction ? normalized.slice(1) : normalized;
@@ -571,7 +772,7 @@ export function parseNarutoArgs(args) {
571
772
  ? positional.join(' ').trim()
572
773
  : '';
573
774
  const positionalHead = String(positional[0] || '').toLowerCase();
574
- const subcommandNames = new Set(['run', 'status', 'subagents', 'proof', 'help']);
775
+ const subcommandNames = new Set(['run', 'status', 'subagents', 'proof', 'parent-summary', 'help']);
575
776
  if (!first && !explicitAction && positionalHead && !subcommandNames.has(positionalHead)) {
576
777
  argumentErrors.push(`unknown_subcommand:${positionalHead}`);
577
778
  }
@@ -599,6 +800,22 @@ export function parseNarutoArgs(args) {
599
800
  }
600
801
  if (action === 'run' && !prompt)
601
802
  argumentErrors.push('empty_task');
803
+ if (action === 'parent-summary') {
804
+ if (!missionOption.present || !missionOption.value || missionOption.value === 'latest') {
805
+ argumentErrors.push('parent_summary_requires_explicit_mission');
806
+ }
807
+ if (missionIdOption.present)
808
+ argumentErrors.push('parent_summary_mission_id_alias_not_supported');
809
+ if (!normalized.includes('--stdin'))
810
+ argumentErrors.push('parent_summary_requires_stdin');
811
+ if (agentsOption.present || maxThreadsOption.present || normalized.includes('--readonly')
812
+ || normalized.includes('--read-only') || normalized.includes('--trusted-project')) {
813
+ argumentErrors.push('parent_summary_unsupported_run_option');
814
+ }
815
+ }
816
+ else if (normalized.includes('--stdin')) {
817
+ argumentErrors.push('stdin_only_supported_for_parent_summary');
818
+ }
602
819
  return {
603
820
  action,
604
821
  prompt,
@@ -606,17 +823,28 @@ export function parseNarutoArgs(args) {
606
823
  maxThreads,
607
824
  missionId: String(missionFlag || positionalMission || 'latest'),
608
825
  json: normalized.includes('--json'),
826
+ stdin: normalized.includes('--stdin'),
609
827
  readOnly: normalized.includes('--readonly') || normalized.includes('--read-only'),
610
828
  trustedProject: optionArgs.includes('--trusted-project'),
611
- argumentErrors: uniqueStrings(argumentErrors)
829
+ argumentErrors: uniqueStrings(argumentErrors),
830
+ credentialPolicy: resolveNarutoCredentialPolicy({
831
+ args: optionArgs,
832
+ env: process.env,
833
+ defaultParentModel: NARUTO_PARENT_MODEL,
834
+ defaultParentEffort: NARUTO_PARENT_EFFORT,
835
+ defaultSubagentModel: DEFAULT_SUBAGENT_MODEL,
836
+ defaultSubagentEffort: DEFAULT_SUBAGENT_EFFORT
837
+ })
612
838
  };
613
839
  }
614
840
  function positionalValues(args) {
615
841
  const valueFlags = new Set([
616
- '--agents', '--max-threads', '--mission', '--mission-id'
842
+ '--agents', '--max-threads', '--mission', '--mission-id',
843
+ '--auth-mode', '--model-provider', '--provider-env-key',
844
+ '--parent-model', '--parent-effort', '--subagent-model', '--subagent-effort'
617
845
  ]);
618
846
  const booleanFlags = new Set([
619
- '--json', '--readonly', '--read-only', '--trusted-project'
847
+ '--json', '--stdin', '--readonly', '--read-only', '--trusted-project', '--no-forced-login-method'
620
848
  ]);
621
849
  const result = [];
622
850
  for (let index = 0; index < args.length; index += 1) {
@@ -679,7 +907,7 @@ function optionErrors(name, option, numeric) {
679
907
  function unknownOptionErrors(args) {
680
908
  const canonical = new Set([
681
909
  '--agents', '--max-threads', '--mission', '--mission-id',
682
- '--json', '--readonly', '--read-only', '--trusted-project', '--help', '-h', '--'
910
+ '--json', '--stdin', '--readonly', '--read-only', '--trusted-project', '--help', '-h', '--'
683
911
  ]);
684
912
  const errors = [];
685
913
  const optionArgs = args.includes('--') ? args.slice(0, args.indexOf('--')) : args;
@@ -693,7 +921,7 @@ function unknownOptionErrors(args) {
693
921
  return errors;
694
922
  }
695
923
  function booleanOptionErrors(args) {
696
- const booleanNames = new Set(['--json', '--readonly', '--read-only', '--trusted-project', '--help', '-h']);
924
+ const booleanNames = new Set(['--json', '--stdin', '--readonly', '--read-only', '--trusted-project', '--no-forced-login-method', '--help', '-h']);
697
925
  const optionArgs = args.includes('--') ? args.slice(0, args.indexOf('--')) : args;
698
926
  const errors = [];
699
927
  for (const arg of optionArgs) {
@@ -785,6 +1013,15 @@ function renderRunResult(result) {
785
1013
  if (result.status === 'delegation_context_ready')
786
1014
  console.log('Continue in the current Naruto parent and wait for every requested child before summarizing.');
787
1015
  }
1016
+ function renderParentSummaryResult(result) {
1017
+ if (result.ok !== true) {
1018
+ for (const line of renderNarutoBlockedLines(result.blockers || []))
1019
+ console.log(line);
1020
+ return;
1021
+ }
1022
+ console.log(`$sks-naruto parent summary committed: ${result.mission_id}`);
1023
+ console.log(`Workflow run: ${result.workflow_run_id}`);
1024
+ }
788
1025
  function renderStatusResult(result) {
789
1026
  if (['blocked', 'incomplete'].includes(result.status) && Array.isArray(result.blockers) && result.blockers.length) {
790
1027
  for (const line of renderNarutoBlockedLines(result.blockers))
@@ -47,7 +47,7 @@ export async function pptCommand(command, args = []) {
47
47
  process.exitCode = 1;
48
48
  if (flag(args, '--json'))
49
49
  return printJson(result);
50
- console.error('PPT build blocked: Codex App imagegen/gpt-image-2 is unavailable.');
50
+ console.error('PPT build blocked: no selected Codex imagegen/gpt-image-2 provider is ready.');
51
51
  for (const action of imagegenRequired.blocker?.next_actions || [])
52
52
  console.error(`- ${action}`);
53
53
  return result;
@@ -159,7 +159,7 @@ async function pptImagegenReview(root, command, action, args = []) {
159
159
  process.exitCode = 1;
160
160
  if (flag(args, '--json'))
161
161
  return printJson(result);
162
- console.error('PPT imagegen review blocked: Codex App imagegen/gpt-image-2 is unavailable.');
162
+ console.error('PPT imagegen review blocked: no selected Codex imagegen/gpt-image-2 provider is ready.');
163
163
  for (const action of imagegenRequired.blocker?.next_actions || [])
164
164
  console.error(`- ${action}`);
165
165
  return result;
@@ -2,7 +2,13 @@ import path from 'node:path';
2
2
  import { isHelpRequest } from '../../cli/help.js';
3
3
  import { globalSksRoot, projectRoot, readJson, readStdin } from '../fsx.js';
4
4
  import { RemoteCodexSessionBindingStore, loadRemoteMachineRegistry, loadRemoteSessionIndex, remoteCodexSessionBindingsPath, remoteMachineRegistryPath, remoteSessionIndexPath, validateRemoteMachineRegistry, validateRemoteSessionIndex } from '../remote/index.js';
5
- import { loadTelegramConfig, resolveTelegramBotToken, TelegramActionBroker, TelegramAuditLedger, TelegramBotApiClient, TelegramHubRouter, TelegramHubRuntime, TelegramIdempotencyLedger, TelegramOwnerLock, TelegramMessageProjector, TelegramPollingHub, TelegramTopicRegistry, installAndStartTelegramHubService, restartTelegramHubService, setupTelegramLocalCoding, stopTelegramHubService, telegramHubPaths, telegramHubServiceStatus, telegramTokenFingerprint, validateTelegramConfig, validateTelegramPrivatePairing } from '../telegram/index.js';
5
+ import { loadTelegramConfig, probeTelegramBotReadiness, resolveTelegramBotToken, TelegramActionBroker, TelegramAuditLedger, TelegramBotApiClient, TelegramBotApiError, TelegramHubRouter, TelegramHubRuntime, TelegramIdempotencyLedger, TelegramOwnerLock, TelegramMessageProjector, TelegramPollingHub, TelegramTopicRegistry, installAndStartTelegramHubService, restartTelegramHubService, setupTelegramLocalCoding, stopTelegramHubService, telegramHubPaths, telegramHubServiceStatus, telegramTokenFingerprint, validateTelegramConfig, validateTelegramPrivatePairing } from '../telegram/index.js';
6
+ export const TELEGRAM_STATUS_NATIVE_DEADLINE_MS = 8_000;
7
+ export const TELEGRAM_STATUS_TOKEN_LOOKUP_TIMEOUT_MS = 1_000;
8
+ export const TELEGRAM_STATUS_BOT_REQUEST_TIMEOUT_MS = 1_500;
9
+ export const TELEGRAM_STATUS_BOT_MAX_RETRIES = 0;
10
+ export const TELEGRAM_STATUS_LIVE_PROBE_BUDGET_MS = TELEGRAM_STATUS_TOKEN_LOOKUP_TIMEOUT_MS
11
+ + TELEGRAM_STATUS_BOT_REQUEST_TIMEOUT_MS * 2;
6
12
  export async function telegramCommand(args = []) {
7
13
  if (isHelpRequest(args)) {
8
14
  printTelegramUsage();
@@ -79,6 +85,7 @@ async function telegramStatus(args, root, paths, configPath) {
79
85
  const owner = await readJson(paths.owner, null);
80
86
  const topics = await new TelegramTopicRegistry(paths.topics).list();
81
87
  const controllingRoot = path.resolve(readOption(args, '--project-root') ?? await projectRoot());
88
+ const servicePromise = telegramHubServiceStatus({ projectRoot: controllingRoot, globalRoot: root });
82
89
  const machineRegistryRaw = await readJson(readOption(args, '--machines') ?? remoteMachineRegistryPath(root), null);
83
90
  const machineValidation = validateRemoteMachineRegistry(machineRegistryRaw);
84
91
  const sessionIndexRaw = await readJson(readOption(args, '--session-index') ?? remoteSessionIndexPath(controllingRoot), null);
@@ -90,17 +97,34 @@ async function telegramStatus(args, root, paths, configPath) {
90
97
  const registered = bindings.filter((binding) => targets.some((target) => (binding.machine_id === target.machine_id
91
98
  && binding.project_id === target.project_id
92
99
  && path.resolve(binding.project_root) === path.resolve(target.project_root))));
93
- const service = await telegramHubServiceStatus({ projectRoot: controllingRoot, globalRoot: root });
94
100
  let tokenConfigured = false;
95
- if (validation.config) {
96
- tokenConfigured = await resolveTelegramBotToken(validation.config.bot_token_ref).then(() => true).catch(() => false);
101
+ let liveProbe = telegramLiveProbeStatus(null);
102
+ const probeConfig = validation.config ?? telegramConfigForStatusProbe(rawConfig);
103
+ if (probeConfig) {
104
+ const token = await resolveTelegramBotToken(probeConfig.bot_token_ref, {
105
+ timeoutMs: TELEGRAM_STATUS_TOKEN_LOOKUP_TIMEOUT_MS
106
+ }).catch(() => null);
107
+ tokenConfigured = token !== null;
108
+ if (token) {
109
+ try {
110
+ liveProbe = telegramLiveProbeStatus(await probeTelegramBotReadiness(new TelegramBotApiClient(token, {
111
+ timeoutMs: TELEGRAM_STATUS_BOT_REQUEST_TIMEOUT_MS,
112
+ maxRetries: TELEGRAM_STATUS_BOT_MAX_RETRIES
113
+ })));
114
+ }
115
+ catch (error) {
116
+ liveProbe = telegramLiveProbeStatus(null, error);
117
+ }
118
+ }
97
119
  }
120
+ const service = await servicePromise;
98
121
  const pairing = telegramPairingReadiness(rawConfig);
99
122
  const blockers = [
100
123
  ...validation.issues.map((issue) => `config:${issue}`),
101
124
  ...machineValidation.issues.map((issue) => `machine:${issue}`),
102
125
  ...sessionValidation.issues.map((issue) => `target:${issue}`),
103
126
  ...(tokenConfigured ? [] : ['telegram_token_not_available']),
127
+ ...(liveProbe.blocker ? [liveProbe.blocker] : []),
104
128
  ...(pairing.blocker ? [pairing.blocker] : []),
105
129
  ...(registered.length ? [] : ['no_registered_codex_session']),
106
130
  ...(service.running ? [] : ['telegram_hub_not_running'])
@@ -110,6 +134,12 @@ async function telegramStatus(args, root, paths, configPath) {
110
134
  ok: blockers.length === 0,
111
135
  configured: rawConfig !== null,
112
136
  token_configured: tokenConfigured,
137
+ bot_verified: liveProbe.bot_verified,
138
+ bot: liveProbe.bot,
139
+ webhook_configured: liveProbe.webhook_configured,
140
+ pending_update_count: liveProbe.pending_update_count,
141
+ long_poll_ready: liveProbe.long_poll_ready,
142
+ telegram_probe_error: liveProbe.telegram_probe_error,
113
143
  pairing_valid: pairing.pairing_valid,
114
144
  pairing_issues: pairing.pairing_issues,
115
145
  hub_running: service.running,
@@ -135,7 +165,8 @@ async function telegramStatus(args, root, paths, configPath) {
135
165
  updated_at: binding.updated_at
136
166
  })),
137
167
  remote_config_issues: [...machineValidation.issues, ...sessionValidation.issues],
138
- blockers
168
+ blockers,
169
+ next_actions: telegramNextActions(blockers)
139
170
  };
140
171
  }
141
172
  function printTelegramUsage() {
@@ -150,26 +181,49 @@ Usage:
150
181
  sks telegram hub run [--project-root <path>] [--json]
151
182
 
152
183
  Pairing (do these first):
153
- 1. Create a private bot with @BotFather in Telegram and copy its token.
184
+ 1. Open @BotFather, send /newbot, choose a name and a unique username ending in "bot",
185
+ then copy the HTTP API token BotFather returns.
154
186
  2. Send /start to that bot from the Telegram account you want to pair.
155
187
  3. printf '%s' "<token>" | sks telegram setup --bot-token-stdin --project-root "$PWD" --json
156
188
  The token is read from stdin only and stored in the macOS Keychain.
157
189
  4. sks telegram hub start --project-root "$PWD" --json
158
190
 
191
+ If status reports telegram_webhook_conflict, remove the webhook in the service
192
+ that configured it, then retry. SKS does not delete external webhook state implicitly.
193
+ If setup or the hub reports telegram_409_conflict, stop the other poller using
194
+ this bot token before retrying.
195
+ Stop the SKS Telegram hub before rerunning setup or rotating its BotFather token.
196
+
159
197
  The same flow is available in SKS Center → Remote & Telegram.
160
198
  See docs/telegram-and-center.md for the full guide and troubleshooting.
161
199
  `);
162
200
  }
163
201
  export function telegramPairingReadiness(value) {
164
202
  const pairing = validateTelegramPrivatePairing(value);
203
+ const multipleIds = hasMultipleTelegramPairingIds(value);
165
204
  return {
166
205
  pairing_valid: pairing.ok,
167
206
  pairing_issues: pairing.issues,
168
207
  blocker: pairing.ok
169
208
  ? null
170
- : pairing.missing
171
- ? 'telegram_pairing_missing'
172
- : `telegram_pairing_invalid:${pairing.issues.join(',')}`
209
+ : multipleIds
210
+ ? 'telegram_pairing_multiple_ids_requires_setup'
211
+ : pairing.missing
212
+ ? 'telegram_pairing_missing'
213
+ : `telegram_pairing_invalid:${pairing.issues.join(',')}`
214
+ };
215
+ }
216
+ export function telegramLiveProbeStatus(readiness, error = null) {
217
+ const probeError = error === null ? null : telegramProbeFailure(error);
218
+ const webhookConfigured = readiness?.webhook_configured === true;
219
+ return {
220
+ bot_verified: readiness !== null,
221
+ bot: readiness?.bot ?? null,
222
+ webhook_configured: webhookConfigured,
223
+ pending_update_count: readiness?.pending_update_count ?? 0,
224
+ long_poll_ready: readiness !== null && !webhookConfigured,
225
+ telegram_probe_error: probeError,
226
+ blocker: probeError ?? (webhookConfigured ? 'telegram_webhook_conflict' : null)
173
227
  };
174
228
  }
175
229
  async function runHub(args, root, paths, configPath, json) {
@@ -243,7 +297,19 @@ async function runHub(args, root, paths, configPath, json) {
243
297
  }
244
298
  async function assertHubSetupReady(controllingRoot, globalRoot, configPath) {
245
299
  const config = await loadTelegramConfig(configPath);
246
- await resolveTelegramBotToken(config.bot_token_ref);
300
+ const token = await resolveTelegramBotToken(config.bot_token_ref);
301
+ let readiness;
302
+ try {
303
+ readiness = await probeTelegramBotReadiness(new TelegramBotApiClient(token, {
304
+ timeoutMs: 8_000,
305
+ maxRetries: 1
306
+ }));
307
+ }
308
+ catch (error) {
309
+ throw new Error(telegramProbeFailure(error));
310
+ }
311
+ if (readiness.webhook_configured)
312
+ throw new Error('telegram_webhook_conflict');
247
313
  const registry = await loadRemoteMachineRegistry(remoteMachineRegistryPath(globalRoot));
248
314
  const index = await loadRemoteSessionIndex(remoteSessionIndexPath(controllingRoot), registry);
249
315
  const bindings = await new RemoteCodexSessionBindingStore(remoteCodexSessionBindingsPath(controllingRoot)).list();
@@ -284,3 +350,50 @@ function publicError(err) {
284
350
  .replace(/(?:\/Users|\/home)\/[^\s]+/g, '[path-redacted]')
285
351
  .slice(0, 500);
286
352
  }
353
+ function telegramProbeFailure(error) {
354
+ return error instanceof TelegramBotApiError && (error.errorCode === 401 || error.errorCode === 404)
355
+ ? 'telegram_bot_auth_failed'
356
+ : 'telegram_bot_probe_failed';
357
+ }
358
+ function telegramNextActions(blockers) {
359
+ const actions = [];
360
+ if (blockers.includes('telegram_token_not_available') || blockers.includes('telegram_bot_auth_failed')) {
361
+ actions.push('Create or regenerate the bot token in @BotFather, then rerun setup with --bot-token-stdin.');
362
+ }
363
+ if (blockers.includes('telegram_webhook_conflict')) {
364
+ actions.push('Remove the existing webhook in the service that configured it, then rerun telegram status.');
365
+ }
366
+ if (blockers.includes('telegram_bot_probe_failed')) {
367
+ actions.push('Check network access to api.telegram.org, then rerun telegram status.');
368
+ }
369
+ if (blockers.includes('telegram_pairing_missing') || blockers.some((blocker) => blocker.startsWith('telegram_pairing_invalid:'))) {
370
+ actions.push('Send /start to the bot in a private chat, then rerun telegram setup.');
371
+ }
372
+ if (blockers.includes('telegram_pairing_multiple_ids_requires_setup')) {
373
+ actions.push('Stop the hub, then rerun telegram setup to replace the ambiguous multi-ID config with one verified private chat and user.');
374
+ }
375
+ if (blockers.includes('no_registered_codex_session')) {
376
+ actions.push('Rerun telegram setup for this project root.');
377
+ }
378
+ if (blockers.includes('telegram_hub_not_running')) {
379
+ actions.push('Run sks telegram hub start --project-root \"$PWD\" --json after other blockers are cleared.');
380
+ }
381
+ return actions;
382
+ }
383
+ function hasMultipleTelegramPairingIds(value) {
384
+ if (!value || typeof value !== 'object')
385
+ return false;
386
+ const record = value;
387
+ return (Array.isArray(record.paired_chat_ids) && record.paired_chat_ids.length > 1)
388
+ || (Array.isArray(record.paired_user_ids) && record.paired_user_ids.length > 1);
389
+ }
390
+ function telegramConfigForStatusProbe(value) {
391
+ if (!value || typeof value !== 'object')
392
+ return null;
393
+ const validation = validateTelegramConfig({
394
+ ...value,
395
+ paired_chat_ids: ['1'],
396
+ paired_user_ids: ['1']
397
+ });
398
+ return validation.config;
399
+ }
@@ -5,11 +5,21 @@ import { computeTriWikiAffectedGraph } from '../triwiki/triwiki-affected-graph.j
5
5
  import { buildTriWikiGateImpactMap } from '../triwiki/triwiki-gate-impact-map.js';
6
6
  import { DEFAULT_TRIWIKI_MODULE_CARDS } from '../triwiki/triwiki-module-card.js';
7
7
  import { summarizeTriWikiProofBank } from '../triwiki/triwiki-proof-bank.js';
8
+ import { isTriWikiGraphSubcommand, triwikiGraphCommand } from './triwiki-graph-command.js';
8
9
  export async function triwikiCommand(args = []) {
9
10
  const root = await projectRoot();
10
11
  const sub = args[0] && !args[0].startsWith('-') ? args[0] : 'index';
11
12
  const json = flag(args, '--json');
12
13
  let result;
14
+ if (isTriWikiGraphSubcommand(sub)) {
15
+ const answer = await triwikiGraphCommand(root, sub, args.slice(1));
16
+ process.exitCode = answer.ok ? 0 : 2;
17
+ if (json)
18
+ return printJson(answer.result);
19
+ for (const line of answer.lines)
20
+ console.log(line);
21
+ return answer.result;
22
+ }
13
23
  if (sub === 'index') {
14
24
  result = {
15
25
  schema: 'sks.triwiki-index.v1',
@@ -26,7 +36,7 @@ export async function triwikiCommand(args = []) {
26
36
  result = summarizeTriWikiProofBank(root);
27
37
  }
28
38
  else {
29
- console.error('Usage: sks triwiki index|affected|proof-bank [--json]');
39
+ console.error('Usage: sks triwiki index|affected|proof-bank|graph-status|graph-lint|graph-query [--json]');
30
40
  process.exitCode = 1;
31
41
  return null;
32
42
  }