@linxin666/dsh-client-ui-task-board 0.3.5 → 0.3.9
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.i18n.yaml +2 -2
- package/README.md +9 -4
- package/README.zh.md +9 -4
- package/lib/client.js +947 -354
- package/lib/client.js.map +1 -1
- package/lib/index.js +712 -180
- package/lib/types/client/TaskBoardSettingsCard.d.ts +2 -1
- package/lib/types/client/board/TaskBoard.d.ts +3 -0
- package/lib/types/client/board/TaskForm.d.ts +28 -0
- package/lib/types/client/host-api.d.ts +2 -2
- package/lib/types/client/index.d.ts +10 -2
- package/lib/types/client/locales.d.ts +19 -0
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/core/controller.d.ts +11 -2
- package/lib/types/core/freeze-snapshot.d.ts +71 -0
- package/lib/types/core/handover.d.ts +58 -0
- package/lib/types/core/tasks.d.ts +70 -1
- package/lib/types/core/use-cases/task-update.d.ts +16 -7
- package/lib/types/host-ledger.d.ts +17 -2
- package/lib/types/host-runner.d.ts +44 -17
- package/lib/types/host-service.d.ts +8 -5
- package/lib/types/index.d.ts +9 -1
- package/lib/types/protocol.d.ts +15 -2
- package/package.json +26 -17
- package/src/client/TaskBoardSettingsCard.tsx +2 -1
- package/src/client/board/EditTaskModal.tsx +19 -55
- package/src/client/board/NewTaskModal.tsx +59 -42
- package/src/client/board/TaskBoard.tsx +5 -3
- package/src/client/board/TaskCard.tsx +3 -0
- package/src/client/board/TaskDetail.tsx +54 -0
- package/src/client/board/TaskForm.tsx +111 -0
- package/src/client/board-mount.tsx +7 -1
- package/src/client/host-api.ts +5 -5
- package/src/client/index.ts +67 -7
- package/src/client/locales.ts +40 -2
- package/src/client/settings-form.ts +78 -92
- package/src/core/controller.ts +36 -16
- package/src/core/freeze-snapshot.ts +213 -0
- package/src/core/handover.ts +117 -0
- package/src/core/store.ts +48 -1
- package/src/core/tasks.ts +87 -0
- package/src/core/use-cases/task-update.ts +30 -10
- package/src/dsh-home.ts +1 -1
- package/src/host-ledger.ts +137 -59
- package/src/host-routes.ts +1 -1
- package/src/host-runner.ts +223 -83
- package/src/host-service.ts +14 -10
- package/src/index.ts +24 -10
- package/src/protocol.ts +98 -14
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* the Web UI plugin group renders, bound to the `task-board` namespace.
|
|
5
5
|
*/
|
|
6
6
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
7
|
-
import type {
|
|
7
|
+
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
8
|
+
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
8
9
|
import { type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts';
|
|
9
10
|
/** The task-board fields this card edits (the namespace's full schema). */
|
|
10
11
|
export interface TaskBoardSettings {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type BoardController } from '../../core/controller.ts';
|
|
2
|
+
import { type TaskRecord } from '../../core/tasks.ts';
|
|
3
|
+
/** Case-insensitive title/description/freeze-snapshot match. */
|
|
4
|
+
export declare function matchesFilter(task: TaskRecord, filter: string): boolean;
|
|
2
5
|
/** Board component; subscribes to the controller snapshot. */
|
|
3
6
|
export declare function TaskBoard({ controller }: {
|
|
4
7
|
controller: BoardController;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared task-modal pieces: the overlay shell (backdrop, form, title, error,
|
|
3
|
+
* footer) and the title/description/prompt field trio used by both the
|
|
4
|
+
* NewTaskModal and the EditTaskModal. State stays in the owning modal; these
|
|
5
|
+
* are controlled components.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
/** Modal overlay: closes on backdrop press, submits through the form. */
|
|
9
|
+
export declare function ModalShell({ ariaLabel, title, error, pending, submitLabel, onSubmit, onClose, children, }: {
|
|
10
|
+
ariaLabel: string;
|
|
11
|
+
title: string;
|
|
12
|
+
error: string | undefined;
|
|
13
|
+
pending: boolean;
|
|
14
|
+
submitLabel: string;
|
|
15
|
+
onSubmit: () => void;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
/** Title + description + prompt fields shared by the new and edit task forms. */
|
|
20
|
+
export declare function TaskContentFields({ title, description, prompt, onTitleChange, onDescriptionChange, onPromptChange, }: {
|
|
21
|
+
title: string;
|
|
22
|
+
description: string;
|
|
23
|
+
prompt: string;
|
|
24
|
+
/** Receives the new title; the owner also clears its error state. */
|
|
25
|
+
onTitleChange: (value: string) => void;
|
|
26
|
+
onDescriptionChange: (value: string) => void;
|
|
27
|
+
onPromptChange: (value: string) => void;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
@@ -3,7 +3,7 @@ import { type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardSnapsho
|
|
|
3
3
|
export interface TaskBoardHostTransport {
|
|
4
4
|
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>;
|
|
5
5
|
state(): Promise<TaskBoardSnapshot>;
|
|
6
|
-
action(action: TaskBoardAction): Promise<TaskBoardSnapshot>;
|
|
6
|
+
action(action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot>;
|
|
7
7
|
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void;
|
|
8
8
|
}
|
|
9
9
|
export declare class HttpTaskBoardHostTransport implements TaskBoardHostTransport {
|
|
@@ -11,7 +11,7 @@ export declare class HttpTaskBoardHostTransport implements TaskBoardHostTranspor
|
|
|
11
11
|
constructor(storage?: Pick<Storage, 'getItem' | 'setItem'> | undefined);
|
|
12
12
|
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>;
|
|
13
13
|
state(): Promise<TaskBoardSnapshot>;
|
|
14
|
-
action(action: TaskBoardAction): Promise<TaskBoardSnapshot>;
|
|
14
|
+
action(action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot>;
|
|
15
15
|
private post;
|
|
16
16
|
private request;
|
|
17
17
|
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void;
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* shell fails the whole boot when a plugin apply throws, and an external
|
|
9
9
|
* plugin must not take the GUI down.
|
|
10
10
|
*/
|
|
11
|
-
import type {
|
|
11
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
12
|
+
import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
12
13
|
import { type TaskBoardKey } from './locales.ts';
|
|
13
14
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
14
15
|
interface LocaleNamespaceMap {
|
|
@@ -46,7 +47,14 @@ declare module '@deepseek-ai/cordis' {
|
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Required services (fiber inject waiting — the runtime must be up first).
|
|
52
|
+
* The generated remote faces are probed at use time instead of injected:
|
|
53
|
+
* `remote.agentPresets` only registers on 0.1.2-alpha.2 hosts (the
|
|
54
|
+
* api-remotes contribution), so a hard wait would pend the entry forever
|
|
55
|
+
* on hosts below that cohort, which serve the same roster through the
|
|
56
|
+
* connection RPC face.
|
|
57
|
+
*/
|
|
50
58
|
export declare const inject: string[];
|
|
51
59
|
/**
|
|
52
60
|
* Mount the task board.
|
|
@@ -39,6 +39,24 @@ export declare const zh: {
|
|
|
39
39
|
'new.submit': string;
|
|
40
40
|
'new.cancel': string;
|
|
41
41
|
'new.required': string;
|
|
42
|
+
'new.freeze': string;
|
|
43
|
+
'new.freezePlaceholder': string;
|
|
44
|
+
'detail.freeze': string;
|
|
45
|
+
'detail.freeze.goal': string;
|
|
46
|
+
'detail.freeze.progress': string;
|
|
47
|
+
'detail.freeze.next': string;
|
|
48
|
+
'detail.freeze.frozenAt': string;
|
|
49
|
+
'detail.freeze.frozenBy': string;
|
|
50
|
+
'detail.freeze.redacted': string;
|
|
51
|
+
'card.frozen': string;
|
|
52
|
+
'new.handover': string;
|
|
53
|
+
'new.handoverPlaceholder': string;
|
|
54
|
+
'detail.handover': string;
|
|
55
|
+
'detail.handover.bundledAt': string;
|
|
56
|
+
'detail.handover.references': string;
|
|
57
|
+
'detail.permissionPending': string;
|
|
58
|
+
'detail.permissionConfirm': string;
|
|
59
|
+
'detail.permissionConfirmed': string;
|
|
42
60
|
'detail.title': string;
|
|
43
61
|
'detail.edit': string;
|
|
44
62
|
'detail.close': string;
|
|
@@ -57,6 +75,7 @@ export declare const zh: {
|
|
|
57
75
|
'detail.viewSession': string;
|
|
58
76
|
'detail.noSession': string;
|
|
59
77
|
'detail.executionStarted': string;
|
|
78
|
+
'detail.execution.initiator': string;
|
|
60
79
|
'detail.executionEnded': string;
|
|
61
80
|
'detail.result.succeeded': string;
|
|
62
81
|
'detail.result.failed': string;
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* each plugin's client bundle; mirrors the official ui-plugin-config
|
|
7
7
|
* card-store pattern.
|
|
8
8
|
*/
|
|
9
|
-
import type { SettingsScope
|
|
9
|
+
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
10
|
+
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
10
11
|
/** The write one field's staged text performs when the card is saved. */
|
|
11
12
|
export type FieldWrite = {
|
|
12
13
|
kind: 'set';
|
|
@@ -21,9 +22,11 @@ export interface FieldSpec {
|
|
|
21
22
|
/**
|
|
22
23
|
* Whether the Host treats this field as a secret and redacts its value from
|
|
23
24
|
* the read-back (role('secret') in the section schema). Redacted secrets are
|
|
24
|
-
* never compared against the draft on save
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* never compared against the draft on save: the Host strips them from every
|
|
26
|
+
* wire view layer, so the settled snapshot carries nothing to read back. A
|
|
27
|
+
* staged secret set is judged by the mutation settling; the rest of its
|
|
28
|
+
* batch, when one exists, is still judged by read-back, and the atomic
|
|
29
|
+
* mutation lands every write or none.
|
|
27
30
|
*/
|
|
28
31
|
secret?: boolean;
|
|
29
32
|
/** Render a stored value as draft text; the empty string when the section carries none. */
|
|
@@ -49,8 +52,8 @@ export interface CardShell {
|
|
|
49
52
|
available: boolean;
|
|
50
53
|
/**
|
|
51
54
|
* Whether the namespace is actually served to this client. False when the
|
|
52
|
-
* Host deployment does not expose it (e.g. the
|
|
53
|
-
*
|
|
55
|
+
* Host deployment does not expose it (e.g. the owning plugin's settings
|
|
56
|
+
* domain is not mounted): the card renders an explanation
|
|
54
57
|
* instead of its form, so a missing namespace never looks like a missing
|
|
55
58
|
* plugin.
|
|
56
59
|
*/
|
|
@@ -83,7 +86,7 @@ export interface CardActions {
|
|
|
83
86
|
/** Drop every staged edit. */
|
|
84
87
|
discard: () => void;
|
|
85
88
|
}
|
|
86
|
-
/** One durable write
|
|
89
|
+
/** One durable write inside the save's atomic scope mutation. */
|
|
87
90
|
export interface BatchedWrite {
|
|
88
91
|
/** Field this entry writes. */
|
|
89
92
|
field: string;
|
|
@@ -92,28 +95,6 @@ export interface BatchedWrite {
|
|
|
92
95
|
/** Value for op set (absent for unset). */
|
|
93
96
|
value?: unknown;
|
|
94
97
|
}
|
|
95
|
-
/** Per-field outcome of one batched scope write. */
|
|
96
|
-
export interface BatchedFieldResult {
|
|
97
|
-
/** Field this entry writes. */
|
|
98
|
-
field: string;
|
|
99
|
-
/** Whether the Host accepted this field's write (per the read-back view). */
|
|
100
|
-
landed: boolean;
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Result of a batched scope write. The bridge scope posts every planned write
|
|
104
|
-
* in one /mutate so the Host validate hook judges baseURL+model together; a
|
|
105
|
-
* batched refusal fails the whole save rather than per-field.
|
|
106
|
-
*/
|
|
107
|
-
export interface BatchResult {
|
|
108
|
-
/** Whether the whole mutate was accepted. */
|
|
109
|
-
ok: boolean;
|
|
110
|
-
/** Per-field success, in the request order (always present when ok). */
|
|
111
|
-
fields: BatchedFieldResult[];
|
|
112
|
-
/** Host rejection code (mutate refused). */
|
|
113
|
-
code?: string;
|
|
114
|
-
/** Host rejection message (mutate refused). */
|
|
115
|
-
message?: string;
|
|
116
|
-
}
|
|
117
98
|
/** Constraints a numeric field's accepted drafts must satisfy, mirroring the host schema. */
|
|
118
99
|
export interface NumberConstraints {
|
|
119
100
|
/** The accepted value must be a whole number. */
|
|
@@ -128,8 +109,8 @@ export declare function textField(field: string): FieldSpec;
|
|
|
128
109
|
/**
|
|
129
110
|
* A free-text field the Host treats as a secret and redacts from the read-back
|
|
130
111
|
* (role('secret') in the section schema). The card still edits it like text,
|
|
131
|
-
* but a save never compares the redacted value back
|
|
132
|
-
*
|
|
112
|
+
* but a save never compares the redacted value back: the staged set is judged
|
|
113
|
+
* by the mutation settling (see {@link FieldSpec.secret}).
|
|
133
114
|
*/
|
|
134
115
|
export declare function secretField(field: string): FieldSpec;
|
|
135
116
|
/** A boolean field, edited through true/false draft text. */
|
|
@@ -171,19 +152,22 @@ export declare class CardForm<T> {
|
|
|
171
152
|
/** The actions the card's slot registration injects. */
|
|
172
153
|
actions(): CardActions;
|
|
173
154
|
/**
|
|
174
|
-
* Write every staged edit, then re-seed from
|
|
155
|
+
* Write every staged edit in one atomic scope mutation, then re-seed from
|
|
156
|
+
* what the Host accepted.
|
|
175
157
|
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
158
|
+
* The whole batch rides one mutate, so cross-field validate hooks
|
|
159
|
+
* (baseURL+model) judge it as a unit: the Host either applies every write
|
|
160
|
+
* or refuses the batch. The 0.1.2 scope contract never rejects a refused
|
|
161
|
+
* mutation — the scope recovers with a fresh Host view and resolves — so
|
|
162
|
+
* resolution alone proves nothing: the outcome is judged by reading the
|
|
163
|
+
* settled snapshot back, one planned write at a time, and one missed write
|
|
164
|
+
* fails the whole save. A scope that still rejects on refusal (the dsh-web
|
|
165
|
+
* bridge scope) reports through the same failure path with its rejection
|
|
166
|
+
* message. A save that did not land keeps its drafts, so the user can
|
|
167
|
+
* correct them instead of retyping.
|
|
168
|
+
* @returns settlement after the mutation and the read-back.
|
|
183
169
|
*/
|
|
184
170
|
save(): Promise<void>;
|
|
185
|
-
/** The scope's batch surface when it supports one; undefined conservatively otherwise. */
|
|
186
|
-
private batchedScope;
|
|
187
171
|
/**
|
|
188
172
|
* Every staged edit a save would write. An entry whose draft is not a value
|
|
189
173
|
* its field accepts carries no write: the form is still dirty, and the save
|
|
@@ -192,8 +176,16 @@ export declare class CardForm<T> {
|
|
|
192
176
|
* @returns the planned writes, in the order the fields were staged.
|
|
193
177
|
*/
|
|
194
178
|
private plan;
|
|
195
|
-
|
|
196
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Read-back judgment for a planned set: the user layer must hold the
|
|
181
|
+
* intended value once the mutation has settled.
|
|
182
|
+
*/
|
|
183
|
+
private landedSet;
|
|
184
|
+
/**
|
|
185
|
+
* Read-back judgment for a planned unset: the field must be gone from the
|
|
186
|
+
* user layer once the mutation has settled.
|
|
187
|
+
*/
|
|
188
|
+
private landedUnset;
|
|
197
189
|
private stage;
|
|
198
190
|
private specOf;
|
|
199
191
|
private snapshotOf;
|
|
@@ -18,7 +18,11 @@ import type { TaskBoardAction, TaskBoardEventPayload, TaskBoardSnapshot } from '
|
|
|
18
18
|
export interface TaskBoardTransport {
|
|
19
19
|
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>;
|
|
20
20
|
state(): Promise<TaskBoardSnapshot>;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Submit one action; the optional initiator is the current DSH session id
|
|
23
|
+
* (issue #6 audit origin), asserted by the client and recorded by the Host.
|
|
24
|
+
*/
|
|
25
|
+
action(action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot>;
|
|
22
26
|
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void;
|
|
23
27
|
}
|
|
24
28
|
/** The sessions face the controller needs for navigation awareness. */
|
|
@@ -74,7 +78,7 @@ export interface ControllerSnapshot {
|
|
|
74
78
|
executionOptions: ExecutionOptionsSnapshot;
|
|
75
79
|
pendingTaskIds: readonly string[];
|
|
76
80
|
transportError?: string;
|
|
77
|
-
host?: Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power'>;
|
|
81
|
+
host?: Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power' | 'sessionDefaultPermission'>;
|
|
78
82
|
}
|
|
79
83
|
/** The selected task (resolved from the ledger), or undefined. */
|
|
80
84
|
export declare function selectedTaskOf(snapshot: ControllerSnapshot): TaskRecord | undefined;
|
|
@@ -186,6 +190,11 @@ export declare class BoardController {
|
|
|
186
190
|
* the run is refused (returns false).
|
|
187
191
|
*/
|
|
188
192
|
runTask(id: string): Promise<boolean>;
|
|
193
|
+
/**
|
|
194
|
+
* Confirm a card's above-default permission binding through the Host
|
|
195
|
+
* (resolves the pending-confirmation transaction; no-op otherwise).
|
|
196
|
+
*/
|
|
197
|
+
confirmPermission(id: string): Promise<boolean>;
|
|
189
198
|
/** Re-run a settled task through the Host (the Host replans and executes). */
|
|
190
199
|
rerunTask(id: string): Promise<void>;
|
|
191
200
|
/**
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freeze-request parser and security gate: parses the system-prompt-agreed
|
|
3
|
+
* freeze format into a structured context snapshot (goal / progress / next),
|
|
4
|
+
* redacts sensitive patterns to markers, rejects slash-prefixed DSH command
|
|
5
|
+
* lines, and enforces the 8 KiB per-field byte limit.
|
|
6
|
+
* Pure functions only: no session, network, or filesystem access.
|
|
7
|
+
*/
|
|
8
|
+
/** Hard per-field limit: 8 KiB measured in UTF-8 bytes. */
|
|
9
|
+
export declare const FREEZE_FIELD_BYTE_LIMIT: number;
|
|
10
|
+
/** Marker replacing every sensitive match. */
|
|
11
|
+
export declare const REDACTED_MARKER = "[REDACTED]";
|
|
12
|
+
/** Structured context snapshot carried by a continuation card. */
|
|
13
|
+
export interface FreezeSnapshot {
|
|
14
|
+
goal: string;
|
|
15
|
+
progress: string;
|
|
16
|
+
next: string;
|
|
17
|
+
}
|
|
18
|
+
/** Non-fatal notices produced by the security gate. */
|
|
19
|
+
export type FreezeWarning = 'redacted';
|
|
20
|
+
export type FreezeResult = {
|
|
21
|
+
ok: true;
|
|
22
|
+
snapshot: FreezeSnapshot;
|
|
23
|
+
warnings: FreezeWarning[];
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
error: {
|
|
27
|
+
code: string;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/** Redact sensitive patterns to the marker; reports whether any hit occurred. */
|
|
32
|
+
export declare function redactSensitive(text: string): {
|
|
33
|
+
text: string;
|
|
34
|
+
redacted: boolean;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* True when any line of the text starts with "/" (a DSH command line).
|
|
38
|
+
* Leading horizontal whitespace before the slash counts too: a frozen
|
|
39
|
+
* body line like ` /kill` is still a command, not prose.
|
|
40
|
+
*/
|
|
41
|
+
export declare function hasSlashCommandLines(text: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Sanitize a structured freeze snapshot (the create/update action payload):
|
|
44
|
+
* shape check, slash-command taint rejection, sensitive redaction, and the
|
|
45
|
+
* per-field byte cap - the same gate parseFreezeRequest applies to
|
|
46
|
+
* free-text freeze requests, exposed for the action data plane (issue #4).
|
|
47
|
+
* @param value - the freeze object carried by an action or read back from disk.
|
|
48
|
+
* @param extraKeys - keys allowed alongside goal/progress/next (e.g. the
|
|
49
|
+
* protocol redacted flag, the ledger frozenAt stamp) and preserved verbatim
|
|
50
|
+
* when present; their validation stays with the caller.
|
|
51
|
+
*/
|
|
52
|
+
export declare function sanitizeFreezeSnapshot(value: unknown, extraKeys?: readonly string[]): {
|
|
53
|
+
ok: true;
|
|
54
|
+
snapshot: FreezeSnapshot;
|
|
55
|
+
redacted: boolean;
|
|
56
|
+
extras: Record<string, unknown>;
|
|
57
|
+
} | {
|
|
58
|
+
ok: false;
|
|
59
|
+
error: {
|
|
60
|
+
code: string;
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Parse the freeze-request format: a <<<FREEZE ... >>>FREEZE block whose body
|
|
66
|
+
* is 目标: / 进度: / 下一步: section headers, each followed by body lines.
|
|
67
|
+
* The gate applies before returning: slash-command taint rejects the whole
|
|
68
|
+
* request, sensitive patterns are redacted to markers, and each field is
|
|
69
|
+
* capped at FREEZE_FIELD_BYTE_LIMIT UTF-8 bytes.
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseFreezeRequest(input: string): FreezeResult;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handover bundle (issue #5, ADR 0001): the resource reference set a
|
|
3
|
+
* continuation card carries — the pinned execution triplet (workspace /
|
|
4
|
+
* agent preset / permission) plus doc/script references — so a picking-up
|
|
5
|
+
* session can start with zero context.
|
|
6
|
+
*
|
|
7
|
+
* Security gate (adversarial scenario b): a bundle whose effective
|
|
8
|
+
* permission is HIGHER than the session default is an unconfirmed binding.
|
|
9
|
+
* It enters the pending-confirmation state (the card stores the binding
|
|
10
|
+
* without a permissionConfirmedAt stamp); manual execution refuses, cron
|
|
11
|
+
* refuses to schedule, and the human confirm-permission action resolves the
|
|
12
|
+
* transaction by stamping the confirmation. Any later permission or
|
|
13
|
+
* handover change re-arms the gate (the confirmation binds the exact
|
|
14
|
+
* permission value, blocking confirm-then-swap escalation).
|
|
15
|
+
*/
|
|
16
|
+
import { type TaskPermission, type TaskRecord } from './tasks.ts';
|
|
17
|
+
export type { TaskPermission };
|
|
18
|
+
/** Wire shape of a handover bundle (before bundledAt is stamped). */
|
|
19
|
+
export interface TaskHandoverInput {
|
|
20
|
+
workspaceId?: string;
|
|
21
|
+
mode?: string;
|
|
22
|
+
permission?: TaskPermission;
|
|
23
|
+
references: string[];
|
|
24
|
+
}
|
|
25
|
+
/** Persisted handover bundle on a task record. */
|
|
26
|
+
export interface TaskHandover extends TaskHandoverInput {
|
|
27
|
+
/** When the bundle was attached/replaced (ms epoch, stamped by the use case). */
|
|
28
|
+
bundledAt: number;
|
|
29
|
+
}
|
|
30
|
+
/** Most references a bundle may carry. */
|
|
31
|
+
export declare const HANDOVER_MAX_REFERENCES = 32;
|
|
32
|
+
/** Per-reference UTF-8 byte cap. */
|
|
33
|
+
export declare const HANDOVER_MAX_REFERENCE_BYTES = 512;
|
|
34
|
+
/** Total references UTF-8 byte cap. */
|
|
35
|
+
export declare const HANDOVER_MAX_TOTAL_BYTES: number;
|
|
36
|
+
/** Per target-id (workspace/preset) byte cap. */
|
|
37
|
+
export declare const HANDOVER_MAX_TARGET_BYTES = 256;
|
|
38
|
+
/** The board's notion of the deployment session-default permission (fail-safe default). */
|
|
39
|
+
export declare const DEFAULT_SESSION_PERMISSION: TaskPermission;
|
|
40
|
+
/** Permission elevation rank (higher = more authority). */
|
|
41
|
+
export declare const PERMISSION_RANK: ReadonlyMap<TaskPermission, number>;
|
|
42
|
+
/**
|
|
43
|
+
* Gate a handover bundle from the wire or disk: exact keys, string targets
|
|
44
|
+
* under the byte cap, a known permission, and a bounded string reference
|
|
45
|
+
* list. Returns the sanitized bundle, or undefined when rejected.
|
|
46
|
+
*/
|
|
47
|
+
export declare function sanitizeHandover(value: unknown): TaskHandoverInput | undefined;
|
|
48
|
+
/** Build the persisted bundle from a sanitized input, stamping bundledAt. */
|
|
49
|
+
export declare function handoverOf(input: TaskHandoverInput, now: number): TaskHandover;
|
|
50
|
+
/** The permission an execution session would actually run under. */
|
|
51
|
+
export declare function effectivePermission(task: Pick<TaskRecord, 'permission' | 'handover'>): TaskPermission | undefined;
|
|
52
|
+
/** Whether a binding's permission is elevated above the session default. */
|
|
53
|
+
export declare function exceedsSessionDefault(permission: TaskPermission | undefined, sessionDefault: TaskPermission): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The confirmation-gate predicate: an elevated permission without a human
|
|
56
|
+
* confirmation stamp. Manual run/rerun and cron must refuse such a card.
|
|
57
|
+
*/
|
|
58
|
+
export declare function requiresPermissionConfirmation(task: Pick<TaskRecord, 'permission' | 'handover' | 'permissionConfirmedAt'>, sessionDefault?: TaskPermission): boolean;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Framework-free (no cordis, no runtime imports) so the state machine is
|
|
5
5
|
* unit-testable in isolation.
|
|
6
6
|
*/
|
|
7
|
+
import type { FreezeSnapshot } from './freeze-snapshot.ts';
|
|
8
|
+
import type { TaskHandover, TaskHandoverInput } from './handover.ts';
|
|
7
9
|
/** Task lifecycle status, one per kanban column. */
|
|
8
10
|
export type TaskStatus = 'backlog' | 'todo' | 'running' | 'done' | 'failed';
|
|
9
11
|
/**
|
|
@@ -24,6 +26,16 @@ export interface ExecutionRecord {
|
|
|
24
26
|
result: 'succeeded' | 'failed' | 'cancelled' | undefined;
|
|
25
27
|
/** Human failure text when the run failed (prompt rejection or agent error). */
|
|
26
28
|
error: string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Session id of the DSH session that issued the run/rerun action (issue #6
|
|
31
|
+
* audit origin). Client-asserted, not a trust boundary; absent when the run
|
|
32
|
+
* was triggered by cron (source unknown).
|
|
33
|
+
*/
|
|
34
|
+
initiatedBy?: string;
|
|
35
|
+
/** Freeze instant captured from the card snapshot when the run opened. */
|
|
36
|
+
frozenAt?: number;
|
|
37
|
+
/** Freeze source session captured from the card snapshot when the run opened. */
|
|
38
|
+
frozenBy?: string;
|
|
27
39
|
}
|
|
28
40
|
/**
|
|
29
41
|
* Maximum number of execution records retained per task. Older settled runs
|
|
@@ -52,6 +64,23 @@ export interface ScheduleRule {
|
|
|
52
64
|
/** Instant of the latest scheduled trigger (ms epoch). */
|
|
53
65
|
lastTriggeredAt: number | undefined;
|
|
54
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Frozen context snapshot carried by a continuation card (issue #4): the
|
|
69
|
+
* goal/progress/next text (already sanitized by the freeze gates) plus the
|
|
70
|
+
* freeze instant and the redaction warning flag.
|
|
71
|
+
*/
|
|
72
|
+
export interface TaskFreeze extends FreezeSnapshot {
|
|
73
|
+
/** When the snapshot was frozen (ms epoch, stamped by the create/update use case). */
|
|
74
|
+
frozenAt: number;
|
|
75
|
+
/** True when the freeze gates redacted sensitive patterns out of the text. */
|
|
76
|
+
redacted?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Session id of the DSH session that authored the frozen snapshot (issue #6
|
|
79
|
+
* provenance): stamped by the Host ledger from the create/update action's
|
|
80
|
+
* initiator, or kept from the wire payload when no initiator was asserted.
|
|
81
|
+
*/
|
|
82
|
+
frozenBy?: string;
|
|
83
|
+
}
|
|
55
84
|
/** One task on the board. */
|
|
56
85
|
export interface TaskRecord {
|
|
57
86
|
/** Stable task id (uuid). */
|
|
@@ -90,6 +119,25 @@ export interface TaskRecord {
|
|
|
90
119
|
* `/permission <id>` slash command; absent leaves the session default.
|
|
91
120
|
*/
|
|
92
121
|
permission?: TaskPermission;
|
|
122
|
+
/**
|
|
123
|
+
* Frozen context snapshot for a continuation card; absent on plain tasks.
|
|
124
|
+
* Sanitized before it enters the ledger (redaction, slash-command taint,
|
|
125
|
+
* 8 KiB per-field cap) by the protocol gate and re-normalized on load.
|
|
126
|
+
*/
|
|
127
|
+
freeze?: TaskFreeze;
|
|
128
|
+
/**
|
|
129
|
+
* Handover bundle carried by a continuation card (issue #5): the pinned
|
|
130
|
+
* execution triplet plus doc/script references. Sanitized before it
|
|
131
|
+
* enters the ledger by the protocol gate and re-normalized on load; the
|
|
132
|
+
* bundle's triplet overrides the legacy pin fields at execution time.
|
|
133
|
+
*/
|
|
134
|
+
handover?: TaskHandover;
|
|
135
|
+
/**
|
|
136
|
+
* Human confirmation stamp for an above-default effective permission
|
|
137
|
+
* (ms epoch). Absent while the binding awaits confirmation; any permission
|
|
138
|
+
* or handover change re-arms the gate by clearing it.
|
|
139
|
+
*/
|
|
140
|
+
permissionConfirmedAt?: number;
|
|
93
141
|
/**
|
|
94
142
|
* When the task was archived (ms epoch). Archived tasks keep their status
|
|
95
143
|
* and execution history, leave the main board, and cannot run until restored;
|
|
@@ -125,6 +173,19 @@ export interface NewTaskInput {
|
|
|
125
173
|
enabled: boolean;
|
|
126
174
|
cron: string;
|
|
127
175
|
};
|
|
176
|
+
/**
|
|
177
|
+
* Optional frozen context snapshot (goal/progress/next, sanitized by the
|
|
178
|
+
* protocol gate) turning the new task into a continuation card.
|
|
179
|
+
*/
|
|
180
|
+
freeze?: FreezeSnapshot & {
|
|
181
|
+
redacted?: boolean;
|
|
182
|
+
frozenBy?: string;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Optional handover bundle (pinned triplet + doc/script references,
|
|
186
|
+
* sanitized by the protocol gate) attached at creation.
|
|
187
|
+
*/
|
|
188
|
+
handover?: TaskHandoverInput;
|
|
128
189
|
}
|
|
129
190
|
/** The five kanban columns, in display order. */
|
|
130
191
|
export declare const COLUMNS: readonly {
|
|
@@ -143,6 +204,14 @@ export declare function isTaskStatus(value: unknown): value is TaskStatus;
|
|
|
143
204
|
export declare function canMoveManually(from: TaskStatus, to: TaskStatus): boolean;
|
|
144
205
|
/** Normalize one optional execution-target string: trim; blank collapses to undefined. */
|
|
145
206
|
export declare function normalizeTargetId(value: string | undefined): string | undefined;
|
|
207
|
+
/**
|
|
208
|
+
* Build the persisted freeze snapshot from a sanitized input, stamping the
|
|
209
|
+
* freeze instant (shared by the create and update use cases).
|
|
210
|
+
*/
|
|
211
|
+
export declare function freezeOf(input: FreezeSnapshot & {
|
|
212
|
+
redacted?: boolean;
|
|
213
|
+
frozenBy?: string;
|
|
214
|
+
}, now: number): TaskFreeze;
|
|
146
215
|
/** Create a task from user input. */
|
|
147
216
|
export declare function createTask(input: NewTaskInput, now: number, id: string): TaskRecord;
|
|
148
217
|
/** Clone a task with an updated status and a fresh updatedAt. */
|
|
@@ -158,7 +227,7 @@ export declare function withSchedule(task: TaskRecord, patch: Partial<ScheduleRu
|
|
|
158
227
|
* Open a fresh execution on a task: move it to 'running' and append a
|
|
159
228
|
* running execution record. Returns the new task and the new execution.
|
|
160
229
|
*/
|
|
161
|
-
export declare function startExecution(task: TaskRecord, now: number, executionId: string): {
|
|
230
|
+
export declare function startExecution(task: TaskRecord, now: number, executionId: string, initiatedBy?: string): {
|
|
162
231
|
task: TaskRecord;
|
|
163
232
|
execution: ExecutionRecord;
|
|
164
233
|
};
|
|
@@ -4,16 +4,25 @@
|
|
|
4
4
|
* fresh updatedAt. Pure ledger transition (no persistence or notify — the
|
|
5
5
|
* controller orchestrates those).
|
|
6
6
|
*
|
|
7
|
-
* An explicit `undefined` in the patch clears
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* can never persist a value the execution service rejects.
|
|
7
|
+
* An explicit `undefined` in the patch clears the field (the task falls
|
|
8
|
+
* back to the runtime default); an unknown permission string is ignored so
|
|
9
|
+
* stale UI can never persist a value the execution service rejects.
|
|
11
10
|
*/
|
|
12
11
|
import { type TaskRecord } from '../tasks.ts';
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
import type { FreezeSnapshot } from '../freeze-snapshot.ts';
|
|
13
|
+
import type { TaskHandoverInput } from '../handover.ts';
|
|
15
14
|
/**
|
|
16
|
-
*
|
|
15
|
+
* Editable fields on a task (the update patch surface). `freeze` replaces the
|
|
16
|
+
* continuation-card snapshot (restamping frozenAt); an explicit null clears it.
|
|
17
|
+
*/
|
|
18
|
+
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission'>> & {
|
|
19
|
+
freeze?: FreezeSnapshot & {
|
|
20
|
+
redacted?: boolean;
|
|
21
|
+
} | null;
|
|
22
|
+
/** Replaces the handover bundle (restamping bundledAt); an explicit null clears it. */
|
|
23
|
+
handover?: TaskHandoverInput | null;
|
|
24
|
+
};
|
|
25
|
+
/** The fields that edit the task's content (what the user reads and what the
|
|
17
26
|
* next execution sends). Unlike the execution targets they stay editable only
|
|
18
27
|
* while the task has never started executing — after the first run the
|
|
19
28
|
* recorded prompt is the record of what actually ran, so it becomes read-only.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ExecutionRecord, type TaskRecord } from './core/tasks.ts';
|
|
2
2
|
import { type TaskBoardAction, type TaskBoardSchedulerSnapshot } from './protocol.ts';
|
|
3
|
+
import { type TaskPermission } from './core/handover.ts';
|
|
3
4
|
export interface LedgerState {
|
|
4
5
|
revision: number;
|
|
5
6
|
tasks: TaskRecord[];
|
|
@@ -47,7 +48,11 @@ export declare class HostTaskLedger {
|
|
|
47
48
|
readonly lockFile: string;
|
|
48
49
|
/** Small sidecar for the 30 s scheduler heartbeat (lastTickAt only). */
|
|
49
50
|
readonly schedulerFile: string;
|
|
50
|
-
|
|
51
|
+
/** Session-default permission the confirmation gate compares against. */
|
|
52
|
+
readonly sessionDefaultPermission: TaskPermission;
|
|
53
|
+
constructor(dir?: string, now?: () => number, options?: {
|
|
54
|
+
sessionDefaultPermission?: TaskPermission;
|
|
55
|
+
});
|
|
51
56
|
/** Revision + scheduler without any task cloning; feeds the SSE event frame. */
|
|
52
57
|
summary(): {
|
|
53
58
|
revision: number;
|
|
@@ -66,7 +71,7 @@ export declare class HostTaskLedger {
|
|
|
66
71
|
dueSchedules(now: number): DueScheduleReference[];
|
|
67
72
|
subscribe(listener: () => void): () => void;
|
|
68
73
|
dispose(): void;
|
|
69
|
-
applyRequest(requestId: string, action: TaskBoardAction): {
|
|
74
|
+
applyRequest(requestId: string, action: TaskBoardAction, initiator?: string): {
|
|
70
75
|
state: LedgerState;
|
|
71
76
|
run?: OpenedRun;
|
|
72
77
|
};
|
|
@@ -78,7 +83,17 @@ export declare class HostTaskLedger {
|
|
|
78
83
|
private apply;
|
|
79
84
|
private repairSchedules;
|
|
80
85
|
private reconcileInterruptedStarts;
|
|
86
|
+
/**
|
|
87
|
+
* Field-preserving v2 to v3 migration. v3 adds no fields yet, so the
|
|
88
|
+
* migration reuses the v3 normalization, but it first proves every task
|
|
89
|
+
* row is structurally valid: a v2 document that would silently drop or
|
|
90
|
+
* coerce rows fails loudly instead (no quarantined-empty restart).
|
|
91
|
+
*/
|
|
92
|
+
private migrateLegacyDocument;
|
|
81
93
|
private load;
|
|
94
|
+
private normalizeDocument;
|
|
95
|
+
/** Quarantine an unreadable document and start from an empty ledger. */
|
|
96
|
+
private recoverCorrupt;
|
|
82
97
|
private syncRecentRequests;
|
|
83
98
|
private readSchedulerSidecar;
|
|
84
99
|
/** Atomic write of the scheduler heartbeat sidecar (0600, tmp + rename + fsync). */
|