@markjaquith/agency 2.71.27 → 2.73.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.
@@ -1,7 +1,7 @@
1
1
  import { Schema, TreeFormatter } from "@effect/schema"
2
2
  import { Data, Effect, Either } from "effect"
3
3
  import { join, resolve } from "node:path"
4
- import { lstat, rename, rm } from "node:fs/promises"
4
+ import { cp, lstat, realpath, rename, rm } from "node:fs/promises"
5
5
  import { FileSystemService } from "./FileSystemService"
6
6
  import { GraphService } from "./GraphService"
7
7
  import { WorkbaseService } from "./WorkbaseService"
@@ -327,6 +327,27 @@ const replaceWithMoveStep = (
327
327
  manualRecovery: `Restore ${backup} to ${current}`,
328
328
  })
329
329
 
330
+ const runGit = (
331
+ fs: Effect.Effect.Success<typeof FileSystemService>,
332
+ args: readonly string[],
333
+ label: string,
334
+ ) =>
335
+ Effect.runPromise(
336
+ fs
337
+ .runCommand(["git", ...args], { captureOutput: true })
338
+ .pipe(
339
+ Effect.flatMap((result) =>
340
+ result.exitCode === 0
341
+ ? Effect.void
342
+ : Effect.fail(
343
+ new Error(
344
+ `${label}: ${result.stderr.trim() || result.stdout.trim()}`,
345
+ ),
346
+ ),
347
+ ),
348
+ ) as Effect.Effect<void, unknown, never>,
349
+ )
350
+
330
351
  const runTransaction = (
331
352
  state: Effect.Effect.Success<ReturnType<typeof configState>>,
332
353
  config: WorkbaseConfig,
@@ -501,6 +522,274 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
501
522
  return destination
502
523
  }),
503
524
 
