agentweaver 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +104 -23
  2. package/dist/artifacts.js +41 -0
  3. package/dist/index.js +252 -27
  4. package/dist/interactive/controller.js +249 -13
  5. package/dist/interactive/ink/index.js +2 -2
  6. package/dist/interactive/state.js +1 -0
  7. package/dist/interactive/web/index.js +179 -0
  8. package/dist/interactive/web/protocol.js +154 -0
  9. package/dist/interactive/web/server.js +575 -0
  10. package/dist/interactive/web/static/app.js +709 -0
  11. package/dist/interactive/web/static/index.html +77 -0
  12. package/dist/interactive/web/static/styles.css +2 -0
  13. package/dist/interactive/web/static/styles.input.css +469 -0
  14. package/dist/pipeline/flow-catalog.js +4 -0
  15. package/dist/pipeline/flow-specs/auto-common-guided.json +313 -0
  16. package/dist/pipeline/flow-specs/auto-common.json +3 -1
  17. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +2 -0
  18. package/dist/pipeline/flow-specs/design-review.json +2 -0
  19. package/dist/pipeline/flow-specs/implement.json +3 -1
  20. package/dist/pipeline/flow-specs/plan.json +4 -0
  21. package/dist/pipeline/flow-specs/playbook-init.json +199 -0
  22. package/dist/pipeline/flow-specs/review/review-fix.json +3 -1
  23. package/dist/pipeline/flow-specs/review/review-loop.json +4 -0
  24. package/dist/pipeline/flow-specs/review/review.json +2 -0
  25. package/dist/pipeline/node-registry.js +45 -0
  26. package/dist/pipeline/nodes/flow-run-node.js +13 -1
  27. package/dist/pipeline/nodes/playbook-ensure-node.js +115 -0
  28. package/dist/pipeline/nodes/playbook-inventory-node.js +51 -0
  29. package/dist/pipeline/nodes/playbook-questions-form-node.js +166 -0
  30. package/dist/pipeline/nodes/playbook-write-node.js +243 -0
  31. package/dist/pipeline/nodes/project-guidance-node.js +69 -0
  32. package/dist/pipeline/prompt-registry.js +4 -1
  33. package/dist/pipeline/prompt-runtime.js +6 -2
  34. package/dist/pipeline/spec-types.js +19 -0
  35. package/dist/pipeline/value-resolver.js +39 -1
  36. package/dist/playbook/practice-candidates.js +12 -0
  37. package/dist/playbook/repo-inventory.js +208 -0
  38. package/dist/prompts.js +31 -0
  39. package/dist/runtime/playbook.js +485 -0
  40. package/dist/runtime/project-guidance.js +339 -0
  41. package/dist/structured-artifact-schema-registry.js +8 -0
  42. package/dist/structured-artifact-schemas.json +235 -0
  43. package/dist/structured-artifacts.js +7 -1
  44. package/docs/declarative-workflows.md +565 -0
  45. package/docs/features.md +77 -0
  46. package/docs/playbook.md +327 -0
  47. package/package.json +8 -3
@@ -21,8 +21,13 @@ import { localScriptCheckNode } from "./nodes/local-script-check-node.js";
21
21
  import { llmPromptNode } from "./nodes/llm-prompt-node.js";
22
22
  import { opencodePromptNode } from "./nodes/opencode-prompt-node.js";
23
23
  import { planCodexNode } from "./nodes/plan-codex-node.js";
24
+ import { playbookInventoryNode } from "./nodes/playbook-inventory-node.js";
25
+ import { playbookEnsureNode } from "./nodes/playbook-ensure-node.js";
26
+ import { playbookQuestionsFormNode } from "./nodes/playbook-questions-form-node.js";
27
+ import { playbookWriteNode } from "./nodes/playbook-write-node.js";
24
28
  import { planningBundleNode } from "./nodes/planning-bundle-node.js";
25
29
  import { planningQuestionsFormNode } from "./nodes/planning-questions-form-node.js";
