oh-my-opencode 4.18.2 → 4.19.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.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +4 -4
- package/README.ru.md +2 -2
- package/README.zh-cn.md +2 -2
- package/dist/agents/atlas/agent.d.ts +6 -5
- package/dist/agents/sisyphus/index.d.ts +5 -0
- package/dist/agents/sisyphus/kimi-k3.d.ts +19 -0
- package/dist/agents/sisyphus-agent-factory.d.ts +1 -1
- package/dist/agents/sisyphus-junior/agent.d.ts +8 -4
- package/dist/agents/sisyphus-junior/index.d.ts +2 -0
- package/dist/agents/sisyphus-junior/kimi-k3.d.ts +12 -0
- package/dist/agents/types.d.ts +2 -2
- package/dist/cli/index.js +648 -136
- package/dist/cli-node/index.js +648 -136
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schema/browser-automation.d.ts +1 -0
- package/dist/config/schema/commands.d.ts +1 -3
- package/dist/config/schema/default-mode.d.ts +1 -1
- package/dist/config/schema/goal.d.ts +7 -0
- package/dist/config/schema/hooks.d.ts +1 -1
- package/dist/config/schema/oh-my-opencode-config.d.ts +6 -10
- package/dist/config/schema/ralph-loop.d.ts +1 -1
- package/dist/config/schema.d.ts +1 -1
- package/dist/create-hooks.d.ts +1 -1
- package/dist/features/builtin-commands/templates/goal.d.ts +1 -0
- package/dist/features/builtin-commands/templates/stop-continuation.d.ts +1 -1
- package/dist/features/builtin-commands/types.d.ts +1 -1
- package/dist/hooks/auto-slash-command/executor.d.ts +1 -0
- package/dist/hooks/goal/command-arguments.d.ts +13 -0
- package/dist/hooks/goal/controller.d.ts +26 -0
- package/dist/hooks/goal/index.d.ts +23 -0
- package/dist/hooks/goal/prompt.d.ts +3 -0
- package/dist/hooks/goal/store.d.ts +8 -0
- package/dist/hooks/goal/tools.d.ts +10 -0
- package/dist/hooks/goal/types.d.ts +93 -0
- package/dist/hooks/goal/validation.d.ts +4 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/keyword-detector/hook.d.ts +1 -2
- package/dist/hooks/ralph-loop/types.d.ts +1 -1
- package/dist/index.js +3515 -4365
- package/dist/oh-my-opencode.schema.json +23 -19
- package/dist/plugin/chat-message/loop-commands.d.ts +1 -1
- package/dist/plugin/chat-message/types.d.ts +23 -10
- package/dist/plugin/hooks/create-core-hooks.d.ts +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +3 -2
- package/dist/plugin/hooks/create-transform-hooks.d.ts +0 -2
- package/dist/plugin/stop-continuation.d.ts +2 -2
- package/dist/shared/system-directive.d.ts +0 -1
- package/dist/skills/ulw-plan/SKILL.md +11 -7
- package/dist/skills/ulw-plan/references/full-workflow.md +117 -6
- package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +46 -10
- package/dist/skills/ulw-research/SKILL.md +3 -3
- package/dist/tools/delegate-task/types.d.ts +0 -1
- package/dist/tui.js +73 -25
- package/package.json +14 -14
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +352 -17
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/AGENTS.md +7 -6
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +97 -30
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/test/hook-exclusion.test.ts +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/AGENTS.md +1 -0
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/AGENTS.md +8 -0
- package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +147 -60
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +3 -71
- package/packages/omo-codex/plugin/components/start-work-continuation/src/plan-checklist.ts +171 -0
- package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +198 -23
- package/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts +4 -3
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +6 -5
- package/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-scaffold.md +15 -0
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/AGENTS.md +10 -10
- package/packages/omo-codex/plugin/components/ultrawork/agents/momus.toml +3 -3
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +11 -7
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +117 -6
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/scripts/scaffold-plan.mjs +46 -10
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/multi-agent-v2-guard.mjs +32 -28
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/subagent-limit-guard.mjs +17 -45
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/toml-section-editor.mjs +455 -0
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +3 -18
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +11 -7
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +117 -6
- package/packages/omo-codex/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +46 -10
- package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +2 -2
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +2 -2
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +6 -6
- package/packages/omo-codex/plugin/test/multi-agent-v2-regression.test.mjs +3 -3
- package/packages/omo-codex/plugin/test/scaffold-plan.test.mjs +62 -0
- package/packages/omo-codex/plugin/test/subagent-limit-migration.test.mjs +271 -14
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +3 -18
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +0 -5
- package/packages/omo-codex/plugin/test/ulw-plan-review-state-contract.test.mjs +293 -0
- package/packages/omo-codex/scripts/install-config.test.mjs +10 -10
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +353 -18
- package/packages/omo-codex/scripts/install-generated-bundle.test.mjs +39 -2
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -7
- package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +117 -6
- package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +46 -10
- package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
- package/dist/features/builtin-commands/templates/ralph-loop.d.ts +0 -3
- package/dist/skills/ultraresearch/SKILL.md +0 -10
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +0 -273
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +0 -12
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/scripts/create-pr-body.mjs +0 -107
- package/packages/shared-skills/skills/lcx-doctor/SKILL.md +0 -128
- package/packages/shared-skills/skills/lcx-doctor/agents/openai.yaml +0 -11
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +0 -268
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +0 -11
- package/packages/shared-skills/skills/ultraresearch/SKILL.md +0 -10
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-contribute-bug-fix/SKILL.md +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-contribute-bug-fix/agents/openai.yaml +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-contribute-bug-fix/scripts/create-pr-body.mjs +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-doctor/SKILL.md +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-doctor/agents/openai.yaml +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-report-bug/SKILL.md +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-report-bug/agents/openai.yaml +0 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
export function findTomlSection(config, headerLine) {
|
|
2
|
+
const targetHeaderPath = parseTomlTableHeader(headerLine);
|
|
3
|
+
const lines = config.match(/[^\n]*\n?|$/g) ?? [];
|
|
4
|
+
let offset = 0;
|
|
5
|
+
let start = -1;
|
|
6
|
+
let multilineQuote = null;
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
if (line.length === 0) break;
|
|
9
|
+
const multilineScan = scanTomlMultilineLine(line, multilineQuote);
|
|
10
|
+
multilineQuote = multilineScan.nextQuote;
|
|
11
|
+
if (multilineScan.wasInside) {
|
|
12
|
+
offset += line.length;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (start === -1) {
|
|
16
|
+
if (tomlTableHeaderMatches(line, targetHeaderPath)) start = offset;
|
|
17
|
+
} else if (isTomlTableHeaderLine(line)) {
|
|
18
|
+
return { start, end: offset, text: config.slice(start, offset) };
|
|
19
|
+
}
|
|
20
|
+
offset += line.length;
|
|
21
|
+
}
|
|
22
|
+
if (start === -1) return null;
|
|
23
|
+
return { start, end: config.length, text: config.slice(start) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function hasTomlSetting(config, keyPath) {
|
|
27
|
+
const targetPath = parseTomlDottedKey(keyPath);
|
|
28
|
+
if (!targetPath) return false;
|
|
29
|
+
|
|
30
|
+
let tablePath = [];
|
|
31
|
+
let multilineQuote = null;
|
|
32
|
+
for (const line of config.split("\n")) {
|
|
33
|
+
const multilineScan = scanTomlMultilineLine(line, multilineQuote);
|
|
34
|
+
multilineQuote = multilineScan.nextQuote;
|
|
35
|
+
if (multilineScan.wasInside) continue;
|
|
36
|
+
const normalizedLine = stripUnquotedInlineComment(line).trim();
|
|
37
|
+
if (normalizedLine.length === 0) continue;
|
|
38
|
+
|
|
39
|
+
const headerPath = parseTomlTableHeader(normalizedLine);
|
|
40
|
+
if (headerPath) {
|
|
41
|
+
tablePath = headerPath;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (isTomlTableHeaderLine(normalizedLine)) {
|
|
45
|
+
tablePath = null;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!tablePath) continue;
|
|
49
|
+
|
|
50
|
+
const assignmentIndex = findUnquotedAssignment(normalizedLine);
|
|
51
|
+
if (assignmentIndex < 0) continue;
|
|
52
|
+
const settingPath = parseTomlDottedKey(normalizedLine.slice(0, assignmentIndex).trim());
|
|
53
|
+
if (!settingPath) continue;
|
|
54
|
+
const fullPath = [...tablePath, ...settingPath];
|
|
55
|
+
if (fullPath.length !== targetPath.length) continue;
|
|
56
|
+
if (fullPath.every((part, index) => part === targetPath[index])) return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function replaceOrInsertRootTomlSetting(config, keyPath, value) {
|
|
62
|
+
const targetPath = parseTomlDottedKey(keyPath);
|
|
63
|
+
if (!targetPath) return config;
|
|
64
|
+
const match = findRootTomlSetting(config, targetPath);
|
|
65
|
+
if (match) {
|
|
66
|
+
const commentIndex = findUnquotedComment(match.lineBody, match.assignmentIndex + 1);
|
|
67
|
+
const comment = commentIndex === -1 ? "" : ` ${match.lineBody.slice(commentIndex).trimStart()}`;
|
|
68
|
+
const replacement = `${match.lineBody.slice(0, match.assignmentIndex + 1)} ${value}${comment}${match.newline}`;
|
|
69
|
+
return config.slice(0, match.offset) + replacement + config.slice(match.offset + match.line.length);
|
|
70
|
+
}
|
|
71
|
+
const firstTable = findFirstTomlTableStart(config);
|
|
72
|
+
const root = config.slice(0, firstTable).trimEnd();
|
|
73
|
+
const suffix = config.slice(firstTable);
|
|
74
|
+
const replacement = `${root}${root.length > 0 ? "\n" : ""}${keyPath} = ${value}\n`;
|
|
75
|
+
if (suffix.length === 0) return replacement;
|
|
76
|
+
return `${replacement.trimEnd()}\n\n${suffix.trimStart()}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function replaceOrInsertTomlSectionSetting(config, section, keyPath, value) {
|
|
80
|
+
const targetPath = parseTomlDottedKey(keyPath);
|
|
81
|
+
if (!targetPath) return config;
|
|
82
|
+
const lines = section.text.match(/[^\n]*\n?|$/g) ?? [];
|
|
83
|
+
let offset = 0;
|
|
84
|
+
let multilineQuote = null;
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (line.length === 0) break;
|
|
87
|
+
const multilineScan = scanTomlMultilineLine(line, multilineQuote);
|
|
88
|
+
multilineQuote = multilineScan.nextQuote;
|
|
89
|
+
if (multilineScan.wasInside) {
|
|
90
|
+
offset += line.length;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const assignmentIndex = findUnquotedAssignment(line);
|
|
94
|
+
if (assignmentIndex >= 0) {
|
|
95
|
+
const settingPath = parseTomlDottedKey(line.slice(0, assignmentIndex).trim());
|
|
96
|
+
if (settingPath && tomlPathMatches(settingPath, targetPath)) {
|
|
97
|
+
const replacement = replaceTomlAssignmentValue(line, assignmentIndex, value);
|
|
98
|
+
const patched = section.text.slice(0, offset) + replacement + section.text.slice(offset + line.length);
|
|
99
|
+
return config.slice(0, section.start) + patched + config.slice(section.end);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
offset += line.length;
|
|
103
|
+
}
|
|
104
|
+
const headerEnd = section.text.indexOf("\n");
|
|
105
|
+
const insertAt = headerEnd === -1 ? section.text.length : headerEnd + 1;
|
|
106
|
+
const patched = `${section.text.slice(0, insertAt)}${headerEnd === -1 ? "\n" : ""}${keyPath} = ${value}\n${section.text.slice(insertAt)}`;
|
|
107
|
+
return config.slice(0, section.start) + patched + config.slice(section.end);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function removeTomlSectionSetting(config, section, keyPath, expectedValue) {
|
|
111
|
+
const targetPath = parseTomlDottedKey(keyPath);
|
|
112
|
+
if (!targetPath) return config;
|
|
113
|
+
const lines = section.text.match(/[^\n]*\n?|$/g) ?? [];
|
|
114
|
+
let offset = 0;
|
|
115
|
+
let multilineQuote = null;
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
if (line.length === 0) break;
|
|
118
|
+
const multilineScan = scanTomlMultilineLine(line, multilineQuote);
|
|
119
|
+
multilineQuote = multilineScan.nextQuote;
|
|
120
|
+
if (multilineScan.wasInside) {
|
|
121
|
+
offset += line.length;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const assignmentIndex = findUnquotedAssignment(line);
|
|
125
|
+
if (assignmentIndex >= 0) {
|
|
126
|
+
const settingPath = parseTomlDottedKey(line.slice(0, assignmentIndex).trim());
|
|
127
|
+
if (settingPath && tomlPathMatches(settingPath, targetPath)) {
|
|
128
|
+
const lineBody = line.endsWith("\n") ? line.slice(0, -1) : line;
|
|
129
|
+
const commentIndex = findUnquotedComment(lineBody, assignmentIndex + 1);
|
|
130
|
+
const valueEnd = commentIndex === -1 ? lineBody.length : commentIndex;
|
|
131
|
+
if (lineBody.slice(assignmentIndex + 1, valueEnd).trim() !== expectedValue) return config;
|
|
132
|
+
const patched = section.text.slice(0, offset) + section.text.slice(offset + line.length);
|
|
133
|
+
return config.slice(0, section.start) + patched + config.slice(section.end);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
offset += line.length;
|
|
137
|
+
}
|
|
138
|
+
return config;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function removeRootTomlSetting(config, keyPath, expectedValue) {
|
|
142
|
+
const targetPath = parseTomlDottedKey(keyPath);
|
|
143
|
+
if (!targetPath) return config;
|
|
144
|
+
const match = findRootTomlSetting(config, targetPath);
|
|
145
|
+
if (!match) return config;
|
|
146
|
+
const commentIndex = findUnquotedComment(match.lineBody, match.assignmentIndex + 1);
|
|
147
|
+
const valueEnd = commentIndex === -1 ? match.lineBody.length : commentIndex;
|
|
148
|
+
if (match.lineBody.slice(match.assignmentIndex + 1, valueEnd).trim() !== expectedValue) return config;
|
|
149
|
+
return config.slice(0, match.offset) + config.slice(match.offset + match.line.length);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function tomlTableHeaderMatches(line, targetHeaderPath) {
|
|
153
|
+
if (!targetHeaderPath) return false;
|
|
154
|
+
const candidateHeaderPath = parseTomlTableHeader(line);
|
|
155
|
+
if (!candidateHeaderPath || candidateHeaderPath.length !== targetHeaderPath.length) return false;
|
|
156
|
+
return candidateHeaderPath.every((part, index) => part === targetHeaderPath[index]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isTomlTableHeaderLine(line) {
|
|
160
|
+
const normalizedLine = stripUnquotedInlineComment(line).trim();
|
|
161
|
+
return normalizedLine.startsWith("[") && normalizedLine.endsWith("]");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function scanTomlMultilineLine(line, currentQuote) {
|
|
165
|
+
if (currentQuote) {
|
|
166
|
+
return {
|
|
167
|
+
wasInside: true,
|
|
168
|
+
nextQuote: findTomlMultilineDelimiter(line, currentQuote, 0) === -1 ? currentQuote : null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let quote = null;
|
|
173
|
+
let index = 0;
|
|
174
|
+
while (index < line.length) {
|
|
175
|
+
const char = line[index];
|
|
176
|
+
if (quote === '"') {
|
|
177
|
+
if (char === "\\") {
|
|
178
|
+
index += 2;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (char === '"') quote = null;
|
|
182
|
+
index += 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (quote === "'") {
|
|
186
|
+
if (char === "'") quote = null;
|
|
187
|
+
index += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (char === "#") break;
|
|
191
|
+
const delimiter = line.startsWith('"""', index) ? '"""' : line.startsWith("'''", index) ? "'''" : null;
|
|
192
|
+
if (delimiter) {
|
|
193
|
+
const closingIndex = findTomlMultilineDelimiter(line, delimiter, index + delimiter.length);
|
|
194
|
+
return { wasInside: false, nextQuote: closingIndex === -1 ? delimiter : null };
|
|
195
|
+
}
|
|
196
|
+
if (char === '"' || char === "'") quote = char;
|
|
197
|
+
index += 1;
|
|
198
|
+
}
|
|
199
|
+
return { wasInside: false, nextQuote: null };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function findTomlMultilineDelimiter(line, delimiter, startIndex) {
|
|
203
|
+
let index = line.indexOf(delimiter, startIndex);
|
|
204
|
+
while (index !== -1) {
|
|
205
|
+
if (delimiter === "'''" || countPrecedingBackslashes(line, index) % 2 === 0) return index;
|
|
206
|
+
index = line.indexOf(delimiter, index + 1);
|
|
207
|
+
}
|
|
208
|
+
return -1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function countPrecedingBackslashes(line, index) {
|
|
212
|
+
let count = 0;
|
|
213
|
+
let cursor = index - 1;
|
|
214
|
+
while (cursor >= 0 && line[cursor] === "\\") {
|
|
215
|
+
count += 1;
|
|
216
|
+
cursor -= 1;
|
|
217
|
+
}
|
|
218
|
+
return count;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function findRootTomlSetting(config, targetPath) {
|
|
222
|
+
const lines = config.match(/[^\n]*\n?|$/g) ?? [];
|
|
223
|
+
let offset = 0;
|
|
224
|
+
let multilineQuote = null;
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
if (line.length === 0) break;
|
|
227
|
+
const multilineScan = scanTomlMultilineLine(line, multilineQuote);
|
|
228
|
+
multilineQuote = multilineScan.nextQuote;
|
|
229
|
+
if (multilineScan.wasInside) {
|
|
230
|
+
offset += line.length;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (isTomlTableHeaderLine(line)) break;
|
|
234
|
+
const assignmentIndex = findUnquotedAssignment(line);
|
|
235
|
+
if (assignmentIndex >= 0) {
|
|
236
|
+
const settingPath = parseTomlDottedKey(line.slice(0, assignmentIndex).trim());
|
|
237
|
+
if (settingPath && tomlPathMatches(settingPath, targetPath)) {
|
|
238
|
+
const newline = line.endsWith("\n") ? "\n" : "";
|
|
239
|
+
return { line, lineBody: newline ? line.slice(0, -1) : line, newline, offset, assignmentIndex };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
offset += line.length;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function findFirstTomlTableStart(config) {
|
|
248
|
+
const lines = config.match(/[^\n]*\n?|$/g) ?? [];
|
|
249
|
+
let offset = 0;
|
|
250
|
+
let multilineQuote = null;
|
|
251
|
+
for (const line of lines) {
|
|
252
|
+
if (line.length === 0) break;
|
|
253
|
+
const multilineScan = scanTomlMultilineLine(line, multilineQuote);
|
|
254
|
+
multilineQuote = multilineScan.nextQuote;
|
|
255
|
+
if (!multilineScan.wasInside && isTomlTableHeaderLine(line)) return offset;
|
|
256
|
+
offset += line.length;
|
|
257
|
+
}
|
|
258
|
+
return config.length;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function findUnquotedComment(line, startIndex) {
|
|
262
|
+
let quote = null;
|
|
263
|
+
let index = startIndex;
|
|
264
|
+
while (index < line.length) {
|
|
265
|
+
const char = line[index];
|
|
266
|
+
if (quote === '"') {
|
|
267
|
+
if (char === "\\") {
|
|
268
|
+
index += 2;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (char === '"') quote = null;
|
|
272
|
+
index += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (quote === "'") {
|
|
276
|
+
if (char === "'") quote = null;
|
|
277
|
+
index += 1;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (char === '"' || char === "'") quote = char;
|
|
281
|
+
else if (char === "#") return index;
|
|
282
|
+
index += 1;
|
|
283
|
+
}
|
|
284
|
+
return -1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function tomlPathMatches(candidate, target) {
|
|
288
|
+
return candidate.length === target.length && candidate.every((part, index) => part === target[index]);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function replaceTomlAssignmentValue(line, assignmentIndex, value) {
|
|
292
|
+
const newline = line.endsWith("\n") ? "\n" : "";
|
|
293
|
+
const lineBody = newline ? line.slice(0, -1) : line;
|
|
294
|
+
const commentIndex = findUnquotedComment(lineBody, assignmentIndex + 1);
|
|
295
|
+
const comment = commentIndex === -1 ? "" : ` ${lineBody.slice(commentIndex).trimStart()}`;
|
|
296
|
+
return `${lineBody.slice(0, assignmentIndex + 1)} ${value}${comment}${newline}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function parseTomlTableHeader(line) {
|
|
300
|
+
const normalizedLine = stripUnquotedInlineComment(line).trim();
|
|
301
|
+
if (!normalizedLine.startsWith("[") || !normalizedLine.endsWith("]") || normalizedLine.startsWith("[[")) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
return parseTomlDottedKey(normalizedLine.slice(1, -1).trim());
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function stripUnquotedInlineComment(line) {
|
|
308
|
+
let quote = null;
|
|
309
|
+
let index = 0;
|
|
310
|
+
while (index < line.length) {
|
|
311
|
+
const char = line[index];
|
|
312
|
+
if (quote === '"') {
|
|
313
|
+
if (char === "\\") {
|
|
314
|
+
index += 2;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (char === '"') quote = null;
|
|
318
|
+
index += 1;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (quote === "'") {
|
|
322
|
+
if (char === "'") quote = null;
|
|
323
|
+
index += 1;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (char === '"' || char === "'") {
|
|
327
|
+
quote = char;
|
|
328
|
+
index += 1;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (char === "#") return line.slice(0, index);
|
|
332
|
+
index += 1;
|
|
333
|
+
}
|
|
334
|
+
return line;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function findUnquotedAssignment(line) {
|
|
338
|
+
let quote = null;
|
|
339
|
+
let index = 0;
|
|
340
|
+
while (index < line.length) {
|
|
341
|
+
const char = line[index];
|
|
342
|
+
if (quote === '"') {
|
|
343
|
+
if (char === "\\") {
|
|
344
|
+
index += 2;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (char === '"') quote = null;
|
|
348
|
+
index += 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (quote === "'") {
|
|
352
|
+
if (char === "'") quote = null;
|
|
353
|
+
index += 1;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (char === '"' || char === "'") {
|
|
357
|
+
quote = char;
|
|
358
|
+
index += 1;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (char === "=") return index;
|
|
362
|
+
index += 1;
|
|
363
|
+
}
|
|
364
|
+
return -1;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function parseTomlDottedKey(input) {
|
|
368
|
+
const parts = [];
|
|
369
|
+
let index = 0;
|
|
370
|
+
while (index < input.length) {
|
|
371
|
+
index = skipWhitespace(input, index);
|
|
372
|
+
const parsedKey = parseTomlKeyPart(input, index);
|
|
373
|
+
if (!parsedKey) return null;
|
|
374
|
+
parts.push(parsedKey.value);
|
|
375
|
+
index = skipWhitespace(input, parsedKey.nextIndex);
|
|
376
|
+
if (index === input.length) return parts;
|
|
377
|
+
if (input[index] !== ".") return null;
|
|
378
|
+
index += 1;
|
|
379
|
+
}
|
|
380
|
+
return parts.length > 0 ? parts : null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function parseTomlKeyPart(input, startIndex) {
|
|
384
|
+
const quote = input[startIndex];
|
|
385
|
+
if (quote === "'") return parseLiteralTomlString(input, startIndex);
|
|
386
|
+
if (quote === '"') return parseBasicTomlString(input, startIndex);
|
|
387
|
+
return parseBareTomlKey(input, startIndex);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function parseLiteralTomlString(input, startIndex) {
|
|
391
|
+
let index = startIndex + 1;
|
|
392
|
+
let value = "";
|
|
393
|
+
while (index < input.length) {
|
|
394
|
+
const char = input[index];
|
|
395
|
+
if (char === "'") return { value, nextIndex: index + 1 };
|
|
396
|
+
value += char;
|
|
397
|
+
index += 1;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function parseBasicTomlString(input, startIndex) {
|
|
403
|
+
let index = startIndex + 1;
|
|
404
|
+
let value = "";
|
|
405
|
+
while (index < input.length) {
|
|
406
|
+
const char = input[index];
|
|
407
|
+
if (char === '"') return { value, nextIndex: index + 1 };
|
|
408
|
+
if (char !== "\\") {
|
|
409
|
+
value += char;
|
|
410
|
+
index += 1;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const escaped = parseBasicTomlEscape(input, index);
|
|
414
|
+
if (!escaped) return null;
|
|
415
|
+
value += escaped.value;
|
|
416
|
+
index = escaped.nextIndex;
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function parseBasicTomlEscape(input, backslashIndex) {
|
|
422
|
+
const escape = input[backslashIndex + 1];
|
|
423
|
+
if (escape === undefined) return null;
|
|
424
|
+
if (escape === "b") return { value: "\b", nextIndex: backslashIndex + 2 };
|
|
425
|
+
if (escape === "t") return { value: "\t", nextIndex: backslashIndex + 2 };
|
|
426
|
+
if (escape === "n") return { value: "\n", nextIndex: backslashIndex + 2 };
|
|
427
|
+
if (escape === "f") return { value: "\f", nextIndex: backslashIndex + 2 };
|
|
428
|
+
if (escape === "r") return { value: "\r", nextIndex: backslashIndex + 2 };
|
|
429
|
+
if (escape === '"') return { value: '"', nextIndex: backslashIndex + 2 };
|
|
430
|
+
if (escape === "\\") return { value: "\\", nextIndex: backslashIndex + 2 };
|
|
431
|
+
if (escape === "u") return parseUnicodeEscape(input, backslashIndex + 2, 4);
|
|
432
|
+
if (escape === "U") return parseUnicodeEscape(input, backslashIndex + 2, 8);
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function parseUnicodeEscape(input, digitsStart, digitCount) {
|
|
437
|
+
const digits = input.slice(digitsStart, digitsStart + digitCount);
|
|
438
|
+
if (digits.length !== digitCount || !/^[0-9A-Fa-f]+$/.test(digits)) return null;
|
|
439
|
+
const codePoint = Number.parseInt(digits, 16);
|
|
440
|
+
if (codePoint > 0x10ffff) return null;
|
|
441
|
+
return { value: String.fromCodePoint(codePoint), nextIndex: digitsStart + digitCount };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function parseBareTomlKey(input, startIndex) {
|
|
445
|
+
let index = startIndex;
|
|
446
|
+
while (index < input.length && /[A-Za-z0-9_-]/.test(input[index])) index += 1;
|
|
447
|
+
if (index === startIndex) return null;
|
|
448
|
+
return { value: input.slice(startIndex, index), nextIndex: index };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function skipWhitespace(input, startIndex) {
|
|
452
|
+
let index = startIndex;
|
|
453
|
+
while (index < input.length && /\s/.test(input[index])) index += 1;
|
|
454
|
+
return index;
|
|
455
|
+
}
|
|
@@ -14,6 +14,9 @@ const ignoredSkillSourceDirNames = new Set([".mypy_cache", ".omo", ".pytest_cach
|
|
|
14
14
|
const ignoredSkillSourceFileNames = new Set([".gitignore", ".npmignore", "pyrightconfig.json"]);
|
|
15
15
|
const skillSources = [
|
|
16
16
|
["comment-checker", "components/comment-checker/skills/comment-checker"],
|
|
17
|
+
["lcx-contribute-bug-fix", "components/lcx/skills/lcx-contribute-bug-fix"],
|
|
18
|
+
["lcx-doctor", "components/lcx/skills/lcx-doctor"],
|
|
19
|
+
["lcx-report-bug", "components/lcx/skills/lcx-report-bug"],
|
|
17
20
|
["lsp", "components/lsp/skills/lsp"],
|
|
18
21
|
["rules", "components/rules/skills/rules"],
|
|
19
22
|
["teammode", "components/teammode/skills/teammode"],
|
|
@@ -22,7 +25,6 @@ const skillSources = [
|
|
|
22
25
|
["ultrawork", "components/ultrawork/skills/ultrawork"],
|
|
23
26
|
];
|
|
24
27
|
const componentSkillNames = new Set(skillSources.map(([name]) => name));
|
|
25
|
-
const codexHiddenSharedSkillNames = new Set(["ultraresearch"]);
|
|
26
28
|
const skillDisplayPrefix = "(OmO) ";
|
|
27
29
|
|
|
28
30
|
function shouldCopySkillSource(source) {
|
|
@@ -182,22 +184,6 @@ prefixes when identity is needed.
|
|
|
182
184
|
`;
|
|
183
185
|
|
|
184
186
|
function applyCodexSkillOverlays(skillName, content) {
|
|
185
|
-
if (skillName === "ulw-research") {
|
|
186
|
-
return content
|
|
187
|
-
.replace(
|
|
188
|
-
", the legacy alias 'ultraresearch', any 'ulw' research wording,",
|
|
189
|
-
", any 'ulw' research wording,",
|
|
190
|
-
)
|
|
191
|
-
.replace(
|
|
192
|
-
'the legacy alias "ultraresearch" (also `/ultraresearch`, `$ultraresearch`), ',
|
|
193
|
-
"",
|
|
194
|
-
)
|
|
195
|
-
.replace(
|
|
196
|
-
"answer those normally, and mention that `ulw-research` is available (legacy alias: `ultraresearch`) when a question would clearly benefit from it.",
|
|
197
|
-
"answer those normally, and mention that `ulw-research` is available when a question would clearly benefit from it.",
|
|
198
|
-
)
|
|
199
|
-
.replace("# Ultraresearch Synthesis: <query>", "# ULW-Research Synthesis: <query>");
|
|
200
|
-
}
|
|
201
187
|
if (skillName === "start-work") {
|
|
202
188
|
return content
|
|
203
189
|
.replace(startWorkOriginalCompletion, startWorkCodexCompletion)
|
|
@@ -270,7 +256,6 @@ async function syncSkills() {
|
|
|
270
256
|
|
|
271
257
|
for (const skillName of sharedSkillNames) {
|
|
272
258
|
if (componentSkillNames.has(skillName)) continue;
|
|
273
|
-
if (codexHiddenSharedSkillNames.has(skillName)) continue;
|
|
274
259
|
await cp(join(sharedSkillsRoot, skillName), join(skillsRoot, skillName), {
|
|
275
260
|
filter: shouldCopySkillSource,
|
|
276
261
|
recursive: true,
|
|
@@ -24,24 +24,28 @@ After grounding, make ONE judgment, record `intent: clear|unclear` plus `review_
|
|
|
24
24
|
|
|
25
25
|
- **OVERRIDE - explicit ask wins:** if the user explicitly asks to be questioned or interviewed ("ask me", "interview me", "why aren't you asking me" - in any language), route **CLEAR**, run the interview, and turn the adopt-default filter OFF: the user has claimed the forks, so every surviving one is ASKED, not defaulted. This beats the OUTCOME test below, even on a fuzzy brief.
|
|
26
26
|
- **CLEAR** - the user knows the outcome; the only open items are preferences/tradeoffs the repo cannot answer (genuine owner-decisions). Read **`references/intent-clear.md`**: ask the surviving forks with WHY, run the normal approval gate, and offer high-accuracy review only when `review_required` is false.
|
|
27
|
-
- **UNCLEAR** - the outcome itself is fuzzy (a vague brief, a bootstrap, `$start-work` with no selectable plan, a goal the user cannot yet articulate). Asking would offload your own job onto the user. Read **`references/intent-unclear.md`**: research maximally, adopt and ANNOUNCE best-practice defaults, do NOT ask the user extra questions, and
|
|
27
|
+
- **UNCLEAR** - the outcome itself is fuzzy (a vague brief, a bootstrap, `$start-work` with no selectable plan, a goal the user cannot yet articulate). Asking would offload your own job onto the user. Read **`references/intent-unclear.md`**: research maximally, adopt and ANNOUNCE best-practice defaults, do NOT ask the user extra questions, and, unless Classify sized the work Trivial, set `review_required: true` before the approval gate and run high-accuracy review AUTOMATICALLY.
|
|
28
28
|
- **ON THE FENCE** - when CLEAR vs UNCLEAR is genuinely ambiguous, treat it as CLEAR and ask exactly ONE question. A user wrongly silenced is worse than one extra question. The dominant failure to guard against is mis-routing a CLEAR request to UNCLEAR, which silently applies defaults and overrides forks the user wanted to own.
|
|
29
29
|
|
|
30
30
|
WORKED: "add a 5/min-per-IP rate-limit to `/login`" = CLEAR. "make auth better" = UNCLEAR.
|
|
31
31
|
|
|
32
32
|
Both intent paths ALSO read **`references/full-workflow.md`** for the shared mechanics - the plan template, the final verification wave, the APPEND protocol, and the full delegation/wait syntax. Read the phase you are in.
|
|
33
33
|
|
|
34
|
-
## RUN THE SCRIPT - do not hand-build
|
|
34
|
+
## RUN THE SCRIPT - do not hand-build artifacts
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
As soon as `<slug>` and intent are known, before recording draft state, RUN:
|
|
37
37
|
|
|
38
38
|
```
|
|
39
|
-
node "<skill-root>/scripts/scaffold-plan.mjs" <slug> [--clear|--unclear]
|
|
39
|
+
node "<skill-root>/scripts/scaffold-plan.mjs" <slug> [--clear|--unclear] --draft-only [--review-required]
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
(Replace `<skill-root>` with this skill's own directory; `bun` is
|
|
42
|
+
(Replace `<skill-root>` with this skill's own directory; `bun` is accepted.) This creates only `.omo/drafts/<slug>.md`, the compaction-safe resume point; it does not create a plan before approval. Include `--review-required` when an explicit modifier requires review or the classified route is non-Trivial UNCLEAR, so the first durable write contains the complete pending review request. After approval, rerun without `--draft-only` to create `.omo/plans/<slug>.md`, then **APPEND** task batches into `## Todos` - never rewrite script-emitted headers.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Both invocations are resume-safe no-ops for artifacts already present. Do NOT hand-build them; use `--reset` only for a structural reset (`--reset --force` discards edits). If a same-named non-artifact file exists, choose another slug.
|
|
45
|
+
|
|
46
|
+
## Plan artifact producer contract
|
|
47
|
+
|
|
48
|
+
When producing the plan, encode every executable item as a column-zero Markdown task row: implementation rows MUST match `- [ ] N. <title>` (where `N` is a positive decimal integer), and final-verifier rows MUST match `- [ ] F<number>. <title>`. Prose headings, numbered paragraphs, and ordinary bullets are not task substitutes and MUST NOT be counted as implementation or final-verifier tasks. Before handoff, run a structural self-check over the plan: verify that every implementation row and final-verifier row is column-zero, matches its required grammar, and appears in the intended `## Todos` or `## Final verification wave` section; verify that no prose heading or bullet is being used as a task; and repair the plan before handoff if any check fails.
|
|
45
49
|
|
|
46
50
|
## Universal invariants (hold on every path)
|
|
47
51
|
|
|
@@ -58,7 +62,7 @@ Run it ONCE at plan generation. A plain re-run on an existing plan is a safe no-
|
|
|
58
62
|
|
|
59
63
|
## Approval gate
|
|
60
64
|
|
|
61
|
-
When exploration is exhausted and the unknowns are answered, record the gate in the draft (`status: awaiting-approval`,
|
|
65
|
+
When exploration is exhausted and the unknowns are answered, record the gate in the draft (`status: awaiting-approval`, approach, and the next workflow action), present a short brief once, then **wait for the user's explicit okay**. Approval authorizes plan creation only; any already-required review runs afterward under its existing authorization. Full mechanics: `references/full-workflow.md`.
|
|
62
66
|
|
|
63
67
|
## Delegation (Codex-native)
|
|
64
68
|
|