agent-orchestrator-kit 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +23 -4
- package/bin/agent-orchestrator.js +461 -114
- package/bin/claude-cost-estimate.js +51 -0
- package/bin/session-client.js +55 -6
- package/bin/spend-collect.js +79 -13
- package/package.json +1 -1
- package/templates/.agents/commands/opsx-propose.md +6 -0
- package/templates/.agents/commands/opsx-review.md +35 -11
- package/templates/.agents/rules/session-handoff.mdc +2 -2
- package/templates/.agents/skills/agent-orchestration/SKILL.md +5 -4
- package/templates/.agents/skills/openspec-propose/SKILL.md +6 -0
- package/templates/.agents/subagents/openspec-guide.md +3 -2
- package/templates/.agents/subagents/session-handoff.md +2 -1
- package/templates/.agents/subagents/spec-architect.md +6 -1
- package/templates/.agents/subagents/spec-archiver.md +2 -1
- package/templates/.agents/subagents/spec-reviewer.md +5 -1
- package/templates/AGENTS.md +1 -1
- package/templates/scripts/cursor-spend-collect.cjs +129 -60
|
@@ -13,7 +13,8 @@ Workflow:
|
|
|
13
13
|
- conflicts with existing `openspec/specs/` requirements;
|
|
14
14
|
- scope creep vs proposal Non-goals;
|
|
15
15
|
- task self-sufficiency: a blind implementer can execute each task from Files/Do/Done-when alone, without design.md.
|
|
16
|
-
|
|
16
|
+
MUST NOT stop at the first blocking finding. Finish the full LLM checklist and a complete artifact scan of proposal.md, design.md, tasks.md, all delta specs, and referenced main specs/repo paths before writing the verdict. One ✗ still means REQUEST CHANGES, but list every blocking issue of that pass.
|
|
17
|
+
3. MUST read the existing `review.md` before overwriting it. Write `review.md` with findings ordered by severity and exactly one verdict: `APPROVE` or `REQUEST CHANGES`. REQUEST CHANGES and re-review MUST use headings Checklist, Findings (Blocker / Major / Minor), Required Before Apply, Previous findings.
|
|
17
18
|
4. On APPROVE, also write `apply-notes.md` (≤ 20 lines): critical constraints, pitfalls, what NOT to touch, verification commands. It is the second and last file you may write.
|
|
18
19
|
5. Approve only when artifacts are implementable without material guessing.
|
|
19
20
|
|
|
@@ -23,6 +24,9 @@ Rules:
|
|
|
23
24
|
- Do NOT implement fixes found during review.
|
|
24
25
|
- Do NOT substitute for `code-reviewer`; that agent reviews the implementation diff after apply.
|
|
25
26
|
- Do NOT approve based only on Tier 1 passing; verify semantics and repository references.
|
|
27
|
+
- The `Previous findings` heading is ALWAYS present after any Tier 2 pass. If no prior `review.md` existed, the body is the literal line `none — first review cycle`. If a prior file existed, each prior Required Before Apply item → `resolved` | `unresolved` plus one-line evidence.
|
|
28
|
+
- Cosmetics stay out of Required Before Apply.
|
|
29
|
+
- Later review MUST carry prior Required Before Apply into Previous findings and MUST still scan the same defect class (LLM-only only: another task whose `Do:` is not executable without design.md; another design behaviour with no delta requirement; another proposal↔tasks drift; another referenced heading/path that does not exist). Tier 1 classes NEVER enter this rescan. MUST NOT emit a one-item RC that lists only the first leftover.
|
|
26
30
|
|
|
27
31
|
Return exactly this report contract:
|
|
28
32
|
|
package/templates/AGENTS.md
CHANGED
|
@@ -25,7 +25,7 @@ Routing table, HARD STOP, and CLI forms: `.agents/rules/` (`agent-orchestration`
|
|
|
25
25
|
|
|
26
26
|
Session Start / Exit are **parent-driven** — canonical protocol in `.agents/rules/session-handoff.mdc`. Start: `status` → `handoff --restore` → `handoff.md` fallback. Exit HARD STOP: parent writes `handoff.md` including `## Metrics` (use `unknown` when a value is missing) → `npx agent-orchestrator-kit handoff <name>` (exit 0; optional `--collect`) → paste the CLI `/opsx:*` prompt. `session-handoff` subagent = fallback only. Do not start the next phase here.
|
|
27
27
|
|
|
28
|
-
Quality gates: `gate-check --tasks <name>` lints the task contract (Files/Do/Done-when, `pipeline.task_contract: warn|strict|off`); `gate-check --review <name>` is deterministic Tier 1 of review — spec-reviewer (Tier 2) is spawned only after it passes and writes `apply-notes.md` on APPROVE.
|
|
28
|
+
Quality gates: `gate-check --tasks <name>` lints the task contract (Files/Do/Done-when, `pipeline.task_contract: warn|strict|off`); `gate-check --review <name>` is deterministic Tier 1 of review — spec-reviewer (Tier 2) is spawned only after it passes and writes `apply-notes.md` on APPROVE. Spec review discovery loops ≤ 2 (optional Tier 1 structural RC plus one semantic Tier 2 RC; a confirmation APPROVE after an exhaustive propose does not count). Anti-pattern: one-finding review loop — fragments defects across many propose/review sessions.
|
|
29
29
|
|
|
30
30
|
## Hard rules
|
|
31
31
|
- One active change (unless mvp profile).
|
|
@@ -303,11 +303,13 @@ function dedupeCursorSources(sources) {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
function leftoverWindowEnd(metrics, last) {
|
|
306
|
-
if (metrics.pending && metrics.pending.startedAt) return metrics.pending.startedAt;
|
|
307
306
|
if (!last || !last.endedAt) return null;
|
|
308
307
|
const end = Date.parse(last.endedAt);
|
|
309
308
|
if (!Number.isFinite(end)) return null;
|
|
310
|
-
|
|
309
|
+
const grace = new Date(end + CURSOR_LEFTOVER_GRACE_MS).toISOString();
|
|
310
|
+
const pending = metrics.pending && metrics.pending.startedAt;
|
|
311
|
+
if (!pending || last.threadId) return pending || grace;
|
|
312
|
+
return Date.parse(pending) < Date.parse(grace) ? pending : grace;
|
|
311
313
|
}
|
|
312
314
|
|
|
313
315
|
function leftoverEndExclusive(metrics) {
|
|
@@ -317,9 +319,7 @@ function leftoverEndExclusive(metrics) {
|
|
|
317
319
|
function existingIds(metrics) {
|
|
318
320
|
const ids = new Set();
|
|
319
321
|
for (const session of metrics.sessions || []) {
|
|
320
|
-
for (const
|
|
321
|
-
if (src && src.id != null && src.id !== '') ids.add(String(src.id));
|
|
322
|
-
}
|
|
322
|
+
for (const id of session.sourceIds || []) ids.add(String(id));
|
|
323
323
|
}
|
|
324
324
|
return ids;
|
|
325
325
|
}
|
|
@@ -417,7 +417,7 @@ function sourceTotals(sources) {
|
|
|
417
417
|
}
|
|
418
418
|
|
|
419
419
|
function looksOverridden(session) {
|
|
420
|
-
const fromSources = sourceTotals(session.
|
|
420
|
+
const fromSources = sourceTotals(session.byModel || []);
|
|
421
421
|
return ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd'].some((key) => {
|
|
422
422
|
const sessionVal = numOrNull(session[key]);
|
|
423
423
|
const sourceVal = numOrNull(fromSources[key]);
|
|
@@ -489,13 +489,7 @@ function recompute(metrics) {
|
|
|
489
489
|
phase.endedAt = session.endedAt;
|
|
490
490
|
}
|
|
491
491
|
for (const spendKey of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated']) {
|
|
492
|
-
const
|
|
493
|
-
let value = fromSession;
|
|
494
|
-
if (value == null) {
|
|
495
|
-
let sum = null;
|
|
496
|
-
for (const src of session.sources || []) sum = addNullable(sum, numOrNull(src[spendKey]));
|
|
497
|
-
value = sum;
|
|
498
|
-
}
|
|
492
|
+
const value = numOrNull(session[spendKey]);
|
|
499
493
|
phase[spendKey] = addNullable(phase[spendKey], value);
|
|
500
494
|
spend[spendKey] = addNullable(spend[spendKey], value);
|
|
501
495
|
}
|
|
@@ -508,25 +502,21 @@ function recompute(metrics) {
|
|
|
508
502
|
}
|
|
509
503
|
phases[key] = phase;
|
|
510
504
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
bucket
|
|
516
|
-
bucket.outputTokens = addNullable(bucket.outputTokens, numOrNull(src.outputTokens));
|
|
517
|
-
bucket.totalTokens = addNullable(bucket.totalTokens, numOrNull(src.totalTokens));
|
|
518
|
-
bucket.costUsd = addNullable(bucket.costUsd, numOrNull(src.costUsd));
|
|
519
|
-
bucket.ampCredits = addNullable(bucket.ampCredits, numOrNull(src.ampCredits));
|
|
520
|
-
bucket.costUsdEstimated = addNullable(bucket.costUsdEstimated, numOrNull(src.costUsdEstimated));
|
|
521
|
-
if (platform === 'claude') bucket.source = 'claude-jsonl';
|
|
522
|
-
else if (platform === 'amp') bucket.source = 'amp-thread';
|
|
523
|
-
else if (platform === 'cursor') bucket.source = 'cursor-hook';
|
|
505
|
+
const platform = session.platform;
|
|
506
|
+
if (platform && byPlatform[platform]) {
|
|
507
|
+
const bucket = byPlatform[platform];
|
|
508
|
+
for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'ampCredits', 'costUsdEstimated']) {
|
|
509
|
+
bucket[key] = addNullable(bucket[key], numOrNull(session[key]));
|
|
524
510
|
}
|
|
511
|
+
if ((session.sourceIds || []).length) bucket.source = platform === 'cursor' ? 'cursor-hook' : `${platform}-jsonl`;
|
|
512
|
+
}
|
|
513
|
+
const modelRows = (session.byModel || []).length ? session.byModel : [session];
|
|
514
|
+
for (const src of modelRows) {
|
|
525
515
|
if (src.model) {
|
|
526
|
-
const modelKey = `${src.model}::${src.platform || ''}`;
|
|
516
|
+
const modelKey = `${src.model}::${src.platform || session.platform || ''}`;
|
|
527
517
|
const row = byModel.get(modelKey) || {
|
|
528
518
|
model: src.model,
|
|
529
|
-
platform: src.platform || null,
|
|
519
|
+
platform: src.platform || session.platform || null,
|
|
530
520
|
inputTokens: null,
|
|
531
521
|
outputTokens: null,
|
|
532
522
|
totalTokens: null,
|
|
@@ -576,7 +566,6 @@ function incomingCursorSources(cwd, existing, fingerprints, windowStart, windowE
|
|
|
576
566
|
const filterId = String(filterConversationId || '').trim();
|
|
577
567
|
const bestById = new Map();
|
|
578
568
|
for (const [id, row] of byId) {
|
|
579
|
-
if (existing.has(id)) continue;
|
|
580
569
|
if (filterId) {
|
|
581
570
|
const rowConversationId = row.conversationId == null || row.conversationId === ''
|
|
582
571
|
? ''
|
|
@@ -624,14 +613,7 @@ function incomingCursorSources(cwd, existing, fingerprints, windowStart, windowE
|
|
|
624
613
|
}
|
|
625
614
|
|
|
626
615
|
function existingFingerprints(metrics) {
|
|
627
|
-
|
|
628
|
-
for (const session of metrics.sessions || []) {
|
|
629
|
-
for (const src of session.sources || []) {
|
|
630
|
-
const fp = cursorSpendFingerprint(src);
|
|
631
|
-
if (fp) set.add(fp);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
return set;
|
|
616
|
+
return new Set();
|
|
635
617
|
}
|
|
636
618
|
|
|
637
619
|
function sessionHasSpendNumbers(session) {
|
|
@@ -654,7 +636,7 @@ function sessionSpendFrozen(session) {
|
|
|
654
636
|
|
|
655
637
|
function syncAdapterSessionTotals(session) {
|
|
656
638
|
if (sessionSpendFrozen(session)) return false;
|
|
657
|
-
const totals = sourceTotals(session.
|
|
639
|
+
const totals = sourceTotals(session.byModel || []);
|
|
658
640
|
let changed = false;
|
|
659
641
|
for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated']) {
|
|
660
642
|
if (session[key] !== totals[key]) {
|
|
@@ -662,7 +644,7 @@ function syncAdapterSessionTotals(session) {
|
|
|
662
644
|
changed = true;
|
|
663
645
|
}
|
|
664
646
|
}
|
|
665
|
-
if ((session.
|
|
647
|
+
if ((session.sourceIds || []).length > 0 && session.spendSource !== 'adapter') {
|
|
666
648
|
session.spendSource = 'adapter';
|
|
667
649
|
changed = true;
|
|
668
650
|
}
|
|
@@ -670,21 +652,12 @@ function syncAdapterSessionTotals(session) {
|
|
|
670
652
|
}
|
|
671
653
|
|
|
672
654
|
function enrichMetrics(metrics, cwd) {
|
|
673
|
-
const byId = loadCursorUsageById(cwd);
|
|
674
655
|
let changed = false;
|
|
675
656
|
for (const session of metrics.sessions || []) {
|
|
676
|
-
const deduped = dedupeCursorSources(session.sources || []);
|
|
677
|
-
if (deduped.length !== (session.sources || []).length) {
|
|
678
|
-
session.sources = deduped;
|
|
679
|
-
changed = true;
|
|
680
|
-
} else {
|
|
681
|
-
session.sources = deduped;
|
|
682
|
-
}
|
|
683
|
-
if (attachCursorEstimates(session.sources || [], byId)) changed = true;
|
|
684
657
|
if (syncAdapterSessionTotals(session)) changed = true;
|
|
685
658
|
if (
|
|
686
659
|
session.spendSource === 'unreported'
|
|
687
|
-
&& (session.inputTokens != null || session.totalTokens != null || (session.
|
|
660
|
+
&& (session.inputTokens != null || session.totalTokens != null || (session.sourceIds || []).length)
|
|
688
661
|
) {
|
|
689
662
|
session.spendSource = 'adapter';
|
|
690
663
|
changed = true;
|
|
@@ -693,6 +666,101 @@ function enrichMetrics(metrics, cwd) {
|
|
|
693
666
|
return changed;
|
|
694
667
|
}
|
|
695
668
|
|
|
669
|
+
function compactModelRows(sources) {
|
|
670
|
+
const rows = new Map();
|
|
671
|
+
for (const source of sources || []) {
|
|
672
|
+
if (!source) continue;
|
|
673
|
+
const model = source.model == null || source.model === '' ? null : String(source.model);
|
|
674
|
+
const key = `${source.platform || ''}::${model || ''}`;
|
|
675
|
+
const row = rows.get(key) || {
|
|
676
|
+
model,
|
|
677
|
+
platform: source.platform || null,
|
|
678
|
+
inputTokens: null,
|
|
679
|
+
outputTokens: null,
|
|
680
|
+
totalTokens: null,
|
|
681
|
+
costUsd: null,
|
|
682
|
+
costUsdEstimated: null,
|
|
683
|
+
};
|
|
684
|
+
for (const field of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd']) {
|
|
685
|
+
row[field] = addNullable(row[field], numOrNull(source[field]));
|
|
686
|
+
}
|
|
687
|
+
row.costUsdEstimated = roundUsd4(addNullable(row.costUsdEstimated, numOrNull(source.costUsdEstimated)));
|
|
688
|
+
if (source.costSource) row.costSource = source.costSource;
|
|
689
|
+
rows.set(key, row);
|
|
690
|
+
}
|
|
691
|
+
return [...rows.values()];
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function mergeModelRows(current, incoming) {
|
|
695
|
+
return compactModelRows([...(current || []), ...(incoming || [])]);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function normalizeMetricsV2(metrics) {
|
|
699
|
+
metrics.version = 2;
|
|
700
|
+
metrics.sessions = Array.isArray(metrics.sessions) ? metrics.sessions : [];
|
|
701
|
+
for (const session of metrics.sessions) {
|
|
702
|
+
if (Array.isArray(session.sources)) {
|
|
703
|
+
const bestById = new Map();
|
|
704
|
+
for (const source of session.sources) {
|
|
705
|
+
if (!source || source.id == null || source.id === '') continue;
|
|
706
|
+
const id = String(source.id);
|
|
707
|
+
const previous = bestById.get(id);
|
|
708
|
+
if (!previous || (numOrNull(source.totalTokens) ?? 0) >= (numOrNull(previous.totalTokens) ?? 0)) bestById.set(id, source);
|
|
709
|
+
}
|
|
710
|
+
const compactSources = [...bestById.values()].map((source) => ({
|
|
711
|
+
...source,
|
|
712
|
+
model: source.model || session.model || null,
|
|
713
|
+
platform: source.platform || session.platform || null,
|
|
714
|
+
}));
|
|
715
|
+
session.sourceIds = [];
|
|
716
|
+
session.sourceTotals = {};
|
|
717
|
+
for (const source of compactSources) {
|
|
718
|
+
if (!source || source.id == null || source.id === '') continue;
|
|
719
|
+
const id = String(source.id);
|
|
720
|
+
if (!session.sourceIds.includes(id)) session.sourceIds.push(id);
|
|
721
|
+
const total = numOrNull(source.totalTokens);
|
|
722
|
+
if (total != null) session.sourceTotals[id] = Math.max(numOrNull(session.sourceTotals[id]) ?? 0, total);
|
|
723
|
+
}
|
|
724
|
+
session.byModel = compactModelRows(compactSources);
|
|
725
|
+
if (!session.platform && compactSources[0] && compactSources[0].platform) session.platform = compactSources[0].platform;
|
|
726
|
+
if (!session.model && compactSources[0] && compactSources[0].model) session.model = compactSources[0].model;
|
|
727
|
+
delete session.sources;
|
|
728
|
+
} else {
|
|
729
|
+
session.sourceIds = Array.isArray(session.sourceIds) ? [...new Set(session.sourceIds.map(String))] : [];
|
|
730
|
+
session.sourceTotals = session.sourceTotals && typeof session.sourceTotals === 'object' ? session.sourceTotals : {};
|
|
731
|
+
session.byModel = Array.isArray(session.byModel) ? session.byModel : [];
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return metrics;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function mergeIncomingCompact(session, incoming) {
|
|
738
|
+
session.sourceIds = Array.isArray(session.sourceIds) ? session.sourceIds : [];
|
|
739
|
+
session.sourceTotals = session.sourceTotals && typeof session.sourceTotals === 'object' ? session.sourceTotals : {};
|
|
740
|
+
const deltaSources = [];
|
|
741
|
+
for (const source of incoming || []) {
|
|
742
|
+
const id = String(source.id);
|
|
743
|
+
const previous = numOrNull(session.sourceTotals[id]);
|
|
744
|
+
const next = numOrNull(source.totalTokens);
|
|
745
|
+
if (!session.sourceIds.includes(id)) session.sourceIds.push(id);
|
|
746
|
+
if (next != null) session.sourceTotals[id] = Math.max(previous ?? 0, next);
|
|
747
|
+
if (previous == null || next == null) {
|
|
748
|
+
deltaSources.push(source);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const delta = Math.max(0, next - previous);
|
|
752
|
+
if (!delta) continue;
|
|
753
|
+
const ratio = next > 0 ? delta / next : 0;
|
|
754
|
+
const copy = { ...source };
|
|
755
|
+
for (const field of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated']) {
|
|
756
|
+
const value = numOrNull(copy[field]);
|
|
757
|
+
if (value != null) copy[field] = field.includes('Usd') ? roundUsd4(value * ratio) : Math.round(value * ratio);
|
|
758
|
+
}
|
|
759
|
+
deltaSources.push(copy);
|
|
760
|
+
}
|
|
761
|
+
session.byModel = mergeModelRows(session.byModel, compactModelRows(deltaSources));
|
|
762
|
+
}
|
|
763
|
+
|
|
696
764
|
function backfillMetricsFile(cwd, filePath) {
|
|
697
765
|
if (!existsSync(filePath)) return;
|
|
698
766
|
let metrics;
|
|
@@ -702,18 +770,14 @@ function backfillMetricsFile(cwd, filePath) {
|
|
|
702
770
|
return;
|
|
703
771
|
}
|
|
704
772
|
if (!metrics || typeof metrics !== 'object') return;
|
|
773
|
+
const schemaChanged = metrics.version !== 2 || (metrics.sessions || []).some((session) => Array.isArray(session.sources));
|
|
774
|
+
normalizeMetricsV2(metrics);
|
|
705
775
|
const sessions = Array.isArray(metrics.sessions) ? metrics.sessions : [];
|
|
706
776
|
if (!sessions.length) return;
|
|
707
|
-
let collapsed = false;
|
|
708
|
-
for (const session of sessions) {
|
|
709
|
-
const next = dedupeCursorSources(session.sources || []);
|
|
710
|
-
if (next.length !== (session.sources || []).length) collapsed = true;
|
|
711
|
-
session.sources = next;
|
|
712
|
-
}
|
|
713
777
|
const last = sessions[sessions.length - 1];
|
|
714
778
|
const byId = loadCursorUsageById(cwd);
|
|
715
779
|
const leftoverEnd = leftoverWindowEnd(metrics, last);
|
|
716
|
-
const incoming = last.endedAt && leftoverEnd
|
|
780
|
+
const incoming = (last.endedAt && leftoverEnd
|
|
717
781
|
? incomingCursorSources(
|
|
718
782
|
cwd,
|
|
719
783
|
existingIds(metrics),
|
|
@@ -724,11 +788,16 @@ function backfillMetricsFile(cwd, filePath) {
|
|
|
724
788
|
leftoverEndExclusive(metrics),
|
|
725
789
|
last.threadId,
|
|
726
790
|
)
|
|
727
|
-
: []
|
|
791
|
+
: []).filter((source) => {
|
|
792
|
+
const previous = numOrNull(last.sourceTotals && last.sourceTotals[source.id]);
|
|
793
|
+
return previous == null || (numOrNull(source.totalTokens) ?? 0) > previous;
|
|
794
|
+
});
|
|
728
795
|
if (incoming.length) {
|
|
729
|
-
|
|
796
|
+
mergeIncomingCompact(last, incoming);
|
|
797
|
+
if (!last.platform) last.platform = 'cursor';
|
|
798
|
+
if (!last.model && incoming[0].model) last.model = incoming[0].model;
|
|
730
799
|
if (!sessionSpendFrozen(last)) {
|
|
731
|
-
const totals = sourceTotals(last.
|
|
800
|
+
const totals = sourceTotals(last.byModel);
|
|
732
801
|
last.inputTokens = totals.inputTokens;
|
|
733
802
|
last.outputTokens = totals.outputTokens;
|
|
734
803
|
last.totalTokens = totals.totalTokens;
|
|
@@ -738,7 +807,7 @@ function backfillMetricsFile(cwd, filePath) {
|
|
|
738
807
|
}
|
|
739
808
|
}
|
|
740
809
|
const enriched = enrichMetrics(metrics, cwd);
|
|
741
|
-
if (!incoming.length && !enriched && !
|
|
810
|
+
if (!incoming.length && !enriched && !schemaChanged) return;
|
|
742
811
|
metrics.updatedAt = new Date().toISOString();
|
|
743
812
|
recompute(metrics);
|
|
744
813
|
writeFileSync(filePath, `${JSON.stringify(metrics, null, 2)}\n`);
|