@dotdrelle/wiki-manager 0.15.71 → 0.15.73
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 +8 -0
- package/mcp.endpoints.example.json +1 -1
- package/package.json +2 -2
- package/src/core/agentEvents.js +13 -2
- package/src/core/agentEvents.test.js +17 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/mcp.js +1 -1
- package/src/core/runtimeLog.js +50 -2
- package/src/core/runtimeLog.test.js +32 -19
- package/src/core/skillChainView.test.js +1 -1
- package/src/core/skillCompiler.js +5 -4
- package/src/core/wikiWorkspaceStart.test.js +36 -0
- package/src/orchestrator/objectiveResolver.test.js +1 -1
- package/src/orchestrator/providers/runtimeProviders.js +14 -0
- package/src/orchestrator/providers/runtimeProviders.test.js +64 -0
- package/src/runtime/runner.test.js +6 -6
- package/src/runtime/skillChain.e2e.test.js +24 -6
- package/src/shell/LeftPane.tsx +14 -7
- package/src/shell/RightPane.tsx +13 -1
- package/src/shell/repl.test.js +27 -0
- package/src/shell/tui.tsx +16 -11
- package/src/shell/useSession.ts +4 -1
- package/wiki-workspace +55 -0
package/README.md
CHANGED
|
@@ -310,6 +310,14 @@ wiki-workspace agents up # start cme, documents…
|
|
|
310
310
|
wiki-workspace agents status # check they respond
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
+
One command starts the whole deployment — agent-runtime, agents and every
|
|
314
|
+
configured workspace — and is safe to repeat while things are already up:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
wiki-workspace start # runtime + agents + all workspaces
|
|
318
|
+
wiki-workspace start --open # …and open the first workspace chat
|
|
319
|
+
```
|
|
320
|
+
|
|
313
321
|
**Step 4 — Move to `wiki-manager` or `serve`.**
|
|
314
322
|
Two entry doors, your choice:
|
|
315
323
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"servers": {
|
|
32
32
|
"llm-wiki": { "allow": ["help_list", "help_read", "help_search", "wiki_workspace_status", "wiki_list_pages", "wiki_read_page", "wiki_read_pages", "wiki_search_context", "wiki_collect_context", "wiki_read_ingested_source", "wiki_outline", "template_read", "template_write", "build_context_write", "wiki_read_deliverable"] },
|
|
33
33
|
"wiki-production": { "allow": ["production_job_status", "production_jobs_list"] },
|
|
34
|
-
"cme": { "allow": ["cme_status", "cme_sources_list", "cme_export_status"] }
|
|
34
|
+
"cme": { "allow": ["cme_status", "cme_sources_list", "cme_export_status", "cme_confluence_search", "cme_wiki_search"] }
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.73",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "bun ./bin/wiki-manager.js",
|
|
22
|
-
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js",
|
|
22
|
+
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js",
|
|
23
23
|
"check-versions": "node scripts/check-versions.js",
|
|
24
24
|
"prepack": "node scripts/check-versions.js",
|
|
25
25
|
"prepublishOnly": "node scripts/check-versions.js",
|
package/src/core/agentEvents.js
CHANGED
|
@@ -342,9 +342,20 @@ function applyEvent(state, event) {
|
|
|
342
342
|
case 'task.created':
|
|
343
343
|
appendCreatedTask(state, event.payload?.task);
|
|
344
344
|
return;
|
|
345
|
-
case 'task.assigned':
|
|
345
|
+
case 'task.assigned': {
|
|
346
|
+
// The plan step stays the only place the UIs read WHO runs a task. The
|
|
347
|
+
// workflow projection (workflow.js taskNode) exposes step.executor, and
|
|
348
|
+
// serve's run-graph inspector renders it as the "Agent" row — without
|
|
349
|
+
// this mutation that row stayed empty ("—") while the agent was only
|
|
350
|
+
// visible in the run journal, under Runtime.
|
|
351
|
+
const assignedStep = planTaskById(state, event.taskId ?? event.payload?.taskId ?? '');
|
|
352
|
+
const assignedAgent = event.payload?.assignment?.agentInstanceId ?? event.payload?.agentInstanceId ?? null;
|
|
353
|
+
if (assignedStep && assignedAgent && !assignedStep.executor) {
|
|
354
|
+
assignedStep.executor = String(assignedAgent);
|
|
355
|
+
}
|
|
346
356
|
appendLog(state, taskLogLine(state, event, 'assigned'));
|
|
347
357
|
return;
|
|
358
|
+
}
|
|
348
359
|
case 'task.started':
|
|
349
360
|
// Silent: always follows `task.assigned` (same task, milliseconds apart),
|
|
350
361
|
// which already printed the "started" line.
|
|
@@ -1044,7 +1055,7 @@ function formatPlanErrors(errors) {
|
|
|
1044
1055
|
: 'unknown';
|
|
1045
1056
|
}
|
|
1046
1057
|
|
|
1047
|
-
const LOG_TIME_PREFIX = /^\d{2}:\d{2}:\d{2}
|
|
1058
|
+
const LOG_TIME_PREFIX = /^\d{2}:\d{2}:\d{2}(?:\s+|\s*·\s*)/;
|
|
1048
1059
|
const LOG_REPEAT_SUFFIX = / \(×\d+\)$/;
|
|
1049
1060
|
|
|
1050
1061
|
/*
|
|
@@ -750,3 +750,20 @@ test('appendLog collapses repeated dispatch plumbing but never a repeated busine
|
|
|
750
750
|
assert.equal(failLines.length, 2);
|
|
751
751
|
assert.equal(failLines.some((l) => / \(×\d+\)$/.test(l)), false);
|
|
752
752
|
});
|
|
753
|
+
|
|
754
|
+
test('task.assigned records the executor on the plan step for the UIs', () => {
|
|
755
|
+
const session = { workspace: 'w', agentEvents: [] };
|
|
756
|
+
dispatchAgentEvent(session, createAgentEvent('plan_set', {
|
|
757
|
+
origin: 'tool',
|
|
758
|
+
payload: { steps: [{ id: 't1', description: 'Ingest', status: 'pending' }] },
|
|
759
|
+
}));
|
|
760
|
+
dispatchAgentEvent(session, createAgentEvent('task.assigned', {
|
|
761
|
+
origin: 'assignment_manager',
|
|
762
|
+
runId: 'r1',
|
|
763
|
+
taskId: 't1',
|
|
764
|
+
payload: { taskId: 't1', assignment: { agentInstanceId: 'production-main', capability: 'knowledge.update' } },
|
|
765
|
+
}));
|
|
766
|
+
const step = (session.agentProjection.plan ?? []).find((candidate) => candidate.id === 't1' || candidate.step === 't1');
|
|
767
|
+
assert.ok(step, 'plan step exists');
|
|
768
|
+
assert.equal(step.executor, 'production-main');
|
|
769
|
+
});
|
package/src/core/buildInfo.json
CHANGED
package/src/core/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
-
const WIKI_MANAGER_VERSION = '0.15.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.15.73';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
package/src/core/runtimeLog.js
CHANGED
|
@@ -32,6 +32,29 @@ const ORDERED_FIELDS = [
|
|
|
32
32
|
'error',
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
+
// The dispatch events whose payload is routing plumbing, not business content:
|
|
36
|
+
// rendered compactly (see formatRuntimeLogPayload). Business events keep the
|
|
37
|
+
// full field=value form.
|
|
38
|
+
const COMPACT_EVENTS = new Set([
|
|
39
|
+
'agent_status',
|
|
40
|
+
'agent_execute',
|
|
41
|
+
'job.accepted',
|
|
42
|
+
'task.ready',
|
|
43
|
+
'task.starting',
|
|
44
|
+
'task.started',
|
|
45
|
+
'task.completed',
|
|
46
|
+
'task.failed',
|
|
47
|
+
'attempt.created',
|
|
48
|
+
'lock.acquired',
|
|
49
|
+
'lock.released',
|
|
50
|
+
'runtime.execute',
|
|
51
|
+
'runtime.accepted',
|
|
52
|
+
'runtime.result_returned',
|
|
53
|
+
'task.result_returned',
|
|
54
|
+
'runtime.params_refused',
|
|
55
|
+
'runtime.blind',
|
|
56
|
+
]);
|
|
57
|
+
|
|
35
58
|
export function normalizeRuntimeLog(input, { session = null } = {}) {
|
|
36
59
|
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
37
60
|
return { message: String(input ?? '') };
|
|
@@ -69,6 +92,28 @@ export function formatRuntimeLogPayload(payload = {}, ts = null) {
|
|
|
69
92
|
}
|
|
70
93
|
const time = timeLabel(ts);
|
|
71
94
|
const event = eventLabel(payload.event);
|
|
95
|
+
// Dispatch plumbing is rendered as a readable sentence, not a field dump:
|
|
96
|
+
// `AGENT_STATUS run=… plan=… group=… attempt=… agentType=… workspace=…`
|
|
97
|
+
// buried the one thing the reader wants — WHO does WHAT on WHICH task, on
|
|
98
|
+
// WHICH job. The verbosity is kept for business payloads, where fields are
|
|
99
|
+
// the content.
|
|
100
|
+
if (COMPACT_EVENTS.has(String(payload.event ?? ''))) {
|
|
101
|
+
const parts = [time, event];
|
|
102
|
+
const who = payload.agentInstanceId ?? payload.agentId ?? payload.agentType;
|
|
103
|
+
if (who) parts.push(shortenUuids(String(who)));
|
|
104
|
+
const what = [payload.capability, payload.operation].filter(Boolean).join('/');
|
|
105
|
+
if (what) parts.push(what);
|
|
106
|
+
const task = shortTaskLabel(payload.taskId);
|
|
107
|
+
if (task) parts.push(task);
|
|
108
|
+
if (payload.jobId) parts.push(shortenUuids(String(payload.jobId)));
|
|
109
|
+
if (payload.status != null) parts.push(String(payload.status));
|
|
110
|
+
if (payload.error) parts.push(shortenUuids(String(payload.error)).slice(0, 120));
|
|
111
|
+
if (payload.detail != null && payload.detail !== ''
|
|
112
|
+
&& String(payload.detail).toUpperCase() !== event) {
|
|
113
|
+
parts.push(shortenUuids(String(payload.detail)));
|
|
114
|
+
}
|
|
115
|
+
return parts.join(' · ');
|
|
116
|
+
}
|
|
72
117
|
const fields = ORDERED_FIELDS
|
|
73
118
|
.map((key) => (key === 'taskId'
|
|
74
119
|
? formatField(FIELD_ALIASES[key], shortTaskLabel(payload[key]))
|
|
@@ -123,10 +168,13 @@ export function shortLogId(value, { maxLength = 40 } = {}) {
|
|
|
123
168
|
// token — so this one shape separates the two without an event-name list
|
|
124
169
|
// (which is what an earlier enumeration got wrong: it only ever matched the
|
|
125
170
|
// two underscore-form events and missed every dotted one).
|
|
126
|
-
const DISPATCH_PLUMBING_LINE = /^(?:\d{1,2}:\d{2}(?::\d{2})?\s
|
|
171
|
+
const DISPATCH_PLUMBING_LINE = /^(?:\d{1,2}:\d{2}(?::\d{2})?\s*(?:·\s*)?)?[A-Z][A-Z0-9_]{2,}(?:\s|$)/;
|
|
127
172
|
|
|
128
173
|
export function isDispatchPlumbingLine(line) {
|
|
129
|
-
|
|
174
|
+
// The Shell tags every runtime line with a "runtime " prefix before it
|
|
175
|
+
// reaches the Log panel; classification must look past it, or AGENT_STATUS
|
|
176
|
+
// rows land in the Runtime tab instead of Agent status.
|
|
177
|
+
return DISPATCH_PLUMBING_LINE.test(String(line ?? '').replace(/^runtime\s+/, ''));
|
|
130
178
|
}
|
|
131
179
|
|
|
132
180
|
export function runtimeLogMatchesFilter(line, filter = '') {
|
|
@@ -39,22 +39,27 @@ function payload(event) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
test('formatRuntimeLogPayload
|
|
42
|
+
test('formatRuntimeLogPayload renders dispatch plumbing as a compact sentence, business events keep their fields', () => {
|
|
43
|
+
// Dispatch plumbing: who · what · task · job — no run/plan/group/attempt
|
|
44
|
+
// soup. Business events (capability.resolving, agent.selected,
|
|
45
|
+
// task.assigned) keep the full field=value form: those fields ARE the
|
|
46
|
+
// content.
|
|
43
47
|
for (const event of CYCLE_EVENTS) {
|
|
44
48
|
const line = formatRuntimeLogPayload(payload(event), '2026-07-08T14:42:18.000Z');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
assert.match(line,
|
|
53
|
-
assert.match(line, /
|
|
54
|
-
assert.match(line, /
|
|
55
|
-
assert.match(line, /
|
|
56
|
-
assert.match(line, /
|
|
57
|
-
assert.match(line, /
|
|
49
|
+
if (['capability.resolving', 'agent.selected', 'task.assigned'].includes(event)) {
|
|
50
|
+
assert.match(line, /run=run-123/);
|
|
51
|
+
assert.match(line, /workspace=docs/);
|
|
52
|
+
assert.match(line, /capability=document\.build/);
|
|
53
|
+
assert.match(line, /operation=build/);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
assert.match(line, /^14:42:18 · [A-Z_]+ · /);
|
|
57
|
+
assert.match(line, /production-main/);
|
|
58
|
+
assert.match(line, /document\.build\/build/);
|
|
59
|
+
assert.match(line, /task-build/);
|
|
60
|
+
assert.match(line, /job-789/);
|
|
61
|
+
assert.match(line, /cycle detail/);
|
|
62
|
+
assert.doesNotMatch(line, /run=|plan=|group=|attempt=|agentType=|agent=|workspace=|capability=|operation=/);
|
|
58
63
|
}
|
|
59
64
|
});
|
|
60
65
|
|
|
@@ -111,12 +116,10 @@ test('long UUIDs collapse to a short prefix so log lines stay on one line', () =
|
|
|
111
116
|
operation: 'build',
|
|
112
117
|
}, '2026-07-08T14:42:18.000Z');
|
|
113
118
|
|
|
114
|
-
assert.match(line, /
|
|
115
|
-
// The taskId's UUID prefix and hash suffix are dropped entirely: the
|
|
119
|
+
assert.match(line, /production-7fadad27…/);
|
|
120
|
+
// The taskId's UUID prefix and hash suffix are dropped entirely: the line
|
|
116
121
|
// names the work ("taxonomy synthesis"), not an opaque id.
|
|
117
|
-
assert.match(line, /
|
|
118
|
-
assert.match(line, /attempt=attempt-7fadad27…/);
|
|
119
|
-
assert.match(line, /agentInstance=production-7fadad27…/);
|
|
122
|
+
assert.match(line, /taxonomy synthesis/);
|
|
120
123
|
assert.doesNotMatch(line, /7fadad27-0be6-4d08-96e5-664fe7ee841e/);
|
|
121
124
|
});
|
|
122
125
|
|
|
@@ -143,6 +146,16 @@ test('isDispatchPlumbingLine leaves the business flow lines for the Runtime tab'
|
|
|
143
146
|
}
|
|
144
147
|
});
|
|
145
148
|
|
|
149
|
+
test('isDispatchPlumbingLine recognises the shell-tagged "runtime " lines for the Agent status tab', () => {
|
|
150
|
+
// The Shell prepends "runtime " to every runtime line (useSession
|
|
151
|
+
// visibleLogs): AGENT_STATUS rows must still classify as plumbing, or they
|
|
152
|
+
// end up in the Runtime tab instead of Agent status.
|
|
153
|
+
const tagged = `runtime ${formatRuntimeLogPayload({ event: 'agent_status', runId: 'r1', taskId: 't1' }, '2026-07-08T14:42:18.000Z')}`;
|
|
154
|
+
assert.equal(isDispatchPlumbingLine(tagged), true, 'a tagged AGENT_STATUS line is dispatch plumbing');
|
|
155
|
+
assert.equal(isDispatchPlumbingLine('runtime 14:42:21 Run failed: No agent provides capability workspace.restore.'), false);
|
|
156
|
+
assert.equal(isDispatchPlumbingLine('runtime 14:42:22 Plan validated for run r1'), false);
|
|
157
|
+
});
|
|
158
|
+
|
|
146
159
|
test('shortLogId caps an over-long task slug while shortening embedded UUIDs', () => {
|
|
147
160
|
const long = `${'x'.repeat(48)}-deadbeef`;
|
|
148
161
|
assert.match(shortLogId(long), /…$/);
|
|
@@ -5,7 +5,7 @@ import { chainStepLabel, projectSkillChains, renderSkillChain, selectionKindLabe
|
|
|
5
5
|
const WIKI_SYNC = [
|
|
6
6
|
{
|
|
7
7
|
id: 'c0', chainId: 'chain-1', chainSequence: 0, skillName: 'wiki-sync', selectionKind: 'description_match', status: 'done',
|
|
8
|
-
input: 'Export
|
|
8
|
+
input: 'Export every configured Confluence source exactly as the connector is currently configured. Check configuration first.',
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
id: 'c1', chainId: 'chain-1', chainSequence: 1, skillName: 'wiki-sync', status: 'running', runId: 'run-b',
|
|
@@ -7,10 +7,11 @@ export async function compileSkillObjectives(skill, args = {}, { llmFallback = n
|
|
|
7
7
|
const body = String(skill?.body ?? '').trim();
|
|
8
8
|
if (!body) throw compileError('Skill body is empty.');
|
|
9
9
|
// Splitting must happen on the body alone. Appending the parameters first
|
|
10
|
-
// makes them part of the last objective only —
|
|
11
|
-
//
|
|
12
|
-
// actually
|
|
13
|
-
// step consumes which parameter, so every
|
|
10
|
+
// makes them part of the last objective only — a skill that compiles into
|
|
11
|
+
// several steps would hand its arguments to the final step and leave the
|
|
12
|
+
// earlier ones, the ones that actually consume them, without them. The
|
|
13
|
+
// compiler cannot know which step consumes which parameter, so every
|
|
14
|
+
// objective carries them.
|
|
14
15
|
const deterministic = deterministicObjectives(body);
|
|
15
16
|
if (!deterministic.ambiguous) {
|
|
16
17
|
return withNaturalArguments(validateCompiledObjectives(deterministic.objectives), args);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
|
|
6
|
+
const script = readFileSync(fileURLToPath(new URL('../../wiki-workspace', import.meta.url)), 'utf8');
|
|
7
|
+
|
|
8
|
+
test('wiki-workspace start advertises itself in the usage text', () => {
|
|
9
|
+
const usage = script.slice(script.indexOf('Commands:'), script.indexOf('Configuration:'));
|
|
10
|
+
assert.match(usage, /start \[--open\]\s+Start everything: agent-runtime, agents and every/);
|
|
11
|
+
assert.match(usage, /configured workspace/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('wiki-workspace start routes to a start_all that composes the three idempotent steps', () => {
|
|
15
|
+
const fn = script.slice(script.indexOf("# `start` — the whole deployment"), script.indexOf('ensure_workspace_dirs()'));
|
|
16
|
+
// The whole deployment is the same three commands an operator runs by hand,
|
|
17
|
+
// in the boot order the runtime expects: runtime, agents, then workspaces.
|
|
18
|
+
assert.match(fn, /runtime_command up/);
|
|
19
|
+
assert.match(fn, /agents_compose up/);
|
|
20
|
+
assert.match(fn, /up_workspace "\$name"/);
|
|
21
|
+
// Every configured workspace joins, discovered exactly like list_workspaces:
|
|
22
|
+
// a directory under WORKSPACES_DIR carrying a .env.
|
|
23
|
+
assert.match(fn, /for entry in "\$WORKSPACES_DIR"\/\*/);
|
|
24
|
+
assert.match(fn, /env_value "\$file" WORKSPACE_NAME/);
|
|
25
|
+
// Without workspaces the command still completes, telling the operator how
|
|
26
|
+
// to create one.
|
|
27
|
+
assert.match(fn, /wiki-workspace config <name>/);
|
|
28
|
+
// Repeatable: each step is idempotent, so `start` is safe on a live stack.
|
|
29
|
+
assert.match(fn, /idempotent/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('the start dispatch accepts only an optional --open', () => {
|
|
33
|
+
const dispatch = script.slice(script.indexOf('# start [--open]'), script.indexOf('# up <workspace> [--open]'));
|
|
34
|
+
assert.match(dispatch, /\[\[ "\$\{2:-\}" == "" \|\| "\$\{2:-\}" == "--open" \]\]/);
|
|
35
|
+
assert.match(dispatch, /start_all "\$start_open"/);
|
|
36
|
+
});
|
|
@@ -86,7 +86,7 @@ test('resolveObjective disambiguates "export" of a Confluence source via alias,
|
|
|
86
86
|
session.llm.completeWithTools = async () => {
|
|
87
87
|
throw new Error('the explicit alias must not depend on LLM selection');
|
|
88
88
|
};
|
|
89
|
-
const result = await resolveObjective('Export
|
|
89
|
+
const result = await resolveObjective('Export every configured Confluence source exactly as the connector is currently configured', session);
|
|
90
90
|
assert.equal(result.capability, 'external-source.export');
|
|
91
91
|
assert.equal(result.operation, 'export');
|
|
92
92
|
assert.equal(result.provider.agentInstanceId, 'cme-1');
|
|
@@ -311,6 +311,20 @@ export async function discoverRuntimeProvidersOnce(session, {
|
|
|
311
311
|
currentDown.add(key);
|
|
312
312
|
if (!session._runtimeProviderDown.has(key)) {
|
|
313
313
|
const kept = preservedByRuntime.get(item.runtimeId)?.length ?? 0;
|
|
314
|
+
// Transient states get the ⚠ glyph, which both UIs render in blue: the
|
|
315
|
+
// gateway answering HTTP 401 before its agents are loaded (workspace
|
|
316
|
+
// switch, boot order), or not listening yet (fetch failed, refused,
|
|
317
|
+
// timeout). An HTTP 4xx/5xx other than 401 — the server answers but is
|
|
318
|
+
// broken — stays a red "unavailable", like the non-HTTP agent contract
|
|
319
|
+
// errors.
|
|
320
|
+
const errorText = String(item.error ?? '');
|
|
321
|
+
const httpMatch = errorText.match(/\bHTTP (\d{3})\b/);
|
|
322
|
+
const connectionFailure = /\b(fetch failed|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|socket hang up|connection refused|network error|timeout|timed out)\b/i.test(errorText);
|
|
323
|
+
const notReady = (httpMatch != null && httpMatch[1] === '401') || (httpMatch == null && connectionFailure);
|
|
324
|
+
if (notReady) {
|
|
325
|
+
dispatchRuntimeLog(session, `⚠ agent-runtimes: ${item.runtimeId} not ready yet (${item.error})${kept > 0 ? ` — keeping ${kept} last-known capabilit${kept === 1 ? 'y' : 'ies'} until it recovers` : ''} — retrying on the next scan`);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
314
328
|
dispatchRuntimeLog(session, kept > 0
|
|
315
329
|
? `agent-runtimes: ${item.runtimeId} unavailable (${item.error}) — keeping ${kept} last-known capabilit${kept === 1 ? 'y' : 'ies'} until it recovers`
|
|
316
330
|
: `agent-runtimes: ${item.runtimeId} unavailable (${item.error})`);
|
|
@@ -265,6 +265,70 @@ test('discoverRuntimeProvidersOnce announces a down runtime only once across re-
|
|
|
265
265
|
assert.equal(logs.length, 1, 'the degradation is announced once, not on every re-scan');
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
+
test('an HTTP 401 gateway probe is announced as not-ready with the ⚠ glyph, not as a failure', async () => {
|
|
269
|
+
const session = sessionWithEvents();
|
|
270
|
+
const originalFetch = globalThis.fetch;
|
|
271
|
+
globalThis.fetch = async (url) => {
|
|
272
|
+
if (String(url).includes('/health')) return new Response('{}', { status: 401 });
|
|
273
|
+
throw new Error('unexpected probe ' + String(url));
|
|
274
|
+
};
|
|
275
|
+
try {
|
|
276
|
+
await discoverRuntimeProvidersOnce(session, {
|
|
277
|
+
config: [{ id: 'deepagents', type: 'deepagents', endpoint: 'http://127.0.0.1:7789', enabled: true }],
|
|
278
|
+
});
|
|
279
|
+
} finally {
|
|
280
|
+
globalThis.fetch = originalFetch;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const logs = (session.agentEvents ?? []).filter((event) => event.type === 'runtime_log'
|
|
284
|
+
&& String(event.payload?.message ?? '').includes('deepagents'));
|
|
285
|
+
assert.equal(logs.length, 1, 'the transient auth state is announced once');
|
|
286
|
+
const message = String(logs[0].payload?.message ?? '');
|
|
287
|
+
assert.match(message, /^⚠ agent-runtimes: deepagents not ready yet \(HTTP 401/);
|
|
288
|
+
assert.match(message, /retrying on the next scan/);
|
|
289
|
+
assert.doesNotMatch(message, /unavailable/, 'a gateway answering before its agents load is not a failure');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('a gateway that is not listening yet (fetch failed) is also announced as not-ready', async () => {
|
|
293
|
+
const session = sessionWithEvents();
|
|
294
|
+
const originalFetch = globalThis.fetch;
|
|
295
|
+
globalThis.fetch = async () => {
|
|
296
|
+
throw new TypeError('fetch failed');
|
|
297
|
+
};
|
|
298
|
+
try {
|
|
299
|
+
await discoverRuntimeProvidersOnce(session, {
|
|
300
|
+
config: [{ id: 'deepagents', type: 'deepagents', endpoint: 'http://127.0.0.1:7789', enabled: true }],
|
|
301
|
+
});
|
|
302
|
+
} finally {
|
|
303
|
+
globalThis.fetch = originalFetch;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const logs = (session.agentEvents ?? []).filter((event) => event.type === 'runtime_log'
|
|
307
|
+
&& String(event.payload?.message ?? '').includes('deepagents'));
|
|
308
|
+
assert.equal(logs.length, 1);
|
|
309
|
+
const message = String(logs[0].payload?.message ?? '');
|
|
310
|
+
assert.match(message, /^⚠ agent-runtimes: deepagents not ready yet \(fetch failed\)/);
|
|
311
|
+
assert.doesNotMatch(message, /unavailable/);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('an HTTP 500 gateway probe stays a red unavailable announcement', async () => {
|
|
315
|
+
const session = sessionWithEvents();
|
|
316
|
+
const originalFetch = globalThis.fetch;
|
|
317
|
+
globalThis.fetch = async () => new Response('{}', { status: 500 });
|
|
318
|
+
try {
|
|
319
|
+
await discoverRuntimeProvidersOnce(session, {
|
|
320
|
+
config: [{ id: 'deepagents', type: 'deepagents', endpoint: 'http://127.0.0.1:7789', enabled: true }],
|
|
321
|
+
});
|
|
322
|
+
} finally {
|
|
323
|
+
globalThis.fetch = originalFetch;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const logs = (session.agentEvents ?? []).filter((event) => event.type === 'runtime_log'
|
|
327
|
+
&& String(event.payload?.message ?? '').includes('deepagents'));
|
|
328
|
+
assert.equal(logs.length, 1);
|
|
329
|
+
assert.match(String(logs[0].payload?.message ?? ''), /^agent-runtimes: deepagents unavailable \(HTTP 500/);
|
|
330
|
+
});
|
|
331
|
+
|
|
268
332
|
test('a transient probe failure keeps the last-known capability set (a failed probe is not a lost agent)', async () => {
|
|
269
333
|
const session = sessionWithEvents();
|
|
270
334
|
const up = [{ id: 'gw', type: 'fake', capabilities: [{ name: 'agent.review', operations: ['run'] }] }];
|
|
@@ -786,14 +786,14 @@ test('runRuntimeParallelPlan dispatches orchestrated tasks without child LLM loo
|
|
|
786
786
|
for (const label of ['READY', 'RESOLVING', 'SELECTED', 'ASSIGNED', 'CREATED', 'ACQUIRED', 'AGENT_EXECUTE', 'ACCEPTED', 'AGENT_STATUS', 'RESULT_RETURNED', 'RELEASED', 'COMPLETED']) {
|
|
787
787
|
assert.match(logs, new RegExp(`\\b${label}\\b`));
|
|
788
788
|
}
|
|
789
|
-
|
|
789
|
+
// Compact dispatch plumbing: the routing fields ride the line as a sentence
|
|
790
|
+
// (who · what · task · job), no run=/plan=/attempt= soup.
|
|
791
|
+
assert.match(logs, /AGENT_EXECUTE · production-main · workspace\.diagnose\/doctor · a\b/);
|
|
792
|
+
assert.match(logs, /ACCEPTED · production-main · workspace\.diagnose\/doctor · a · job-a · queued/);
|
|
790
793
|
assert.match(logs, /task=a/);
|
|
791
|
-
|
|
792
|
-
assert.match(logs, /
|
|
793
|
-
assert.match(logs, /job=job-a/);
|
|
794
|
+
// Business events keep the full field=value form.
|
|
795
|
+
assert.match(logs, /run=run-dispatch/);
|
|
794
796
|
assert.match(logs, /workspace=demo-workspace/);
|
|
795
|
-
assert.match(logs, /capability=workspace\.diagnose/);
|
|
796
|
-
assert.match(logs, /operation=doctor/);
|
|
797
797
|
});
|
|
798
798
|
|
|
799
799
|
test('runRuntimeParallelPlan fails cleanly when scheduler budget is exceeded', async () => {
|
|
@@ -136,16 +136,16 @@ test('E2E-002 wiki-sync: two objectives, two ordered runs, one chainId', async (
|
|
|
136
136
|
const env = await harness(t, { skills: { 'wiki-sync': null } });
|
|
137
137
|
if (!env) return;
|
|
138
138
|
|
|
139
|
-
const { body } = await env.post('/run?workspace=acme', { input: '/wiki-sync
|
|
139
|
+
const { body } = await env.post('/run?workspace=acme', { input: '/wiki-sync' });
|
|
140
140
|
await env.settle();
|
|
141
141
|
|
|
142
142
|
assert.equal(body.objectives, 2);
|
|
143
143
|
assert.equal(env.runs.length, 2, 'the second objective must run after the first');
|
|
144
|
-
assert.match(env.runs[0].input, /^Export
|
|
144
|
+
assert.match(env.runs[0].input, /^Export every configured Confluence source/);
|
|
145
145
|
assert.match(env.runs[1].input, /^Run the production pipeline step ingest over the newly exported Markdown/);
|
|
146
|
-
// CME first, Production second —
|
|
147
|
-
//
|
|
148
|
-
for (const run of env.runs) assert.
|
|
146
|
+
// CME first, Production second — the skill carries no source parameter, so
|
|
147
|
+
// no run may receive a source selector (the export step must stay "all").
|
|
148
|
+
for (const run of env.runs) assert.doesNotMatch(run.input, /User parameters:/);
|
|
149
149
|
const items = env.chain();
|
|
150
150
|
assert.equal(items.length, 2);
|
|
151
151
|
assert.equal(items[0].chainId, items[1].chainId);
|
|
@@ -154,6 +154,24 @@ test('E2E-002 wiki-sync: two objectives, two ordered runs, one chainId', async (
|
|
|
154
154
|
assert.deepEqual(items.map((item) => item.skillName), ['wiki-sync', 'wiki-sync']);
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
test('E2E-002b parameters reach every objective of a multi-step skill', async (t) => {
|
|
158
|
+
const env = await harness(t, {
|
|
159
|
+
skills: {
|
|
160
|
+
'two-step': '---\nname: two-step\nparams:\n - source\n---\nCollect the sources.\n\nThen ingest them.',
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
if (!env) return;
|
|
164
|
+
|
|
165
|
+
const { body } = await env.post('/run?workspace=acme', { input: '/two-step docs' });
|
|
166
|
+
await env.settle();
|
|
167
|
+
|
|
168
|
+
assert.equal(body.objectives, 2);
|
|
169
|
+
assert.equal(env.runs.length, 2);
|
|
170
|
+
// The parameter reaches every step that consumes it, not only the last
|
|
171
|
+
// objective of the chain.
|
|
172
|
+
for (const run of env.runs) assert.match(run.input, /User parameters:\nsource: docs/);
|
|
173
|
+
});
|
|
174
|
+
|
|
157
175
|
test('E2E-003 cancel: the running step and its chain stop, unrelated queue survives', async (t) => {
|
|
158
176
|
const env = await harness(t, {
|
|
159
177
|
autoFinish: false,
|
|
@@ -308,7 +326,7 @@ test('E2E-010 skill stack: every run receives the ancestors of its chain', async
|
|
|
308
326
|
const env = await harness(t, { skills: { 'wiki-sync': null } });
|
|
309
327
|
if (!env) return;
|
|
310
328
|
|
|
311
|
-
await env.post('/run?workspace=acme', { input: '/wiki-sync
|
|
329
|
+
await env.post('/run?workspace=acme', { input: '/wiki-sync' });
|
|
312
330
|
await env.settle();
|
|
313
331
|
|
|
314
332
|
assert.ok(env.runs.length >= 1);
|
package/src/shell/LeftPane.tsx
CHANGED
|
@@ -830,14 +830,21 @@ export function LeftPane(props: {
|
|
|
830
830
|
to the foot of the pane: they are an ambient state one glances at, not
|
|
831
831
|
something read before every message, and at the top they pushed the
|
|
832
832
|
conversation down by a line for standing information.
|
|
833
|
+
|
|
834
|
+
The hint row exists only while there is a hint. It is null almost
|
|
835
|
+
always, so reserving a blank line for it left a dead gap between the
|
|
836
|
+
mode bar and the thread once the standing info moved to the foot;
|
|
837
|
+
`conversationRows` in tui.tsx adds the row back only when it shows.
|
|
833
838
|
*/}
|
|
834
|
-
<box height={2} flexDirection="column">
|
|
839
|
+
<box height={props.hintLine ? 2 : 1} flexDirection="column">
|
|
835
840
|
<box height={1} flexDirection="row" backgroundColor={modeColor()} paddingX={1}>
|
|
836
841
|
<text fg="#0B1020">{modeLabel()}</text>
|
|
837
842
|
</box>
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
843
|
+
{props.hintLine ? (
|
|
844
|
+
<box height={1} flexDirection="row">
|
|
845
|
+
<text fg="#FBBF24">[ {props.hintLine} ]</text>
|
|
846
|
+
</box>
|
|
847
|
+
) : null}
|
|
841
848
|
</box>
|
|
842
849
|
{showWelcome() ? (
|
|
843
850
|
<WelcomeHelpPanels width={props.conversationColumns} />
|
|
@@ -875,9 +882,9 @@ export function LeftPane(props: {
|
|
|
875
882
|
onHeightChange={props.onInputHeightChange}
|
|
876
883
|
/>
|
|
877
884
|
{/*
|
|
878
|
-
Status foot. One line
|
|
879
|
-
`conversationRows` (
|
|
880
|
-
it
|
|
885
|
+
Status foot. One line: it replaced the standing header line rather than
|
|
886
|
+
adding one. `conversationRows` (tui.tsx) counts padding (2) + mode bar
|
|
887
|
+
(1) + this foot (1), plus the hint row only when it is present.
|
|
881
888
|
*/}
|
|
882
889
|
<box height={1} flexDirection="row">
|
|
883
890
|
<text fg="#D6DEE8">{props.title}</text>
|
package/src/shell/RightPane.tsx
CHANGED
|
@@ -349,6 +349,13 @@ function logMessageColor(message: string): string {
|
|
|
349
349
|
// would give them anyway.
|
|
350
350
|
if (/^\s*✓/.test(message)) return '#A6E3A1';
|
|
351
351
|
if (/^\s*✗/.test(message)) return '#F38BA8';
|
|
352
|
+
// ⚠ announces a state the reader should know about, not an error: the
|
|
353
|
+
// gateway answering 401 before its agents are loaded, a re-scan retry.
|
|
354
|
+
// Blue, with the glyph carrying the warning — the red rules below must not
|
|
355
|
+
// claim it (the text may still contain "HTTP 401"). Keyed on the glyph
|
|
356
|
+
// alone: every legitimate "not ready" line already carries it, whereas a
|
|
357
|
+
// genuine failure ("Run failed: agent X not ready") must stay red.
|
|
358
|
+
if (/^\s*⚠/.test(message)) return '#89B4FA';
|
|
352
359
|
// A doctor summary with ZERO errors is a warning by construction
|
|
353
360
|
// ("⚠ 0 error(s), 2 warning(s)") — amber, never red, even though it
|
|
354
361
|
// mentions the word "error".
|
|
@@ -419,7 +426,11 @@ export function LogPanel(props: { logs: string[]; width: number; filter?: string
|
|
|
419
426
|
// above the Runtime/Agent status tabs left a dead gap under a short Plan
|
|
420
427
|
// and made the two panels look detached. The Plan/Queue panel now grows
|
|
421
428
|
// into that space instead, so its bottom edge meets this panel's header.
|
|
422
|
-
|
|
429
|
+
// minHeight keeps this panel on screen while a long plan is running: with
|
|
430
|
+
// no floor the flex column shrank it to zero rows during a treatment — the
|
|
431
|
+
// plan grew, the Runtime/Agent status tabs vanished — while the Queue tab
|
|
432
|
+
// (fixed height, no shrink) kept them visible.
|
|
433
|
+
<box flexGrow={2} minHeight={10} flexDirection="column" paddingX={1} focusable={false}>
|
|
423
434
|
<text width={lineWidth()} fg="#4B5563" content={'─'.repeat(lineWidth())} />
|
|
424
435
|
<box height={1} flexDirection="row">
|
|
425
436
|
<text
|
|
@@ -438,6 +449,7 @@ export function LogPanel(props: { logs: string[]; width: number; filter?: string
|
|
|
438
449
|
</box>
|
|
439
450
|
<scrollbox
|
|
440
451
|
flexGrow={1}
|
|
452
|
+
minHeight={4}
|
|
441
453
|
focusable={false}
|
|
442
454
|
scrollY={true}
|
|
443
455
|
scrollX={false}
|
package/src/shell/repl.test.js
CHANGED
|
@@ -186,6 +186,28 @@ test('a doctor summary with a nonzero error count is colored as an error, not a
|
|
|
186
186
|
assert.doesNotMatch(colorFn, /\/0 error\\\(s\\\)\/i\.test/);
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
+
test('a ⚠ not-ready announcement is blue, even when its text mentions HTTP 401', async () => {
|
|
190
|
+
const source = await readFile(new URL('./RightPane.tsx', import.meta.url), 'utf8');
|
|
191
|
+
const colorFn = source.slice(
|
|
192
|
+
source.indexOf('function logMessageColor'),
|
|
193
|
+
source.indexOf('function logRenderLines'),
|
|
194
|
+
);
|
|
195
|
+
const blueRule = colorFn.indexOf("return '#89B4FA'");
|
|
196
|
+
const redRule = colorFn.indexOf('HTTP 4\\d\\d');
|
|
197
|
+
assert.ok(blueRule !== -1 && redRule !== -1, 'the blue and red rules both exist');
|
|
198
|
+
assert.ok(blueRule < redRule, 'the ⚠ rule must win over the HTTP 4xx red rule');
|
|
199
|
+
assert.match(colorFn, /\/\^\\s\*⚠\//);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('local log lines carry a fixed 24h timestamp like the runtime lines', async () => {
|
|
203
|
+
const source = await readFile(new URL('./useSession.ts', import.meta.url), 'utf8');
|
|
204
|
+
// A locale default rendered "9:50:37 PM" on some machines while runtime
|
|
205
|
+
// lines read "21:50:37" — two time dialects in one panel, and the
|
|
206
|
+
// Runtime/Agent-status classification reads the time prefix structurally.
|
|
207
|
+
assert.match(source, /toLocaleTimeString\('en-GB', \{ hour12: false \}\)/);
|
|
208
|
+
assert.doesNotMatch(source, /toLocaleTimeString\(\)/);
|
|
209
|
+
});
|
|
210
|
+
|
|
189
211
|
test('runtime logs have a separator and a concise Runtime tab label', async () => {
|
|
190
212
|
const source = await readFile(new URL('./RightPane.tsx', import.meta.url), 'utf8');
|
|
191
213
|
const logPanel = source.slice(
|
|
@@ -195,6 +217,11 @@ test('runtime logs have a separator and a concise Runtime tab label', async () =
|
|
|
195
217
|
assert.match(logPanel, /content=\{'─'\.repeat\(lineWidth\(\)\)\}/);
|
|
196
218
|
assert.match(logPanel, /content=" Runtime "/);
|
|
197
219
|
assert.doesNotMatch(logPanel, /Flow \/ Trace/);
|
|
220
|
+
// The panel keeps a floor so a long plan running above it cannot shrink it
|
|
221
|
+
// to zero rows — the Queue tab's fixed height is what used to keep it
|
|
222
|
+
// visible, and a treatment must not make the tabs vanish.
|
|
223
|
+
assert.match(logPanel, /flexGrow=\{2\} minHeight=\{10\}/);
|
|
224
|
+
assert.match(logPanel, /minHeight=\{4\}/);
|
|
198
225
|
});
|
|
199
226
|
|
|
200
227
|
test('ShellUI turns HTTP URLs into valid links without trailing punctuation', () => {
|
package/src/shell/tui.tsx
CHANGED
|
@@ -175,9 +175,22 @@ function App(props: {
|
|
|
175
175
|
let lastCopiedSelection = '';
|
|
176
176
|
const state = useSession(props);
|
|
177
177
|
const startup = createMemo(() => startupInfo(props.packageJson, props.initialWorkspaceName));
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
178
|
+
// Header line that changes the meaning of what is typed. Null most of the
|
|
179
|
+
// time — a transient exit/copy/runtime prompt only — so the header collapses
|
|
180
|
+
// to the single mode-bar row when it is absent (see LeftPane).
|
|
181
|
+
const hintLine = () => {
|
|
182
|
+
if (exitStatus()) return exitStatus();
|
|
183
|
+
if (copyHint()) return copyHint();
|
|
184
|
+
if (exitHint()) return 'Press Ctrl+C again to exit.';
|
|
185
|
+
if (state.runtimeHint()) return state.runtimeHint();
|
|
186
|
+
return null;
|
|
187
|
+
};
|
|
188
|
+
// Non-conversation rows: outer padding (2) + mode bar (1) + status foot (1),
|
|
189
|
+
// plus the hint row only when a hint is showing, plus the composer height and
|
|
190
|
+
// the Activity strip (4 rows, now above the composer).
|
|
191
|
+
const conversationRows = createMemo(() =>
|
|
192
|
+
Math.max(4, dimensions().height - 4 - (hintLine() ? 1 : 0) - chatInputHeight() - 4),
|
|
193
|
+
);
|
|
181
194
|
const rightColumns = createMemo(() => {
|
|
182
195
|
const width = dimensions().width;
|
|
183
196
|
// 38% + 2 columns: the Plan/Activity/Logs panes carry job labels, file
|
|
@@ -359,14 +372,6 @@ function App(props: {
|
|
|
359
372
|
}
|
|
360
373
|
});
|
|
361
374
|
|
|
362
|
-
const hintLine = () => {
|
|
363
|
-
if (exitStatus()) return exitStatus();
|
|
364
|
-
if (copyHint()) return copyHint();
|
|
365
|
-
if (exitHint()) return 'Press Ctrl+C again to exit.';
|
|
366
|
-
if (state.runtimeHint()) return state.runtimeHint();
|
|
367
|
-
return null;
|
|
368
|
-
};
|
|
369
|
-
|
|
370
375
|
return (
|
|
371
376
|
<Show
|
|
372
377
|
when={screen() === 'startup'}
|
package/src/shell/useSession.ts
CHANGED
|
@@ -82,7 +82,10 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
82
82
|
setActiveEditor(editor);
|
|
83
83
|
};
|
|
84
84
|
const addLog = (line: string) => {
|
|
85
|
-
|
|
85
|
+
// Fixed 24h HH:MM:SS, like the runtime's own log lines: a locale default
|
|
86
|
+
// rendered "9:50:37 PM" on some machines, and the Runtime/Agent-status
|
|
87
|
+
// split classifies lines structurally from that time prefix.
|
|
88
|
+
setLogs((items) => [...items, `${new Date().toLocaleTimeString('en-GB', { hour12: false })} ${line}`].slice(-200));
|
|
86
89
|
};
|
|
87
90
|
const runtimeUnavailableReason = createMemo(() => {
|
|
88
91
|
if (props.runtime?.url) return null;
|
package/wiki-workspace
CHANGED
|
@@ -97,6 +97,8 @@ Usage:
|
|
|
97
97
|
Commands:
|
|
98
98
|
config <workspace> [path] Create workspace folder, auto-select ports, run wiki init
|
|
99
99
|
up <workspace> [--open] Start workspace stack: wiki + production
|
|
100
|
+
start [--open] Start everything: agent-runtime, agents and every
|
|
101
|
+
configured workspace (--open opens the first chat)
|
|
100
102
|
list List configured workspaces
|
|
101
103
|
refresh Stop runtime/stacks and remove their Compose images
|
|
102
104
|
|
|
@@ -1438,6 +1440,49 @@ wait_and_open() {
|
|
|
1438
1440
|
open_app_mode "${url}"
|
|
1439
1441
|
}
|
|
1440
1442
|
|
|
1443
|
+
# `start` — the whole deployment in one command: the host agent-runtime, the
|
|
1444
|
+
# external agents, then every configured workspace stack. Each step is the
|
|
1445
|
+
# same idempotent command the operator would run by hand, so `start` is safe
|
|
1446
|
+
# to repeat while things are already up. A workspace failure stops the run —
|
|
1447
|
+
# the failing compose command already says why.
|
|
1448
|
+
start_all() {
|
|
1449
|
+
local open_browser="${1:-0}"
|
|
1450
|
+
|
|
1451
|
+
printf -- '── agent-runtime ──\n'
|
|
1452
|
+
runtime_command up
|
|
1453
|
+
printf '\n'
|
|
1454
|
+
|
|
1455
|
+
printf -- '── external agents ──\n'
|
|
1456
|
+
agents_compose up
|
|
1457
|
+
printf '\n'
|
|
1458
|
+
|
|
1459
|
+
mkdir -p "$WORKSPACES_DIR"
|
|
1460
|
+
local found=0 first_name="" entry file name
|
|
1461
|
+
for entry in "$WORKSPACES_DIR"/*; do
|
|
1462
|
+
[[ -d "$entry" ]] || continue
|
|
1463
|
+
file="$entry/.env"
|
|
1464
|
+
[[ -f "$file" ]] || continue
|
|
1465
|
+
name="$(env_value "$file" WORKSPACE_NAME "$(basename "$entry")")"
|
|
1466
|
+
found=1
|
|
1467
|
+
[[ -n "$first_name" ]] || first_name="$name"
|
|
1468
|
+
printf -- '── workspace: %s ──\n' "$name"
|
|
1469
|
+
up_workspace "$name"
|
|
1470
|
+
printf '\n'
|
|
1471
|
+
done
|
|
1472
|
+
|
|
1473
|
+
if [[ "$found" -eq 0 ]]; then
|
|
1474
|
+
printf 'No workspaces configured in %s\n' "$WORKSPACES_DIR"
|
|
1475
|
+
printf 'Create one with: wiki-workspace config <name>\n'
|
|
1476
|
+
return 0
|
|
1477
|
+
fi
|
|
1478
|
+
|
|
1479
|
+
if [[ "$open_browser" == "1" ]]; then
|
|
1480
|
+
local serve_port
|
|
1481
|
+
serve_port="$(workspace_value "$first_name" WIKI_SERVE_PORT 3100)"
|
|
1482
|
+
wait_and_open "http://localhost:${serve_port}/chat" &
|
|
1483
|
+
fi
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1441
1486
|
ensure_workspace_dirs() {
|
|
1442
1487
|
local ws_path="$1"
|
|
1443
1488
|
mkdir -p "$ws_path/.wiki/vector-index"
|
|
@@ -1575,6 +1620,16 @@ main() {
|
|
|
1575
1620
|
exit 0
|
|
1576
1621
|
fi
|
|
1577
1622
|
|
|
1623
|
+
# start [--open] — everything: runtime, agents, every configured workspace
|
|
1624
|
+
if [[ $# -ge 1 && "$1" == "start" ]]; then
|
|
1625
|
+
[[ $# -le 2 ]] || die "start takes at most --open"
|
|
1626
|
+
[[ "${2:-}" == "" || "${2:-}" == "--open" ]] || die "start takes at most --open"
|
|
1627
|
+
local start_open=0
|
|
1628
|
+
[[ "${2:-}" == "--open" ]] && start_open=1
|
|
1629
|
+
start_all "$start_open"
|
|
1630
|
+
exit 0
|
|
1631
|
+
fi
|
|
1632
|
+
|
|
1578
1633
|
# up <workspace> [--open]
|
|
1579
1634
|
if [[ $# -ge 2 && "$1" == "up" ]]; then
|
|
1580
1635
|
local up_open=0
|