@xzzpig/pi-openspec-x 0.1.0 → 0.2.1
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/CHANGELOG.md +28 -0
- package/README.md +6 -2
- package/dist/public.d.ts +48 -0
- package/package.json +9 -5
- package/src/agent-prompts.ts +4 -1
- package/src/agents.ts +25 -0
- package/src/child-report-extensions/gap-analysis.ts +23 -0
- package/src/child-report-extensions/plan-review.ts +21 -0
- package/src/child-report-extensions/worker.ts +26 -0
- package/src/child-report-ingestion.ts +1530 -0
- package/src/child-report-reader.ts +955 -0
- package/src/dispatch-gate.ts +25 -18
- package/src/goal-base.ts +394 -0
- package/src/implement-command.ts +620 -60
- package/src/index.ts +113 -9
- package/src/objective.ts +2 -2
- package/src/plan-command.ts +46 -0
- package/src/progress-observer.ts +48 -54
- package/src/report-tools.ts +10 -9
- package/src/worker-dispatch.ts +2 -2
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
5
|
|
|
3
6
|
import { loadOpsxConfig, type OpsxConfig } from "./config.ts";
|
|
4
7
|
import {
|
|
@@ -15,6 +18,9 @@ import {
|
|
|
15
18
|
} from "./dependencies.ts";
|
|
16
19
|
import { createResourcesDiscoverHandler } from "./discover.ts";
|
|
17
20
|
import { createOpsxRecoverySessionStartHandler } from "./goal-base.ts";
|
|
21
|
+
import { readGoalBaseSummaries } from "./goal-base.ts";
|
|
22
|
+
import { registerChildReportIngestion } from "./child-report-ingestion.ts";
|
|
23
|
+
import { childReportFlowId } from "./child-report-ingestion.ts";
|
|
18
24
|
import {
|
|
19
25
|
registerOpsxAuditorResolver,
|
|
20
26
|
buildOpsxAuditorOverride,
|
|
@@ -27,13 +33,17 @@ import {
|
|
|
27
33
|
import {
|
|
28
34
|
registerImplementFlow,
|
|
29
35
|
activeImplementFlow,
|
|
36
|
+
rebindImplementFlow,
|
|
30
37
|
} from "./implement-command.ts";
|
|
31
38
|
import { createOpsxSessionStartHandler } from "./mode.ts";
|
|
32
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
registerPlanFlow,
|
|
41
|
+
activePlanSession,
|
|
42
|
+
rebindPlanFlow,
|
|
43
|
+
} from "./plan-command.ts";
|
|
33
44
|
import { registerRollbackFlow } from "./rollback-command.ts";
|
|
34
45
|
import { registerPlanGateTools } from "./plan-gate.ts";
|
|
35
|
-
import {
|
|
36
|
-
import { registerReportTools } from "./report-tools.ts";
|
|
46
|
+
import { recordChildReportProgress } from "./progress-observer.ts";
|
|
37
47
|
import { buildSandboxProfiles } from "./sandbox-profiles.ts";
|
|
38
48
|
|
|
39
49
|
// Read once at extension load (merge semantics in loadOpsxConfig).
|
|
@@ -65,8 +75,10 @@ const config: OpsxConfig = loadOpsxConfig();
|
|
|
65
75
|
* semantics; see mode.ts).
|
|
66
76
|
*
|
|
67
77
|
* Registers the three child reporting tools (report_gap_analysis,
|
|
68
|
-
* report_plan_review, report_work)
|
|
69
|
-
*
|
|
78
|
+
* report_plan_review, report_work) through the role-specific child-only
|
|
79
|
+
* extensions declared in agents.ts (design D1): each tool is registered into
|
|
80
|
+
* its own subagent's process and never into the main session, so the parent's
|
|
81
|
+
* tool declaration and prompt rules stay free of them.
|
|
70
82
|
*
|
|
71
83
|
* pi-subagents is probed the same lazy way: on success the four opsx
|
|
72
84
|
* subagents (opsx-gap-analysis / opsx-plan-review / opsx-worker / opsx-reviewer) are
|
|
@@ -95,10 +107,12 @@ export default async function piOpenspecX(pi: ExtensionAPI): Promise<void> {
|
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
registerReportTools(pi);
|
|
99
|
-
// Parent-side projection of the subagents' structured progress (task 9.2).
|
|
100
|
-
registerProgressObserver(pi);
|
|
101
110
|
// Progress lines for the flow entries (task 9.1).
|
|
111
|
+
//
|
|
112
|
+
// The progress board is no longer fed by a main-session `tool_result`
|
|
113
|
+
// listener: the report tools are not registered here any more (design D1),
|
|
114
|
+
// so the only honest source is an accepted child report, delivered through
|
|
115
|
+
// the ingestion callback below (task 4.4).
|
|
102
116
|
registerFlowRenderers(pi);
|
|
103
117
|
// `/opsx:plan`: planner mode + append-only phase injection (task 7.1).
|
|
104
118
|
registerPlanFlow(pi);
|
|
@@ -170,4 +184,94 @@ export default async function piOpenspecX(pi: ExtensionAPI): Promise<void> {
|
|
|
170
184
|
void disposeSessionStart;
|
|
171
185
|
void disposeRecoverySessionStart;
|
|
172
186
|
void disposeStatusSnapshot;
|
|
187
|
+
|
|
188
|
+
// Flow recovery (design D5): the flow registries are process memory, so a
|
|
189
|
+
// cold start leaves them empty while the branch still holds the flow's own
|
|
190
|
+
// entries. Re-bind on every session boundary and, as a lazy fallback, on the
|
|
191
|
+
// first tool call of a session that missed them — otherwise a restored
|
|
192
|
+
// session would silently skip the completion gate.
|
|
193
|
+
const rebindKeys = new Set<string>();
|
|
194
|
+
const rebindFlows = async (ctx: ExtensionContext): Promise<void> => {
|
|
195
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
196
|
+
const key = `${sessionId}:${ctx.sessionManager.getLeafId() ?? "?"}`;
|
|
197
|
+
if (rebindKeys.has(key)) return;
|
|
198
|
+
rebindKeys.add(key);
|
|
199
|
+
try {
|
|
200
|
+
rebindPlanFlow(sessionId, ctx);
|
|
201
|
+
} catch {
|
|
202
|
+
// Recovery is best-effort; an unbound flow stays unbound rather than
|
|
203
|
+
// being bound to the wrong thing.
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const result = await rebindImplementFlow(ctx, {
|
|
207
|
+
readGoals: (innerCtx: ExtensionContext) =>
|
|
208
|
+
readGoalBaseSummaries({
|
|
209
|
+
cwd: innerCtx.cwd,
|
|
210
|
+
sessionManager: innerCtx.sessionManager,
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
if (!result.ok) {
|
|
214
|
+
pi.sendMessage(
|
|
215
|
+
{
|
|
216
|
+
customType: "pi-openspec-x/recovery-notice",
|
|
217
|
+
content: result.reason,
|
|
218
|
+
display: true,
|
|
219
|
+
},
|
|
220
|
+
{ deliverAs: "followUp" },
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
// Best-effort: never break a session boundary.
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const disposeRebindStart = pi.on(
|
|
228
|
+
"session_start",
|
|
229
|
+
(_event, ctx: ExtensionContext) => rebindFlows(ctx),
|
|
230
|
+
);
|
|
231
|
+
const disposeRebindSwitch = pi.on(
|
|
232
|
+
"session_before_switch",
|
|
233
|
+
(_event, ctx: ExtensionContext) => rebindFlows(ctx),
|
|
234
|
+
);
|
|
235
|
+
const disposeRebindTree = pi.on(
|
|
236
|
+
"session_tree",
|
|
237
|
+
(_event, ctx: ExtensionContext) => rebindFlows(ctx),
|
|
238
|
+
);
|
|
239
|
+
const disposeRebindToolCall = pi.on(
|
|
240
|
+
"tool_call",
|
|
241
|
+
(_event, ctx: ExtensionContext) => rebindFlows(ctx),
|
|
242
|
+
);
|
|
243
|
+
void disposeRebindStart;
|
|
244
|
+
void disposeRebindSwitch;
|
|
245
|
+
void disposeRebindTree;
|
|
246
|
+
void disposeRebindToolCall;
|
|
247
|
+
|
|
248
|
+
// Automatic child-report ingestion (design D3): the report tools live only in
|
|
249
|
+
// their own child processes, so the parent reconstructs delivery from its own
|
|
250
|
+
// launch records plus the child's canonical session record.
|
|
251
|
+
registerChildReportIngestion(pi, {
|
|
252
|
+
activeFlow: (sessionId) => {
|
|
253
|
+
const flow = activeImplementFlow(sessionId);
|
|
254
|
+
if (!flow) return undefined;
|
|
255
|
+
return {
|
|
256
|
+
flowId: childReportFlowId(sessionId, flow.changeId),
|
|
257
|
+
changeId: flow.changeId,
|
|
258
|
+
goalId: flow.goalId,
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
// Display-only projection of an accepted report's structured progress.
|
|
262
|
+
// It never gates, completes, or approves anything (task 4.4).
|
|
263
|
+
onProgress: ({ sessionId, toolName, report }) => {
|
|
264
|
+
recordChildReportProgress(pi, sessionId, { toolName, report });
|
|
265
|
+
},
|
|
266
|
+
// A durable record that cannot be confirmed is a real gap — the receipt
|
|
267
|
+
// will not survive a restart. Surfacing it keeps a silent persistence
|
|
268
|
+
// failure from looking like persistence was never wired (design D5).
|
|
269
|
+
onDiagnostic: (message) => {
|
|
270
|
+
pi.sendMessage({
|
|
271
|
+
customType: "pi-openspec-x/recovery-notice",
|
|
272
|
+
content: `[OPSX CHILD REPORT] ${message}`,
|
|
273
|
+
display: true,
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
});
|
|
173
277
|
}
|
package/src/objective.ts
CHANGED
|
@@ -125,14 +125,14 @@ function agentBoundaries(writable: readonly string[]): string[] {
|
|
|
125
125
|
"- openspec/ plan files (tasks.md, proposal.md, specs/, design.md) are read-only for opsx-worker; only the main agent ticks tasks.md, after review.",
|
|
126
126
|
"",
|
|
127
127
|
"Worker dispatch contract (task 8.2): every opsx-worker dispatch carries these six sections, in this order:",
|
|
128
|
-
"1. Task — the task id and title.",
|
|
128
|
+
"1. Task — the task id and title, and on its own line the exact marker `[OPSX WORKER DISPATCH task=<taskId>]` with the task id percent-encoded. Every dispatch, and every resume of it, must carry the marker: it is what attributes the worker's report to the task.",
|
|
129
129
|
"2. Expected output — the concrete files/behavior the dispatch must produce.",
|
|
130
130
|
"3. Tools — the tools the worker needs for this task.",
|
|
131
131
|
"4. Must do — the task-specific obligations.",
|
|
132
132
|
"5. Must not do — the task-specific prohibitions, plus:",
|
|
133
133
|
...STANDARD_WORKER_MUST_NOT_DO.map((rule) => ` - ${rule}`),
|
|
134
134
|
"6. Context — the files/behavior/constraints the worker needs to know.",
|
|
135
|
-
"After the dispatch:
|
|
135
|
+
"After the dispatch: the worker's own report is recorded automatically from its run, so never transcribe or re-submit it yourself and do not look for a tool to file it with. Review the full diff and the verification output against that report, then tick the task. Ticking a task whose dispatch produced no accepted report is blocked by the gate, and a report that claims files the dispatch did not change (or omits files it did) is held for reconciliation.",
|
|
136
136
|
"",
|
|
137
137
|
"Don'ts:",
|
|
138
138
|
"- Do not edit source files directly, not even for a one-line fix; dispatch opsx-worker.",
|
package/src/plan-command.ts
CHANGED
|
@@ -100,6 +100,52 @@ export function endPlanFlow(sessionId: string): string | undefined {
|
|
|
100
100
|
return session.changeId;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Restore a plan binding from the current branch (design D5).
|
|
105
|
+
*
|
|
106
|
+
* Read-only: it re-establishes which plan flow this session is in so the
|
|
107
|
+
* phase injection and status snapshots keep working after a restart. It never
|
|
108
|
+
* re-runs the command, re-approves a plan, or changes a mode.
|
|
109
|
+
*/
|
|
110
|
+
export function rebindPlanFlow(
|
|
111
|
+
sessionId: string,
|
|
112
|
+
ctx: { sessionManager?: { getBranch(): readonly unknown[] } },
|
|
113
|
+
): { changeId: string } | undefined {
|
|
114
|
+
const existing = planSessions.get(sessionId);
|
|
115
|
+
if (existing) return { changeId: existing.changeId };
|
|
116
|
+
|
|
117
|
+
let branch: Array<{ customType?: string; data?: unknown }> = [];
|
|
118
|
+
try {
|
|
119
|
+
branch = (ctx.sessionManager?.getBranch() ?? []) as typeof branch;
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
let changeId: string | undefined;
|
|
124
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
125
|
+
const entry = branch[index];
|
|
126
|
+
if (entry?.customType === OPSX_ENTRY_TYPES.modeExited) break;
|
|
127
|
+
if (entry?.customType !== OPSX_ENTRY_TYPES.modeEntered) continue;
|
|
128
|
+
const data = entry.data as
|
|
129
|
+
| { changeId?: unknown; mode?: unknown }
|
|
130
|
+
| undefined;
|
|
131
|
+
if (data?.mode === "plan" && typeof data.changeId === "string") {
|
|
132
|
+
changeId = data.changeId;
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
if (!changeId) return undefined;
|
|
137
|
+
|
|
138
|
+
planSessions.set(sessionId, {
|
|
139
|
+
changeId,
|
|
140
|
+
injected: new Set<string>(),
|
|
141
|
+
// Suppress the one-shot notices for a recovered flow: the user already saw
|
|
142
|
+
// them before the restart, and re-sending them would be noise.
|
|
143
|
+
degradedNotified: true,
|
|
144
|
+
reviewNotified: true,
|
|
145
|
+
});
|
|
146
|
+
return { changeId };
|
|
147
|
+
}
|
|
148
|
+
|
|
103
149
|
function defaultFetchStatus(
|
|
104
150
|
changeId: string,
|
|
105
151
|
options: OpenspecCliOptions,
|
package/src/progress-observer.ts
CHANGED
|
@@ -3,76 +3,70 @@
|
|
|
3
3
|
* MUST 经结构化上报(阶段标签 + 百分比,或等效字段),由主会话投影为看板展示).
|
|
4
4
|
*
|
|
5
5
|
* The reporting tools carry an optional structured `progress` field. A child's
|
|
6
|
-
* tool call runs in the child's own session, so the parent
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the board.
|
|
6
|
+
* tool call runs in the child's own session, so the parent never sees the call
|
|
7
|
+
* itself — and since the report tools are no longer registered in the main
|
|
8
|
+
* session at all (design D1), the parent cannot observe them as tool results
|
|
9
|
+
* either. Progress therefore arrives from the ingestion layer, which is the one
|
|
10
|
+
* component that holds an accepted child report (design D3).
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* This module only folds those accepted updates into one row per agent+phase
|
|
13
|
+
* and appends a `progress` flow entry whose renderer draws the board.
|
|
14
|
+
*
|
|
15
|
+
* Progress is display-only: it never gates anything, it never completes a task,
|
|
16
|
+
* and a session with no reports appends nothing.
|
|
15
17
|
*/
|
|
16
|
-
import type {
|
|
17
|
-
ExtensionAPI,
|
|
18
|
-
ExtensionContext,
|
|
19
|
-
ExtensionHandler,
|
|
20
|
-
ToolResultEvent,
|
|
21
|
-
ToolResultEventResult,
|
|
22
|
-
} from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
19
|
|
|
24
20
|
import { appendFlowEntry, OPSX_ENTRY_TYPES } from "./flow-entries.ts";
|
|
25
21
|
import {
|
|
26
22
|
progressUpdateFrom,
|
|
27
23
|
projectProgress,
|
|
28
|
-
type
|
|
24
|
+
type ProjectedProgress,
|
|
29
25
|
} from "./progress-projection.ts";
|
|
30
26
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The projected board per session.
|
|
29
|
+
*
|
|
30
|
+
* Only the folded rows are retained — one per agent+phase — so a chatty
|
|
31
|
+
* subagent cannot grow the parent's memory without bound (design D4).
|
|
32
|
+
*/
|
|
33
|
+
const boardsBySession = new Map<string, ProjectedProgress[]>();
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
/** Reset the per-session progress board. Test-only. */
|
|
36
|
+
export function resetProgressObserverForTests(): void {
|
|
37
|
+
boardsBySession.clear();
|
|
38
|
+
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} catch {
|
|
44
|
-
return "unknown";
|
|
45
|
-
}
|
|
40
|
+
/** The current board for a session. Test-observable. */
|
|
41
|
+
export function progressBoardFor(sessionId: string): ProjectedProgress[] {
|
|
42
|
+
return boardsBySession.get(sessionId) ?? [];
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
46
|
+
* Record the structured progress carried by an accepted child report.
|
|
47
|
+
*
|
|
48
|
+
* Returns whether a board entry was appended. A report without progress leaves
|
|
49
|
+
* the board untouched, and a repeated update folds into the existing row rather
|
|
50
|
+
* than appending a second entry, so duplicates never reach the interface.
|
|
51
|
+
*
|
|
52
|
+
* The caller is the ingestion layer, which only ever passes a report that
|
|
53
|
+
* already reached a successful terminal state with a matching attempt nonce —
|
|
54
|
+
* so a failed run or a stale nonce is never projected.
|
|
51
55
|
*/
|
|
52
|
-
export function
|
|
56
|
+
export function recordChildReportProgress(
|
|
53
57
|
pi: Pick<ExtensionAPI, "appendEntry">,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const update = progressUpdateFrom(event.toolName, event.input);
|
|
63
|
-
if (!update) return;
|
|
58
|
+
sessionId: string,
|
|
59
|
+
input: { toolName: string; report: Record<string, unknown> },
|
|
60
|
+
): boolean {
|
|
61
|
+
const update = progressUpdateFrom(input.toolName, input.report);
|
|
62
|
+
if (!update) return false;
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
appendFlowEntry(pi, OPSX_ENTRY_TYPES.progress, {
|
|
71
|
-
rows: projectProgress(updates),
|
|
72
|
-
});
|
|
73
|
-
};
|
|
74
|
-
}
|
|
64
|
+
const current = boardsBySession.get(sessionId) ?? [];
|
|
65
|
+
const next = projectProgress([...current, update]);
|
|
66
|
+
const changed = JSON.stringify(next) !== JSON.stringify(current);
|
|
67
|
+
boardsBySession.set(sessionId, next);
|
|
68
|
+
if (!changed) return false;
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
appendFlowEntry(pi, OPSX_ENTRY_TYPES.progress, { rows: next });
|
|
71
|
+
return true;
|
|
78
72
|
}
|
package/src/report-tools.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* number of times; projection of the calls is a later concern.
|
|
24
24
|
*/
|
|
25
25
|
import { Type } from "@earendil-works/pi-ai";
|
|
26
|
-
import { defineTool
|
|
26
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
27
27
|
|
|
28
28
|
export const REPORT_GAP_ANALYSIS_TOOL_NAME = "report_gap_analysis";
|
|
29
29
|
export const REPORT_PLAN_REVIEW_TOOL_NAME = "report_plan_review";
|
|
@@ -304,14 +304,15 @@ export const reportWorkTool = defineTool({
|
|
|
304
304
|
},
|
|
305
305
|
});
|
|
306
306
|
|
|
307
|
-
/**
|
|
307
|
+
/**
|
|
308
|
+
* The three reporting tools, in registration order.
|
|
309
|
+
*
|
|
310
|
+
* These are pure definitions: nothing here registers them into a session.
|
|
311
|
+
* Each role's child-only extension (design D1,
|
|
312
|
+
* `src/child-report-extensions/<role>.ts`) wraps exactly one of them with the
|
|
313
|
+
* attempt metadata envelope and registers that. The main session registers
|
|
314
|
+
* none of them (design D1, spec "结构化裁决上报").
|
|
315
|
+
*/
|
|
308
316
|
export function createReportTools() {
|
|
309
317
|
return [reportGapAnalysisTool, reportPlanReviewTool, reportWorkTool];
|
|
310
318
|
}
|
|
311
|
-
|
|
312
|
-
/** Register the three reporting tools into the running pi process. */
|
|
313
|
-
export function registerReportTools(pi: ExtensionAPI): void {
|
|
314
|
-
for (const tool of createReportTools()) {
|
|
315
|
-
pi.registerTool(tool);
|
|
316
|
-
}
|
|
317
|
-
}
|
package/src/worker-dispatch.ts
CHANGED
|
@@ -62,7 +62,7 @@ function normalize(filePath: string): string {
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Decide whether a task may be ticked. Holds (never ticks) when:
|
|
65
|
-
* - no report was
|
|
65
|
+
* - no accepted worker report was recorded;
|
|
66
66
|
* - the report claims files the diff does not contain (phantom work);
|
|
67
67
|
* - the diff contains files the report omitted (unclaimed work);
|
|
68
68
|
* - the verification did not run or did not pass.
|
|
@@ -77,7 +77,7 @@ export function decideTaskTick(
|
|
|
77
77
|
kind: "hold",
|
|
78
78
|
taskId,
|
|
79
79
|
reason:
|
|
80
|
-
"No
|
|
80
|
+
"No accepted worker report was recorded for this dispatch; let the worker run to completion, then tick.",
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|