@plainconceptsplatform/agent-harness 2.0.0 → 2.1.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/README.md +426 -419
- package/package.json +3 -3
- package/src/commands/join.js +244 -244
- package/src/commands/shared.js +27 -27
- package/src/commands/single.js +79 -79
- package/src/commands/update.js +109 -109
- package/src/commands/wizard.js +134 -134
- package/src/content/.agents/skills/browser-automation/SKILL.md +66 -66
- package/src/content/.agents/skills/pc-guardrails-generic/SKILL.md +68 -68
- package/src/content/.agents/skills/pc-guardrails-project/SKILL.md +8 -8
- package/src/content/.agents/skills/pc-make-architecture/SKILL.md +51 -51
- package/src/content/.agents/skills/pc-make-architecture/structure-template.md +38 -38
- package/src/content/.agents/skills/pc-make-design/SKILL.md +68 -68
- package/src/content/.agents/skills/pc-make-engineer/SKILL.md +219 -219
- package/src/content/.agents/skills/pc-make-engineer/signal-mapping.md +68 -68
- package/src/content/.agents/skills/pc-make-engineer/template.md +81 -81
- package/src/content/.agents/skills/pc-make-evidence-scaffold/SKILL.md +18 -18
- package/src/content/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +29 -29
- package/src/content/.agents/skills/pc-make-guardrails/SKILL.md +74 -74
- package/src/content/.agents/skills/pc-make-guardrails/category-reference.md +68 -68
- package/src/content/.agents/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
- package/src/content/.agents/skills/pc-make-merge-risk-assess/category-reference.md +98 -98
- package/src/content/.agents/skills/pc-make-user-model/SKILL.md +66 -66
- package/src/content/.agents/skills/pc-ops-evidence/SKILL.md +127 -127
- package/src/content/.agents/skills/pc-ops-ship/SKILL.md +18 -18
- package/src/content/.agents/skills/pc-plan-apply/SKILL.md +83 -83
- package/src/content/.agents/skills/pc-plan-apply/simple-mode.md +21 -21
- package/src/content/.agents/skills/pc-plan-archive/SKILL.md +63 -63
- package/src/content/.agents/skills/pc-plan-explore/SKILL.md +9 -9
- package/src/content/.agents/skills/pc-plan-goal/SKILL.md +94 -94
- package/src/content/.agents/skills/pc-plan-goal/branching.md +30 -30
- package/src/content/.agents/skills/pc-plan-goal/failure-policy.md +30 -30
- package/src/content/.agents/skills/pc-plan-goal/output-mode.md +9 -9
- package/src/content/.agents/skills/pc-plan-goal/output.md +68 -68
- package/src/content/.agents/skills/pc-plan-propose/SKILL.md +125 -125
- package/src/content/.agents/skills/pc-plan-propose/task-annotation.md +39 -39
- package/src/content/.agents/skills/pc-plan-quick/SKILL.md +62 -62
- package/src/content/.agents/skills/pc-plan-story/SKILL.md +146 -146
- package/src/content/.agents/skills/pc-repo-audit/SKILL.md +44 -44
- package/src/content/.agents/skills/pc-repo-help/SKILL.md +91 -91
- package/src/content/.agents/skills/pc-repo-initialize/SKILL.md +130 -130
- package/src/content/.agents/skills/pc-repo-onboard/SKILL.md +87 -87
- package/src/content/.agents/skills/pc-repo-verify/SKILL.md +34 -34
- package/src/content/.agents/skills/pc-userstory-az/SKILL.md +157 -157
- package/src/content/.agents/skills/pc-userstory-browser/SKILL.md +132 -132
- package/src/content/.agents/skills/pc-userstory-gh/SKILL.md +120 -120
- package/src/content/.agents/skills/pc-userstory-jira/SKILL.md +131 -131
- package/src/content/.opencode/_gitignore +9 -7
- package/src/content/.opencode/commands/init.md +5 -5
- package/src/content/.opencode/commands/make-architecture.md +5 -5
- package/src/content/.opencode/commands/make-design.md +5 -5
- package/src/content/.opencode/commands/make-engineer.md +5 -5
- package/src/content/.opencode/commands/make-evidence-scaffold.md +5 -5
- package/src/content/.opencode/commands/make-guardrails.md +5 -5
- package/src/content/.opencode/commands/make-user-model.md +5 -5
- package/src/content/.opencode/commands/ops-backlog.md +10 -10
- package/src/content/.opencode/commands/ops-evidence.md +9 -9
- package/src/content/.opencode/commands/ops-review.md +8 -8
- package/src/content/.opencode/commands/ops-ship.md +9 -9
- package/src/content/.opencode/commands/plan-apply.md +9 -9
- package/src/content/.opencode/commands/plan-archive.md +5 -5
- package/src/content/.opencode/commands/plan-explore.md +9 -9
- package/src/content/.opencode/commands/plan-goal.md +5 -5
- package/src/content/.opencode/commands/plan-propose.md +9 -9
- package/src/content/.opencode/commands/plan-quick.md +5 -5
- package/src/content/.opencode/commands/plan-story.md +9 -9
- package/src/content/.opencode/commands/repo-audit.md +5 -5
- package/src/content/.opencode/commands/repo-help.md +5 -5
- package/src/content/.opencode/commands/repo-initialize.md +5 -5
- package/src/content/.opencode/commands/repo-onboard.md +5 -5
- package/src/content/.opencode/commands/repo-verify.md +5 -5
- package/src/content/.opencode/plugins/pc-subagent-monitor.js +139 -139
- package/src/content/.opencode/plugins/pc-subagent-tiers.js +281 -179
- package/src/content/.opencode/plugins/pc-system-reminders.js +96 -96
- package/src/content/.opencode/tui/pc-subagents.tsx +98 -98
- package/src/content/.opencode/tui.json +6 -6
- package/src/content/AGENTS.md +71 -71
- package/src/content/opencode.jsonc +39 -31
- package/src/fragments/archive/az.md +95 -95
- package/src/fragments/archive/gh.md +94 -94
- package/src/fragments/archive/gl.md +94 -94
- package/src/fragments/archive/none.md +73 -73
- package/src/fragments/guardrails/codegraph.md +7 -7
- package/src/fragments/guardrails/humanizer.md +4 -4
- package/src/fragments/guardrails/memory.md +4 -4
- package/src/fragments/guardrails/rtk.md +3 -3
- package/src/fragments/guardrails/simple-english.md +4 -4
- package/src/fragments/ops-backlog/az.md +28 -28
- package/src/fragments/ops-backlog/gh.md +29 -29
- package/src/fragments/ops-backlog/jira.md +28 -28
- package/src/fragments/ops-evidence/az.md +41 -41
- package/src/fragments/ops-evidence/gh.md +53 -53
- package/src/fragments/ops-evidence/jira.md +38 -38
- package/src/fragments/ops-review/az.md +62 -62
- package/src/fragments/ops-review/gh.md +52 -52
- package/src/fragments/ops-review/gl.md +56 -56
- package/src/fragments/ops-ship/az.md +80 -80
- package/src/fragments/ops-ship/gh.md +68 -68
- package/src/fragments/ops-ship/gl.md +85 -85
- package/src/index.js +107 -107
- package/src/presets/agents-content.json +53 -53
- package/src/presets/models.json +68 -68
- package/src/steps/copy/agents.js +118 -118
- package/src/steps/copy/commands.js +91 -91
- package/src/steps/copy/fullstack-engineer.js +85 -83
- package/src/steps/copy/index.js +88 -88
- package/src/steps/copy/opencode-json.js +147 -129
- package/src/steps/copy/skills.js +196 -196
- package/src/steps/metadata/index.js +108 -108
- package/src/steps/models/write.js +34 -34
- package/src/steps/optimization/patch-guardrails.js +108 -108
- package/src/utils/copy.js +108 -108
- package/src/utils/legacy-check.js +30 -30
- package/src/utils/models-cache.js +58 -58
- package/src/utils/paths.js +67 -64
- package/src/utils/update-manifest.js +49 -49
- package/src/content/.opencode/plugins/pc-system-reminders.test.js +0 -35
package/src/presets/models.json
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
"costTiers": [
|
|
3
|
-
{ "max": 1, "label": "[$]" },
|
|
4
|
-
{ "max": 10, "label": "[$$]" },
|
|
5
|
-
{ "label": "[$$$]" }
|
|
6
|
-
],
|
|
7
|
-
"providers": [
|
|
8
|
-
{ "value": "opencode", "name": "OpenCode Zen" },
|
|
9
|
-
{ "value": "opencode-go", "name": "OpenCode Go" },
|
|
10
|
-
{ "value": "github-copilot", "name": "GitHub Copilot" },
|
|
11
|
-
{ "value": "anthropic", "name": "Anthropic" },
|
|
12
|
-
{ "value": "google", "name": "Google" },
|
|
13
|
-
{ "value": "openai", "name": "OpenAI" },
|
|
14
|
-
{ "value": "deepseek", "name": "DeepSeek" },
|
|
15
|
-
{ "value": "", "name": "Other (show all)" }
|
|
16
|
-
],
|
|
17
|
-
"roles": {
|
|
18
|
-
"plan": {
|
|
19
|
-
"prompt": "Plan model:",
|
|
20
|
-
"info": [
|
|
21
|
-
"PLAN: main session: reads issues, writes proposals, coordinates the agent team.",
|
|
22
|
-
"Needs to be strong. A weak model here will silently skip steps and break the workflow."
|
|
23
|
-
],
|
|
24
|
-
"suggestions": {
|
|
25
|
-
"opencode": "opencode/claude-sonnet-4",
|
|
26
|
-
"opencode-go": null,
|
|
27
|
-
"github-copilot": "github-copilot/claude-sonnet-4.5",
|
|
28
|
-
"anthropic": "anthropic/claude-sonnet-4-5",
|
|
29
|
-
"google": "google/gemini-2.5-pro",
|
|
30
|
-
"openai": "openai/o3",
|
|
31
|
-
"deepseek": null
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"build": {
|
|
35
|
-
"prompt": "Build model:",
|
|
36
|
-
"info": [
|
|
37
|
-
"BUILD: engineers: implements code, runs tests, commits.",
|
|
38
|
-
"Code quality matters here. DeepSeek V4 is outstanding value. Claude Sonnet is the gold standard."
|
|
39
|
-
],
|
|
40
|
-
"suggestions": {
|
|
41
|
-
"opencode": "opencode/deepseek-v4-flash",
|
|
42
|
-
"opencode-go": "opencode-go/deepseek-v4-pro",
|
|
43
|
-
"github-copilot": "github-copilot/claude-sonnet-4.5",
|
|
44
|
-
"anthropic": "anthropic/claude-sonnet-4-5",
|
|
45
|
-
"google": "google/gemini-2.5-flash",
|
|
46
|
-
"openai": "openai/gpt-4.1",
|
|
47
|
-
"deepseek": "deepseek/deepseek-v4-pro"
|
|
48
|
-
},
|
|
49
|
-
"agents": ["fullstack-engineer"]
|
|
50
|
-
},
|
|
51
|
-
"fast": {
|
|
52
|
-
"prompt": "Fast model:",
|
|
53
|
-
"info": [
|
|
54
|
-
"FAST: light commands: /pc-pullrequest, /pc-explore, /pc-archive, status checks.",
|
|
55
|
-
"No heavy reasoning needed. Cheap and fast is the right call here."
|
|
56
|
-
],
|
|
57
|
-
"suggestions": {
|
|
58
|
-
"opencode": "opencode/glm-4.7",
|
|
59
|
-
"opencode-go": "opencode-go/qwen3.6-plus",
|
|
60
|
-
"github-copilot": "github-copilot/gpt-5.4-nano",
|
|
61
|
-
"anthropic": "anthropic/claude-haiku-4-5",
|
|
62
|
-
"google": "google/gemini-2.0-flash",
|
|
63
|
-
"openai": "openai/gpt-4o-mini",
|
|
64
|
-
"deepseek": null
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"costTiers": [
|
|
3
|
+
{ "max": 1, "label": "[$]" },
|
|
4
|
+
{ "max": 10, "label": "[$$]" },
|
|
5
|
+
{ "label": "[$$$]" }
|
|
6
|
+
],
|
|
7
|
+
"providers": [
|
|
8
|
+
{ "value": "opencode", "name": "OpenCode Zen" },
|
|
9
|
+
{ "value": "opencode-go", "name": "OpenCode Go" },
|
|
10
|
+
{ "value": "github-copilot", "name": "GitHub Copilot" },
|
|
11
|
+
{ "value": "anthropic", "name": "Anthropic" },
|
|
12
|
+
{ "value": "google", "name": "Google" },
|
|
13
|
+
{ "value": "openai", "name": "OpenAI" },
|
|
14
|
+
{ "value": "deepseek", "name": "DeepSeek" },
|
|
15
|
+
{ "value": "", "name": "Other (show all)" }
|
|
16
|
+
],
|
|
17
|
+
"roles": {
|
|
18
|
+
"plan": {
|
|
19
|
+
"prompt": "Plan model:",
|
|
20
|
+
"info": [
|
|
21
|
+
"PLAN: main session: reads issues, writes proposals, coordinates the agent team.",
|
|
22
|
+
"Needs to be strong. A weak model here will silently skip steps and break the workflow."
|
|
23
|
+
],
|
|
24
|
+
"suggestions": {
|
|
25
|
+
"opencode": "opencode/claude-sonnet-4",
|
|
26
|
+
"opencode-go": null,
|
|
27
|
+
"github-copilot": "github-copilot/claude-sonnet-4.5",
|
|
28
|
+
"anthropic": "anthropic/claude-sonnet-4-5",
|
|
29
|
+
"google": "google/gemini-2.5-pro",
|
|
30
|
+
"openai": "openai/o3",
|
|
31
|
+
"deepseek": null
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"build": {
|
|
35
|
+
"prompt": "Build model:",
|
|
36
|
+
"info": [
|
|
37
|
+
"BUILD: engineers: implements code, runs tests, commits.",
|
|
38
|
+
"Code quality matters here. DeepSeek V4 is outstanding value. Claude Sonnet is the gold standard."
|
|
39
|
+
],
|
|
40
|
+
"suggestions": {
|
|
41
|
+
"opencode": "opencode/deepseek-v4-flash",
|
|
42
|
+
"opencode-go": "opencode-go/deepseek-v4-pro",
|
|
43
|
+
"github-copilot": "github-copilot/claude-sonnet-4.5",
|
|
44
|
+
"anthropic": "anthropic/claude-sonnet-4-5",
|
|
45
|
+
"google": "google/gemini-2.5-flash",
|
|
46
|
+
"openai": "openai/gpt-4.1",
|
|
47
|
+
"deepseek": "deepseek/deepseek-v4-pro"
|
|
48
|
+
},
|
|
49
|
+
"agents": ["fullstack-engineer"]
|
|
50
|
+
},
|
|
51
|
+
"fast": {
|
|
52
|
+
"prompt": "Fast model:",
|
|
53
|
+
"info": [
|
|
54
|
+
"FAST: light commands: /pc-pullrequest, /pc-explore, /pc-archive, status checks.",
|
|
55
|
+
"No heavy reasoning needed. Cheap and fast is the right call here."
|
|
56
|
+
],
|
|
57
|
+
"suggestions": {
|
|
58
|
+
"opencode": "opencode/glm-4.7",
|
|
59
|
+
"opencode-go": "opencode-go/qwen3.6-plus",
|
|
60
|
+
"github-copilot": "github-copilot/gpt-5.4-nano",
|
|
61
|
+
"anthropic": "anthropic/claude-haiku-4-5",
|
|
62
|
+
"google": "google/gemini-2.0-flash",
|
|
63
|
+
"openai": "openai/gpt-4o-mini",
|
|
64
|
+
"deepseek": null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/steps/copy/agents.js
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
import fse from 'fs-extra'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { fileURLToPath } from 'url'
|
|
4
|
-
import { info, success, warn } from '../../utils/exec.js'
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
-
const agentsContent = await fse.readJson(path.resolve(__dirname, '../../presets/agents-content.json'))
|
|
8
|
-
|
|
9
|
-
// Steps are matched by their title text, not by exact heading string:
|
|
10
|
-
// heading level (###/####) and step numbers have drifted before and silently
|
|
11
|
-
// broke removal. Matching `^#{3,4} Step N, <title>` survives both.
|
|
12
|
-
const HISTORY_STEP_TITLE = 'Archive project history'
|
|
13
|
-
const CHAIN_STEP_TITLE = 'Chain make commands'
|
|
14
|
-
|
|
15
|
-
const CHAIN_CONFIRM_LINE = '- ARCHITECTURE.md generated'
|
|
16
|
-
|
|
17
|
-
function stepHeadingPattern(title) {
|
|
18
|
-
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
19
|
-
return new RegExp(`^#{2,4} Step \\d+, ${escaped}`)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// The step heading is kept and its body replaced with an explicit skip note.
|
|
23
|
-
// Removing the block and renumbering the remaining steps (the old approach)
|
|
24
|
-
// went stale against prose cross-references like "Skip steps 2, 3, and 4";
|
|
25
|
-
// stable numbering plus a visible reason is unambiguous for the agent.
|
|
26
|
-
export function skipStepBlock(content, title, note) {
|
|
27
|
-
const lines = content.split('\n')
|
|
28
|
-
const pattern = stepHeadingPattern(title)
|
|
29
|
-
const start = lines.findIndex(l => pattern.test(l.trim()))
|
|
30
|
-
if (start === -1) return { content, matched: false }
|
|
31
|
-
|
|
32
|
-
let end = lines.length
|
|
33
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
34
|
-
if (lines[i].trim() === '---') { end = i; break }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
lines.splice(start + 1, end - start - 1, '', `> ${note}`, '')
|
|
38
|
-
return { content: lines.join('\n'), matched: true }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function removeConfirmLine(content, line) {
|
|
42
|
-
return content.split('\n').filter(l => l.trim() !== line.trim()).join('\n')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const PLATFORM_WORKFLOW_START = '<!-- PC-PLATFORM-WORKFLOW-START -->'
|
|
46
|
-
const PLATFORM_WORKFLOW_END = '<!-- PC-PLATFORM-WORKFLOW-END -->'
|
|
47
|
-
const PLATFORM_SKILLS_GUIDE_START = '<!-- PC-PLATFORM-SKILLS-GUIDE-START -->'
|
|
48
|
-
const PLATFORM_SKILLS_GUIDE_END = '<!-- PC-PLATFORM-SKILLS-GUIDE-END -->'
|
|
49
|
-
|
|
50
|
-
function platformContent(platform, key) {
|
|
51
|
-
const p = agentsContent.platform[platform] ?? agentsContent.platform.github
|
|
52
|
-
return p[key] ?? ''
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function mixedPlatformContent(backlogPlatform, repoPlatform, key) {
|
|
56
|
-
if (backlogPlatform === repoPlatform) {
|
|
57
|
-
return platformContent(backlogPlatform, key)
|
|
58
|
-
}
|
|
59
|
-
const backlog = agentsContent.platform[backlogPlatform] ?? agentsContent.platform.github
|
|
60
|
-
const repo = agentsContent.platform[repoPlatform] ?? agentsContent.platform.github
|
|
61
|
-
const mixed = agentsContent.platform.mixed ?? {}
|
|
62
|
-
const mixedKey = `${backlogPlatform}-${repoPlatform}`
|
|
63
|
-
return mixed[mixedKey]?.[key] ?? mixed.default?.[key]?.replace('{backlog}', backlog[key] ?? '').replace('{repo}', repo[key] ?? '') ?? platformContent(repoPlatform, key)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function replaceBetween(content, start, end, replacement) {
|
|
67
|
-
if (!content.includes(start) || !content.includes(end)) return content
|
|
68
|
-
const pattern = new RegExp(`${start}[\\s\\S]*?${end}`)
|
|
69
|
-
return content.replace(pattern, `${start}\n${replacement.trim()}\n${end}`)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function patchAgentsMd(ctx) {
|
|
73
|
-
const obInitSkillPath = path.join(process.cwd(), '.agents', 'skills', 'pc-repo-initialize', 'SKILL.md')
|
|
74
|
-
if (!await fse.pathExists(obInitSkillPath)) return
|
|
75
|
-
|
|
76
|
-
let content = await fse.readFile(obInitSkillPath, 'utf-8')
|
|
77
|
-
const patches = []
|
|
78
|
-
|
|
79
|
-
const skips = [
|
|
80
|
-
[ctx.hasOpenspec, HISTORY_STEP_TITLE, null,
|
|
81
|
-
'Skipped during onboarding: this project already had an openspec/ history. Do not archive again; continue with the next step.'],
|
|
82
|
-
[ctx.hasDesign || ctx.hasArchitecture, CHAIN_STEP_TITLE, CHAIN_CONFIRM_LINE,
|
|
83
|
-
'Skipped during onboarding: project files already exist. Run /make-architecture or /make-design individually to regenerate.'],
|
|
84
|
-
]
|
|
85
|
-
|
|
86
|
-
for (const [enabled, title, confirmLine, note] of skips) {
|
|
87
|
-
if (!enabled) continue
|
|
88
|
-
const result = skipStepBlock(content, title, note)
|
|
89
|
-
if (!result.matched) {
|
|
90
|
-
warn(`pc-repo-initialize SKILL.md step "${title}" not found: template drift? Skipping this patch.`)
|
|
91
|
-
continue
|
|
92
|
-
}
|
|
93
|
-
content = result.content
|
|
94
|
-
if (confirmLine) content = removeConfirmLine(content, confirmLine)
|
|
95
|
-
patches.push(`Step "${title}" marked as skipped, file already exists`)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (patches.length > 0) {
|
|
99
|
-
await fse.writeFile(obInitSkillPath, content, 'utf-8')
|
|
100
|
-
for (const msg of patches) info(msg)
|
|
101
|
-
success('pc-repo-initialize SKILL.md patched for existing project state')
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export async function patchAgentGuidance(backlogPlatform, repoPlatform, cwd = process.cwd()) {
|
|
106
|
-
const agentsMdPath = path.join(cwd, 'AGENTS.md')
|
|
107
|
-
if (await fse.pathExists(agentsMdPath)) {
|
|
108
|
-
const repo = repoPlatform ?? backlogPlatform ?? 'github'
|
|
109
|
-
let content = await fse.readFile(agentsMdPath, 'utf-8')
|
|
110
|
-
content = replaceBetween(content, PLATFORM_WORKFLOW_START, PLATFORM_WORKFLOW_END, mixedPlatformContent(backlogPlatform, repo, 'workflow'))
|
|
111
|
-
content = replaceBetween(content, PLATFORM_SKILLS_GUIDE_START, PLATFORM_SKILLS_GUIDE_END, mixedPlatformContent(backlogPlatform, repo, 'skillsGuide'))
|
|
112
|
-
await fse.writeFile(agentsMdPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
|
|
113
|
-
const label = backlogPlatform === repo ? repo : `${backlogPlatform}→${repo}`
|
|
114
|
-
success(`AGENTS.md patched for platform workflow: ${label}`)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { info, success, warn } from '../../utils/exec.js'
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const agentsContent = await fse.readJson(path.resolve(__dirname, '../../presets/agents-content.json'))
|
|
8
|
+
|
|
9
|
+
// Steps are matched by their title text, not by exact heading string:
|
|
10
|
+
// heading level (###/####) and step numbers have drifted before and silently
|
|
11
|
+
// broke removal. Matching `^#{3,4} Step N, <title>` survives both.
|
|
12
|
+
const HISTORY_STEP_TITLE = 'Archive project history'
|
|
13
|
+
const CHAIN_STEP_TITLE = 'Chain make commands'
|
|
14
|
+
|
|
15
|
+
const CHAIN_CONFIRM_LINE = '- ARCHITECTURE.md generated'
|
|
16
|
+
|
|
17
|
+
function stepHeadingPattern(title) {
|
|
18
|
+
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
19
|
+
return new RegExp(`^#{2,4} Step \\d+, ${escaped}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The step heading is kept and its body replaced with an explicit skip note.
|
|
23
|
+
// Removing the block and renumbering the remaining steps (the old approach)
|
|
24
|
+
// went stale against prose cross-references like "Skip steps 2, 3, and 4";
|
|
25
|
+
// stable numbering plus a visible reason is unambiguous for the agent.
|
|
26
|
+
export function skipStepBlock(content, title, note) {
|
|
27
|
+
const lines = content.split('\n')
|
|
28
|
+
const pattern = stepHeadingPattern(title)
|
|
29
|
+
const start = lines.findIndex(l => pattern.test(l.trim()))
|
|
30
|
+
if (start === -1) return { content, matched: false }
|
|
31
|
+
|
|
32
|
+
let end = lines.length
|
|
33
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
34
|
+
if (lines[i].trim() === '---') { end = i; break }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
lines.splice(start + 1, end - start - 1, '', `> ${note}`, '')
|
|
38
|
+
return { content: lines.join('\n'), matched: true }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function removeConfirmLine(content, line) {
|
|
42
|
+
return content.split('\n').filter(l => l.trim() !== line.trim()).join('\n')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PLATFORM_WORKFLOW_START = '<!-- PC-PLATFORM-WORKFLOW-START -->'
|
|
46
|
+
const PLATFORM_WORKFLOW_END = '<!-- PC-PLATFORM-WORKFLOW-END -->'
|
|
47
|
+
const PLATFORM_SKILLS_GUIDE_START = '<!-- PC-PLATFORM-SKILLS-GUIDE-START -->'
|
|
48
|
+
const PLATFORM_SKILLS_GUIDE_END = '<!-- PC-PLATFORM-SKILLS-GUIDE-END -->'
|
|
49
|
+
|
|
50
|
+
function platformContent(platform, key) {
|
|
51
|
+
const p = agentsContent.platform[platform] ?? agentsContent.platform.github
|
|
52
|
+
return p[key] ?? ''
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function mixedPlatformContent(backlogPlatform, repoPlatform, key) {
|
|
56
|
+
if (backlogPlatform === repoPlatform) {
|
|
57
|
+
return platformContent(backlogPlatform, key)
|
|
58
|
+
}
|
|
59
|
+
const backlog = agentsContent.platform[backlogPlatform] ?? agentsContent.platform.github
|
|
60
|
+
const repo = agentsContent.platform[repoPlatform] ?? agentsContent.platform.github
|
|
61
|
+
const mixed = agentsContent.platform.mixed ?? {}
|
|
62
|
+
const mixedKey = `${backlogPlatform}-${repoPlatform}`
|
|
63
|
+
return mixed[mixedKey]?.[key] ?? mixed.default?.[key]?.replace('{backlog}', backlog[key] ?? '').replace('{repo}', repo[key] ?? '') ?? platformContent(repoPlatform, key)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function replaceBetween(content, start, end, replacement) {
|
|
67
|
+
if (!content.includes(start) || !content.includes(end)) return content
|
|
68
|
+
const pattern = new RegExp(`${start}[\\s\\S]*?${end}`)
|
|
69
|
+
return content.replace(pattern, `${start}\n${replacement.trim()}\n${end}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function patchAgentsMd(ctx) {
|
|
73
|
+
const obInitSkillPath = path.join(process.cwd(), '.agents', 'skills', 'pc-repo-initialize', 'SKILL.md')
|
|
74
|
+
if (!await fse.pathExists(obInitSkillPath)) return
|
|
75
|
+
|
|
76
|
+
let content = await fse.readFile(obInitSkillPath, 'utf-8')
|
|
77
|
+
const patches = []
|
|
78
|
+
|
|
79
|
+
const skips = [
|
|
80
|
+
[ctx.hasOpenspec, HISTORY_STEP_TITLE, null,
|
|
81
|
+
'Skipped during onboarding: this project already had an openspec/ history. Do not archive again; continue with the next step.'],
|
|
82
|
+
[ctx.hasDesign || ctx.hasArchitecture, CHAIN_STEP_TITLE, CHAIN_CONFIRM_LINE,
|
|
83
|
+
'Skipped during onboarding: project files already exist. Run /make-architecture or /make-design individually to regenerate.'],
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
for (const [enabled, title, confirmLine, note] of skips) {
|
|
87
|
+
if (!enabled) continue
|
|
88
|
+
const result = skipStepBlock(content, title, note)
|
|
89
|
+
if (!result.matched) {
|
|
90
|
+
warn(`pc-repo-initialize SKILL.md step "${title}" not found: template drift? Skipping this patch.`)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
content = result.content
|
|
94
|
+
if (confirmLine) content = removeConfirmLine(content, confirmLine)
|
|
95
|
+
patches.push(`Step "${title}" marked as skipped, file already exists`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (patches.length > 0) {
|
|
99
|
+
await fse.writeFile(obInitSkillPath, content, 'utf-8')
|
|
100
|
+
for (const msg of patches) info(msg)
|
|
101
|
+
success('pc-repo-initialize SKILL.md patched for existing project state')
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function patchAgentGuidance(backlogPlatform, repoPlatform, cwd = process.cwd()) {
|
|
106
|
+
const agentsMdPath = path.join(cwd, 'AGENTS.md')
|
|
107
|
+
if (await fse.pathExists(agentsMdPath)) {
|
|
108
|
+
const repo = repoPlatform ?? backlogPlatform ?? 'github'
|
|
109
|
+
let content = await fse.readFile(agentsMdPath, 'utf-8')
|
|
110
|
+
content = replaceBetween(content, PLATFORM_WORKFLOW_START, PLATFORM_WORKFLOW_END, mixedPlatformContent(backlogPlatform, repo, 'workflow'))
|
|
111
|
+
content = replaceBetween(content, PLATFORM_SKILLS_GUIDE_START, PLATFORM_SKILLS_GUIDE_END, mixedPlatformContent(backlogPlatform, repo, 'skillsGuide'))
|
|
112
|
+
await fse.writeFile(agentsMdPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
|
|
113
|
+
const label = backlogPlatform === repo ? repo : `${backlogPlatform}→${repo}`
|
|
114
|
+
success(`AGENTS.md patched for platform workflow: ${label}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
import fse from 'fs-extra'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { fileURLToPath } from 'url'
|
|
4
|
-
import { success } from '../../utils/exec.js'
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
-
|
|
8
|
-
const _archive = {
|
|
9
|
-
azure: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/az.md'), 'utf-8'),
|
|
10
|
-
github: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gh.md'), 'utf-8'),
|
|
11
|
-
gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gl.md'), 'utf-8'),
|
|
12
|
-
none: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/none.md'), 'utf-8'),
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const _ship = {
|
|
16
|
-
github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gh.md'), 'utf-8'),
|
|
17
|
-
azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/az.md'), 'utf-8'),
|
|
18
|
-
gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gl.md'), 'utf-8'),
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const _review = {
|
|
22
|
-
github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gh.md'), 'utf-8'),
|
|
23
|
-
azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/az.md'), 'utf-8'),
|
|
24
|
-
gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gl.md'), 'utf-8'),
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const _backlog = {
|
|
28
|
-
github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/gh.md'), 'utf-8'),
|
|
29
|
-
azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/az.md'), 'utf-8'),
|
|
30
|
-
jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/jira.md'), 'utf-8'),
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Evidence comments target the backlog platform (where the issue/work-item lives).
|
|
34
|
-
const _evidence = {
|
|
35
|
-
github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/gh.md'), 'utf-8'),
|
|
36
|
-
azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/az.md'), 'utf-8'),
|
|
37
|
-
jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/jira.md'), 'utf-8'),
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// relativePath is POSIX-style relative to the project root; commands live in
|
|
41
|
-
// .opencode/commands/, skill-backed procedures (plan-archive, ops-ship) in
|
|
42
|
-
// .agents/skills/<name>/SKILL.md.
|
|
43
|
-
function patchFile(relativePath, startMarker, endMarker, content, platform, cwd = process.cwd()) {
|
|
44
|
-
const targetPath = path.join(cwd, ...relativePath.split('/'))
|
|
45
|
-
if (!fse.pathExistsSync(targetPath)) return
|
|
46
|
-
|
|
47
|
-
let fileContent = fse.readFileSync(targetPath, 'utf-8')
|
|
48
|
-
if (!fileContent.includes(startMarker) || !fileContent.includes(endMarker)) return
|
|
49
|
-
|
|
50
|
-
const pattern = new RegExp(`${startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
|
|
51
|
-
fileContent = fileContent.replace(pattern, `${startMarker}\n${content.trim()}\n${endMarker}`)
|
|
52
|
-
fse.writeFileSync(targetPath, `${fileContent.replace(/\s*$/, '')}\n`, 'utf-8')
|
|
53
|
-
success(`${relativePath} content injected for platform: ${platform}`)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function patchArchiveCommand(platform, cwd = process.cwd()) {
|
|
57
|
-
const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? platform.backlogPlatform ?? 'github') : platform
|
|
58
|
-
const replacement = _archive[repoPlatform]
|
|
59
|
-
if (!replacement) return
|
|
60
|
-
patchFile('.agents/skills/pc-plan-archive/SKILL.md', '<!-- PC-PLATFORM-ARCHIVE-START -->', '<!-- PC-PLATFORM-ARCHIVE-END -->', replacement, repoPlatform, cwd)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function patchOpsShip(platform, cwd = process.cwd()) {
|
|
64
|
-
const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
|
|
65
|
-
const replacement = _ship[repoPlatform]
|
|
66
|
-
if (!replacement) return
|
|
67
|
-
patchFile('.agents/skills/pc-ops-ship/SKILL.md', '<!-- PC-PLATFORM-SHIP-START -->', '<!-- PC-PLATFORM-SHIP-END -->', replacement, repoPlatform, cwd)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function patchOpsReview(platform, cwd = process.cwd()) {
|
|
71
|
-
const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
|
|
72
|
-
const replacement = _review[repoPlatform]
|
|
73
|
-
if (!replacement) return
|
|
74
|
-
patchFile('.opencode/commands/ops-review.md', '<!-- PC-PLATFORM-REVIEW-START -->', '<!-- PC-PLATFORM-REVIEW-END -->', replacement, repoPlatform, cwd)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export async function patchOpsBacklog(platform, cwd = process.cwd()) {
|
|
78
|
-
const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
|
|
79
|
-
const replacement = _backlog[backlogPlatform]
|
|
80
|
-
if (!replacement) return
|
|
81
|
-
patchFile('.opencode/commands/ops-backlog.md', '<!-- PC-PLATFORM-BACKLOG-START -->', '<!-- PC-PLATFORM-BACKLOG-END -->', replacement, backlogPlatform, cwd)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function patchOpsEvidence(platform, cwd = process.cwd()) {
|
|
85
|
-
const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
|
|
86
|
-
const replacement = _evidence[backlogPlatform]
|
|
87
|
-
// browser/none have no CLI to post with: leave the markers empty (the skill skips commenting).
|
|
88
|
-
if (!replacement) return
|
|
89
|
-
patchFile('.agents/skills/pc-ops-evidence/SKILL.md', '<!-- PC-PLATFORM-EVIDENCE-START -->', '<!-- PC-PLATFORM-EVIDENCE-END -->', replacement, backlogPlatform, cwd)
|
|
90
|
-
}
|
|
91
|
-
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { success } from '../../utils/exec.js'
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
const _archive = {
|
|
9
|
+
azure: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/az.md'), 'utf-8'),
|
|
10
|
+
github: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gh.md'), 'utf-8'),
|
|
11
|
+
gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gl.md'), 'utf-8'),
|
|
12
|
+
none: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/none.md'), 'utf-8'),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const _ship = {
|
|
16
|
+
github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gh.md'), 'utf-8'),
|
|
17
|
+
azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/az.md'), 'utf-8'),
|
|
18
|
+
gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gl.md'), 'utf-8'),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const _review = {
|
|
22
|
+
github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gh.md'), 'utf-8'),
|
|
23
|
+
azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/az.md'), 'utf-8'),
|
|
24
|
+
gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gl.md'), 'utf-8'),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const _backlog = {
|
|
28
|
+
github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/gh.md'), 'utf-8'),
|
|
29
|
+
azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/az.md'), 'utf-8'),
|
|
30
|
+
jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/jira.md'), 'utf-8'),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Evidence comments target the backlog platform (where the issue/work-item lives).
|
|
34
|
+
const _evidence = {
|
|
35
|
+
github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/gh.md'), 'utf-8'),
|
|
36
|
+
azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/az.md'), 'utf-8'),
|
|
37
|
+
jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/jira.md'), 'utf-8'),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// relativePath is POSIX-style relative to the project root; commands live in
|
|
41
|
+
// .opencode/commands/, skill-backed procedures (plan-archive, ops-ship) in
|
|
42
|
+
// .agents/skills/<name>/SKILL.md.
|
|
43
|
+
function patchFile(relativePath, startMarker, endMarker, content, platform, cwd = process.cwd()) {
|
|
44
|
+
const targetPath = path.join(cwd, ...relativePath.split('/'))
|
|
45
|
+
if (!fse.pathExistsSync(targetPath)) return
|
|
46
|
+
|
|
47
|
+
let fileContent = fse.readFileSync(targetPath, 'utf-8')
|
|
48
|
+
if (!fileContent.includes(startMarker) || !fileContent.includes(endMarker)) return
|
|
49
|
+
|
|
50
|
+
const pattern = new RegExp(`${startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
|
|
51
|
+
fileContent = fileContent.replace(pattern, `${startMarker}\n${content.trim()}\n${endMarker}`)
|
|
52
|
+
fse.writeFileSync(targetPath, `${fileContent.replace(/\s*$/, '')}\n`, 'utf-8')
|
|
53
|
+
success(`${relativePath} content injected for platform: ${platform}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function patchArchiveCommand(platform, cwd = process.cwd()) {
|
|
57
|
+
const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? platform.backlogPlatform ?? 'github') : platform
|
|
58
|
+
const replacement = _archive[repoPlatform]
|
|
59
|
+
if (!replacement) return
|
|
60
|
+
patchFile('.agents/skills/pc-plan-archive/SKILL.md', '<!-- PC-PLATFORM-ARCHIVE-START -->', '<!-- PC-PLATFORM-ARCHIVE-END -->', replacement, repoPlatform, cwd)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function patchOpsShip(platform, cwd = process.cwd()) {
|
|
64
|
+
const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
|
|
65
|
+
const replacement = _ship[repoPlatform]
|
|
66
|
+
if (!replacement) return
|
|
67
|
+
patchFile('.agents/skills/pc-ops-ship/SKILL.md', '<!-- PC-PLATFORM-SHIP-START -->', '<!-- PC-PLATFORM-SHIP-END -->', replacement, repoPlatform, cwd)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function patchOpsReview(platform, cwd = process.cwd()) {
|
|
71
|
+
const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
|
|
72
|
+
const replacement = _review[repoPlatform]
|
|
73
|
+
if (!replacement) return
|
|
74
|
+
patchFile('.opencode/commands/ops-review.md', '<!-- PC-PLATFORM-REVIEW-START -->', '<!-- PC-PLATFORM-REVIEW-END -->', replacement, repoPlatform, cwd)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function patchOpsBacklog(platform, cwd = process.cwd()) {
|
|
78
|
+
const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
|
|
79
|
+
const replacement = _backlog[backlogPlatform]
|
|
80
|
+
if (!replacement) return
|
|
81
|
+
patchFile('.opencode/commands/ops-backlog.md', '<!-- PC-PLATFORM-BACKLOG-START -->', '<!-- PC-PLATFORM-BACKLOG-END -->', replacement, backlogPlatform, cwd)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function patchOpsEvidence(platform, cwd = process.cwd()) {
|
|
85
|
+
const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
|
|
86
|
+
const replacement = _evidence[backlogPlatform]
|
|
87
|
+
// browser/none have no CLI to post with: leave the markers empty (the skill skips commenting).
|
|
88
|
+
if (!replacement) return
|
|
89
|
+
patchFile('.agents/skills/pc-ops-evidence/SKILL.md', '<!-- PC-PLATFORM-EVIDENCE-START -->', '<!-- PC-PLATFORM-EVIDENCE-END -->', replacement, backlogPlatform, cwd)
|
|
90
|
+
}
|
|
91
|
+
|