30
+ import { projectGuidanceNode } from "./nodes/project-guidance-node.js";
26
31
  import { readFileNode } from "./nodes/read-file-node.js";
27
32
  import { reviewFindingsFormNode } from "./nodes/review-findings-form-node.js";
28
33
  import { reviewVerdictNode } from "./nodes/review-verdict-node.js";
@@ -57,8 +62,13 @@ export const BUILT_IN_NODE_KINDS = [
57
62
  "llm-prompt",
58
63
  "opencode-prompt",
59
64
  "plan-codex",
65
+ "playbook-inventory",
66
+ "playbook-ensure",
67
+ "playbook-questions-form",
68
+ "playbook-write",
60
69
  "planning-bundle",
61
70
  "planning-questions-form",
71
+ "project-guidance",
62
72
  "read-file",
63
73
  "review-findings-form",
64
74
  "review-verdict",
@@ -93,8 +103,13 @@ const builtInNodes = {
93
103
  "llm-prompt": llmPromptNode,
94
104
  "opencode-prompt": opencodePromptNode,
95
105
  "plan-codex": planCodexNode,
106
+ "playbook-inventory": playbookInventoryNode,
107
+ "playbook-ensure": playbookEnsureNode,
108
+ "playbook-questions-form": playbookQuestionsFormNode,
109
+ "playbook-write": playbookWriteNode,
96
110
  "planning-bundle": planningBundleNode,
97
111
  "planning-questions-form": planningQuestionsFormNode,
112
+ "project-guidance": projectGuidanceNode,
98
113
  "read-file": readFileNode,
99
114
  "review-findings-form": reviewFindingsFormNode,
100
115
  "review-verdict": reviewVerdictNode,
@@ -228,6 +243,30 @@ const builtInNodeMetadata = {
228
243
  requiredParams: ["prompt", "requiredArtifacts"],
229
244
  executors: ["codex"],
230
245
  },
246
+ "playbook-inventory": {
247
+ kind: "playbook-inventory",
248
+ version: 1,
249
+ prompt: "forbidden",
250
+ requiredParams: ["outputJsonFile", "outputFile"],
251
+ },
252
+ "playbook-ensure": {
253
+ kind: "playbook-ensure",
254
+ version: 1,
255
+ prompt: "forbidden",
256
+ requiredParams: ["writeResultJsonFile"],
257
+ },
258
+ "playbook-questions-form": {
259
+ kind: "playbook-questions-form",
260
+ version: 1,
261
+ prompt: "forbidden",
262
+ requiredParams: ["questionsJsonFile", "answersJsonFile", "formId", "title"],
263
+ },
264
+ "playbook-write": {
265
+ kind: "playbook-write",
266
+ version: 1,
267
+ prompt: "forbidden",
268
+ requiredParams: ["draftJsonFile", "answersJsonFile", "writeResultJsonFile"],
269
+ },
231
270
  "planning-bundle": {
232
271
  kind: "planning-bundle",
233
272
  version: 1,
@@ -240,6 +279,12 @@ const builtInNodeMetadata = {
240
279
  prompt: "forbidden",
241
280
  requiredParams: ["planningQuestionsJsonFile", "formId", "title"],
242
281
  },
282
+ "project-guidance": {
283
+ kind: "project-guidance",
284
+ version: 1,
285
+ prompt: "forbidden",
286
+ requiredParams: ["taskContextJsonFile", "phase", "outputJsonFile", "outputFile"],
287
+ },
243
288
  "read-file": { kind: "read-file", version: 1, prompt: "forbidden", requiredParams: ["path"] },
244
289
  "review-findings-form": {
245
290
  kind: "review-findings-form",
@@ -64,6 +64,8 @@ export function resolveNestedFlowParams(flowKind, flowParams) {
64
64
  hasTaskInputJsonFile: contract.hasTaskInputJsonFile,
65
65
  taskInputJsonFilePath: contract.taskInputJsonFilePath,
66
66
  taskInputJsonFile: contract.taskInputJsonFile,
67
+ projectGuidanceFile: flowParams["projectGuidanceFile"] ?? "not provided",
68
+ projectGuidanceJsonFile: flowParams["projectGuidanceJsonFile"] ?? "not provided",
67
69
  }, {
68
70
  "params.designFile": contract.designFile,
69
71
  "params.designJsonFile": contract.designJsonFile,
@@ -135,6 +137,8 @@ export function resolveNestedFlowParams(flowKind, flowParams) {
135
137
  hasTaskInputJsonFile: contract.hasTaskInputJsonFile,
136
138
  taskInputJsonFilePath: contract.taskInputJsonFilePath,
137
139
  taskInputJsonFile: contract.taskInputJsonFile,
140
+ projectGuidanceFile: flowParams["projectGuidanceFile"] ?? "not provided",
141
+ projectGuidanceJsonFile: flowParams["projectGuidanceJsonFile"] ?? "not provided",
138
142
  }, {
139
143
  "params.reviewFile": contract.reviewFile,
140
144
  "params.reviewJsonFile": contract.reviewJsonFile,
@@ -188,6 +192,8 @@ export function resolveNestedFlowParams(flowKind, flowParams) {
188
192
  hasTaskInputJsonFile: contract.hasTaskInputJsonFile,
189
193
  taskInputJsonFilePath: contract.taskInputJsonFilePath,
190
194
  taskInputJsonFile: contract.taskInputJsonFile,
195
+ projectGuidanceFile: flowParams["projectGuidanceFile"] ?? "not provided",
196
+ projectGuidanceJsonFile: flowParams["projectGuidanceJsonFile"] ?? "not provided",
191
197
  }, {
192
198
  "params.designFile": contract.designFile,
193
199
  "params.designJsonFile": contract.designJsonFile,
@@ -218,7 +224,13 @@ export const flowRunNode = {
218
224
  const flow = await loadNamedDeclarativeFlow(fileName, context.cwd, {
219
225
  ...(context.registryContext ? { registryContext: context.registryContext } : {}),
220
226
  });
221
- const resolvedFlowParams = resolveNestedFlowParams(flow.kind, flowParams);
227
+ const resolvedFlowParams = resolveNestedFlowParams(flow.kind, {
228
+ projectGuidanceFile: "not provided",
229
+ projectGuidanceJsonFile: "not provided",
230
+ repairProjectGuidanceFile: "not provided",
231
+ repairProjectGuidanceJsonFile: "not provided",
232
+ ...flowParams,
233
+ });
222
234
  const resumeValue = isFlowRunResumeEnvelope(context.resumeStepValue)
223
235
  && context.resumeStepValue.flowKind === flow.kind
224
236
  && context.resumeStepValue.flowVersion === flow.version
@@ -0,0 +1,115 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
4
+ import { loadProjectPlaybook, PLAYBOOK_DIR, PLAYBOOK_MANIFEST } from "../../runtime/playbook.js";
5
+ import { validateStructuredArtifactValue } from "../../structured-artifacts.js";
6
+ function readWriteStatus(filePath) {
7
+ if (!existsSync(filePath)) {
8
+ return null;
9
+ }
10
+ try {
11
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"));
12
+ return typeof parsed.status === "string" ? parsed.status : null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function writeResult(scopeKey, filePath, result) {
19
+ const artifact = {
20
+ status: result.status,
21
+ message: result.message,
22
+ written_files: result.written_files,
23
+ skipped_files: result.skipped_files,
24
+ existing_playbook_path: result.existing_playbook_path,
25
+ intended_files: result.intended_files,
26
+ blocked_paths: result.blocked_paths,
27
+ };
28
+ validateStructuredArtifactValue(artifact, "playbook-write-result/v1", filePath);
29
+ mkdirSync(path.dirname(filePath), { recursive: true });
30
+ writeFileSync(filePath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
31
+ return [
32
+ {
33
+ kind: "artifact",
34
+ path: filePath,
35
+ required: true,
36
+ manifest: {
37
+ publish: true,
38
+ logicalKey: buildLogicalKeyForPayload(scopeKey, filePath),
39
+ payloadFamily: "structured-json",
40
+ schemaId: "playbook-write-result/v1",
41
+ schemaVersion: 1,
42
+ },
43
+ },
44
+ ];
45
+ }
46
+ export const playbookEnsureNode = {
47
+ kind: "playbook-ensure",
48
+ version: 1,
49
+ async run(context, params) {
50
+ const playbookRoot = path.join(context.cwd, PLAYBOOK_DIR);
51
+ const manifestPath = path.join(playbookRoot, PLAYBOOK_MANIFEST);
52
+ const intendedFiles = [manifestPath];
53
+ const priorStatus = params.verifyAfterInit ? readWriteStatus(params.writeResultJsonFile) : null;
54
+ if (!existsSync(manifestPath)) {
55
+ if (params.verifyAfterInit && priorStatus === "dry_run_written") {
56
+ const result = {
57
+ status: "dry_run_written",
58
+ message: "Dry-run playbook generation was accepted; manifest.yaml was not written in dry-run mode.",
59
+ written_files: [],
60
+ skipped_files: [],
61
+ existing_playbook_path: "",
62
+ intended_files: intendedFiles,
63
+ blocked_paths: [],
64
+ shouldRunPlaybookInit: false,
65
+ manifestPath,
66
+ };
67
+ return { value: result, outputs: writeResult(context.issueKey, params.writeResultJsonFile, result) };
68
+ }
69
+ const accepted = params.acceptPlaybookDraft === true;
70
+ const result = {
71
+ status: accepted ? "missing_playbook" : "blocked",
72
+ message: accepted
73
+ ? `Playbook manifest is missing: ${manifestPath}. Running playbook-init because acceptPlaybookDraft is true.`
74
+ : `Playbook manifest is missing: ${manifestPath}. Run 'agentweaver playbook-init --accept-playbook-draft' first, or rerun 'agentweaver auto-common-guided --accept-playbook-draft <jira>' to explicitly accept generated playbook content before planning.`,
75
+ written_files: [],
76
+ skipped_files: [],
77
+ existing_playbook_path: "",
78
+ intended_files: intendedFiles,
79
+ blocked_paths: accepted ? [] : [manifestPath],
80
+ shouldRunPlaybookInit: accepted,
81
+ manifestPath,
82
+ };
83
+ return { value: result, outputs: writeResult(context.issueKey, params.writeResultJsonFile, result) };
84
+ }
85
+ try {
86
+ loadProjectPlaybook(context.cwd);
87
+ }
88
+ catch (error) {
89
+ const result = {
90
+ status: "invalid_manifest",
91
+ message: `Invalid project playbook ${manifestPath}: ${error.message}`,
92
+ written_files: [],
93
+ skipped_files: [],
94
+ existing_playbook_path: "",
95
+ intended_files: intendedFiles,
96
+ blocked_paths: [manifestPath],
97
+ shouldRunPlaybookInit: false,
98
+ manifestPath,
99
+ };
100
+ return { value: result, outputs: writeResult(context.issueKey, params.writeResultJsonFile, result) };
101
+ }
102
+ const result = {
103
+ status: "skipped_valid_existing",
104
+ message: `Valid project playbook manifest exists: ${manifestPath}.`,
105
+ written_files: [],
106
+ skipped_files: [manifestPath],
107
+ existing_playbook_path: manifestPath,
108
+ intended_files: intendedFiles,
109
+ blocked_paths: [],
110
+ shouldRunPlaybookInit: false,
111
+ manifestPath,
112
+ };
113
+ return { value: result, outputs: writeResult(context.issueKey, params.writeResultJsonFile, result) };
114
+ },
115
+ };
@@ -0,0 +1,51 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
4
+ import { validateStructuredArtifactValue } from "../../structured-artifacts.js";
5
+ import { collectRepoInventory, renderRepoInventoryMarkdown } from "../../playbook/repo-inventory.js";
6
+ export const playbookInventoryNode = {
7
+ kind: "playbook-inventory",
8
+ version: 1,
9
+ async run(context, params) {
10
+ const inventory = collectRepoInventory(context.cwd);
11
+ validateStructuredArtifactValue(inventory, "repo-inventory/v1", params.outputJsonFile);
12
+ mkdirSync(path.dirname(params.outputJsonFile), { recursive: true });
13
+ mkdirSync(path.dirname(params.outputFile), { recursive: true });
14
+ writeFileSync(params.outputJsonFile, `${JSON.stringify(inventory, null, 2)}\n`, "utf8");
15
+ writeFileSync(params.outputFile, renderRepoInventoryMarkdown(inventory), "utf8");
16
+ return {
17
+ value: {
18
+ summary: inventory.summary,
19
+ outputJsonFile: params.outputJsonFile,
20
+ outputFile: params.outputFile,
21
+ evidenceCount: inventory.evidence.length,
22
+ },
23
+ outputs: [
24
+ {
25
+ kind: "artifact",
26
+ path: params.outputJsonFile,
27
+ required: true,
28
+ manifest: {
29
+ publish: true,
30
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.outputJsonFile),
31
+ payloadFamily: "structured-json",
32
+ schemaId: "repo-inventory/v1",
33
+ schemaVersion: 1,
34
+ },
35
+ },
36
+ {
37
+ kind: "artifact",
38
+ path: params.outputFile,
39
+ required: true,
40
+ manifest: {
41
+ publish: true,
42
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.outputFile),
43
+ payloadFamily: "markdown",
44
+ schemaId: "markdown/v1",
45
+ schemaVersion: 1,
46
+ },
47
+ },
48
+ ],
49
+ };
50
+ },
51
+ };
@@ -0,0 +1,166 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
4
+ import { TaskRunnerError } from "../../errors.js";
5
+ import { validateStructuredArtifactValue } from "../../structured-artifacts.js";
6
+ import { requestUserInputInTerminal } from "../../user-input.js";
7
+ function nowIso8601() {
8
+ return new Date().toISOString();
9
+ }
10
+ function readQuestions(filePath) {
11
+ if (!existsSync(filePath)) {
12
+ return [];
13
+ }
14
+ try {
15
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"));
16
+ return Array.isArray(parsed.questions) ? parsed.questions : [];
17
+ }
18
+ catch (error) {
19
+ throw new TaskRunnerError(`Failed to read playbook questions from ${filePath}: ${error.message}`);
20
+ }
21
+ }
22
+ function readExistingAnswers(filePath) {
23
+ if (!existsSync(filePath)) {
24
+ return null;
25
+ }
26
+ try {
27
+ return JSON.parse(readFileSync(filePath, "utf8"));
28
+ }
29
+ catch (error) {
30
+ throw new TaskRunnerError(`Failed to read playbook answers from ${filePath}: ${error.message}`);
31
+ }
32
+ }
33
+ function fieldForQuestion(question, index) {
34
+ const text = typeof question.text === "string" ? question.text.trim() : "";
35
+ if (!text) {
36
+ return null;
37
+ }
38
+ const id = typeof question.id === "string" && question.id.trim() ? question.id.trim() : `question_${index + 1}`;
39
+ return {
40
+ id,
41
+ type: "text",
42
+ label: text,
43
+ required: false,
44
+ multiline: true,
45
+ default: "",
46
+ ...(typeof question.rationale === "string" && question.rationale.trim() ? { help: question.rationale.trim() } : {}),
47
+ };
48
+ }
49
+ function answersFromValues(fields, values) {
50
+ return fields.map((field) => {
51
+ const value = values[field.id];
52
+ return {
53
+ question_id: field.id,
54
+ answer: typeof value === "string" ? value : value === undefined ? "" : JSON.stringify(value),
55
+ };
56
+ });
57
+ }
58
+ function writeAnswers(filePath, artifact) {
59
+ validateStructuredArtifactValue(artifact, "playbook-answers/v1", filePath);
60
+ mkdirSync(path.dirname(filePath), { recursive: true });
61
+ writeFileSync(filePath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
62
+ }
63
+ export const playbookQuestionsFormNode = {
64
+ kind: "playbook-questions-form",
65
+ version: 1,
66
+ async run(context, params) {
67
+ const mode = params.mode ?? "clarifications";
68
+ const existing = readExistingAnswers(params.answersJsonFile);
69
+ if (mode === "acceptance") {
70
+ let finalWriteAccepted = params.acceptDraft === true;
71
+ const interactive = context.requestUserInput !== requestUserInputInTerminal || (process.stdin.isTTY && process.stdout.isTTY);
72
+ if (params.acceptDraft !== true && interactive) {
73
+ const result = await (context.requestUserInput ?? requestUserInputInTerminal)({
74
+ formId: params.formId,
75
+ title: params.title,
76
+ submitLabel: "Confirm",
77
+ fields: [
78
+ {
79
+ id: "final_write_accepted",
80
+ type: "boolean",
81
+ label: "Write final .agentweaver/playbook files if safety checks pass?",
82
+ required: true,
83
+ default: false,
84
+ },
85
+ ],
86
+ });
87
+ finalWriteAccepted = result.values.final_write_accepted === true;
88
+ }
89
+ const artifact = {
90
+ summary: finalWriteAccepted ? "Final playbook write was accepted." : "Final playbook write was not accepted.",
91
+ answered_at: nowIso8601(),
92
+ answers: existing?.answers ?? [],
93
+ final_write_accepted: finalWriteAccepted,
94
+ };
95
+ writeAnswers(params.answersJsonFile, artifact);
96
+ return {
97
+ value: {
98
+ formId: params.formId,
99
+ questionCount: existing?.answers.length ?? 0,
100
+ finalWriteAccepted,
101
+ outputFile: params.answersJsonFile,
102
+ },
103
+ outputs: [
104
+ {
105
+ kind: "artifact",
106
+ path: params.answersJsonFile,
107
+ required: true,
108
+ manifest: {
109
+ publish: true,
110
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.answersJsonFile),
111
+ payloadFamily: "structured-json",
112
+ schemaId: "playbook-answers/v1",
113
+ schemaVersion: 1,
114
+ },
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ const fields = readQuestions(params.questionsJsonFile)
120
+ .map((question, index) => fieldForQuestion(question, index))
121
+ .filter((field) => field !== null);
122
+ let answers = [];
123
+ const interactive = fields.length === 0 || context.requestUserInput !== requestUserInputInTerminal || (process.stdin.isTTY && process.stdout.isTTY);
124
+ if (fields.length > 0 && !interactive) {
125
+ answers = fields.map((field) => ({ question_id: field.id, answer: "" }));
126
+ }
127
+ else if (fields.length > 0) {
128
+ const result = await (context.requestUserInput ?? requestUserInputInTerminal)({
129
+ formId: params.formId,
130
+ title: params.title,
131
+ submitLabel: "Continue",
132
+ fields,
133
+ });
134
+ answers = answersFromValues(fields, result.values);
135
+ }
136
+ const artifact = {
137
+ summary: fields.length === 0 ? "No clarification questions were required." : "Playbook clarification answers were recorded.",
138
+ answered_at: nowIso8601(),
139
+ answers,
140
+ final_write_accepted: params.acceptDraft === true,
141
+ };
142
+ writeAnswers(params.answersJsonFile, artifact);
143
+ return {
144
+ value: {
145
+ formId: params.formId,
146
+ questionCount: fields.length,
147
+ finalWriteAccepted: artifact.final_write_accepted,
148
+ outputFile: params.answersJsonFile,
149
+ },
150
+ outputs: [
151
+ {
152
+ kind: "artifact",
153
+ path: params.answersJsonFile,
154
+ required: true,
155
+ manifest: {
156
+ publish: true,
157
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.answersJsonFile),
158
+ payloadFamily: "structured-json",
159
+ schemaId: "playbook-answers/v1",
160
+ schemaVersion: 1,
161
+ },
162
+ },
163
+ ],
164
+ };
165
+ },
166
+ };