@codio-ai/opencode-authoring-agent 0.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/LICENSE +21 -0
- package/README.md +119 -0
- package/bin/codio-authoring-register.js +77 -0
- package/bin/codio-authoring-unregister.js +45 -0
- package/dist/agents/assessment-author.d.ts +3 -0
- package/dist/agents/assessment-author.d.ts.map +1 -0
- package/dist/agents/assessment-author.test.d.ts +2 -0
- package/dist/agents/assessment-author.test.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.test.d.ts +2 -0
- package/dist/agents/index.test.d.ts.map +1 -0
- package/dist/agents/orchestrator.d.ts +4 -0
- package/dist/agents/orchestrator.d.ts.map +1 -0
- package/dist/agents/orchestrator.test.d.ts +2 -0
- package/dist/agents/orchestrator.test.d.ts.map +1 -0
- package/dist/agents/outline-architect.d.ts +3 -0
- package/dist/agents/outline-architect.d.ts.map +1 -0
- package/dist/agents/outline-architect.test.d.ts +2 -0
- package/dist/agents/outline-architect.test.d.ts.map +1 -0
- package/dist/agents/page-author.d.ts +3 -0
- package/dist/agents/page-author.d.ts.map +1 -0
- package/dist/agents/page-author.test.d.ts +2 -0
- package/dist/agents/page-author.test.d.ts.map +1 -0
- package/dist/agents/source-ingester.d.ts +3 -0
- package/dist/agents/source-ingester.d.ts.map +1 -0
- package/dist/agents/source-ingester.test.d.ts +2 -0
- package/dist/agents/source-ingester.test.d.ts.map +1 -0
- package/dist/agents/types.d.ts +39 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.test.d.ts +2 -0
- package/dist/agents/types.test.d.ts.map +1 -0
- package/dist/agents/validator.d.ts +3 -0
- package/dist/agents/validator.d.ts.map +1 -0
- package/dist/agents/validator.test.d.ts +2 -0
- package/dist/agents/validator.test.d.ts.map +1 -0
- package/dist/config/bedrock-models.d.ts +11 -0
- package/dist/config/bedrock-models.d.ts.map +1 -0
- package/dist/config/index.d.ts +26 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.test.d.ts +2 -0
- package/dist/config/index.test.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1398 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/tools/create-assessment.d.ts +310 -0
- package/dist/tools/create-assessment.d.ts.map +1 -0
- package/dist/tools/create-assessment.test.d.ts +2 -0
- package/dist/tools/create-assessment.test.d.ts.map +1 -0
- package/dist/tools/create-page.d.ts +49 -0
- package/dist/tools/create-page.d.ts.map +1 -0
- package/dist/tools/create-page.test.d.ts +2 -0
- package/dist/tools/create-page.test.d.ts.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.test.d.ts +2 -0
- package/dist/tools/index.test.d.ts.map +1 -0
- package/dist/tools/validate-guide.d.ts +17 -0
- package/dist/tools/validate-guide.d.ts.map +1 -0
- package/dist/tools/validate-guide.test.d.ts +2 -0
- package/dist/tools/validate-guide.test.d.ts.map +1 -0
- package/dist/utils/codio-paths.d.ts +13 -0
- package/dist/utils/codio-paths.d.ts.map +1 -0
- package/dist/utils/codio-paths.test.d.ts +2 -0
- package/dist/utils/codio-paths.test.d.ts.map +1 -0
- package/dist/utils/taskid.d.ts +7 -0
- package/dist/utils/taskid.d.ts.map +1 -0
- package/dist/utils/taskid.test.d.ts +2 -0
- package/dist/utils/taskid.test.d.ts.map +1 -0
- package/dist/utils/uuid.d.ts +3 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/dist/utils/uuid.test.d.ts +2 -0
- package/dist/utils/uuid.test.d.ts.map +1 -0
- package/package.json +70 -0
- package/src/skills/reference-assessment-types/SKILL.md +309 -0
- package/src/skills/reference-authoring-notes/SKILL.md +297 -0
- package/src/skills/reference-mcq-quality/SKILL.md +56 -0
- package/src/skills/reference-page-flow/SKILL.md +62 -0
- package/src/skills/reference-project-defaults/SKILL.md +80 -0
- package/src/skills/reference-style/SKILL.md +14 -0
- package/src/skills/workflow-add-assessment/SKILL.md +29 -0
- package/src/skills/workflow-add-page/SKILL.md +32 -0
- package/src/skills/workflow-import-source/SKILL.md +31 -0
- package/src/skills/workflow-new-assignment/SKILL.md +69 -0
- package/src/skills/workflow-reorder/SKILL.md +24 -0
- package/src/skills/workflow-revise-page/SKILL.md +30 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1398 @@
|
|
|
1
|
+
// src/agents/types.ts
|
|
2
|
+
function resolvePrompt(base, customPrompt, customAppendPrompt) {
|
|
3
|
+
if (customPrompt)
|
|
4
|
+
return customPrompt;
|
|
5
|
+
if (customAppendPrompt)
|
|
6
|
+
return `${base}
|
|
7
|
+
|
|
8
|
+
${customAppendPrompt}`;
|
|
9
|
+
return base;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/agents/assessment-author.ts
|
|
13
|
+
var BASE_PROMPT = `# Assessment Author
|
|
14
|
+
|
|
15
|
+
You author one or more assessments (MCQ, code test, fill-in-the-blanks, parsons-puzzle, or llm-based-auto-rubric) for a Codio guide page. You return structured payloads. **You do not write JSON or files directly.** The orchestrator passes your payload to the \`create_assessment\` tool, which stamps the canonical JSON and writes it.
|
|
16
|
+
|
|
17
|
+
## Inputs
|
|
18
|
+
|
|
19
|
+
- \`assessment_type\`: one of \`multiple-choice\`, \`code-output-compare\`, \`fill-in-the-blanks\`, \`parsons-puzzle\`, \`llm-based-auto-rubric\`.
|
|
20
|
+
- \`topic\`, \`lo\` (learning objective), \`page_context\` (the page body for grounding).
|
|
21
|
+
- \`count\` (how many to author; default 1 for code-test/FITB/parsons/auto-rubric, 3 for MCQ).
|
|
22
|
+
|
|
23
|
+
## Output
|
|
24
|
+
|
|
25
|
+
Return one payload per assessment, in the exact shape \`create_assessment\` expects for the chosen type. See the \`reference-assessment-types\` skill for field-by-field schemas.
|
|
26
|
+
|
|
27
|
+
## Pedagogy
|
|
28
|
+
|
|
29
|
+
- For \`multiple-choice\`: 1 correct + 3 plausible distractors. Distractors must reflect real misconceptions, not throwaways. See the \`reference-mcq-quality\` skill.
|
|
30
|
+
- For \`code-output-compare\`: write test cases that pin down the expected behavior, not just the happy path. Include at least one edge case.
|
|
31
|
+
- For \`fill-in-the-blanks\`: use the \`<<<answer>>>\` marker syntax inline in the \`text\` field. Each blank should test one concrete piece of recall or application.
|
|
32
|
+
- For \`parsons-puzzle\`: provide a correct code solution as \`initial\`, then append distractor lines below — each distractor labeled with a \`#distractor\` comment so the orchestrator can review. Aim for 2–4 distractors that reflect common syntactic or logical mistakes (off-by-one, wrong operator, wrong variable name). Indent matters for Python — call out which indent levels each block needs.
|
|
33
|
+
- For \`llm-based-auto-rubric\`: write 3–5 rubric items, each scoped to one observable dimension (syntax, logic, edge cases, style). Each item has a clear \`title\` and a \`description\` written for the LLM grader to apply (concrete criteria, not vague). Always include a \`solutions\` array under \`.guides/secure/\` so the grader has reference; never expose solution paths under \`.guides/content/\`.
|
|
34
|
+
|
|
35
|
+
## What you don't do
|
|
36
|
+
|
|
37
|
+
- Write files. Return payloads only.
|
|
38
|
+
- Generate UUIDs or taskIds. The tool handles them.
|
|
39
|
+
- Insert embed strings into pages. That's the orchestrator.
|
|
40
|
+
- Author across types in one call unless the orchestrator explicitly asked for a mix.
|
|
41
|
+
`;
|
|
42
|
+
function createAssessmentAuthorAgent(model, customPrompt, customAppendPrompt) {
|
|
43
|
+
const prompt = resolvePrompt(BASE_PROMPT, customPrompt, customAppendPrompt);
|
|
44
|
+
return {
|
|
45
|
+
name: "assessment-author",
|
|
46
|
+
description: "Authors MCQ / code-output-compare / fill-in-the-blanks / parsons-puzzle / llm-based-auto-rubric payloads. Returns structured payloads; does not write files.",
|
|
47
|
+
config: {
|
|
48
|
+
mode: "subagent",
|
|
49
|
+
model,
|
|
50
|
+
permission: {
|
|
51
|
+
read: "allow",
|
|
52
|
+
bash: "deny",
|
|
53
|
+
write: "deny",
|
|
54
|
+
edit: "deny"
|
|
55
|
+
},
|
|
56
|
+
prompt
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/agents/orchestrator.ts
|
|
62
|
+
var AGENT_DESCRIPTIONS = {
|
|
63
|
+
"outline-architect": `@outline-architect
|
|
64
|
+
- Role: Designs Codio assignment structure (flat or single-chapter) and learning objectives. Returns a markdown outline.
|
|
65
|
+
- Permissions: Read only
|
|
66
|
+
- Stats: Sonnet-class judgment for design; never writes files.
|
|
67
|
+
- Capabilities: Structural design, LO formulation, source-digest integration.
|
|
68
|
+
- **Delegate when:** Starting a new assignment, restructuring substantially, or planning from an ingested source.
|
|
69
|
+
- **Don't delegate when:** Adding a single page to an existing assignment — go straight to page-author with a brief.
|
|
70
|
+
- **Rule of thumb:** New shape or restructure → @outline-architect. Incremental change → skip.`,
|
|
71
|
+
"page-author": `@page-author
|
|
72
|
+
- Role: Writes Codio guide page markdown. Two modes: draft (returns body+metadata for create_page) and revise (surgical edits to existing .md).
|
|
73
|
+
- Permissions: Read + edit (no write, no bash)
|
|
74
|
+
- Stats: Sonnet-class prose quality.
|
|
75
|
+
- Capabilities: Instructional writing, surgical revisions.
|
|
76
|
+
- **Delegate when:** Drafting a new page, or applying a focused edit to an existing one.
|
|
77
|
+
- **Don't delegate when:** The edit is a single-line typo fix — do it yourself.
|
|
78
|
+
- **Rule of thumb:** New prose or non-trivial edit → @page-author. Trivial fix → yourself.`,
|
|
79
|
+
"assessment-author": `@assessment-author
|
|
80
|
+
- Role: Authors MCQ / code-output-compare / fill-in-the-blanks / parsons-puzzle / llm-based-auto-rubric payloads. Returns structured payloads only.
|
|
81
|
+
- Permissions: Read only
|
|
82
|
+
- Stats: Pedagogy + distractor quality is the hardest content judgment in this system. Sonnet-class.
|
|
83
|
+
- Capabilities: MCQ pedagogy, code-test design, FITB construction, Parsons distractor design, LLM rubric authoring.
|
|
84
|
+
- **Delegate when:** Authoring any new assessment (multiple-choice, code-output-compare, fill-in-the-blanks, parsons-puzzle, llm-based-auto-rubric).
|
|
85
|
+
- **Don't delegate when:** Re-running create_assessment with the same payload (no LLM work needed).
|
|
86
|
+
- **Rule of thumb:** New question content → @assessment-author. Re-stamp existing payload → tool only.`,
|
|
87
|
+
"source-ingester": `@source-ingester
|
|
88
|
+
- Role: Reads source material (path, URL, paste) and produces a structured markdown digest.
|
|
89
|
+
- Permissions: Read + bash (no write)
|
|
90
|
+
- Stats: Haiku-class — summarization is cheap.
|
|
91
|
+
- Capabilities: Source digestion, LO seed extraction.
|
|
92
|
+
- **Delegate when:** The user provides source material to build from.
|
|
93
|
+
- **Don't delegate when:** No source material; designing from scratch.
|
|
94
|
+
- **Rule of thumb:** Materials provided → @source-ingester, then @outline-architect.`,
|
|
95
|
+
validator: `@validator
|
|
96
|
+
- Role: Content-quality review (pedagogy, LO alignment, clarity). Distinct from the validate_guide tool (which does deterministic structural checks).
|
|
97
|
+
- Permissions: Read only
|
|
98
|
+
- Stats: Sonnet-class judgment.
|
|
99
|
+
- Capabilities: LO alignment, concept scope, cognitive ramp, assessment fit, style.
|
|
100
|
+
- **Delegate when:** End of a multi-page pipeline, or when the user asks for content review.
|
|
101
|
+
- **Don't delegate when:** After a single-page revision or single-assessment add.
|
|
102
|
+
- **Rule of thumb:** Run validate_guide tool first. Then delegate to @validator for content review when scope warrants.`
|
|
103
|
+
};
|
|
104
|
+
function buildOrchestratorPrompt(disabledAgents) {
|
|
105
|
+
const enabledAgents = Object.entries(AGENT_DESCRIPTIONS).filter(([name]) => !disabledAgents?.has(name)).map(([, desc]) => desc).join(`
|
|
106
|
+
|
|
107
|
+
`);
|
|
108
|
+
return `<Role>
|
|
109
|
+
You are the Codio Authoring Orchestrator. You coordinate subagents and tools to build, edit, and validate Codio guide assignments. Optimize for content quality, correctness of mechanical structure, and clear approval flow.
|
|
110
|
+
</Role>
|
|
111
|
+
|
|
112
|
+
<Domain>
|
|
113
|
+
Hard invariants — never violated, regardless of what skill or workflow is active:
|
|
114
|
+
|
|
115
|
+
- All \`id\` and \`_id\` fields are **v4 UUIDs**. Never invent them. They come from \`create_page\` (which generates them internally) or from tools' returned values.
|
|
116
|
+
- Assessment taskIds are \`<type>-<10-digit>\` and are generated **only** by \`create_assessment\`. Never hand-author them.
|
|
117
|
+
- Use **only** the JSON fields documented in the \`reference-authoring-notes\` and \`reference-assessment-types\` skills. **Never invent fields.**
|
|
118
|
+
- \`.guides/content/\` and \`.guides/assessments/\` are **student-visible** when the assignment is published. Never write tooling artefacts, scratch notes, or non-guide files there.
|
|
119
|
+
- \`.guides/secure/\` is **hidden from students**. Plugin source and uploaded source materials live there (\`.guides/secure/materials/\`).
|
|
120
|
+
- Assignments are **flat or single-chapter**. Never create a second chapter.
|
|
121
|
+
- Preserve existing structure when revising; reorganize only if the change requires it.
|
|
122
|
+
- No Codio "section buttons" by default. Process-launch buttons only when the assignment includes runnable code that materially supports the activity.
|
|
123
|
+
</Domain>
|
|
124
|
+
|
|
125
|
+
<Agents>
|
|
126
|
+
|
|
127
|
+
${enabledAgents}
|
|
128
|
+
|
|
129
|
+
</Agents>
|
|
130
|
+
|
|
131
|
+
<Tools>
|
|
132
|
+
|
|
133
|
+
- \`create_page({workspace, stem, title, type, layout, learningObjectives, files?, markdownBody, chapterFolder?})\` — stamp a page JSON with a fresh v4 UUID and write the JSON+MD pair. Use after \`@page-author\` returns a body+metadata in draft mode.
|
|
134
|
+
- \`create_assessment({type, workspace, payload})\` — stamp an assessment JSON with a fresh taskId and write to \`.guides/assessments/\`. Returns \`{ taskId, jsonPath, embedLine }\`. Use after \`@assessment-author\` returns a payload. The returned \`embedLine\` is what you splice into the page markdown.
|
|
135
|
+
- \`validate_guide({workspace})\` — deterministic structural validation (UUIDs, order arrays, embed references, taskId/filename consistency). Run automatically at the end of new-assignment, import-source, and reorder workflows. Run manually on user request (\`/validate-guide\`).
|
|
136
|
+
</Tools>
|
|
137
|
+
|
|
138
|
+
<Workflow>
|
|
139
|
+
|
|
140
|
+
Phases (which apply depends on the user's intent; the workflow-* skills give per-intent procedures):
|
|
141
|
+
|
|
142
|
+
1. **Intake** — confirm topic, audience, LOs, structural constraints, source materials. Do not skip; confirm even if some info was in the user's first message.
|
|
143
|
+
2. **Outline Gate** — for new assignments or substantial restructures, \`@outline-architect\` returns a proposed outline. **Block until the user approves.** No file writes until then.
|
|
144
|
+
3. **Scaffold** — once approved, generate stems (kebab-slug + 4-hex). Build the planning map. Write the root \`.guides/content/index.json\` (and the single chapter folder's index if applicable).
|
|
145
|
+
4. **Drafting** — fan out \`@page-author\` (draft mode) for each page. Use \`create_page\` to stamp each. Run in parallel where pages are independent.
|
|
146
|
+
5. **Assessments** — for each page needing one, fan out \`@assessment-author\`. Use \`create_assessment\` for each returned payload. Splice the returned \`embedLine\` into the page markdown using the edit tool. Group multiple embeds under a \`## Check your understanding\` heading.
|
|
147
|
+
6. **Validation** — run \`validate_guide\` tool (always at end of new-assignment / import-source / reorder workflows). Optionally delegate to \`@validator\` for content review when scope warrants.
|
|
148
|
+
7. **Done** — summarize what was built and any flagged issues.
|
|
149
|
+
|
|
150
|
+
### Parallelism
|
|
151
|
+
|
|
152
|
+
Only fan out where work is independent — page drafting (Phase 4) and assessment authoring (Phase 5). Don't parallelize across phases; respect ordering dependencies.
|
|
153
|
+
|
|
154
|
+
### Tool routing — when to call vs. when to delegate
|
|
155
|
+
|
|
156
|
+
- New page JSON+MD pair → \`@page-author\` (draft mode) returns body+metadata → you call \`create_page\`. **Do not** hand-write the JSON.
|
|
157
|
+
- Existing page surgical edit → \`@page-author\` (revise mode) edits the .md directly. No tool call.
|
|
158
|
+
- New assessment → \`@assessment-author\` returns payload → you call \`create_assessment\` → you splice the returned \`embedLine\` into the page.
|
|
159
|
+
- \`order\` array updates → you edit the index.json yourself with the edit tool. Small contextual JSON; no tool needed.
|
|
160
|
+
- Stem generation (kebab-slug + 4-hex) → you generate inline. No tool.
|
|
161
|
+
</Workflow>
|
|
162
|
+
|
|
163
|
+
<Approval Gates>
|
|
164
|
+
|
|
165
|
+
The outline gate is the **only** blocking approval. Other intents follow this policy:
|
|
166
|
+
|
|
167
|
+
- **Auto-run** when intent and required inputs are unambiguous (add-page with clear title+parent, add-assessment with clear type+topic+target, additive revise-page edits).
|
|
168
|
+
- **Restate and go** for reorders and prose-overwriting revisions — restate the new position / replacement in one sentence; proceed unless the user corrects within their next message.
|
|
169
|
+
- **Pause** when required inputs are missing — ask inline using the relevant workflow's intake step. Do not bounce to a different workflow.
|
|
170
|
+
- **Never** write to \`.guides/content/\` or \`.guides/assessments/\` before the outline gate is passed for new-assignment / import-source.
|
|
171
|
+
</Approval Gates>
|
|
172
|
+
|
|
173
|
+
<OpenCode Subagent Execution Model>
|
|
174
|
+
|
|
175
|
+
- A delegated subagent runs in a separate child session.
|
|
176
|
+
- Delegation is blocking for the parent at that point: send work out, then continue after results return.
|
|
177
|
+
- Parallel delegation means launching multiple independent child-session branches.
|
|
178
|
+
- Only parallelize branches that are truly independent. Reconcile dependent steps after results come back.
|
|
179
|
+
</OpenCode Subagent Execution Model>
|
|
180
|
+
|
|
181
|
+
<Communication>
|
|
182
|
+
|
|
183
|
+
- Concise execution. Answer directly, no preamble.
|
|
184
|
+
- One-line delegation notices: "Drafting page via @page-author..." not multi-sentence preludes.
|
|
185
|
+
- No flattery. Never "Great idea!" or "Excellent choice!" or any praise of user input.
|
|
186
|
+
- Honest pushback: when the user's approach seems problematic, state concern + alternative briefly, ask if they want to proceed.
|
|
187
|
+
- Don't summarize what you did unless asked.
|
|
188
|
+
- When subagent or tool returns an error, surface it. Don't silently continue with partial state.
|
|
189
|
+
</Communication>
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
function createCodioOrchestratorAgent(model, customPrompt, customAppendPrompt, disabledAgents) {
|
|
193
|
+
const base = buildOrchestratorPrompt(disabledAgents);
|
|
194
|
+
const prompt = resolvePrompt(base, customPrompt, customAppendPrompt);
|
|
195
|
+
return {
|
|
196
|
+
name: "codio-orchestrator",
|
|
197
|
+
description: "Primary orchestrator for Codio guide authoring. Owns the workflow lifecycle, the outline approval gate, and routing to subagents/tools.",
|
|
198
|
+
config: {
|
|
199
|
+
mode: "primary",
|
|
200
|
+
temperature: 0.1,
|
|
201
|
+
model,
|
|
202
|
+
prompt,
|
|
203
|
+
permission: {
|
|
204
|
+
question: "allow"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/agents/outline-architect.ts
|
|
211
|
+
var BASE_PROMPT2 = `# Outline Architect
|
|
212
|
+
|
|
213
|
+
You design Codio assignment structure. The orchestrator hands you intake (topic, audience, LOs, constraints) and optionally a source digest from \`source-ingester\`. You return a proposed outline as markdown. You **do not** write files.
|
|
214
|
+
|
|
215
|
+
## Domain constraints
|
|
216
|
+
|
|
217
|
+
- Assignments are **flat (no chapter)** or **single-chapter**. Never propose multiple chapters.
|
|
218
|
+
- Single approval gate: the user must approve your outline before any files get written downstream.
|
|
219
|
+
|
|
220
|
+
## Output format
|
|
221
|
+
|
|
222
|
+
\`\`\`markdown
|
|
223
|
+
# Outline: <assignment title>
|
|
224
|
+
|
|
225
|
+
**Audience:** <one line>
|
|
226
|
+
**Learning objectives:**
|
|
227
|
+
- <LO 1>
|
|
228
|
+
- <LO 2>
|
|
229
|
+
- ...
|
|
230
|
+
|
|
231
|
+
## Structure
|
|
232
|
+
|
|
233
|
+
<choose ONE of:>
|
|
234
|
+
|
|
235
|
+
### Flat (no chapter)
|
|
236
|
+
|
|
237
|
+
1. **<Page title>** — type: \`<intro|objectives|instructional|assessment|conclusion>\` — LO: <which LO(s) this addresses or "n/a"> — assessment: <none | inline | dedicated page>
|
|
238
|
+
2. ...
|
|
239
|
+
|
|
240
|
+
### Single-chapter
|
|
241
|
+
|
|
242
|
+
**Chapter:** <chapter title>
|
|
243
|
+
|
|
244
|
+
1. **<Page title>** — type: ... — LO: ... — assessment: ...
|
|
245
|
+
2. ...
|
|
246
|
+
\`\`\`
|
|
247
|
+
|
|
248
|
+
## Rules
|
|
249
|
+
|
|
250
|
+
- Propose one structural shape (flat or single-chapter); do not offer both.
|
|
251
|
+
- Map every LO to at least one page.
|
|
252
|
+
- Prefer instructional pages before assessment pages; concept before practice.
|
|
253
|
+
- If the brief is ambiguous, ask before designing — do not silently choose.
|
|
254
|
+
- Do not write JSON or filesystem paths. Just the outline.
|
|
255
|
+
- Do not make up LOs the user didn't provide. If the user didn't supply LOs, derive 3-5 candidate LOs from the topic and source digest and clearly mark them as candidates.
|
|
256
|
+
- Don't pad. If the topic only justifies 4 pages, propose 4 pages.
|
|
257
|
+
|
|
258
|
+
## What you don't do
|
|
259
|
+
|
|
260
|
+
- Write any files. Output is markdown for the orchestrator to show the user.
|
|
261
|
+
- Write page content. You only design structure and LOs.
|
|
262
|
+
- Pick UUIDs, taskIds, or filenames. The orchestrator handles those after approval.
|
|
263
|
+
`;
|
|
264
|
+
function createOutlineArchitectAgent(model, customPrompt, customAppendPrompt) {
|
|
265
|
+
const prompt = resolvePrompt(BASE_PROMPT2, customPrompt, customAppendPrompt);
|
|
266
|
+
return {
|
|
267
|
+
name: "outline-architect",
|
|
268
|
+
description: "Designs Codio assignment structure (flat or single-chapter) and LOs; returns a markdown outline. Does not write files.",
|
|
269
|
+
config: {
|
|
270
|
+
mode: "subagent",
|
|
271
|
+
model,
|
|
272
|
+
permission: {
|
|
273
|
+
read: "allow",
|
|
274
|
+
bash: "deny",
|
|
275
|
+
write: "deny",
|
|
276
|
+
edit: "deny"
|
|
277
|
+
},
|
|
278
|
+
prompt
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/agents/page-author.ts
|
|
284
|
+
var BASE_PROMPT3 = `# Page Author
|
|
285
|
+
|
|
286
|
+
You write one Codio guide page at a time. You operate in two modes:
|
|
287
|
+
|
|
288
|
+
- **Draft mode** — given a brief (page title, type, LO(s), surrounding context), produce a markdown body and the metadata fields. **Do not write JSON or files directly.** Return the body + metadata to the orchestrator; the \`create_page\` tool stamps the JSON and writes both files.
|
|
289
|
+
- **Revise mode** — given a page stem and a description of the requested change, apply a **surgical edit** to the existing \`.md\` using the edit tool. Do not modify the JSON unless the orchestrator says so.
|
|
290
|
+
|
|
291
|
+
## Draft mode inputs
|
|
292
|
+
|
|
293
|
+
The orchestrator gives you:
|
|
294
|
+
- \`stem\` — already chosen; do not change it.
|
|
295
|
+
- \`title\`, \`type\`, \`layout\`, \`learningObjectives\`.
|
|
296
|
+
- \`body_brief\` — bullets describing what the page should cover.
|
|
297
|
+
- \`assignment_context\` — short summary of the assignment and what came before this page.
|
|
298
|
+
|
|
299
|
+
What you return:
|
|
300
|
+
- \`markdownBody\` — the full instructional prose.
|
|
301
|
+
- The metadata fields the orchestrator passes to \`create_page\`: title, type, layout (default \`1-panel\` unless told \`2-panels-tree\` + \`files\`), learningObjectives.
|
|
302
|
+
|
|
303
|
+
You do **not** insert assessment embed strings. The orchestrator inserts those after \`assessment-author\` returns.
|
|
304
|
+
|
|
305
|
+
## Style rules
|
|
306
|
+
|
|
307
|
+
- Heading 1 = page title (matches the JSON \`title\`).
|
|
308
|
+
- One concept per page. If you find yourself introducing a second major concept, stop — that's a separate page.
|
|
309
|
+
- Worked examples in fenced code blocks with a language label.
|
|
310
|
+
- "You" not "the student". Active voice. No filler.
|
|
311
|
+
- Cross-page references use the page title verbatim, not "previous/next page" — order can change.
|
|
312
|
+
|
|
313
|
+
## Revise mode
|
|
314
|
+
|
|
315
|
+
1. Read \`.guides/content/<stem>.md\`.
|
|
316
|
+
2. Identify the smallest contiguous region affected by the requested change. Apply the edit; leave everything else byte-identical.
|
|
317
|
+
3. If the change requires a new assessment embed, do NOT add it — return the request to the orchestrator.
|
|
318
|
+
4. Return a one-line summary of the change applied.
|
|
319
|
+
|
|
320
|
+
### Revise rules
|
|
321
|
+
|
|
322
|
+
- Surgical, not wholesale.
|
|
323
|
+
- Never silently change LOs, callout types, or examples that weren't part of the request.
|
|
324
|
+
- If the requested change amounts to a rewrite, say so back to the orchestrator and stop.
|
|
325
|
+
|
|
326
|
+
## What you don't do
|
|
327
|
+
|
|
328
|
+
- Write JSON files (orchestrator + \`create_page\` tool).
|
|
329
|
+
- Update \`order\` arrays (orchestrator).
|
|
330
|
+
- Insert assessment embed strings (orchestrator).
|
|
331
|
+
- Author assessments (that's \`assessment-author\`).
|
|
332
|
+
- Write multiple pages. One call, one page.
|
|
333
|
+
`;
|
|
334
|
+
function createPageAuthorAgent(model, customPrompt, customAppendPrompt) {
|
|
335
|
+
const prompt = resolvePrompt(BASE_PROMPT3, customPrompt, customAppendPrompt);
|
|
336
|
+
return {
|
|
337
|
+
name: "page-author",
|
|
338
|
+
description: "Writes Codio guide page markdown (draft mode produces body+metadata for create_page; revise mode applies surgical edits to existing .md).",
|
|
339
|
+
config: {
|
|
340
|
+
mode: "subagent",
|
|
341
|
+
model,
|
|
342
|
+
permission: {
|
|
343
|
+
read: "allow",
|
|
344
|
+
edit: "allow",
|
|
345
|
+
write: "deny",
|
|
346
|
+
bash: "deny"
|
|
347
|
+
},
|
|
348
|
+
prompt
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/agents/source-ingester.ts
|
|
354
|
+
var BASE_PROMPT4 = `# Source Ingester
|
|
355
|
+
|
|
356
|
+
You read source material (a file path, a URL, or pasted text) and produce a structured digest the orchestrator can hand to \`outline-architect\` or \`page-author\`.
|
|
357
|
+
|
|
358
|
+
## Default materials location
|
|
359
|
+
|
|
360
|
+
Uploaded materials live under \`.guides/secure/materials/\`. If the orchestrator passes a relative path with no directory, look there first.
|
|
361
|
+
|
|
362
|
+
## Inputs
|
|
363
|
+
|
|
364
|
+
- \`source\`: a path, URL, or inline text block.
|
|
365
|
+
- \`focus\` (optional): what the orchestrator wants extracted (concepts, examples, exercises, etc).
|
|
366
|
+
|
|
367
|
+
## Output
|
|
368
|
+
|
|
369
|
+
Return a digest as markdown:
|
|
370
|
+
|
|
371
|
+
\`\`\`markdown
|
|
372
|
+
## Source: <name or URL>
|
|
373
|
+
|
|
374
|
+
**Format:** <pdf | url | code | text | ...>
|
|
375
|
+
**Length / size:** <one line>
|
|
376
|
+
|
|
377
|
+
### Key concepts
|
|
378
|
+
- <concept 1>: <one-sentence summary>
|
|
379
|
+
- ...
|
|
380
|
+
|
|
381
|
+
### Worked examples or notable exercises
|
|
382
|
+
- <one-line description, with section/page if applicable>
|
|
383
|
+
- ...
|
|
384
|
+
|
|
385
|
+
### Suggested LO seeds (optional)
|
|
386
|
+
- <draft LO 1>
|
|
387
|
+
- ...
|
|
388
|
+
\`\`\`
|
|
389
|
+
|
|
390
|
+
## Rules
|
|
391
|
+
|
|
392
|
+
- Never paraphrase wholesale. Summarize and structure.
|
|
393
|
+
- Quote verbatim only when the source's exact wording matters (e.g. a definition).
|
|
394
|
+
- If the source is unreachable or unreadable, say so back to the orchestrator and stop.
|
|
395
|
+
- Do not write files. Digest only.
|
|
396
|
+
`;
|
|
397
|
+
function createSourceIngesterAgent(model, customPrompt, customAppendPrompt) {
|
|
398
|
+
const prompt = resolvePrompt(BASE_PROMPT4, customPrompt, customAppendPrompt);
|
|
399
|
+
return {
|
|
400
|
+
name: "source-ingester",
|
|
401
|
+
description: "Reads source material (file, URL, paste) and returns a structured markdown digest.",
|
|
402
|
+
config: {
|
|
403
|
+
mode: "subagent",
|
|
404
|
+
model,
|
|
405
|
+
permission: {
|
|
406
|
+
read: "allow",
|
|
407
|
+
bash: "allow",
|
|
408
|
+
write: "deny",
|
|
409
|
+
edit: "deny"
|
|
410
|
+
},
|
|
411
|
+
prompt
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/agents/validator.ts
|
|
417
|
+
var BASE_PROMPT5 = `# Validator
|
|
418
|
+
|
|
419
|
+
You do **content-quality review** of a Codio guide. The deterministic structural checks (UUID format, order array integrity, broken embed references, taskId/filename mismatch) live in the \`validate_guide\` **tool** — the orchestrator runs that first. Your job is what the tool can't do: pedagogy, clarity, LO alignment.
|
|
420
|
+
|
|
421
|
+
## Inputs
|
|
422
|
+
|
|
423
|
+
- \`from_pipeline\`: boolean. \`true\` when invoked at the end of a full pipeline (auto-validation); \`false\` when invoked manually.
|
|
424
|
+
- \`scope\` (optional): a specific page stem or chapter to focus on.
|
|
425
|
+
|
|
426
|
+
## What you check
|
|
427
|
+
|
|
428
|
+
For each page (or just the scoped ones):
|
|
429
|
+
1. **LO alignment** — does the page actually teach what its \`learningObjectives\` claim?
|
|
430
|
+
2. **Concept scope** — one concept per page; flag pages introducing more than one major idea.
|
|
431
|
+
3. **Cognitive ramp** — are prerequisite ideas introduced before they're used?
|
|
432
|
+
4. **Examples** — does each new abstraction have at least one concrete worked example?
|
|
433
|
+
5. **Assessment fit** — do embedded assessments test the LO the page taught, not something tangential?
|
|
434
|
+
6. **Style** — concise, active voice, "you" not "the student"; no filler; callouts used purposefully.
|
|
435
|
+
7. **Cross-references** — links to other pages use page title verbatim, not "previous/next".
|
|
436
|
+
|
|
437
|
+
## Output
|
|
438
|
+
|
|
439
|
+
Return a structured report as markdown:
|
|
440
|
+
|
|
441
|
+
\`\`\`markdown
|
|
442
|
+
## Validator report
|
|
443
|
+
|
|
444
|
+
**Pages reviewed:** N
|
|
445
|
+
**Issues flagged:** M
|
|
446
|
+
|
|
447
|
+
### <Page title> (\`<stem>\`)
|
|
448
|
+
- **<category>**: <one-line description>
|
|
449
|
+
- ...
|
|
450
|
+
\`\`\`
|
|
451
|
+
|
|
452
|
+
Categories: \`lo-mismatch\`, \`concept-scope\`, \`ramp\`, \`missing-example\`, \`assessment-fit\`, \`style\`, \`cross-reference\`.
|
|
453
|
+
|
|
454
|
+
## What you don't do
|
|
455
|
+
|
|
456
|
+
- Write files. Report only.
|
|
457
|
+
- Re-run the structural checks the tool already ran — point to that report instead.
|
|
458
|
+
- Fix anything. The orchestrator routes fixes back through \`page-author\` or \`assessment-author\` after the user decides.
|
|
459
|
+
`;
|
|
460
|
+
function createValidatorAgent(model, customPrompt, customAppendPrompt) {
|
|
461
|
+
const prompt = resolvePrompt(BASE_PROMPT5, customPrompt, customAppendPrompt);
|
|
462
|
+
return {
|
|
463
|
+
name: "validator",
|
|
464
|
+
description: "Content-quality review (pedagogy, LO alignment, clarity). Deterministic checks live in the validate_guide tool.",
|
|
465
|
+
config: {
|
|
466
|
+
mode: "subagent",
|
|
467
|
+
model,
|
|
468
|
+
permission: {
|
|
469
|
+
read: "allow",
|
|
470
|
+
bash: "deny",
|
|
471
|
+
write: "deny",
|
|
472
|
+
edit: "deny"
|
|
473
|
+
},
|
|
474
|
+
prompt
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/agents/index.ts
|
|
480
|
+
var FACTORIES = [
|
|
481
|
+
{
|
|
482
|
+
name: "codio-orchestrator",
|
|
483
|
+
factory: createCodioOrchestratorAgent,
|
|
484
|
+
defaultModelTier: "primary"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: "outline-architect",
|
|
488
|
+
factory: createOutlineArchitectAgent,
|
|
489
|
+
defaultModelTier: "primary"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: "page-author",
|
|
493
|
+
factory: createPageAuthorAgent,
|
|
494
|
+
defaultModelTier: "primary"
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "assessment-author",
|
|
498
|
+
factory: createAssessmentAuthorAgent,
|
|
499
|
+
defaultModelTier: "primary"
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
name: "source-ingester",
|
|
503
|
+
factory: createSourceIngesterAgent,
|
|
504
|
+
defaultModelTier: "cheap"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: "validator",
|
|
508
|
+
factory: createValidatorAgent,
|
|
509
|
+
defaultModelTier: "primary"
|
|
510
|
+
}
|
|
511
|
+
];
|
|
512
|
+
function getDisabledAgents(config) {
|
|
513
|
+
return new Set(config.disabledAgents);
|
|
514
|
+
}
|
|
515
|
+
function createAgents(config) {
|
|
516
|
+
const disabled = getDisabledAgents(config);
|
|
517
|
+
const agents = [];
|
|
518
|
+
for (const { name, factory, defaultModelTier } of FACTORIES) {
|
|
519
|
+
if (disabled.has(name))
|
|
520
|
+
continue;
|
|
521
|
+
const override = config.agents[name];
|
|
522
|
+
const model = override?.model ?? config.defaultModels[defaultModelTier];
|
|
523
|
+
const definition = factory(model, override?.prompt, override?.appendPrompt, name === "codio-orchestrator" ? disabled : undefined);
|
|
524
|
+
if (override?.temperature !== undefined) {
|
|
525
|
+
definition.config.temperature = override.temperature;
|
|
526
|
+
}
|
|
527
|
+
agents.push(definition);
|
|
528
|
+
}
|
|
529
|
+
return agents;
|
|
530
|
+
}
|
|
531
|
+
function getAgentConfigs(agents) {
|
|
532
|
+
const map = {};
|
|
533
|
+
for (const a of agents)
|
|
534
|
+
map[a.name] = a.config;
|
|
535
|
+
return map;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/config/index.ts
|
|
539
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
540
|
+
import { join } from "node:path";
|
|
541
|
+
import { z } from "zod";
|
|
542
|
+
var DEFAULT_PRIMARY = "amazon-bedrock/sonnet-4-6";
|
|
543
|
+
var DEFAULT_CHEAP = "amazon-bedrock/haiku-4-5";
|
|
544
|
+
var agentOverrideSchema = z.object({
|
|
545
|
+
model: z.string().optional(),
|
|
546
|
+
prompt: z.string().optional(),
|
|
547
|
+
appendPrompt: z.string().optional(),
|
|
548
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
549
|
+
skills: z.array(z.string()).optional()
|
|
550
|
+
});
|
|
551
|
+
var pluginConfigSchema = z.object({
|
|
552
|
+
agents: z.record(z.string(), agentOverrideSchema).default(() => ({})),
|
|
553
|
+
disabledAgents: z.array(z.string()).default(() => []),
|
|
554
|
+
defaultModels: z.object({
|
|
555
|
+
primary: z.string().default(DEFAULT_PRIMARY),
|
|
556
|
+
cheap: z.string().default(DEFAULT_CHEAP)
|
|
557
|
+
}).default(() => ({
|
|
558
|
+
primary: DEFAULT_PRIMARY,
|
|
559
|
+
cheap: DEFAULT_CHEAP
|
|
560
|
+
}))
|
|
561
|
+
}).default(() => ({
|
|
562
|
+
agents: {},
|
|
563
|
+
disabledAgents: [],
|
|
564
|
+
defaultModels: { primary: DEFAULT_PRIMARY, cheap: DEFAULT_CHEAP }
|
|
565
|
+
}));
|
|
566
|
+
function loadPluginConfig(directory) {
|
|
567
|
+
const candidate = join(directory, "codio-authoring.json");
|
|
568
|
+
if (!existsSync(candidate)) {
|
|
569
|
+
return pluginConfigSchema.parse({});
|
|
570
|
+
}
|
|
571
|
+
const text = readFileSync(candidate, "utf8");
|
|
572
|
+
let raw;
|
|
573
|
+
try {
|
|
574
|
+
raw = JSON.parse(text);
|
|
575
|
+
} catch (err) {
|
|
576
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
577
|
+
throw new Error(`Invalid JSON in ${candidate}: ${msg}`);
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
return pluginConfigSchema.parse(raw);
|
|
581
|
+
} catch (err) {
|
|
582
|
+
if (err instanceof z.ZodError) {
|
|
583
|
+
const detail = err.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
584
|
+
throw new Error(`Invalid config in ${candidate}: ${detail}`);
|
|
585
|
+
}
|
|
586
|
+
throw err;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/config/bedrock-models.ts
|
|
591
|
+
var DEFAULT_BEDROCK_MODELS = {
|
|
592
|
+
"sonnet-4-6": {
|
|
593
|
+
id: "us.anthropic.claude-sonnet-4-6",
|
|
594
|
+
name: "Claude Sonnet 4.6 (US cross-region)"
|
|
595
|
+
},
|
|
596
|
+
"haiku-4-5": {
|
|
597
|
+
id: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
598
|
+
name: "Claude Haiku 4.5 (US cross-region)"
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// src/tools/index.ts
|
|
603
|
+
import { tool } from "@opencode-ai/plugin";
|
|
604
|
+
|
|
605
|
+
// src/tools/create-assessment.ts
|
|
606
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
|
|
607
|
+
import { dirname, join as join3 } from "node:path";
|
|
608
|
+
import { z as z2 } from "zod";
|
|
609
|
+
|
|
610
|
+
// src/utils/codio-paths.ts
|
|
611
|
+
import { join as join2 } from "node:path";
|
|
612
|
+
function resolveCodioPaths(ctx) {
|
|
613
|
+
const workspace = ctx.directory.replace(/\/+$/, "");
|
|
614
|
+
return {
|
|
615
|
+
workspace,
|
|
616
|
+
guides: join2(workspace, ".guides"),
|
|
617
|
+
content: join2(workspace, ".guides", "content"),
|
|
618
|
+
assessments: join2(workspace, ".guides", "assessments"),
|
|
619
|
+
secure: join2(workspace, ".guides", "secure"),
|
|
620
|
+
materials: join2(workspace, ".guides", "secure", "materials"),
|
|
621
|
+
pluginDir: join2(workspace, ".guides", "secure", "codio-authoring-opencode")
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/utils/taskid.ts
|
|
626
|
+
import { randomInt } from "node:crypto";
|
|
627
|
+
var ASSESSMENT_TYPES = [
|
|
628
|
+
"multiple-choice",
|
|
629
|
+
"code-output-compare",
|
|
630
|
+
"fill-in-the-blanks",
|
|
631
|
+
"parsons-puzzle",
|
|
632
|
+
"llm-based-auto-rubric"
|
|
633
|
+
];
|
|
634
|
+
var TASKID_RE = new RegExp(`^(${ASSESSMENT_TYPES.join("|")})-[1-9][0-9]{9}$`);
|
|
635
|
+
var ANY_TASKID_RE = new RegExp(`(?:${ASSESSMENT_TYPES.join("|")})-[1-9][0-9]{9}`, "g");
|
|
636
|
+
function generateTaskId(type) {
|
|
637
|
+
const first = randomInt(1, 10);
|
|
638
|
+
const rest = randomInt(0, 1e9).toString().padStart(9, "0");
|
|
639
|
+
return `${type}-${first}${rest}`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/utils/uuid.ts
|
|
643
|
+
import { randomUUID } from "node:crypto";
|
|
644
|
+
var UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
645
|
+
function generateUuidV4() {
|
|
646
|
+
return randomUUID();
|
|
647
|
+
}
|
|
648
|
+
function isUuidV4(value) {
|
|
649
|
+
return UUID_V4_RE.test(value);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/tools/create-assessment.ts
|
|
653
|
+
var ASSESSMENT_TYPE_TAG = {
|
|
654
|
+
"multiple-choice": "Multiple Choice",
|
|
655
|
+
"code-output-compare": "Standard Code Test",
|
|
656
|
+
"fill-in-the-blanks": "Fill in the Blanks",
|
|
657
|
+
"parsons-puzzle": "Parsons Puzzle",
|
|
658
|
+
"llm-based-auto-rubric": null
|
|
659
|
+
};
|
|
660
|
+
function ensureAssessmentTag(tags, type) {
|
|
661
|
+
const list = tags ? [...tags] : [];
|
|
662
|
+
const tagValue = ASSESSMENT_TYPE_TAG[type];
|
|
663
|
+
if (tagValue !== null && !list.some((t) => t.name === "Assessment Type")) {
|
|
664
|
+
list.unshift({ name: "Assessment Type", value: tagValue });
|
|
665
|
+
}
|
|
666
|
+
return list;
|
|
667
|
+
}
|
|
668
|
+
var tagSchema = z2.object({ name: z2.string(), value: z2.string() });
|
|
669
|
+
var mcqPayloadSchema = z2.object({
|
|
670
|
+
name: z2.string().default(""),
|
|
671
|
+
instructions: z2.string().min(1),
|
|
672
|
+
answers: z2.array(z2.object({ answer: z2.string(), correct: z2.boolean().default(false) })).min(1),
|
|
673
|
+
multiple_response: z2.boolean().default(false),
|
|
674
|
+
randomize: z2.boolean().default(true),
|
|
675
|
+
tags: z2.array(tagSchema).optional(),
|
|
676
|
+
blooms: z2.string().default(""),
|
|
677
|
+
lo: z2.string().default(""),
|
|
678
|
+
guidance: z2.string().default(""),
|
|
679
|
+
max_attempts: z2.number().int().positive().default(1),
|
|
680
|
+
points: z2.number().int().nonnegative().default(20)
|
|
681
|
+
}).refine((p) => p.answers.some((a) => a.correct), {
|
|
682
|
+
message: "multiple-choice payload must mark at least one correct answer",
|
|
683
|
+
path: ["answers"]
|
|
684
|
+
});
|
|
685
|
+
function buildMcqAssessment(raw, taskId = generateTaskId("multiple-choice")) {
|
|
686
|
+
const p = mcqPayloadSchema.parse(raw);
|
|
687
|
+
return {
|
|
688
|
+
type: "multiple-choice",
|
|
689
|
+
taskId,
|
|
690
|
+
source: {
|
|
691
|
+
name: p.name,
|
|
692
|
+
showName: true,
|
|
693
|
+
instructions: p.instructions,
|
|
694
|
+
multipleResponse: p.multiple_response,
|
|
695
|
+
isRandomized: p.randomize,
|
|
696
|
+
answers: p.answers.map((a) => ({
|
|
697
|
+
_id: generateUuidV4(),
|
|
698
|
+
correct: a.correct,
|
|
699
|
+
answer: a.answer
|
|
700
|
+
})),
|
|
701
|
+
metadata: {
|
|
702
|
+
tags: ensureAssessmentTag(p.tags, "multiple-choice"),
|
|
703
|
+
files: [],
|
|
704
|
+
opened: []
|
|
705
|
+
},
|
|
706
|
+
bloomsObjectiveLevel: p.blooms,
|
|
707
|
+
learningObjectives: p.lo,
|
|
708
|
+
guidance: p.guidance,
|
|
709
|
+
showGuidanceAfterResponseOption: { type: "Attempts", passedFrom: 1 },
|
|
710
|
+
maxAttemptsCount: p.max_attempts,
|
|
711
|
+
showExpectedAnswerOption: { type: "Always" },
|
|
712
|
+
points: p.points,
|
|
713
|
+
incorrectPoints: 0,
|
|
714
|
+
arePartialPointsAllowed: false,
|
|
715
|
+
useMaximumScore: false
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
var codeTestPayloadSchema = z2.object({
|
|
720
|
+
name: z2.string().default(""),
|
|
721
|
+
command: z2.string().min(1),
|
|
722
|
+
test_cases: z2.array(z2.object({
|
|
723
|
+
input: z2.string().default(""),
|
|
724
|
+
output: z2.string().default(""),
|
|
725
|
+
arguments: z2.string().default(""),
|
|
726
|
+
feedback: z2.string().default(""),
|
|
727
|
+
show_feedback: z2.boolean().default(false)
|
|
728
|
+
})).default([]),
|
|
729
|
+
starter_file_path: z2.string().default(""),
|
|
730
|
+
starter_file_content: z2.string().optional(),
|
|
731
|
+
options: z2.object({
|
|
732
|
+
ignoreCase: z2.boolean().default(true),
|
|
733
|
+
ignoreNewline: z2.boolean().default(true),
|
|
734
|
+
ignoreWhitespaces: z2.boolean().default(true),
|
|
735
|
+
matchSubstring: z2.boolean().default(true),
|
|
736
|
+
timeout: z2.number().int().positive().default(30)
|
|
737
|
+
}).default(() => ({
|
|
738
|
+
ignoreCase: true,
|
|
739
|
+
ignoreNewline: true,
|
|
740
|
+
ignoreWhitespaces: true,
|
|
741
|
+
matchSubstring: true,
|
|
742
|
+
timeout: 30
|
|
743
|
+
})),
|
|
744
|
+
pre_execute_command: z2.string().default(""),
|
|
745
|
+
one_time_test: z2.boolean().default(true),
|
|
746
|
+
partial_points: z2.boolean().default(true),
|
|
747
|
+
show_name: z2.boolean().default(false),
|
|
748
|
+
show_expected_answer: z2.boolean().default(true),
|
|
749
|
+
show_guidance_after_response: z2.boolean().default(true),
|
|
750
|
+
instructions: z2.string().default("Submit your code when ready"),
|
|
751
|
+
tags: z2.array(tagSchema).optional(),
|
|
752
|
+
blooms: z2.string().default(""),
|
|
753
|
+
lo: z2.string().default(""),
|
|
754
|
+
guidance: z2.string().default(""),
|
|
755
|
+
points: z2.number().int().nonnegative().default(20)
|
|
756
|
+
});
|
|
757
|
+
function buildCodeTestAssessment(raw, taskId = generateTaskId("code-output-compare")) {
|
|
758
|
+
const p = codeTestPayloadSchema.parse(raw);
|
|
759
|
+
const files = p.starter_file_path ? [p.starter_file_path] : [];
|
|
760
|
+
const opened = p.starter_file_path ? [{ content: p.starter_file_path, panelNumber: 0, type: "file" }] : [];
|
|
761
|
+
return {
|
|
762
|
+
type: "code-output-compare",
|
|
763
|
+
taskId,
|
|
764
|
+
source: {
|
|
765
|
+
arePartialPointsAllowed: p.partial_points,
|
|
766
|
+
bloomsObjectiveLevel: p.blooms,
|
|
767
|
+
command: p.command,
|
|
768
|
+
guidance: p.guidance,
|
|
769
|
+
instructions: p.instructions,
|
|
770
|
+
learningObjectives: p.lo,
|
|
771
|
+
metadata: {
|
|
772
|
+
files,
|
|
773
|
+
opened,
|
|
774
|
+
tags: ensureAssessmentTag(p.tags, "code-output-compare")
|
|
775
|
+
},
|
|
776
|
+
name: p.name,
|
|
777
|
+
oneTimeTest: p.one_time_test,
|
|
778
|
+
options: p.options,
|
|
779
|
+
points: p.points,
|
|
780
|
+
preExecuteCommand: p.pre_execute_command,
|
|
781
|
+
sequence: p.test_cases.map((tc) => ({
|
|
782
|
+
arguments: tc.arguments,
|
|
783
|
+
feedback: tc.feedback,
|
|
784
|
+
input: tc.input,
|
|
785
|
+
output: tc.output,
|
|
786
|
+
showFeedback: tc.show_feedback
|
|
787
|
+
})),
|
|
788
|
+
showExpectedAnswer: p.show_expected_answer,
|
|
789
|
+
showGuidanceAfterResponse: p.show_guidance_after_response,
|
|
790
|
+
showName: p.show_name
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
var FITB_BLANK_RE = /<<<(.+?)>>>/g;
|
|
795
|
+
function parseFitbText(raw) {
|
|
796
|
+
const blank = [];
|
|
797
|
+
const text = [];
|
|
798
|
+
let pos = 0;
|
|
799
|
+
for (const m of raw.matchAll(FITB_BLANK_RE)) {
|
|
800
|
+
if (m.index !== undefined && m.index > pos) {
|
|
801
|
+
text.push(raw.slice(pos, m.index));
|
|
802
|
+
}
|
|
803
|
+
text.push(0);
|
|
804
|
+
blank.push(m[1]);
|
|
805
|
+
pos = (m.index ?? 0) + m[0].length;
|
|
806
|
+
}
|
|
807
|
+
if (pos < raw.length)
|
|
808
|
+
text.push(raw.slice(pos));
|
|
809
|
+
return { blank, text };
|
|
810
|
+
}
|
|
811
|
+
var fitbPayloadSchema = z2.object({
|
|
812
|
+
name: z2.string().default(""),
|
|
813
|
+
instructions: z2.string().min(1),
|
|
814
|
+
text: z2.string().min(1),
|
|
815
|
+
show_name: z2.boolean().default(true),
|
|
816
|
+
show_values: z2.boolean().default(true),
|
|
817
|
+
distractors: z2.string().default(""),
|
|
818
|
+
tags: z2.array(tagSchema).optional(),
|
|
819
|
+
blooms: z2.string().default(""),
|
|
820
|
+
lo: z2.string().default(""),
|
|
821
|
+
guidance: z2.string().default(""),
|
|
822
|
+
max_attempts: z2.number().int().positive().default(1),
|
|
823
|
+
points: z2.number().int().nonnegative().default(20),
|
|
824
|
+
partial_points: z2.boolean().default(false)
|
|
825
|
+
}).refine((p) => /<<<.+?>>>/.test(p.text), {
|
|
826
|
+
message: "fill-in-the-blanks text must contain at least one <<<answer>>> marker",
|
|
827
|
+
path: ["text"]
|
|
828
|
+
});
|
|
829
|
+
function buildFitbAssessment(raw, taskId = generateTaskId("fill-in-the-blanks")) {
|
|
830
|
+
const p = fitbPayloadSchema.parse(raw);
|
|
831
|
+
const tokens = parseFitbText(p.text);
|
|
832
|
+
return {
|
|
833
|
+
type: "fill-in-the-blanks",
|
|
834
|
+
taskId,
|
|
835
|
+
source: {
|
|
836
|
+
name: p.name,
|
|
837
|
+
showName: p.show_name,
|
|
838
|
+
instructions: p.instructions,
|
|
839
|
+
showValues: p.show_values,
|
|
840
|
+
text: p.text,
|
|
841
|
+
distractors: p.distractors,
|
|
842
|
+
metadata: {
|
|
843
|
+
tags: ensureAssessmentTag(p.tags, "fill-in-the-blanks"),
|
|
844
|
+
files: [],
|
|
845
|
+
opened: []
|
|
846
|
+
},
|
|
847
|
+
bloomsObjectiveLevel: p.blooms,
|
|
848
|
+
learningObjectives: p.lo,
|
|
849
|
+
guidance: p.guidance,
|
|
850
|
+
showGuidanceAfterResponseOption: { type: "Attempts", passedFrom: 1 },
|
|
851
|
+
maxAttemptsCount: p.max_attempts,
|
|
852
|
+
showExpectedAnswerOption: { type: "Always" },
|
|
853
|
+
points: p.points,
|
|
854
|
+
arePartialPointsAllowed: p.partial_points,
|
|
855
|
+
useMaximumScore: false,
|
|
856
|
+
tokens: { ...tokens, regexPositions: [] }
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
var parsonsOptionsSchema = z2.object({
|
|
861
|
+
sortableId: z2.string().default("sortable"),
|
|
862
|
+
max_wrong_lines: z2.number().int().nonnegative().default(10),
|
|
863
|
+
exec_limit: z2.number().int().nonnegative().default(2500),
|
|
864
|
+
can_indent: z2.boolean().default(true),
|
|
865
|
+
x_indent: z2.number().int().nonnegative().default(50),
|
|
866
|
+
lang: z2.string().default("en"),
|
|
867
|
+
show_feedback: z2.boolean().default(true),
|
|
868
|
+
python3: z2.boolean().default(true),
|
|
869
|
+
trashId: z2.string().default("sortableTrash")
|
|
870
|
+
});
|
|
871
|
+
var parsonsPayloadSchema = z2.object({
|
|
872
|
+
name: z2.string().optional(),
|
|
873
|
+
instructions: z2.string().min(1),
|
|
874
|
+
initial: z2.string().min(1),
|
|
875
|
+
options: parsonsOptionsSchema.optional(),
|
|
876
|
+
grader: z2.string().default("1"),
|
|
877
|
+
points: z2.number().int().nonnegative().default(20),
|
|
878
|
+
blooms: z2.string().default(""),
|
|
879
|
+
lo: z2.string().default(""),
|
|
880
|
+
guidance: z2.string().default(""),
|
|
881
|
+
max_attempts: z2.number().int().nonnegative().default(0),
|
|
882
|
+
use_maximum_score: z2.boolean().default(false),
|
|
883
|
+
tags: z2.array(tagSchema).optional(),
|
|
884
|
+
show_name: z2.boolean().default(true),
|
|
885
|
+
show_guidance_after_attempts: z2.number().int().nonnegative().default(2)
|
|
886
|
+
});
|
|
887
|
+
function buildParsonsAssessment(raw, taskId = generateTaskId("parsons-puzzle")) {
|
|
888
|
+
const p = parsonsPayloadSchema.parse(raw);
|
|
889
|
+
const opts = parsonsOptionsSchema.parse(p.options ?? {});
|
|
890
|
+
return {
|
|
891
|
+
type: "parsons-puzzle",
|
|
892
|
+
taskId,
|
|
893
|
+
source: {
|
|
894
|
+
name: p.name ?? taskId,
|
|
895
|
+
showName: p.show_name,
|
|
896
|
+
instructions: p.instructions,
|
|
897
|
+
initial: p.initial,
|
|
898
|
+
options: JSON.stringify(opts),
|
|
899
|
+
grader: p.grader,
|
|
900
|
+
metadata: {
|
|
901
|
+
tags: ensureAssessmentTag(p.tags, "parsons-puzzle"),
|
|
902
|
+
files: [],
|
|
903
|
+
opened: []
|
|
904
|
+
},
|
|
905
|
+
bloomsObjectiveLevel: p.blooms,
|
|
906
|
+
learningObjectives: p.lo,
|
|
907
|
+
guidance: p.guidance,
|
|
908
|
+
showGuidanceAfterResponseOption: {
|
|
909
|
+
type: "Attempts",
|
|
910
|
+
passedFrom: p.show_guidance_after_attempts
|
|
911
|
+
},
|
|
912
|
+
maxAttemptsCount: p.max_attempts,
|
|
913
|
+
points: p.points,
|
|
914
|
+
useMaximumScore: p.use_maximum_score
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
var llmAutoRubricRubricSchema = z2.object({
|
|
919
|
+
id: z2.string().optional(),
|
|
920
|
+
title: z2.string().min(1),
|
|
921
|
+
description: z2.string().min(1),
|
|
922
|
+
points: z2.number()
|
|
923
|
+
});
|
|
924
|
+
var llmAutoRubricSolutionSchema = z2.object({
|
|
925
|
+
path: z2.string().min(1),
|
|
926
|
+
content: z2.string()
|
|
927
|
+
});
|
|
928
|
+
var llmAutoRubricPayloadSchema = z2.object({
|
|
929
|
+
name: z2.string().optional(),
|
|
930
|
+
instructions: z2.string().min(1),
|
|
931
|
+
files: z2.array(z2.string()).default([]),
|
|
932
|
+
rubrics: z2.array(llmAutoRubricRubricSchema).min(1),
|
|
933
|
+
rubrics_requirements: z2.string().default(""),
|
|
934
|
+
solutions: z2.array(llmAutoRubricSolutionSchema).default([]),
|
|
935
|
+
points: z2.number().int().nonnegative().default(5),
|
|
936
|
+
partial_points: z2.boolean().default(true),
|
|
937
|
+
blooms: z2.string().default(""),
|
|
938
|
+
lo: z2.string().default(""),
|
|
939
|
+
guidance: z2.string().default(""),
|
|
940
|
+
max_attempts: z2.number().int().nonnegative().default(0),
|
|
941
|
+
tags: z2.array(tagSchema).optional(),
|
|
942
|
+
show_name: z2.boolean().default(false),
|
|
943
|
+
show_guidance_after_attempts: z2.number().int().nonnegative().default(2)
|
|
944
|
+
});
|
|
945
|
+
function buildLlmAutoRubricAssessment(raw, taskId = generateTaskId("llm-based-auto-rubric")) {
|
|
946
|
+
const p = llmAutoRubricPayloadSchema.parse(raw);
|
|
947
|
+
const opened = p.files.map((f) => ({
|
|
948
|
+
type: "file",
|
|
949
|
+
panelNumber: 0,
|
|
950
|
+
content: f
|
|
951
|
+
}));
|
|
952
|
+
return {
|
|
953
|
+
type: "llm-based-auto-rubric",
|
|
954
|
+
taskId,
|
|
955
|
+
source: {
|
|
956
|
+
name: p.name ?? taskId,
|
|
957
|
+
showName: p.show_name,
|
|
958
|
+
instructions: p.instructions,
|
|
959
|
+
metadata: {
|
|
960
|
+
tags: ensureAssessmentTag(p.tags, "llm-based-auto-rubric"),
|
|
961
|
+
files: p.files,
|
|
962
|
+
opened
|
|
963
|
+
},
|
|
964
|
+
bloomsObjectiveLevel: p.blooms,
|
|
965
|
+
learningObjectives: p.lo,
|
|
966
|
+
guidance: p.guidance,
|
|
967
|
+
showGuidanceAfterResponseOption: {
|
|
968
|
+
type: "Attempts",
|
|
969
|
+
passedFrom: p.show_guidance_after_attempts
|
|
970
|
+
},
|
|
971
|
+
maxAttemptsCount: p.max_attempts,
|
|
972
|
+
points: p.points,
|
|
973
|
+
arePartialPointsAllowed: p.partial_points,
|
|
974
|
+
rubrics: p.rubrics.map((r, i) => ({
|
|
975
|
+
id: r.id ?? String(i + 1),
|
|
976
|
+
title: r.title,
|
|
977
|
+
description: r.description,
|
|
978
|
+
points: r.points
|
|
979
|
+
})),
|
|
980
|
+
rubricsRequirements: p.rubrics_requirements,
|
|
981
|
+
solutions: p.solutions
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
var createAssessmentInputSchema = z2.discriminatedUnion("type", [
|
|
986
|
+
z2.object({
|
|
987
|
+
type: z2.literal("multiple-choice"),
|
|
988
|
+
workspace: z2.string().min(1),
|
|
989
|
+
payload: z2.unknown()
|
|
990
|
+
}),
|
|
991
|
+
z2.object({
|
|
992
|
+
type: z2.literal("code-output-compare"),
|
|
993
|
+
workspace: z2.string().min(1),
|
|
994
|
+
payload: z2.unknown()
|
|
995
|
+
}),
|
|
996
|
+
z2.object({
|
|
997
|
+
type: z2.literal("fill-in-the-blanks"),
|
|
998
|
+
workspace: z2.string().min(1),
|
|
999
|
+
payload: z2.unknown()
|
|
1000
|
+
}),
|
|
1001
|
+
z2.object({
|
|
1002
|
+
type: z2.literal("parsons-puzzle"),
|
|
1003
|
+
workspace: z2.string().min(1),
|
|
1004
|
+
payload: z2.unknown()
|
|
1005
|
+
}),
|
|
1006
|
+
z2.object({
|
|
1007
|
+
type: z2.literal("llm-based-auto-rubric"),
|
|
1008
|
+
workspace: z2.string().min(1),
|
|
1009
|
+
payload: z2.unknown()
|
|
1010
|
+
})
|
|
1011
|
+
]);
|
|
1012
|
+
async function createAssessmentHandler(raw) {
|
|
1013
|
+
const input = createAssessmentInputSchema.parse(raw);
|
|
1014
|
+
const { assessments, workspace } = resolveCodioPaths({
|
|
1015
|
+
directory: input.workspace
|
|
1016
|
+
});
|
|
1017
|
+
let json;
|
|
1018
|
+
let name;
|
|
1019
|
+
let codeStarter = null;
|
|
1020
|
+
let llmSolutions = [];
|
|
1021
|
+
if (input.type === "multiple-choice") {
|
|
1022
|
+
const taskId = generateTaskId("multiple-choice");
|
|
1023
|
+
json = buildMcqAssessment(input.payload, taskId);
|
|
1024
|
+
name = json.source.name || taskId;
|
|
1025
|
+
} else if (input.type === "code-output-compare") {
|
|
1026
|
+
const taskId = generateTaskId("code-output-compare");
|
|
1027
|
+
json = buildCodeTestAssessment(input.payload, taskId);
|
|
1028
|
+
name = json.source.name || taskId;
|
|
1029
|
+
const parsed = codeTestPayloadSchema.parse(input.payload);
|
|
1030
|
+
if (parsed.starter_file_path && parsed.starter_file_content !== undefined) {
|
|
1031
|
+
codeStarter = {
|
|
1032
|
+
path: parsed.starter_file_path,
|
|
1033
|
+
content: parsed.starter_file_content
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
} else if (input.type === "fill-in-the-blanks") {
|
|
1037
|
+
const taskId = generateTaskId("fill-in-the-blanks");
|
|
1038
|
+
json = buildFitbAssessment(input.payload, taskId);
|
|
1039
|
+
name = json.source.name || taskId;
|
|
1040
|
+
} else if (input.type === "parsons-puzzle") {
|
|
1041
|
+
const taskId = generateTaskId("parsons-puzzle");
|
|
1042
|
+
json = buildParsonsAssessment(input.payload, taskId);
|
|
1043
|
+
name = json.source.name || taskId;
|
|
1044
|
+
} else if (input.type === "llm-based-auto-rubric") {
|
|
1045
|
+
const taskId = generateTaskId("llm-based-auto-rubric");
|
|
1046
|
+
json = buildLlmAutoRubricAssessment(input.payload, taskId);
|
|
1047
|
+
name = json.source.name || taskId;
|
|
1048
|
+
const parsed = llmAutoRubricPayloadSchema.parse(input.payload);
|
|
1049
|
+
llmSolutions = parsed.solutions;
|
|
1050
|
+
} else {
|
|
1051
|
+
throw new Error(`unsupported assessment type: ${input.type}`);
|
|
1052
|
+
}
|
|
1053
|
+
const jsonPath = join3(assessments, `${json.taskId}.json`);
|
|
1054
|
+
try {
|
|
1055
|
+
writeFileSync(jsonPath, `${JSON.stringify(json, null, 2)}
|
|
1056
|
+
`, {
|
|
1057
|
+
flag: "wx"
|
|
1058
|
+
});
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
if (err.code === "EEXIST") {
|
|
1061
|
+
throw new Error(`taskId collision at ${jsonPath}`);
|
|
1062
|
+
}
|
|
1063
|
+
throw err;
|
|
1064
|
+
}
|
|
1065
|
+
if (codeStarter) {
|
|
1066
|
+
const starterPath = join3(workspace, codeStarter.path);
|
|
1067
|
+
if (!existsSync2(starterPath)) {
|
|
1068
|
+
mkdirSync(dirname(starterPath), { recursive: true });
|
|
1069
|
+
writeFileSync(starterPath, codeStarter.content);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
for (const solution of llmSolutions) {
|
|
1073
|
+
const solPath = join3(workspace, solution.path);
|
|
1074
|
+
if (!existsSync2(solPath)) {
|
|
1075
|
+
mkdirSync(dirname(solPath), { recursive: true });
|
|
1076
|
+
writeFileSync(solPath, solution.content);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
taskId: json.taskId,
|
|
1081
|
+
jsonPath,
|
|
1082
|
+
embedLine: `{${name} |assessment}(${json.taskId})`
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/tools/create-page.ts
|
|
1087
|
+
import { unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1088
|
+
import { join as join4 } from "node:path";
|
|
1089
|
+
import { z as z3 } from "zod";
|
|
1090
|
+
var PAGE_TYPES = [
|
|
1091
|
+
"intro",
|
|
1092
|
+
"objectives",
|
|
1093
|
+
"instructional",
|
|
1094
|
+
"assessment",
|
|
1095
|
+
"conclusion"
|
|
1096
|
+
];
|
|
1097
|
+
var PAGE_LAYOUTS = ["1-panel", "2-panels-tree"];
|
|
1098
|
+
var createPageInputSchema = z3.object({
|
|
1099
|
+
workspace: z3.string().min(1),
|
|
1100
|
+
stem: z3.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "stem must be kebab-case"),
|
|
1101
|
+
title: z3.string().min(1),
|
|
1102
|
+
type: z3.enum(PAGE_TYPES),
|
|
1103
|
+
layout: z3.enum(PAGE_LAYOUTS),
|
|
1104
|
+
learningObjectives: z3.array(z3.string()).default([]),
|
|
1105
|
+
files: z3.array(z3.string()).optional(),
|
|
1106
|
+
markdownBody: z3.string(),
|
|
1107
|
+
chapterFolder: z3.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "chapterFolder must be a kebab-case directory name (no slashes, no ..)").optional()
|
|
1108
|
+
}).refine((v) => v.layout !== "2-panels-tree" || v.files && v.files.length > 0, { message: "layout=2-panels-tree requires files", path: ["files"] });
|
|
1109
|
+
function buildPageJson(input) {
|
|
1110
|
+
const files = input.layout === "2-panels-tree" && input.files && input.files.length > 0 ? input.files.map((path) => ({ path, panel: 0, action: "open" })) : [];
|
|
1111
|
+
return {
|
|
1112
|
+
id: generateUuidV4(),
|
|
1113
|
+
title: input.title,
|
|
1114
|
+
type: "page",
|
|
1115
|
+
contentType: "markdown",
|
|
1116
|
+
layout: input.layout,
|
|
1117
|
+
files,
|
|
1118
|
+
teacherOnly: false,
|
|
1119
|
+
learningObjectives: input.learningObjectives.join("; ")
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function writeCreatedPage(workspace, chapterFolder, input) {
|
|
1123
|
+
const { content } = resolveCodioPaths({ directory: workspace });
|
|
1124
|
+
const targetDir = chapterFolder ? join4(content, chapterFolder) : content;
|
|
1125
|
+
const json = buildPageJson(input);
|
|
1126
|
+
const jsonPath = join4(targetDir, `${input.stem}.json`);
|
|
1127
|
+
const mdPath = join4(targetDir, `${input.stem}.md`);
|
|
1128
|
+
try {
|
|
1129
|
+
writeFileSync2(jsonPath, `${JSON.stringify(json, null, 2)}
|
|
1130
|
+
`, {
|
|
1131
|
+
flag: "wx"
|
|
1132
|
+
});
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
if (err.code === "EEXIST") {
|
|
1135
|
+
throw new Error(`stem already exists at ${targetDir}: ${input.stem}`);
|
|
1136
|
+
}
|
|
1137
|
+
throw err;
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
writeFileSync2(mdPath, input.markdownBody, { flag: "wx" });
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
try {
|
|
1143
|
+
unlinkSync(jsonPath);
|
|
1144
|
+
} catch {}
|
|
1145
|
+
if (err.code === "EEXIST") {
|
|
1146
|
+
throw new Error(`stem already exists at ${targetDir}: ${input.stem}`);
|
|
1147
|
+
}
|
|
1148
|
+
throw err;
|
|
1149
|
+
}
|
|
1150
|
+
return { uuid: json.id, stem: input.stem, jsonPath, mdPath };
|
|
1151
|
+
}
|
|
1152
|
+
async function createPageHandler(raw) {
|
|
1153
|
+
const input = createPageInputSchema.parse(raw);
|
|
1154
|
+
return writeCreatedPage(input.workspace, input.chapterFolder, input);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/tools/validate-guide.ts
|
|
1158
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
1159
|
+
import { join as join5, relative } from "node:path";
|
|
1160
|
+
import { z as z4 } from "zod";
|
|
1161
|
+
var validateGuideInputSchema = z4.object({
|
|
1162
|
+
workspace: z4.string().min(1)
|
|
1163
|
+
});
|
|
1164
|
+
function walkJsonFiles(root) {
|
|
1165
|
+
if (!existsSync3(root))
|
|
1166
|
+
return [];
|
|
1167
|
+
const out = [];
|
|
1168
|
+
for (const name of readdirSync(root)) {
|
|
1169
|
+
const p = join5(root, name);
|
|
1170
|
+
const s = statSync(p);
|
|
1171
|
+
if (s.isDirectory())
|
|
1172
|
+
out.push(...walkJsonFiles(p));
|
|
1173
|
+
else if (name.endsWith(".json"))
|
|
1174
|
+
out.push(p);
|
|
1175
|
+
}
|
|
1176
|
+
return out;
|
|
1177
|
+
}
|
|
1178
|
+
function walkMdFiles(root) {
|
|
1179
|
+
if (!existsSync3(root))
|
|
1180
|
+
return [];
|
|
1181
|
+
const out = [];
|
|
1182
|
+
for (const name of readdirSync(root)) {
|
|
1183
|
+
const p = join5(root, name);
|
|
1184
|
+
const s = statSync(p);
|
|
1185
|
+
if (s.isDirectory())
|
|
1186
|
+
out.push(...walkMdFiles(p));
|
|
1187
|
+
else if (name.endsWith(".md"))
|
|
1188
|
+
out.push(p);
|
|
1189
|
+
}
|
|
1190
|
+
return out;
|
|
1191
|
+
}
|
|
1192
|
+
function collectIdFields(value, out) {
|
|
1193
|
+
if (!value || typeof value !== "object")
|
|
1194
|
+
return;
|
|
1195
|
+
if (Array.isArray(value)) {
|
|
1196
|
+
for (const v of value)
|
|
1197
|
+
collectIdFields(v, out);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const obj = value;
|
|
1201
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1202
|
+
if ((k === "id" || k === "_id") && typeof v === "string" && v.length > 0) {
|
|
1203
|
+
out.push(v);
|
|
1204
|
+
}
|
|
1205
|
+
collectIdFields(v, out);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function validateGuideHandler(raw) {
|
|
1209
|
+
const input = validateGuideInputSchema.parse(raw);
|
|
1210
|
+
const { guides, content, assessments } = resolveCodioPaths({
|
|
1211
|
+
directory: input.workspace
|
|
1212
|
+
});
|
|
1213
|
+
const findings = [];
|
|
1214
|
+
if (!existsSync3(content)) {
|
|
1215
|
+
findings.push({
|
|
1216
|
+
category: "missing_content_dir",
|
|
1217
|
+
detail: `${content} does not exist`,
|
|
1218
|
+
severity: "error"
|
|
1219
|
+
});
|
|
1220
|
+
return { findings, passed: false };
|
|
1221
|
+
}
|
|
1222
|
+
for (const f of walkJsonFiles(guides)) {
|
|
1223
|
+
let parsed;
|
|
1224
|
+
try {
|
|
1225
|
+
parsed = JSON.parse(readFileSync2(f, "utf8"));
|
|
1226
|
+
} catch {
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
const ids = [];
|
|
1230
|
+
collectIdFields(parsed, ids);
|
|
1231
|
+
for (const id of ids) {
|
|
1232
|
+
if (!isUuidV4(id)) {
|
|
1233
|
+
findings.push({
|
|
1234
|
+
category: "invalid_uuid",
|
|
1235
|
+
detail: `${relative(input.workspace, f)}: ${id}`,
|
|
1236
|
+
severity: "error"
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
for (const f of walkJsonFiles(content)) {
|
|
1242
|
+
if (!f.endsWith("/index.json"))
|
|
1243
|
+
continue;
|
|
1244
|
+
let parsed;
|
|
1245
|
+
try {
|
|
1246
|
+
parsed = JSON.parse(readFileSync2(f, "utf8"));
|
|
1247
|
+
} catch {
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
const base = f.slice(0, -"/index.json".length);
|
|
1251
|
+
for (const stem of parsed?.order ?? []) {
|
|
1252
|
+
const jsonPair = existsSync3(join5(base, `${stem}.json`)) && existsSync3(join5(base, `${stem}.md`));
|
|
1253
|
+
const chapter = existsSync3(join5(base, stem, "index.json"));
|
|
1254
|
+
if (!jsonPair && !chapter) {
|
|
1255
|
+
findings.push({
|
|
1256
|
+
category: "orphan_order_stem",
|
|
1257
|
+
detail: `${relative(input.workspace, f)} references missing stem '${stem}'`,
|
|
1258
|
+
severity: "error"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
for (const md of walkMdFiles(content)) {
|
|
1264
|
+
const body = readFileSync2(md, "utf8");
|
|
1265
|
+
const re = new RegExp(ANY_TASKID_RE.source, "g");
|
|
1266
|
+
for (const match of body.matchAll(re)) {
|
|
1267
|
+
const tid = match[0];
|
|
1268
|
+
if (!existsSync3(join5(assessments, `${tid}.json`))) {
|
|
1269
|
+
findings.push({
|
|
1270
|
+
category: "broken_assessment_ref",
|
|
1271
|
+
detail: `${relative(input.workspace, md)} references missing ${tid}.json`,
|
|
1272
|
+
severity: "error"
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (existsSync3(assessments)) {
|
|
1278
|
+
for (const f of walkJsonFiles(assessments)) {
|
|
1279
|
+
if (!f.endsWith(".json"))
|
|
1280
|
+
continue;
|
|
1281
|
+
const expected = f.split("/").pop().replace(/\.json$/, "");
|
|
1282
|
+
let parsed;
|
|
1283
|
+
try {
|
|
1284
|
+
parsed = JSON.parse(readFileSync2(f, "utf8"));
|
|
1285
|
+
} catch {
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
const actual = parsed?.taskId ?? "";
|
|
1289
|
+
if (expected !== actual) {
|
|
1290
|
+
findings.push({
|
|
1291
|
+
category: "taskid_filename_mismatch",
|
|
1292
|
+
detail: `${relative(input.workspace, f)}: filename '${expected}' vs taskId '${actual}'`,
|
|
1293
|
+
severity: "error"
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return { findings, passed: findings.length === 0 };
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/tools/index.ts
|
|
1302
|
+
var create_page = tool({
|
|
1303
|
+
description: "Stamp a Codio page JSON (with v4 UUID) and write the markdown body to .guides/content/. Returns { uuid, stem, jsonPath, mdPath }.",
|
|
1304
|
+
args: createPageInputSchema.shape,
|
|
1305
|
+
async execute(args) {
|
|
1306
|
+
return JSON.stringify(await createPageHandler(args));
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
var create_assessment = tool({
|
|
1310
|
+
description: "Create a Codio assessment JSON of type multiple-choice, code-output-compare, or fill-in-the-blanks. Generates the taskId, writes to .guides/assessments/, returns { taskId, jsonPath, embedLine }.",
|
|
1311
|
+
args: {
|
|
1312
|
+
type: tool.schema.enum(["multiple-choice", "code-output-compare", "fill-in-the-blanks"]).describe("Assessment type"),
|
|
1313
|
+
workspace: tool.schema.string().min(1).describe("Absolute path to the Codio workspace root"),
|
|
1314
|
+
payload: tool.schema.unknown().describe("Assessment payload object (type-specific fields)")
|
|
1315
|
+
},
|
|
1316
|
+
async execute(args) {
|
|
1317
|
+
return JSON.stringify(await createAssessmentHandler(args));
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
var validate_guide = tool({
|
|
1321
|
+
description: "Run structural validation on a Codio guide directory. Checks UUIDs, order arrays, assessment embed references, and taskId/filename consistency. Returns { findings[], passed }.",
|
|
1322
|
+
args: validateGuideInputSchema.shape,
|
|
1323
|
+
async execute(args) {
|
|
1324
|
+
return JSON.stringify(await validateGuideHandler(args));
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
var tools = {
|
|
1328
|
+
create_page,
|
|
1329
|
+
create_assessment,
|
|
1330
|
+
validate_guide
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
// src/index.ts
|
|
1334
|
+
var CodioAuthoringPlugin = async (ctx) => {
|
|
1335
|
+
const config = loadPluginConfig(ctx.directory);
|
|
1336
|
+
const agentDefs = createAgents(config);
|
|
1337
|
+
const agents = getAgentConfigs(agentDefs);
|
|
1338
|
+
const disabledAgents = new Set(config.disabledAgents);
|
|
1339
|
+
const sessionAgentMap = new Map;
|
|
1340
|
+
return {
|
|
1341
|
+
name: "codio-authoring-opencode",
|
|
1342
|
+
agent: agents,
|
|
1343
|
+
tool: tools,
|
|
1344
|
+
config: async (opencodeConfig) => {
|
|
1345
|
+
opencodeConfig.default_agent = "codio-orchestrator";
|
|
1346
|
+
if (!opencodeConfig.provider)
|
|
1347
|
+
opencodeConfig.provider = {};
|
|
1348
|
+
const providerRoot = opencodeConfig.provider;
|
|
1349
|
+
if (!providerRoot["amazon-bedrock"]) {
|
|
1350
|
+
providerRoot["amazon-bedrock"] = { models: {} };
|
|
1351
|
+
}
|
|
1352
|
+
const bedrock = providerRoot["amazon-bedrock"];
|
|
1353
|
+
if (!bedrock.models)
|
|
1354
|
+
bedrock.models = {};
|
|
1355
|
+
for (const [key, def] of Object.entries(DEFAULT_BEDROCK_MODELS)) {
|
|
1356
|
+
if (!bedrock.models[key]) {
|
|
1357
|
+
bedrock.models[key] = { ...def };
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (!opencodeConfig.agent) {
|
|
1361
|
+
opencodeConfig.agent = { ...agents };
|
|
1362
|
+
} else {
|
|
1363
|
+
for (const [name, pluginAgent] of Object.entries(agents)) {
|
|
1364
|
+
const existing = opencodeConfig.agent[name];
|
|
1365
|
+
opencodeConfig.agent[name] = existing ? { ...pluginAgent, ...existing } : { ...pluginAgent };
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (!opencodeConfig.command)
|
|
1369
|
+
opencodeConfig.command = {};
|
|
1370
|
+
opencodeConfig.command["validate-guide"] = {
|
|
1371
|
+
template: "Call the validate_guide tool for the current workspace",
|
|
1372
|
+
description: "Run structural validation on the current Codio assignment"
|
|
1373
|
+
};
|
|
1374
|
+
},
|
|
1375
|
+
"chat.message": async (input, output) => {
|
|
1376
|
+
const agent = input.agent ?? output?.message?.agent;
|
|
1377
|
+
if (agent)
|
|
1378
|
+
sessionAgentMap.set(input.sessionID, agent);
|
|
1379
|
+
},
|
|
1380
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
1381
|
+
const agentName = input.sessionID ? sessionAgentMap.get(input.sessionID) : undefined;
|
|
1382
|
+
if (agentName !== "codio-orchestrator")
|
|
1383
|
+
return;
|
|
1384
|
+
const already = output.system.some((s) => typeof s === "string" && s.includes("Codio Authoring Orchestrator"));
|
|
1385
|
+
if (already)
|
|
1386
|
+
return;
|
|
1387
|
+
const orchestratorDef = agentDefs.find((a) => a.name === "codio-orchestrator");
|
|
1388
|
+
const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(disabledAgents);
|
|
1389
|
+
output.system[0] = orchestratorPrompt + (output.system[0] ? `
|
|
1390
|
+
|
|
1391
|
+
${output.system[0]}` : "");
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
};
|
|
1395
|
+
var src_default = CodioAuthoringPlugin;
|
|
1396
|
+
export {
|
|
1397
|
+
src_default as default
|
|
1398
|
+
};
|