525
+ materialize: (alias: string, startPath: string = process.cwd()) =>
526
+ Effect.gen(function* () {
527
+ const fs = yield* FileSystemService
528
+ const versionControl = yield* VersionControlService
529
+ const repository = yield* find(alias, startPath)
530
+ if (repository.kind !== "symlink" || !repository.target) {
531
+ return yield* new RepositoryError({
532
+ message: `Repository alias '${alias}' is not linked`,
533
+ })
534
+ }
535
+ if (repository.states.includes("invalid")) {
536
+ return yield* new RepositoryError({
537
+ message: `Repository alias '${alias}' has an invalid linked repository`,
538
+ })
539
+ }
540
+ if (repository.states.includes("remote-drifted")) {
541
+ return yield* new RepositoryError({
542
+ message: `Repository alias '${alias}' cannot be materialized while its origin differs from the portable declaration`,
543
+ })
544
+ }
545
+ if (!repository.declaredRemote) {
546
+ return yield* new RepositoryError({
547
+ message: `Repository alias '${alias}' has no portable remote declaration`,
548
+ })
549
+ }
550
+
551
+ const state = yield* configState(startPath)
552
+ const backend = yield* versionControl.forWorkbase(state.root)
553
+ if (backend.kind !== "git") {
554
+ return yield* new RepositoryError({
555
+ message: `Repository alias materialization is only supported for Git workbases`,
556
+ })
557
+ }
558
+
559
+ const source = yield* fs.realPath(repository.path)
560
+ const registered = yield* backend.listWorkspaces(repository.path)
561
+ const registeredPaths = registered
562
+ .map((workspace) => workspace.path)
563
+ .sort()
564
+ const worktrees: (typeof registered)[number][] = []
565
+ for (const workspace of registered) {
566
+ if (!(yield* fs.isDirectory(workspace.path))) {
567
+ return yield* new RepositoryError({
568
+ message: `Repository alias '${alias}' has a stale worktree registration: ${workspace.path}`,
569
+ })
570
+ }
571
+ if (
572
+ (yield* fs.inspectFile(join(workspace.path, ".git"))).kind ===
573
+ "file"
574
+ )
575
+ worktrees.push(workspace)
576
+ }
577
+
578
+ const commonDirectory = yield* fs
579
+ .runCommand(
580
+ [
581
+ "git",
582
+ "-C",
583
+ source,
584
+ "rev-parse",
585
+ "--path-format=absolute",
586
+ "--git-common-dir",
587
+ ],
588
+ { captureOutput: true },
589
+ )
590
+ .pipe(
591
+ Effect.flatMap((result) =>
592
+ result.exitCode === 0
593
+ ? Effect.succeed(result.stdout.trim())
594
+ : Effect.fail(
595
+ new RepositoryError({
596
+ message: `Failed to locate Git metadata for repository alias '${alias}'`,
597
+ }),
598
+ ),
599
+ ),
600
+ )
601
+ const sourceWorktrees = join(commonDirectory, "worktrees")
602
+ const hasWorktreeMetadata = yield* fs.isDirectory(sourceWorktrees)
603
+ if (worktrees.length > 0 && !hasWorktreeMetadata) {
604
+ return yield* new RepositoryError({
605
+ message: `Repository alias '${alias}' is missing Git metadata for its registered worktrees`,
606
+ })
607
+ }
608
+
609
+ const suffix = `${process.pid}-${Date.now()}`
610
+ const staging = join(
611
+ state.root,
612
+ "repos",
613
+ `.agency-materialize-${repository.alias}-${suffix}`,
614
+ )
615
+ const aliasBackup = join(
616
+ state.root,
617
+ "repos",
618
+ `.agency-linked-${repository.alias}-${suffix}`,
619
+ )
620
+ const metadataBackup = `${sourceWorktrees}.agency-materialize-${suffix}`
621
+ yield* fs.createDirectory(join(state.root, "repos"))
622
+ yield* backend.cloneRepository(source, staging).pipe(
623
+ Effect.catchAll((cause) =>
624
+ fs.deleteDirectory(staging).pipe(
625
+ Effect.ignore,
626
+ Effect.zipRight(
627
+ Effect.fail(
628
+ new RepositoryError({
629
+ message: `Failed to materialize repository '${alias}': ${cause instanceof Error ? cause.message : String(cause)}`,
630
+ cause,
631
+ }),
632
+ ),
633
+ ),
634
+ ),
635
+ ),
636
+ )
637
+ yield* backend
638
+ .setRemoteUrl(staging, "origin", repository.declaredRemote)
639
+ .pipe(
640
+ Effect.catchAll((cause) =>
641
+ fs
642
+ .deleteDirectory(staging)
643
+ .pipe(Effect.ignore, Effect.zipRight(Effect.fail(cause))),
644
+ ),
645
+ )
646
+ for (const workspace of worktrees) {
647
+ if (!workspace.commit) continue
648
+ const object = yield* fs.runCommand(
649
+ [
650
+ "git",
651
+ "--git-dir",
652
+ staging,
653
+ "cat-file",
654
+ "-e",
655
+ `${workspace.commit}^{commit}`,
656
+ ],
657
+ { captureOutput: true },
658
+ )
659
+ if (object.exitCode !== 0) {
660
+ yield* fs.deleteDirectory(staging).pipe(Effect.ignore)
661
+ return yield* new RepositoryError({
662
+ message: `Registered worktree commit '${workspace.commit}' is missing from the materialized repository`,
663
+ })
664
+ }
665
+ }
666
+
667
+ let metadataMoved = false
668
+ let aliasMoved = false
669
+ let cloneInstalled = false
670
+ const workspacePaths = worktrees.map((workspace) => workspace.path)
671
+ const repair = (gitDirectory: string) =>
672
+ workspacePaths.length === 0
673
+ ? Promise.resolve()
674
+ : runGit(
675
+ fs,
676
+ [
677
+ "--git-dir",
678
+ gitDirectory,
679
+ "worktree",
680
+ "repair",
681
+ ...workspacePaths,
682
+ ],
683
+ "Failed to repair Git worktrees",
684
+ )
685
+ const rollbackMigration = async () => {
686
+ const errors: unknown[] = []
687
+ if (metadataMoved) {
688
+ try {
689
+ await rename(metadataBackup, sourceWorktrees)
690
+ metadataMoved = false
691
+ await repair(commonDirectory)
692
+ } catch (error) {
693
+ errors.push(error)
694
+ }
695
+ }
696
+ if (cloneInstalled) {
697
+ try {
698
+ await rename(repository.path, staging)
699
+ cloneInstalled = false
700
+ } catch (error) {
701
+ errors.push(error)
702
+ }
703
+ }
704
+ if (aliasMoved) {
705
+ try {
706
+ await rename(aliasBackup, repository.path)
707
+ aliasMoved = false
708
+ } catch (error) {
709
+ errors.push(error)
710
+ }
711
+ }
712
+ if (errors.length > 0) throw new AggregateError(errors)
713
+ }
714
+ const migration: TransactionStep = {
715
+ label: `materialize linked repository ${repository.alias}`,
716
+ preflight: async () => {
717
+ const stats = await lstat(repository.path)
718
+ if (
719
+ !stats.isSymbolicLink() ||
720
+ (await realpath(repository.path)) !== source
721
+ )
722
+ throw new Error(
723
+ `Repository alias '${repository.alias}' changed during materialization`,
724
+ )
725
+ const current = await Effect.runPromise(
726
+ backend
727
+ .listWorkspaces(repository.path)
728
+ .pipe(
729
+ Effect.provideService(FileSystemService, fs),
730
+ ) as unknown as Effect.Effect<
731
+ readonly { readonly path: string }[],
732
+ unknown,
733
+ never
734
+ >,
735
+ )
736
+ const currentPaths = current
737
+ .map((workspace) => workspace.path)
738
+ .sort()
739
+ if (
740
+ JSON.stringify(currentPaths) !== JSON.stringify(registeredPaths)
741
+ )
742
+ throw new Error(
743
+ `Git worktree registrations changed during materialization`,
744
+ )
745
+ },
746
+ apply: async () => {
747
+ try {
748
+ if (hasWorktreeMetadata) {
749
+ await rename(sourceWorktrees, metadataBackup)
750
+ metadataMoved = true
751
+ await cp(metadataBackup, join(staging, "worktrees"), {
752
+ recursive: true,
753
+ })
754
+ }
755
+ await rename(repository.path, aliasBackup)
756
+ aliasMoved = true
757
+ await rename(staging, repository.path)
758
+ cloneInstalled = true
759
+ await repair(repository.path)
760
+ } catch (cause) {
761
+ try {
762
+ await rollbackMigration()
763
+ } catch (rollbackCause) {
764
+ throw new Error(
765
+ `Repository materialization failed and rollback requires manual recovery: restore ${aliasBackup} to ${repository.path} and ${metadataBackup} to ${sourceWorktrees}`,
766
+ { cause: new AggregateError([cause, rollbackCause]) },
767
+ )
768
+ }
769
+ throw cause
770
+ }
771
+ },
772
+ rollback: rollbackMigration,
773
+ finalize: async () => {
774
+ await rm(aliasBackup, { recursive: true, force: true })
775
+ await rm(metadataBackup, { recursive: true, force: true })
776
+ },
777
+ manualRecovery: `Restore ${aliasBackup} to ${repository.path} and ${metadataBackup} to ${sourceWorktrees}`,
778
+ }
779
+
780
+ yield* runLifecycleTransaction({
781
+ root: state.root,
782
+ preconditions: [{ path: state.path, revision: state.revision }],
783
+ steps: [migration],
784
+ }).pipe(
785
+ Effect.mapError(
786
+ (cause) => new RepositoryError({ message: cause.message, cause }),
787
+ ),
788
+ Effect.ensuring(fs.deleteDirectory(staging).pipe(Effect.ignore)),
789
+ )
790
+ return yield* find(alias, startPath)
791
+ }),
792
+
504
793
  list: (startPath: string = process.cwd()) =>
