openspecpm 0.1.0-alpha.0 → 1.0.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 +109 -86
- package/README.md +373 -352
- package/cli/bin/openspecpm.js +201 -198
- package/cli/src/audit.js +12 -3
- package/cli/src/bdd/judge.js +216 -0
- package/cli/src/commands/doctor.js +11 -0
- package/cli/src/commands/propose.js +41 -6
- package/cli/src/commands/sync.js +27 -1
- package/cli/src/commands/validate.js +32 -1
- package/package.json +2 -1
- package/skill/openspecpm/SKILL.md +74 -74
- package/skill/openspecpm/references/conventions.md +106 -105
- package/skill/openspecpm/references/structure.md +52 -52
- package/skill/openspecpm/references/sync.md +56 -56
package/cli/bin/openspecpm.js
CHANGED
|
@@ -1,198 +1,201 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { audited } from '../src/audit.js';
|
|
4
|
-
import { runInit } from '../src/commands/init.js';
|
|
5
|
-
import { runDoctor } from '../src/commands/doctor.js';
|
|
6
|
-
import { runPropose } from '../src/commands/propose.js';
|
|
7
|
-
import { runDecompose } from '../src/commands/decompose.js';
|
|
8
|
-
import { runSync } from '../src/commands/sync.js';
|
|
9
|
-
import { runComment } from '../src/commands/comment.js';
|
|
10
|
-
import { runReconcile } from '../src/commands/reconcile.js';
|
|
11
|
-
import { runStatus } from '../src/commands/status.js';
|
|
12
|
-
import { runStandup } from '../src/commands/standup.js';
|
|
13
|
-
import { runNext } from '../src/commands/next.js';
|
|
14
|
-
import { runBlocked } from '../src/commands/blocked.js';
|
|
15
|
-
import { runValidate } from '../src/commands/validate.js';
|
|
16
|
-
import { runSearch } from '../src/commands/search.js';
|
|
17
|
-
import { runFanOut } from '../src/commands/fan-out.js';
|
|
18
|
-
import { runBugReport } from '../src/commands/bug-report.js';
|
|
19
|
-
import { runShip } from '../src/commands/ship.js';
|
|
20
|
-
import { runHelp } from '../src/commands/help.js';
|
|
21
|
-
import { runAssign } from '../src/commands/assign.js';
|
|
22
|
-
import { runSyncAll, runShipAllReady } from '../src/commands/bulk.js';
|
|
23
|
-
import { runWatch } from '../src/commands/watch.js';
|
|
24
|
-
|
|
25
|
-
const program = new Command();
|
|
26
|
-
|
|
27
|
-
program
|
|
28
|
-
.name('openspecpm')
|
|
29
|
-
.description('Spec-driven, BDD-shaped project management for AI agents.')
|
|
30
|
-
.version('0.1.0-alpha.0');
|
|
31
|
-
|
|
32
|
-
program
|
|
33
|
-
.command('init')
|
|
34
|
-
.description('Interactive wizard: pick a PM tool and write .openspecpm/config.json')
|
|
35
|
-
.option('--non-interactive', 'Fail instead of prompting when input is required')
|
|
36
|
-
.action((opts) => audited('init', runInit)(opts).catch(fatal));
|
|
37
|
-
|
|
38
|
-
program
|
|
39
|
-
.command('doctor [adapter]')
|
|
40
|
-
.description('Check auth + tooling health for one or all adapters')
|
|
41
|
-
.option('--install', 'Print OS-specific install commands for missing CLIs')
|
|
42
|
-
.option('--setup-auth', 'Print PAT/token URLs and required scopes')
|
|
43
|
-
.action((adapter, opts) => audited('doctor', runDoctor)({ adapter, install: opts.install, setupAuth: opts.setupAuth }).catch(fatal));
|
|
44
|
-
|
|
45
|
-
program
|
|
46
|
-
.command('propose <feature>')
|
|
47
|
-
.description('Create an OpenSpec proposal (proposal.md, design.md, tasks.md, specs/) for <feature>')
|
|
48
|
-
.option('-p, --prompt <text>', 'One-line description for the AI to seed the proposal')
|
|
49
|
-
.option('-t, --type <type>', 'Change type: feature | bug | refactor | incident', 'feature')
|
|
50
|
-
.option('--offline', 'Scaffold from templates without calling the openspec CLI')
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
.
|
|
63
|
-
.option('--
|
|
64
|
-
.option('--
|
|
65
|
-
.option('--
|
|
66
|
-
.option('
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
.
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
.
|
|
151
|
-
.option('--
|
|
152
|
-
.option('--
|
|
153
|
-
.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.
|
|
166
|
-
.
|
|
167
|
-
.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { audited } from '../src/audit.js';
|
|
4
|
+
import { runInit } from '../src/commands/init.js';
|
|
5
|
+
import { runDoctor } from '../src/commands/doctor.js';
|
|
6
|
+
import { runPropose } from '../src/commands/propose.js';
|
|
7
|
+
import { runDecompose } from '../src/commands/decompose.js';
|
|
8
|
+
import { runSync } from '../src/commands/sync.js';
|
|
9
|
+
import { runComment } from '../src/commands/comment.js';
|
|
10
|
+
import { runReconcile } from '../src/commands/reconcile.js';
|
|
11
|
+
import { runStatus } from '../src/commands/status.js';
|
|
12
|
+
import { runStandup } from '../src/commands/standup.js';
|
|
13
|
+
import { runNext } from '../src/commands/next.js';
|
|
14
|
+
import { runBlocked } from '../src/commands/blocked.js';
|
|
15
|
+
import { runValidate } from '../src/commands/validate.js';
|
|
16
|
+
import { runSearch } from '../src/commands/search.js';
|
|
17
|
+
import { runFanOut } from '../src/commands/fan-out.js';
|
|
18
|
+
import { runBugReport } from '../src/commands/bug-report.js';
|
|
19
|
+
import { runShip } from '../src/commands/ship.js';
|
|
20
|
+
import { runHelp } from '../src/commands/help.js';
|
|
21
|
+
import { runAssign } from '../src/commands/assign.js';
|
|
22
|
+
import { runSyncAll, runShipAllReady } from '../src/commands/bulk.js';
|
|
23
|
+
import { runWatch } from '../src/commands/watch.js';
|
|
24
|
+
|
|
25
|
+
const program = new Command();
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.name('openspecpm')
|
|
29
|
+
.description('Spec-driven, BDD-shaped project management for AI agents.')
|
|
30
|
+
.version('0.1.0-alpha.0');
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('init')
|
|
34
|
+
.description('Interactive wizard: pick a PM tool and write .openspecpm/config.json')
|
|
35
|
+
.option('--non-interactive', 'Fail instead of prompting when input is required')
|
|
36
|
+
.action((opts) => audited('init', runInit)(opts).catch(fatal));
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command('doctor [adapter]')
|
|
40
|
+
.description('Check auth + tooling health for one or all adapters')
|
|
41
|
+
.option('--install', 'Print OS-specific install commands for missing CLIs')
|
|
42
|
+
.option('--setup-auth', 'Print PAT/token URLs and required scopes')
|
|
43
|
+
.action((adapter, opts) => audited('doctor', runDoctor)({ adapter, install: opts.install, setupAuth: opts.setupAuth }).catch(fatal));
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('propose <feature>')
|
|
47
|
+
.description('Create an OpenSpec proposal (proposal.md, design.md, tasks.md, specs/) for <feature>')
|
|
48
|
+
.option('-p, --prompt <text>', 'One-line description for the AI to seed the proposal')
|
|
49
|
+
.option('-t, --type <type>', 'Change type: feature | bug | refactor | incident', 'feature')
|
|
50
|
+
.option('--offline', 'Scaffold from templates without calling the openspec CLI')
|
|
51
|
+
.option('--llm', 'Augment BDD soft-lint with the LLM judge (requires ANTHROPIC_API_KEY)')
|
|
52
|
+
.action((feature, opts) => audited('propose', runPropose)({ feature, prompt: opts.prompt, type: opts.type, offline: opts.offline, llm: opts.llm }).catch(fatal));
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.command('decompose <feature>')
|
|
56
|
+
.description('Extract tasks from proposal + BDD scenarios into tasks.md')
|
|
57
|
+
.option('--force', 'Merge into an existing tasks.md instead of refusing')
|
|
58
|
+
.action((feature, opts) => audited('decompose', runDecompose)({ feature, force: opts.force }).catch(fatal));
|
|
59
|
+
|
|
60
|
+
program
|
|
61
|
+
.command('sync [feature]')
|
|
62
|
+
.description('Push an OpenSpec change to the configured PM tool (idempotent; BDD-linted)')
|
|
63
|
+
.option('--all', 'Sync every change under openspec/changes/')
|
|
64
|
+
.option('--dry-run', 'Print the call plan without making remote changes')
|
|
65
|
+
.option('--force', 'Bypass BDD lint errors')
|
|
66
|
+
.option('--diff', 'Show the call plan in detail')
|
|
67
|
+
.option('--llm', 'Augment BDD hard-lint with the LLM judge (requires ANTHROPIC_API_KEY)')
|
|
68
|
+
.option('-y, --yes', 'Skip the confirmation prompt for --all')
|
|
69
|
+
.action((feature, opts) => {
|
|
70
|
+
if (opts.all) {
|
|
71
|
+
return audited('sync-all', runSyncAll)({ dryRun: opts.dryRun, force: opts.force, yes: opts.yes }).catch(fatal);
|
|
72
|
+
}
|
|
73
|
+
if (!feature) {
|
|
74
|
+
process.stderr.write('error: provide a <feature> or pass --all\n');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
return audited('sync', runSync)({ feature, dryRun: opts.dryRun, force: opts.force, diff: opts.diff, llm: opts.llm }).catch(fatal);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
program
|
|
81
|
+
.command('comment <feature> <task>')
|
|
82
|
+
.description('Post a progress comment to the PM tool work item')
|
|
83
|
+
.option('-m, --message <text>', 'Inline message (otherwise reads from local progress.md)')
|
|
84
|
+
.option('--dry-run', 'Print the comment instead of posting')
|
|
85
|
+
.action((feature, task, opts) =>
|
|
86
|
+
audited('comment', runComment)({ feature, task, message: opts.message, dryRun: opts.dryRun }).catch(fatal),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.command('reconcile <feature>')
|
|
91
|
+
.description('Pull remote state back into local task frontmatter')
|
|
92
|
+
.option('--dry-run', 'Show drift without writing local changes')
|
|
93
|
+
.action((feature, opts) => audited('reconcile', runReconcile)({ feature, dryRun: opts.dryRun }).catch(fatal));
|
|
94
|
+
|
|
95
|
+
program
|
|
96
|
+
.command('status')
|
|
97
|
+
.description('Local snapshot: configured adapter + per-change task counts')
|
|
98
|
+
.action(() => audited('status', runStatus)().catch(fatal));
|
|
99
|
+
|
|
100
|
+
program
|
|
101
|
+
.command('standup')
|
|
102
|
+
.description('Show progress updates within a recent window')
|
|
103
|
+
.option('--since <window>', 'Time window: e.g. 12h, 2d, 1w', '24h')
|
|
104
|
+
.option('--broadcast', 'Also POST to configured Slack/Teams/generic webhooks')
|
|
105
|
+
.action((opts) => audited('standup', runStandup)({ since: opts.since, broadcast: opts.broadcast }).catch(fatal));
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command('next')
|
|
109
|
+
.description('List tasks ready to start (no unmet dependencies)')
|
|
110
|
+
.option('-l, --limit <n>', 'Max items to show', (v) => parseInt(v, 10), 5)
|
|
111
|
+
.action((opts) => audited('next', runNext)({ limit: opts.limit }).catch(fatal));
|
|
112
|
+
|
|
113
|
+
program
|
|
114
|
+
.command('blocked')
|
|
115
|
+
.description('List tasks waiting on unmet dependencies')
|
|
116
|
+
.action(() => audited('blocked', runBlocked)().catch(fatal));
|
|
117
|
+
|
|
118
|
+
program
|
|
119
|
+
.command('validate')
|
|
120
|
+
.description('Schema + dependency + BDD-lint sweep across every change')
|
|
121
|
+
.option('--llm', 'Augment BDD lint with the LLM judge (requires ANTHROPIC_API_KEY)')
|
|
122
|
+
.action((opts) => audited('validate', runValidate)({ llm: opts.llm }).catch(fatal));
|
|
123
|
+
|
|
124
|
+
program
|
|
125
|
+
.command('search <query>')
|
|
126
|
+
.description('Grep across proposals, specs, tasks, and progress notes')
|
|
127
|
+
.option('-l, --limit <n>', 'Max matches to show', (v) => parseInt(v, 10), 50)
|
|
128
|
+
.option('--case-sensitive', 'Match case')
|
|
129
|
+
.action((query, opts) =>
|
|
130
|
+
audited('search', runSearch)({ query, limit: opts.limit, caseSensitive: opts.caseSensitive }).catch(fatal),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
program
|
|
134
|
+
.command('fan-out <feature>')
|
|
135
|
+
.description('Emit ready-to-paste agent prompts for parallel:true tasks')
|
|
136
|
+
.option('-l, --limit <n>', 'Max prompts to emit', (v) => parseInt(v, 10), 5)
|
|
137
|
+
.action((feature, opts) => audited('fan-out', runFanOut)({ feature, limit: opts.limit }).catch(fatal));
|
|
138
|
+
|
|
139
|
+
program
|
|
140
|
+
.command('bug-report <feature> <task>')
|
|
141
|
+
.description('File a regression bug linked to a shipped task')
|
|
142
|
+
.option('-t, --title <text>', 'Bug title (required)')
|
|
143
|
+
.option('-b, --body <text>', 'Bug body')
|
|
144
|
+
.action((feature, task, opts) =>
|
|
145
|
+
audited('bug-report', runBugReport)({ feature, task, title: opts.title, body: opts.body }).catch(fatal),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
program
|
|
149
|
+
.command('assign <feature> <task>')
|
|
150
|
+
.description('Set assignee / sprint / iteration / area / story-points on a synced task')
|
|
151
|
+
.option('--assignee <id>', 'Assignee id (backend-specific)')
|
|
152
|
+
.option('--sprint <id>', 'Sprint / cycle / milestone id (whichever the backend supports)')
|
|
153
|
+
.option('--iteration <path>', 'Iteration path (Azure DevOps; alias for --sprint elsewhere)')
|
|
154
|
+
.option('--area <path>', 'Area path (Azure DevOps)')
|
|
155
|
+
.option('--story-points <n>', 'Story points / estimate / weight')
|
|
156
|
+
.action((feature, task, opts) =>
|
|
157
|
+
audited('assign', runAssign)({
|
|
158
|
+
feature, task,
|
|
159
|
+
assignee: opts.assignee, sprint: opts.sprint, iteration: opts.iteration,
|
|
160
|
+
area: opts.area, storyPoints: opts.storyPoints,
|
|
161
|
+
}).catch(fatal),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
program
|
|
165
|
+
.command('ship [feature]')
|
|
166
|
+
.description('Close all work items for <feature> and archive the OpenSpec change')
|
|
167
|
+
.option('--all-ready', 'Ship every change whose tasks are all synced (no pending/failed)')
|
|
168
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
169
|
+
.option('--skip-archive', 'Close work items but leave openspec/changes/<feature>/ in place')
|
|
170
|
+
.action((feature, opts) => {
|
|
171
|
+
if (opts.allReady) {
|
|
172
|
+
return audited('ship-all-ready', runShipAllReady)({ yes: opts.yes, skipArchive: opts.skipArchive }).catch(fatal);
|
|
173
|
+
}
|
|
174
|
+
if (!feature) {
|
|
175
|
+
process.stderr.write('error: provide a <feature> or pass --all-ready\n');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
return audited('ship', runShip)({ feature, yes: opts.yes, skipArchive: opts.skipArchive }).catch(fatal);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
program
|
|
182
|
+
.command('watch [feature]')
|
|
183
|
+
.description('Re-lint on file change. Use --all to validate the whole project on every change.')
|
|
184
|
+
.option('--all', 'Validate every change instead of linting one feature')
|
|
185
|
+
.option('--debounce <ms>', 'Debounce window in milliseconds', (v) => parseInt(v, 10), 300)
|
|
186
|
+
.action((feature, opts) =>
|
|
187
|
+
audited('watch', runWatch)({ feature, allChanges: opts.all, debounceMs: opts.debounce }).catch(fatal),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
program
|
|
191
|
+
.command('help-table [topic]')
|
|
192
|
+
.description('Context-aware help grouped by workflow phase')
|
|
193
|
+
.action((topic) => { runHelp({ topic }); });
|
|
194
|
+
|
|
195
|
+
program.parseAsync(process.argv);
|
|
196
|
+
|
|
197
|
+
function fatal(err) {
|
|
198
|
+
process.stderr.write(`\n✖ ${err.message}\n`);
|
|
199
|
+
if (err.remediation) process.stderr.write(` → ${err.remediation}\n`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
package/cli/src/audit.js
CHANGED
|
@@ -9,7 +9,7 @@ export function auditPath(cwd = process.cwd()) {
|
|
|
9
9
|
return join(cwd, DIR, FILE);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export async function record({ command, args = {}, result = null, error = null, cwd = process.cwd() } = {}) {
|
|
12
|
+
export async function record({ command, args = {}, result = null, error = null, meta = null, cwd = process.cwd() } = {}) {
|
|
13
13
|
if (!command) return;
|
|
14
14
|
const path = auditPath(cwd);
|
|
15
15
|
await mkdir(dirname(path), { recursive: true });
|
|
@@ -20,6 +20,7 @@ export async function record({ command, args = {}, result = null, error = null,
|
|
|
20
20
|
result: result ? truncate(result, 500) : null,
|
|
21
21
|
error: error ? truncate(typeof error === 'string' ? error : error.message ?? String(error), 500) : null,
|
|
22
22
|
};
|
|
23
|
+
if (meta && typeof meta === 'object') entry.meta = scrub(meta);
|
|
23
24
|
await appendFile(path, JSON.stringify(entry) + '\n', 'utf8');
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -33,14 +34,22 @@ export async function tail(n = 50, cwd = process.cwd()) {
|
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const
|
|
37
|
+
const SECRET_SEGMENTS = new Set(['token', 'secret', 'password', 'pat', 'auth', 'credential']);
|
|
38
|
+
|
|
39
|
+
function isSecretKey(k) {
|
|
40
|
+
if (/api[_-]?key/i.test(k)) return true;
|
|
41
|
+
for (const seg of k.toLowerCase().split(/[^a-z]+/)) {
|
|
42
|
+
if (seg && SECRET_SEGMENTS.has(seg)) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
37
46
|
|
|
38
47
|
function scrub(obj) {
|
|
39
48
|
if (!obj || typeof obj !== 'object') return obj;
|
|
40
49
|
if (Array.isArray(obj)) return obj.map(scrub);
|
|
41
50
|
const out = {};
|
|
42
51
|
for (const [k, v] of Object.entries(obj)) {
|
|
43
|
-
if (
|
|
52
|
+
if (isSecretKey(k)) {
|
|
44
53
|
out[k] = '<redacted>';
|
|
45
54
|
} else if (v && typeof v === 'object') {
|
|
46
55
|
out[k] = scrub(v);
|