505
794
  Effect.gen(function* () {
506
795
  const fs = yield* FileSystemService
@@ -4,6 +4,76 @@ This directory is an Agency workbase. Epics, tasks, and phases are durable
4
4
  Markdown documents; repository aliases and generated Git worktrees or jj
5
5
  workspaces provide code access according to the workbase's `vcs` setting.
6
6
 
7
+ ## Command Fast Paths
8
+
9
+ When a request clearly matches one of these intents, use the exact recipe without
10
+ probing CLI help or listing unrelated workbase state. Substitute known values,
11
+ retain `--if-revision` guards when shown, and do not add flags that are not shown.
12
+
13
+ 1. Create a single-phase task only:
14
+ `agency task create <slug> --repo <alias> --base <base> --description <text> --json`.
15
+ Add `--authoritative-source <absolute-path-or-url>` only for already known
16
+ sources. Return the creation result and stop.
17
+ 2. Create, materialize, and start a single-phase task: run the create-only command,
18
+ then
19
+ `agency work prepare <slug> --evidence <creation-json-or-path> --json`.
20
+ Return the applied execution contract so the caller can run `commands.work`.
21
+ 3. Materialize an existing execution unit without starting it:
22
+ `agency work prepare <task-or-document> --json`. Return the applied execution
23
+ contract and stop. Add `--dry-run` only when the user asks for a preview.
24
+ 4. Reconcile remote pull-request state and completion:
25
+ `agency sync <task> [phase] --json`.
26
+ 5. Convert an existing single-phase task and add a phase:
27
+ `agency phase create <task> <new-phase> --first-phase <existing-phase> --repo <alias> --branch <branch> --base <base> [--depends-on <existing-phase>] --json`.
28
+ 6. Archive terminal work: first run `agency archive task <task> --dry-run --json`,
29
+ `agency archive phase <task> <phase> --dry-run --json`, or
30
+ `agency archive epic <epic> --dry-run --json`; if the preflight is safe,
31
+ repeat the same command without `--dry-run`.
32
+ 7. Create and start review work: run either
33
+ `agency task create <slug> --review <alias> --pull-request <url-or-number> --json`
34
+ or `agency task create <slug> --review <alias> --ref <remote-ref> --json`, then
35
+ run `agency work prepare <slug> --evidence <creation-json-or-path> --json` and
36
+ return the applied execution contract so the caller can run `commands.work`.
37
+ 8. Inspect one item with `agency context <task-or-document> --json`; inspect the
38
+ whole workbase with `agency status --json`.
39
+ 9. Drop work with the current document revision: use
40
+ `agency task status <task> dropped --if-revision <revision> --json` or
41
+ `agency phase status <task> <phase> dropped --if-revision <revision> --json`.
42
+ 10. Continue already materialized work: run
43
+ `agency work prepare <task-or-document> --json` and return the applied
44
+ execution contract so the caller can run `commands.work`.
45
+ 11. Publish without a pull request from the execution checkout with
46
+ `agency push --json`. Create and record a pull request with
47
+ `agency pr create <task> [phase] [--draft] [--title <title>] [--label <label>] --json`;
48
+ do not run a separate push first because `pr create` owns publication.
49
+ 12. Complete genuine non-PR work. For an active claim, run
50
+ `agency finish <task> [phase] --session-id <id> --revision <revision> --outcome done --no-pull-request --summary <text> [--evidence-url <url>]`.
51
+ Without a claim, run
52
+ `agency task status <task> done --if-revision <revision> --no-pull-request --summary <text> [--evidence-url <url>] --json`
53
+ or
54
+ `agency phase status <task> <phase> done --if-revision <revision> --no-pull-request --summary <text> [--evidence-url <url>] --json`.
55
+ 13. Create a multi-phase task initially with
56
+ `agency task create <slug> --multi-phase --description <text> --json`, then
57
+ create each execution phase with
58
+ `agency phase create <slug> <phase> --repo <alias> --branch <branch> --base <base> [--depends-on <phase>] --json`.
59
+ 14. Hand off an investigation to distinct implementation work with
60
+ `agency task handoff <investigation-task> <new-task> [--source-phase <phase>] --repo <alias> --base <base> --json`, then verify the returned destination with
61
+ `agency context <new-task> --json`. Do not prepare or start it unless requested.
62
+ 15. Refresh a pinned review task with the current revision:
63
+ `agency review refresh <task> --if-revision <revision> --json`.
64
+
65
+ Never pass `--work` or `--auto` to `agency task create`. Do not run separate
66
+ `agency validate`, `agency worktree prepare`, `agency graph`, `agency task list`,
67
+ or `agency repo list` commands before these recipes when the required parameters
68
+ are already known. `agency work prepare` owns validation, readiness checks,
69
+ workspace materialization, and the versioned `agency-execution-v1` contract.
70
+
71
+ These fast paths take precedence over separately installed Agency skill guidance.
72
+ Use `agency <command> --help` only as a recovery step when no recipe matches or a
73
+ prescribed command rejects known-current syntax. The caller owns how prepared
74
+ execution is presented and started; Agency returns domain facts and native
75
+ commands without prescribing an execution environment.
76
+
7
77
  ## Bootstrap
8
78
 
9
79
  Start every session with one read-only command:
@@ -18,7 +88,8 @@ from directory names or stale prose.
18
88
 
19
89
  At the workbase root, use `agency next --json` or `agency graph --json` to choose
20
90
  work, then inspect the returned document path or explicit entity selectors. Use
21
- `agency --help` and `agency <command> --help` for exact command syntax.
91
+ the command fast path above whenever the user's intent already identifies the
92
+ operation and required parameters.
22
93
 
23
94
  ## Adding a Repository
24
95
 
@@ -106,41 +177,6 @@ revision stale, and Agency must not silently rewrite that evidence.
106
177
 
107
178
  ## Execution
108
179
 
109
- ### Canonical create and kickoff
110
-
111
- This Agency recipe takes precedence over generic Herdr defaults whenever the
112
- request creates, opens, works, launches, starts, or kicks off an Agency item.
113
- Do not rediscover commands that this recipe or a known-current
114
- `agency-kickoff-v1` plan supplies.
115
-
116
- 1. Create noninteractively with explicit recalled context when available:
117
- `agency task create <slug> --context-repo <alias> --context-base <base> --context-slug <slug> --authoritative-source <absolute-path-or-url> --json`.
118
- Repeat `--authoritative-source` as needed. Supplied context must agree with
119
- explicit task flags; Agency rejects stale or conflicting values.
120
- 2. For create-only intent, return the creation result and stop. For open intent,
121
- prepare the task with
122
- `agency work prepare <slug> --evidence <creation-json> --dry-run --json`, then
123
- execute the returned plan through `task-document-split` to prepare the
124
- checkout and open or reuse the background tab. Stop before `agent-start`.
125
- 3. For work/launch/start/kickoff intent, run that same preflight and execute its
126
- ordered kickoff steps. The plan owns worktree dry-run/preparation, a
127
- retry-safe background Herdr tab, the side-by-side task document,
128
- `agency work . --auto`, and exactly one final
129
- `agency context <document-path> --json` verification.
130
- 4. When the orchestrator has known-current support for the plan's
131
- `agency-kickoff-v1` capability, execute the supplied actions directly. Do not
132
- call Herdr help, skill, or CLI discovery. If capability/version evidence is
133
- absent or stale, discovery is the compatibility path; then resume the same
134
- idempotency key rather than creating another tab, checkout, or agent.
135
- 5. After the one final context verification succeeds, leave the agent in the
136
- background and stop. Do not inspect, poll, or babysit it unless the user asks.
137
-
138
- Validation evidence is a local, auditable optimization, not authority. Preflight
139
- refreshes it after workbase, target document, configuration, repository mapping,
140
- payload digest, or kickoff-contract changes. Readiness, claims, repository
141
- materialization, branch ownership, reference drift, and dirty-workspace checks
142
- still run on every preparation.
143
-
144
180
  For implementation work, read the task and phase prose returned by context,
145
181
  change only the writable checkout, keep durable decisions current, and run the
146
182
  repository's formatting, type checks, build, dead-code checks, and focused tests.
@@ -171,8 +207,8 @@ environment variables. If the variables and prompt marker are absent, fail safe
171
207
  when the initial instruction is a generated `Start`, `Continue`, or `Work on`
172
208
  prompt whose absolute document paths match the current directory and the active,
173
209
  valid `agency context`: treat the process as the current worker and do not
174
- recursively launch. Herdr state is never part of worker identity. If the prompt
175
- and context disagree, stop and ask the user rather than launching.
210
+ recursively launch. External session state is never part of worker identity. If
211
+ the prompt and context disagree, stop and ask the user rather than launching.
176
212
 
177
213
  For OpenCode, Agency's managed plugin validates the generated marker against
178
214
  `agency context`, binds that identity to the OpenCode session, injects an
@@ -1,19 +1,19 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
2
  import { mkdir } from "node:fs/promises"
3
- import { join } from "node:path"
3
+ import { dirname, join } from "node:path"
4
4
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
5
5
  import { documentRevision } from "./document-revision"
6
6
  import {
7
7
  assessValidationEvidence,
8
- buildKickoffPlan,
8
+ buildExecutionContract,
9
9
  buildValidationEvidence,
10
- KICKOFF_SOURCE_LOCATIONS,
10
+ EXECUTION_SOURCE_LOCATIONS,
11
11
  normalizeRecalledContext,
12
12
  parseValidationEvidence,
13
13
  readValidationEvidence,
14
- } from "./kickoff-contract"
14
+ } from "./execution-contract"
15
15
 
16
- describe("kickoff contract", () => {
16
+ describe("execution contract", () => {
17
17
  let root: string
18
18
  let taskPath: string
19
19
  let taskContent: string
@@ -102,44 +102,60 @@ describe("kickoff contract", () => {
102
102
  )
103
103
  })
104
104
 
105
- test("plans retry-safe single-phase and phased launches with one verification", () => {
106
- const single = buildKickoffPlan({
105
+ test("describes prepared execution without prescribing orchestration", () => {
106
+ const checkoutPath = join(root, "tasks/example/code/agency")
107
+ const applied = buildExecutionContract({
107
108
  workbaseRoot: root,
108
109
  target: "execution-unit:task/example",
109
- taskId: "example",
110
110
  taskPath,
111
- checkoutPath: join(root, "tasks/example/code/agency"),
111
+ checkoutPath,
112
112
  documentRevision: "a".repeat(64),
113
+ dryRun: false,
113
114
  })
114
- const phased = buildKickoffPlan({
115
+ const phasePath = join(root, "tasks/example/phases/implementation/PHASE.md")
116
+ const preview = buildExecutionContract({
115
117
  workbaseRoot: root,
116
118
  target: "execution-unit:phase/example/implementation",
117
- taskId: "example",
118
- phaseId: "implementation",
119
119
  taskPath,
120
- phasePath: join(root, "tasks/example/phases/implementation/PHASE.md"),
120
+ phasePath,
121
+ checkoutPath: join(
122
+ root,
123
+ "tasks/example/phases/implementation/code/agency",
124
+ ),
121
125
  documentRevision: "b".repeat(64),
126
+ dryRun: true,
122
127
  })
123
- expect(single.steps[0]?.argv).toContain("example")
124
- expect(phased.steps[0]?.argv).toEqual(
125
- expect.arrayContaining(["example", "implementation"]),
126
- )
127
- expect(
128
- single.steps.filter(({ id }) => id === "final-context-verification"),
129
- ).toHaveLength(1)
130
- expect(single.orchestrator.knownCurrentCommandsBypassDiscovery).toBe(true)
131
- expect(single.sourceLocations).toEqual(KICKOFF_SOURCE_LOCATIONS)
132
- expect(
133
- single.steps.find(({ id }) => id === "herdr-tab")?.recovery,
134
- ).toContain("never create a duplicate")
135
- expect(single.idempotencyKey).toBe(
136
- buildKickoffPlan({
128
+ expect(applied).toMatchObject({
129
+ capability: "agency-execution-v1",
130
+ mode: "applied",
131
+ workspace: {
132
+ state: "materialized",
133
+ checkoutPath,
134
+ },
135
+ commands: {
136
+ work: {
137
+ cwd: dirname(taskPath),
138
+ argv: ["agency", "work", ".", "--auto"],
139
+ },
140
+ },
141
+ })
142
+ expect(applied.sourceLocations).toEqual(EXECUTION_SOURCE_LOCATIONS)
143
+ expect(preview).toMatchObject({
144
+ mode: "preview",
145
+ workspace: { state: "planned" },
146
+ plannedActions: [{ kind: "workspace-materialization" }],
147
+ commands: {
148
+ context: { cwd: dirname(phasePath) },
149
+ },
150
+ })
151
+ expect(applied.executionIdentity.key).toBe(
152
+ buildExecutionContract({
137
153
  workbaseRoot: root,
138
154
  target: "execution-unit:task/example",
139
- taskId: "example",
140
155
  taskPath,
141
156
  documentRevision: "a".repeat(64),
142
- }).idempotencyKey,
157
+ dryRun: true,
158
+ }).executionIdentity.key,
143
159
  )
144
160
  })
145
161
  })