diff --git a/packages/plugins/starter-pack/business-management/expected-lines.json b/packages/plugins/starter-pack/business-management/expected-lines.json new file mode 100644 index 000000000..b05ecf23b --- /dev/null +++ b/packages/plugins/starter-pack/business-management/expected-lines.json @@ -0,0 +1,63 @@ +{ + "chief": { "template": "chief-of-staff", "sha256": "b98473a67e7d765074ad5a94085a2236905e159085036831602799f42f51fb2f" }, + "hires": { + "pm-1": { + "definition": "pm", + "flagged": [ + {"field":"authority.withinRole","value":"decision.resolve.delivery","reasons":["resolve-right"]}, + {"field":"authority.withinRole","value":"task.create","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.assign","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.reassign","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.schedule","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.update.assigned","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.close","reasons":["above-chief"]}, + {"field":"authority.crossRole","value":"task.priority.change","reasons":["above-chief"]}, + {"field":"authority.crossRole","value":"task.scope.change","reasons":["above-chief"]}, + {"field":"tools","value":"write","reasons":["above-chief"]}, + {"field":"tools","value":"edit","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:issue","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks.read_one","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks.update","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_assignees.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_assignees.delete","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_relations.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_relations.delete","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_labels.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_labels.delete","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_comments.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"labels.read_all","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"projects.views_buckets_tasks","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"write:issue"}, + {"service":"gitea","scope":"read:repository"}, + {"service":"gitea","scope":"read:user"}, + {"service":"vikunja","scope":"tasks.read_one"}, + {"service":"vikunja","scope":"tasks.create"}, + {"service":"vikunja","scope":"tasks.update"}, + {"service":"vikunja","scope":"tasks_assignees.create"}, + {"service":"vikunja","scope":"tasks_assignees.delete"}, + {"service":"vikunja","scope":"tasks_relations.create"}, + {"service":"vikunja","scope":"tasks_relations.delete"}, + {"service":"vikunja","scope":"tasks_labels.create"}, + {"service":"vikunja","scope":"tasks_labels.delete"}, + {"service":"vikunja","scope":"tasks_comments.create"}, + {"service":"vikunja","scope":"labels.read_all"}, + {"service":"vikunja","scope":"projects.views_buckets_tasks"} + ] + }, + "research-1": { + "definition": "research", + "flagged": [ + {"field":"authority.withinRole","value":"library.write","reasons":["above-chief"]}, + {"field":"tools","value":"web","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:repository","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"read:user"}, + {"service":"gitea","scope":"write:repository"} + ] + } + } +} diff --git a/packages/plugins/starter-pack/business-management/index.mjs b/packages/plugins/starter-pack/business-management/index.mjs new file mode 100644 index 000000000..812c57327 --- /dev/null +++ b/packages/plugins/starter-pack/business-management/index.mjs @@ -0,0 +1,51 @@ +// Shipped starter pack "business-management" (#1873 SP2). Design: starter-packs-design section 7. +// Every definition is a whole V2 template pinned by its templateHash, so a template edit refuses +// this pack (starter-pack-template-changed) until it is re-pinned. expected-lines.json beside this +// file holds the lines each proposed hire carries against the chief; the shipped-pack test checks it. +export const interfaceVersion = 1; + +export function starterPack() { + return { + packVersion: 1, + id: "business-management", + version: "1.0.0", + label: "Business management", + description: "Run the business: research gathers facts, a pm writes the task, the relevant officer drafts, and you decide.", + scope: "org", + definitions: { + "chief-executive-officer": { template: "chief-executive-officer", sha256: "1cb3916320f89ce18d9a524f155877af8ed03c5423f89e9be023197f3b745cee" }, + "chief-financial-officer": { template: "chief-financial-officer", sha256: "f6d25a8c04511db0cee8cde2b188accb702285b53e91e0357af2a444541989ab" }, + "chief-information-officer": { template: "chief-information-officer", sha256: "b4922e423f6ee521bc7a57b12deb0d066c81901c721979e54180b5045e0893f0" }, + "chief-information-security-officer": { template: "chief-information-security-officer", sha256: "c8dfa3bc9d9097ab37edb250afa5b5f68ecfd2e6fb678a6d2e8ab2401536b8af" }, + pm: { template: "pm", sha256: "d0dad787802fdced716b002edb2f7cc395e2ed2f3b5bac8711d7190b07699192" }, + research: { template: "research", sha256: "6c181f0c0fef4730d9edd8dee3b73ee14cf71cb730d42e32b8fb2bb06b20cbe4" }, + continuity: { template: "continuity", sha256: "8ce4abfc894733a12b8cce7d1029178896dc2043971db897554483aeb7467951" }, + }, + hires: [ + { name: "pm-1", definition: "pm", reason: "writes and tracks the tasks" }, + { name: "research-1", definition: "research", reason: "gathers the facts for each decision" }, + ], + board: { + columns: [ + { state: "todo", label: "To do" }, + { state: "in-progress", label: "Doing" }, + { state: "in-review", label: "Waiting on review" }, + { state: "blocked", label: "Blocked" }, + { state: "done", label: "Done" }, + ], + layout: "list", + }, + workflows: { + "decision-brief": { + description: "One decision from facts to your call.", + steps: [ + { id: "facts", title: "Gather the facts", roles: ["research"], state: "todo" }, + { id: "task", title: "Write the task", roles: ["pm"], state: "todo" }, + { id: "draft", title: "Draft the brief", roles: ["chief-executive-officer", "chief-financial-officer", "chief-information-officer", "chief-information-security-officer"], state: "in-progress" }, + { id: "decide", title: "Decide", roles: ["human"], state: "done" }, + ], + }, + }, + skills: ["ms-unslop", "ms-goal", "ms-communications"], + }; +} diff --git a/packages/plugins/starter-pack/coding/expected-lines.json b/packages/plugins/starter-pack/coding/expected-lines.json new file mode 100644 index 000000000..1877b9e74 --- /dev/null +++ b/packages/plugins/starter-pack/coding/expected-lines.json @@ -0,0 +1,101 @@ +{ + "chief": { "template": "chief-of-staff", "sha256": "b98473a67e7d765074ad5a94085a2236905e159085036831602799f42f51fb2f" }, + "hires": { + "pm-1": { + "definition": "pm", + "flagged": [ + {"field":"authority.withinRole","value":"decision.resolve.delivery","reasons":["resolve-right"]}, + {"field":"authority.withinRole","value":"task.create","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.assign","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.reassign","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.schedule","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.update.assigned","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.close","reasons":["above-chief"]}, + {"field":"authority.crossRole","value":"task.priority.change","reasons":["above-chief"]}, + {"field":"authority.crossRole","value":"task.scope.change","reasons":["above-chief"]}, + {"field":"tools","value":"write","reasons":["above-chief"]}, + {"field":"tools","value":"edit","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:issue","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks.read_one","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks.update","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_assignees.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_assignees.delete","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_relations.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_relations.delete","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_labels.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_labels.delete","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_comments.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"labels.read_all","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"projects.views_buckets_tasks","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"write:issue"}, + {"service":"gitea","scope":"read:repository"}, + {"service":"gitea","scope":"read:user"}, + {"service":"vikunja","scope":"tasks.read_one"}, + {"service":"vikunja","scope":"tasks.create"}, + {"service":"vikunja","scope":"tasks.update"}, + {"service":"vikunja","scope":"tasks_assignees.create"}, + {"service":"vikunja","scope":"tasks_assignees.delete"}, + {"service":"vikunja","scope":"tasks_relations.create"}, + {"service":"vikunja","scope":"tasks_relations.delete"}, + {"service":"vikunja","scope":"tasks_labels.create"}, + {"service":"vikunja","scope":"tasks_labels.delete"}, + {"service":"vikunja","scope":"tasks_comments.create"}, + {"service":"vikunja","scope":"labels.read_all"}, + {"service":"vikunja","scope":"projects.views_buckets_tasks"} + ] + }, + "coder-1": { + "definition": "coder", + "flagged": [ + {"field":"authority.withinRole","value":"task.update.assigned","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"git.push.working","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"review.request","reasons":["above-chief"]}, + {"field":"authority.crossRole","value":"task.scope.change","reasons":["above-chief"]}, + {"field":"tools","value":"write","reasons":["above-chief"]}, + {"field":"tools","value":"edit","reasons":["above-chief"]}, + {"field":"tools","value":"bash","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:issue","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:repository","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks.read_one","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks.update","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_comments.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"projects.views_buckets_tasks","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"write:issue"}, + {"service":"gitea","scope":"write:repository"}, + {"service":"gitea","scope":"read:user"}, + {"service":"vikunja","scope":"tasks.read_one"}, + {"service":"vikunja","scope":"tasks.update"}, + {"service":"vikunja","scope":"tasks_comments.create"}, + {"service":"vikunja","scope":"projects.views_buckets_tasks"} + ] + }, + "reviewer-1": { + "definition": "reviewer", + "flagged": [ + {"field":"authority.withinRole","value":"review.verdict","reasons":["above-chief"]}, + {"field":"authority.withinRole","value":"task.update.assigned","reasons":["above-chief"]}, + {"field":"tools","value":"bash","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:issue","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:repository","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks.read_one","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks.update","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"tasks_comments.create","reasons":["above-chief"]}, + {"field":"credentials.vikunja","value":"projects.views_buckets_tasks","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"write:issue"}, + {"service":"gitea","scope":"write:repository"}, + {"service":"gitea","scope":"read:user"}, + {"service":"vikunja","scope":"tasks.read_one"}, + {"service":"vikunja","scope":"tasks.update"}, + {"service":"vikunja","scope":"tasks_comments.create"}, + {"service":"vikunja","scope":"projects.views_buckets_tasks"} + ] + } + } +} diff --git a/packages/plugins/starter-pack/coding/index.mjs b/packages/plugins/starter-pack/coding/index.mjs new file mode 100644 index 000000000..a84213c38 --- /dev/null +++ b/packages/plugins/starter-pack/coding/index.mjs @@ -0,0 +1,51 @@ +// Shipped starter pack "coding" (#1873 SP2). Design: starter-packs-design section 7. +// Every definition is a whole V2 template pinned by its templateHash, so a template edit refuses +// this pack (starter-pack-template-changed) until it is re-pinned. expected-lines.json beside this +// file holds the lines each proposed hire carries against the chief; the shipped-pack test checks it. +export const interfaceVersion = 1; + +export function starterPack() { + return { + packVersion: 1, + id: "coding", + version: "1.0.0", + label: "Coding", + description: "A small software team: a pm writes the task, a coder builds it, a reviewer reviews it, and you approve the merge.", + scope: "org", + definitions: { + coder: { template: "coder", sha256: "8d2564179a6322983aff460302e96e735181bb0ed98d8439a0b04df66eb2dbf5" }, + reviewer: { template: "reviewer", sha256: "07dd6d84bbc26acdfe7d62c545ee9373dd1faa0bc98c763eb52e616746a4be51" }, + "security-reviewer": { template: "security-reviewer", sha256: "a17088a4e7b0ef6d310a023c9a83826d32719bfebfcff436260ed07ade3a588f" }, + pm: { template: "pm", sha256: "d0dad787802fdced716b002edb2f7cc395e2ed2f3b5bac8711d7190b07699192" }, + "merge-gate": { template: "merge-gate", sha256: "019667e8f15b6b58c349c4bad439658eae026fa4185828b7cc02da771f25f453" }, + cto: { template: "cto", sha256: "df3777ee1eeddb1df6805b56e56d85cd0d45292493bf61115af912119b1111f6" }, + }, + hires: [ + { name: "pm-1", definition: "pm", reason: "writes and assigns the tasks" }, + { name: "coder-1", definition: "coder", reason: "implements each change" }, + { name: "reviewer-1", definition: "reviewer", reason: "reviews each change before merge" }, + ], + board: { + columns: [ + { state: "todo", label: "Backlog" }, + { state: "in-progress", label: "Building" }, + { state: "in-review", label: "Review" }, + { state: "blocked", label: "Blocked" }, + { state: "done", label: "Merged" }, + ], + layout: "kanban", + }, + workflows: { + change: { + description: "One change from task to merge.", + steps: [ + { id: "plan", title: "Write the task", roles: ["pm"], state: "todo" }, + { id: "build", title: "Implement the change", roles: ["coder"], state: "in-progress" }, + { id: "review", title: "Review the change", roles: ["reviewer"], state: "in-review" }, + { id: "merge", title: "Approve the merge", roles: ["human", "merge-gate"], state: "done" }, + ], + }, + }, + skills: ["ms-sdlc", "ms-unslop", "ms-file-read", "ms-file-write", "ms-goal", "ms-communications"], + }; +} diff --git a/packages/plugins/starter-pack/creative-writing/expected-lines.json b/packages/plugins/starter-pack/creative-writing/expected-lines.json new file mode 100644 index 000000000..74abfc892 --- /dev/null +++ b/packages/plugins/starter-pack/creative-writing/expected-lines.json @@ -0,0 +1,38 @@ +{ + "chief": { "template": "chief-of-staff", "sha256": "b98473a67e7d765074ad5a94085a2236905e159085036831602799f42f51fb2f" }, + "hires": { + "ideas-1": { + "definition": "ideas", + "flagged": [ + {"field":"authority.withinRole","value":"library.write","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:repository","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"read:user"}, + {"service":"gitea","scope":"write:repository"} + ] + }, + "proofreader-1": { + "definition": "proofreader", + "flagged": [ + {"field":"authority.withinRole","value":"library.write","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:repository","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"read:user"}, + {"service":"gitea","scope":"write:repository"} + ] + }, + "continuity-1": { + "definition": "continuity", + "flagged": [ + {"field":"authority.withinRole","value":"library.write","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:repository","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"read:user"}, + {"service":"gitea","scope":"write:repository"} + ] + } + } +} diff --git a/packages/plugins/starter-pack/creative-writing/index.mjs b/packages/plugins/starter-pack/creative-writing/index.mjs new file mode 100644 index 000000000..6f2d2c49f --- /dev/null +++ b/packages/plugins/starter-pack/creative-writing/index.mjs @@ -0,0 +1,51 @@ +// Shipped starter pack "creative-writing" (#1873 SP2). Design: starter-packs-design section 7. +// Every definition is a whole V2 template pinned by its templateHash, so a template edit refuses +// this pack (starter-pack-template-changed) until it is re-pinned. expected-lines.json beside this +// file holds the lines each proposed hire carries against the chief; the shipped-pack test checks it. +export const interfaceVersion = 1; + +export function starterPack() { + return { + packVersion: 1, + id: "creative-writing", + version: "1.0.0", + label: "Creative writing", + description: "Write a book or a series: ideas outlines, you or a companion draft, continuity checks and a proofreader edits.", + scope: "org", + definitions: { + ideas: { template: "ideas", sha256: "bdaf8eb8a97606a52ba653dfae04044c18c3ceca3f90e43073b3136db1e5da95" }, + research: { template: "research", sha256: "6c181f0c0fef4730d9edd8dee3b73ee14cf71cb730d42e32b8fb2bb06b20cbe4" }, + proofreader: { template: "proofreader", sha256: "6df42775fc2185e8ecf0223acaf7fefa7413f7f45bfaded0b0d5b2720d61cca6" }, + "fact-checker": { template: "fact-checker", sha256: "c6e26f27b0b2eb18edf56bd69b9388504652762f7f9bf8e1da02ed4151a4e0ff" }, + continuity: { template: "continuity", sha256: "8ce4abfc894733a12b8cce7d1029178896dc2043971db897554483aeb7467951" }, + companion: { template: "companion", sha256: "4030f46528680ddf038386c42dce4eb66ad7e41d4670694d87b65a44ab2d1f81" }, + }, + hires: [ + { name: "ideas-1", definition: "ideas", reason: "outlines each chapter" }, + { name: "proofreader-1", definition: "proofreader", reason: "edits each draft" }, + { name: "continuity-1", definition: "continuity", reason: "checks each draft against the story so far" }, + ], + board: { + columns: [ + { state: "todo", label: "Ideas" }, + { state: "in-progress", label: "Drafting" }, + { state: "in-review", label: "Editing" }, + { state: "blocked", label: "On hold" }, + { state: "done", label: "Finished" }, + ], + layout: "kanban", + }, + workflows: { + chapter: { + description: "One chapter from outline to edited draft.", + steps: [ + { id: "outline", title: "Outline the chapter", roles: ["ideas"], state: "todo" }, + { id: "draft", title: "Draft the chapter", roles: ["human", "companion"], state: "in-progress" }, + { id: "continuity", title: "Check continuity", roles: ["continuity"], state: "in-review" }, + { id: "edit", title: "Edit the draft", roles: ["proofreader"], state: "in-review" }, + ], + }, + }, + skills: ["ms-unslop"], + }; +} diff --git a/packages/plugins/starter-pack/marketing/expected-lines.json b/packages/plugins/starter-pack/marketing/expected-lines.json new file mode 100644 index 000000000..c2828f789 --- /dev/null +++ b/packages/plugins/starter-pack/marketing/expected-lines.json @@ -0,0 +1,33 @@ +{ + "chief": { "template": "chief-of-staff", "sha256": "b98473a67e7d765074ad5a94085a2236905e159085036831602799f42f51fb2f" }, + "hires": { + "cmo": { + "definition": "chief-marketing-officer", + "flagged": [], + "credentialLines": [] + }, + "research-1": { + "definition": "research", + "flagged": [ + {"field":"authority.withinRole","value":"library.write","reasons":["above-chief"]}, + {"field":"tools","value":"web","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:repository","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"read:user"}, + {"service":"gitea","scope":"write:repository"} + ] + }, + "proofreader-1": { + "definition": "proofreader", + "flagged": [ + {"field":"authority.withinRole","value":"library.write","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:repository","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"read:user"}, + {"service":"gitea","scope":"write:repository"} + ] + } + } +} diff --git a/packages/plugins/starter-pack/marketing/index.mjs b/packages/plugins/starter-pack/marketing/index.mjs new file mode 100644 index 000000000..378f8be84 --- /dev/null +++ b/packages/plugins/starter-pack/marketing/index.mjs @@ -0,0 +1,51 @@ +// Shipped starter pack "marketing" (#1873 SP2). Design: starter-packs-design section 7. +// Every definition is a whole V2 template pinned by its templateHash, so a template edit refuses +// this pack (starter-pack-template-changed) until it is re-pinned. expected-lines.json beside this +// file holds the lines each proposed hire carries against the chief; the shipped-pack test checks it. +export const interfaceVersion = 1; + +export function starterPack() { + return { + packVersion: 1, + id: "marketing", + version: "1.0.0", + label: "Marketing", + description: "Plan and write campaigns: research finds the angle, ideas drafts, claims are checked and edited, and you approve publishing.", + scope: "org", + definitions: { + "chief-marketing-officer": { template: "chief-marketing-officer", sha256: "fb3fe65980a19d9c596451a3b722d0ba9ac819bdec8f65ee91b434e938c2edca" }, + research: { template: "research", sha256: "6c181f0c0fef4730d9edd8dee3b73ee14cf71cb730d42e32b8fb2bb06b20cbe4" }, + ideas: { template: "ideas", sha256: "bdaf8eb8a97606a52ba653dfae04044c18c3ceca3f90e43073b3136db1e5da95" }, + proofreader: { template: "proofreader", sha256: "6df42775fc2185e8ecf0223acaf7fefa7413f7f45bfaded0b0d5b2720d61cca6" }, + "fact-checker": { template: "fact-checker", sha256: "c6e26f27b0b2eb18edf56bd69b9388504652762f7f9bf8e1da02ed4151a4e0ff" }, + }, + hires: [ + { name: "cmo", definition: "chief-marketing-officer", reason: "leads the campaigns" }, + { name: "research-1", definition: "research", reason: "finds the angle for each campaign" }, + { name: "proofreader-1", definition: "proofreader", reason: "edits each draft" }, + ], + board: { + columns: [ + { state: "todo", label: "Ideas" }, + { state: "in-progress", label: "In progress" }, + { state: "in-review", label: "Approval" }, + { state: "blocked", label: "Blocked" }, + { state: "done", label: "Live" }, + ], + layout: "kanban", + }, + workflows: { + campaign: { + description: "One campaign from angle to published.", + steps: [ + { id: "angle", title: "Find the angle", roles: ["research"], state: "todo" }, + { id: "draft", title: "Draft the campaign", roles: ["ideas"], state: "in-progress" }, + { id: "check", title: "Check the claims", roles: ["fact-checker"], state: "in-review" }, + { id: "edit", title: "Edit the copy", roles: ["proofreader"], state: "in-review" }, + { id: "publish", title: "Approve publishing", roles: ["human"], state: "done" }, + ], + }, + }, + skills: ["ms-unslop"], + }; +} diff --git a/packages/plugins/starter-pack/montage/expected-lines.json b/packages/plugins/starter-pack/montage/expected-lines.json new file mode 100644 index 000000000..49b4712d4 --- /dev/null +++ b/packages/plugins/starter-pack/montage/expected-lines.json @@ -0,0 +1,28 @@ +{ + "chief": { "template": "chief-of-staff", "sha256": "b98473a67e7d765074ad5a94085a2236905e159085036831602799f42f51fb2f" }, + "hires": { + "ideas-1": { + "definition": "ideas", + "flagged": [ + {"field":"authority.withinRole","value":"library.write","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:repository","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"read:user"}, + {"service":"gitea","scope":"write:repository"} + ] + }, + "research-1": { + "definition": "research", + "flagged": [ + {"field":"authority.withinRole","value":"library.write","reasons":["above-chief"]}, + {"field":"tools","value":"web","reasons":["above-chief"]}, + {"field":"credentials.gitea","value":"write:repository","reasons":["above-chief"]} + ], + "credentialLines": [ + {"service":"gitea","scope":"read:user"}, + {"service":"gitea","scope":"write:repository"} + ] + } + } +} diff --git a/packages/plugins/starter-pack/montage/index.mjs b/packages/plugins/starter-pack/montage/index.mjs new file mode 100644 index 000000000..126f70122 --- /dev/null +++ b/packages/plugins/starter-pack/montage/index.mjs @@ -0,0 +1,48 @@ +// Shipped starter pack "montage" (#1873 SP2). Design: starter-packs-design section 7. +// Every definition is a whole V2 template pinned by its templateHash, so a template edit refuses +// this pack (starter-pack-template-changed) until it is re-pinned. expected-lines.json beside this +// file holds the lines each proposed hire carries against the chief; the shipped-pack test checks it. +export const interfaceVersion = 1; + +export function starterPack() { + return { + packVersion: 1, + id: "montage", + version: "1.0.0", + label: "Mosaic Montage", + description: "Script video episodes: research gathers sources, ideas writes the script, a proofreader edits and a reviewer checks.", + scope: "org", + definitions: { + ideas: { template: "ideas", sha256: "bdaf8eb8a97606a52ba653dfae04044c18c3ceca3f90e43073b3136db1e5da95" }, + research: { template: "research", sha256: "6c181f0c0fef4730d9edd8dee3b73ee14cf71cb730d42e32b8fb2bb06b20cbe4" }, + proofreader: { template: "proofreader", sha256: "6df42775fc2185e8ecf0223acaf7fefa7413f7f45bfaded0b0d5b2720d61cca6" }, + reviewer: { template: "reviewer", sha256: "07dd6d84bbc26acdfe7d62c545ee9373dd1faa0bc98c763eb52e616746a4be51" }, + }, + hires: [ + { name: "ideas-1", definition: "ideas", reason: "writes each script" }, + { name: "research-1", definition: "research", reason: "gathers the sources for each episode" }, + ], + board: { + columns: [ + { state: "todo", label: "Concept" }, + { state: "in-progress", label: "Script" }, + { state: "in-review", label: "Review" }, + { state: "blocked", label: "Blocked" }, + { state: "done", label: "Final" }, + ], + layout: "kanban", + }, + workflows: { + episode: { + description: "One episode from sources to checked script.", + steps: [ + { id: "sources", title: "Gather the sources", roles: ["research"], state: "todo" }, + { id: "script", title: "Write the script", roles: ["ideas"], state: "in-progress" }, + { id: "edit", title: "Edit the script", roles: ["proofreader"], state: "in-review" }, + { id: "check", title: "Check the script", roles: ["reviewer"], state: "in-review" }, + ], + }, + }, + skills: ["ms-unslop"], + }; +} diff --git a/packages/plugins/tests/starter-pack-shipped.test.mjs b/packages/plugins/tests/starter-pack-shipped.test.mjs new file mode 100644 index 000000000..ab13a199f --- /dev/null +++ b/packages/plugins/tests/starter-pack-shipped.test.mjs @@ -0,0 +1,252 @@ +// #1873 SP2: the five shipped starter packs. Design: starter-packs-design section 7 (contents) and +// SP2 in section 8 (acceptance). Each pack loads through SP1's reader with origin shipped, holds +// what section 7 lists, pins every template at today's templateHash, and records in its +// expected-lines.json the flagged and credential lines each proposed hire carries against the chief. +import test from "node:test"; +import assert from "node:assert/strict"; +import { randomUUID } from "node:crypto"; +import { appendFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { hasModule, loadInstalled } from "../src/loader.mjs"; +import { checkPack, installedStarterPacks } from "../src/starter-pack.mjs"; +import { installPlugin } from "../src/install.mjs"; +import { pluginHash } from "../src/pinned.mjs"; +import { compositionOf, entryFromTemplate, readTemplate, templateHash } from "../../organization/src/definitions.mjs"; +import { loadRole } from "../../organization/src/role.mjs"; +import { buildRegistry, checkComposition } from "../../organization/src/composition.mjs"; +import { credentialLines, hireDecisions } from "../../organization/src/hire-decisions.mjs"; +import { organizationWriter } from "../../organization/src/organization.mjs"; +import { businessDoc, rolesCopy, tmp, writeJson } from "../../organization/tests/helpers.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO = resolve(HERE, "../../.."); +const ROLES = join(REPO, "roles"); +const PACK_DIR = join(REPO, "packages/plugins/starter-pack"); +const CHIEF = "chief-of-staff"; +const OFFICERS = ["chief-executive-officer", "chief-financial-officer", "chief-information-officer", "chief-information-security-officer"]; + +// Section 7, item by item. Columns are labels in the order todo, in-progress, in-review, blocked, +// done. Each workflow is its step roles in order. +const SECTION_7 = { + coding: { + definitions: ["coder", "reviewer", "security-reviewer", "pm", "merge-gate", "cto"], + hires: [["pm-1", "pm"], ["coder-1", "coder"], ["reviewer-1", "reviewer"]], + columns: ["Backlog", "Building", "Review", "Blocked", "Merged"], layout: "kanban", + workflows: { change: [["pm"], ["coder"], ["reviewer"], ["human", "merge-gate"]] }, + skills: ["ms-sdlc", "ms-unslop", "ms-file-read", "ms-file-write", "ms-goal", "ms-communications"], + }, + "creative-writing": { + definitions: ["ideas", "research", "proofreader", "fact-checker", "continuity", "companion"], + hires: [["ideas-1", "ideas"], ["proofreader-1", "proofreader"], ["continuity-1", "continuity"]], + columns: ["Ideas", "Drafting", "Editing", "On hold", "Finished"], layout: "kanban", + workflows: { chapter: [["ideas"], ["human", "companion"], ["continuity"], ["proofreader"]] }, + skills: ["ms-unslop"], + }, + marketing: { + definitions: ["chief-marketing-officer", "research", "ideas", "proofreader", "fact-checker"], + hires: [["cmo", "chief-marketing-officer"], ["research-1", "research"], ["proofreader-1", "proofreader"]], + columns: ["Ideas", "In progress", "Approval", "Blocked", "Live"], layout: "kanban", + workflows: { campaign: [["research"], ["ideas"], ["fact-checker"], ["proofreader"], ["human"]] }, + skills: ["ms-unslop"], + }, + montage: { + definitions: ["ideas", "research", "proofreader", "reviewer"], + hires: [["ideas-1", "ideas"], ["research-1", "research"]], + columns: ["Concept", "Script", "Review", "Blocked", "Final"], layout: "kanban", + workflows: { episode: [["research"], ["ideas"], ["proofreader"], ["reviewer"]] }, + skills: ["ms-unslop"], + }, + "business-management": { + definitions: [...OFFICERS, "pm", "research", "continuity"], + hires: [["pm-1", "pm"], ["research-1", "research"]], + columns: ["To do", "Doing", "Waiting on review", "Blocked", "Done"], layout: "list", + workflows: { "decision-brief": [["research"], ["pm"], OFFICERS, ["human"]] }, + skills: ["ms-unslop", "ms-goal", "ms-communications"], + }, +}; +const IDS = Object.keys(SECTION_7); +const STATES = ["todo", "in-progress", "in-review", "blocked", "done"]; + +const moduleOf = (id) => import(pathToFileURL(join(PACK_DIR, id, "index.mjs")).href); +const expectedLines = (id) => JSON.parse(readFileSync(join(PACK_DIR, id, "expected-lines.json"), "utf8")); + +// The lines one template carries against the chief, as the broker computes them for a hire +// (hire-decisions.mjs prepare): checkComposition's flagged lines and every credential line. +function linesFor(rolesDir, template) { + const chief = loadRole(rolesDir, CHIEF); + const { composition, flagged } = checkComposition(compositionOf(entryFromTemplate(readTemplate(rolesDir, template))), + buildRegistry(), chief, { assertClean() {} }); + return { flagged: JSON.parse(JSON.stringify(flagged)), credentialLines: credentialLines(composition.credentials) }; +} + +// Every proposal of one pack against its expected-lines.json, under rolesDir. +async function checkLines(id, rolesDir) { + const { starterPack } = await moduleOf(id); + const pack = starterPack(); + const expected = expectedLines(id); + assert.deepEqual(Object.keys(expected), ["chief", "hires"], id); + assert.deepEqual(expected.chief, { template: CHIEF, sha256: templateHash(rolesDir, CHIEF) }, `${id}: the chief moved; recompute expected-lines.json`); + assert.deepEqual(Object.keys(expected.hires), pack.hires.map((h) => h.name), id); + for (const hire of pack.hires) { + const want = expected.hires[hire.name]; + assert.equal(want.definition, hire.definition, `${id} ${hire.name}`); + assert.deepEqual({ definition: want.definition, ...linesFor(rolesDir, pack.definitions[hire.definition].template) }, want, `${id} ${hire.name}`); + } +} + +// Red before: on BASE no starter-pack module exists. +test("SP2 loading starter-pack/coding finds its module, and the loader finds all five", async () => { + assert.equal(hasModule("starter-pack", "coding"), true, "loading starter-pack/coding finds no module"); + const { modules, refused } = await loadInstalled("starter-pack"); + assert.deepEqual(refused, []); + assert.deepEqual(modules.map((m) => m.name), [...IDS].sort()); +}); + +test("SP2 each shipped pack loads through SP1 with origin shipped", async () => { + const { packs, refused } = await installedStarterPacks({ rolesDir: ROLES }); + assert.deepEqual(refused, []); + assert.deepEqual(packs.map((p) => [p.id, p.origin]), [...IDS].sort().map((id) => [id, "shipped"])); + for (const { id, pack } of packs) { + const { starterPack } = await moduleOf(id); + assert.deepEqual(pack, starterPack(), id); + assert.deepEqual(checkPack(starterPack(), { origin: "shipped", rolesDir: ROLES, name: id }), pack, id); + } +}); + +test("SP2 each pack holds what section 7 lists", async () => { + for (const id of IDS) { + const want = SECTION_7[id]; + const pack = (await moduleOf(id)).starterPack(); + assert.equal(pack.id, id); + assert.equal(pack.scope, "org", id); + assert.deepEqual(Object.keys(pack.definitions), want.definitions, id); + assert.deepEqual(pack.hires.map((h) => [h.name, h.definition]), want.hires, id); + assert.deepEqual(pack.board.columns.map((c) => c.state), STATES, id); + assert.deepEqual(pack.board.columns.map((c) => c.label), want.columns, id); + assert.equal(pack.board.layout, want.layout, id); + assert.deepEqual(Object.keys(pack.workflows), Object.keys(want.workflows), id); + for (const [name, roles] of Object.entries(want.workflows)) { + assert.deepEqual(pack.workflows[name].steps.map((s) => s.roles), roles, `${id} ${name}`); + // Every step role is a definition of this pack or the user, so SP3's applyPack always + // resolves it once the pack's definitions are in the organization. + for (const step of pack.workflows[name].steps) { + for (const role of step.roles) assert.ok(role === "human" || Object.hasOwn(pack.definitions, role), `${id} ${name} ${step.id}`); + } + } + assert.deepEqual(pack.skills, want.skills, id); + } +}); + +test("SP2 every definition is a whole template pinned at today's templateHash", async () => { + for (const id of IDS) { + const pack = (await moduleOf(id)).starterPack(); + for (const [name, entry] of Object.entries(pack.definitions)) { + assert.deepEqual(entry, { template: name, sha256: templateHash(ROLES, name) }, `${id} ${name}`); + } + } +}); + +test("SP2 starterPack returns a fresh document on every call", async () => { + for (const id of IDS) { + const { starterPack } = await moduleOf(id); + const first = starterPack(); + first.hires.length = 0; + first.definitions.extra = { template: "coder", sha256: "0".repeat(64) }; + assert.notEqual(starterPack().hires.length, 0, id); + assert.equal(Object.hasOwn(starterPack().definitions, "extra"), false, id); + } +}); + +test("SP2 each proposed hire's lines against the chief equal expected-lines.json", async () => { + for (const id of IDS) await checkLines(id, ROLES); +}); + +test("SP2 chief-marketing-officer is the only proposal with no lines", async () => { + const empty = new Set(), carrying = new Set(); + for (const id of IDS) { + const { hires } = expectedLines(id); + for (const { definition, flagged, credentialLines: lines } of Object.values(hires)) { + (flagged.length === 0 && lines.length === 0 ? empty : carrying).add(definition); + } + } + assert.deepEqual([...empty], ["chief-marketing-officer"]); + assert.deepEqual([...carrying].sort(), ["coder", "continuity", "ideas", "pm", "proofreader", "research", "reviewer"]); +}); + +// The broker's own hire proposal (hire-decisions.mjs prepare) in an organization whose chief is +// roles/chief-of-staff.json computes the same lists, messaging lines included. +test("SP2 the broker's hire proposal computes the lines expected-lines.json holds", async (t) => { + const root = tmp(); + t.after(() => rmSync(root, { recursive: true, force: true })); + const rolesDir = rolesCopy(root); + const doc = businessDoc(root); + doc.plugins = { harness: ["pi"] }; + assert.equal(doc.roles[doc.launch.by].definition, CHIEF); + writeJson(join(root, "businesses/acme.json"), doc); + const writer = organizationWriter({ dir: root, rolesDir }); + const service = hireDecisions({ writer, rolesDir, configDir: root, dataRoot: join(root, "data"), assertClean: () => {} }); + const seen = new Set(); + for (const id of IDS) { + const pack = (await moduleOf(id)).starterPack(); + const expected = expectedLines(id); + for (const hire of pack.hires) { + const ref = pack.definitions[hire.definition]; + const p = await service.prepare("acme", { + op: "hire", level: "org", name: `sp2-${seen.size}`, role: { template: ref.template, sha256: ref.sha256 }, + route: { harness: "pi", model: "claude-opus-5-5" }, reason: hire.reason, + }, { id: randomUUID(), expiresAt: "2099-01-01T00:00:00Z" }); + seen.add(`${id}/${hire.name}`); + const want = expected.hires[hire.name]; + assert.deepEqual(JSON.parse(JSON.stringify({ flagged: p.flagged, credentialLines: p.credentialLines })), + { flagged: want.flagged, credentialLines: want.credentialLines }, `${id} ${hire.name}`); + } + } + assert.equal(seen.size, 13); +}); + +// A template edit that moves a hash turns the shipped listing red until the pack is re-pinned, +// which is the intended review point. A chief edit turns the expected lines red the same way. +test("SP2 a template edit refuses the packs that pin it until they are re-pinned", async (t) => { + const root = tmp(); + t.after(() => rmSync(root, { recursive: true, force: true })); + const rolesDir = rolesCopy(root); + appendFileSync(join(rolesDir, "coder.md"), "\nOne more line.\n"); + assert.notEqual(templateHash(rolesDir, "coder"), templateHash(ROLES, "coder")); + await assert.rejects(installedStarterPacks({ rolesDir }), (e) => { + assert.equal(e?.name, "PluginError"); + assert.equal(e.code, "starter-pack-template-changed"); + assert.equal(e.plugin, "coding"); + return true; + }); + for (const id of IDS) { + const doc = (await moduleOf(id)).starterPack(); + if (Object.hasOwn(doc.definitions, "coder")) { + assert.throws(() => checkPack(doc, { origin: "shipped", rolesDir, name: id }), (e) => e.code === "starter-pack-template-changed"); + } else { + checkPack(doc, { origin: "shipped", rolesDir, name: id }); + } + } + appendFileSync(join(rolesDir, `${CHIEF}.md`), "\nOne more line.\n"); + await assert.rejects(checkLines("coding", rolesDir), /the chief moved/); +}); + +// An installed plug-in cannot take a shipped pack's name, so it cannot replace a reviewed pack. +test("SP2 installing a plug-in named after a shipped pack refuses core-name", async (t) => { + const root = tmp(); + t.after(() => rmSync(root, { recursive: true, force: true })); + for (const id of IDS) { + const src = join(root, `src-${id}`), dataRoot = join(root, `data-${id}`); + mkdirSync(join(src, "starter-pack"), { recursive: true }); + mkdirSync(dataRoot); + const doc = (await moduleOf(id)).starterPack(); + writeFileSync(join(src, "starter-pack", "index.mjs"), `export const interfaceVersion = 1;\nexport function starterPack() { return ${JSON.stringify(doc)}; }\n`); + const manifest = { name: id, version: "1.0.0", kinds: ["starter-pack"], interfaceVersions: { "starter-pack": 1 }, sha256: "0".repeat(64), + source: { path: "fixture", version: "1.0.0" }, signature: null, tools: [], actions: [], services: [], roleTemplates: [] }; + writeFileSync(join(src, "plugin.json"), JSON.stringify(manifest)); + manifest.sha256 = pluginHash(src); + writeFileSync(join(src, "plugin.json"), JSON.stringify(manifest)); + assert.throws(() => installPlugin({ dataRoot, source: src, expectedHash: manifest.sha256 }), + (e) => e?.name === "PluginError" && e.code === "core-name", id); + } +}); diff --git a/packages/plugins/tests/starter-pack.test.mjs b/packages/plugins/tests/starter-pack.test.mjs index c778b28d1..d5fd6ee23 100644 --- a/packages/plugins/tests/starter-pack.test.mjs +++ b/packages/plugins/tests/starter-pack.test.mjs @@ -508,9 +508,16 @@ function tree(t) { const moduleText = (doc) => `export const interfaceVersion = 1;\nexport function starterPack() { return ${JSON.stringify(doc)}; }\n`; -test("SP1 installedStarterPacks lists shipped packs with origin shipped; the repository ships none yet", async (t) => { +// SP2 ships five core packs; starter-pack-shipped.test.mjs checks them by name. Assertions about +// installed packs below read only the installed ones. +const installedOnly = (res) => res.packs.filter((p) => p.origin !== "shipped"); + +test("SP1 installedStarterPacks lists shipped packs with origin shipped", async (t) => { const { installedStarterPacks } = await sp(); - assert.deepEqual(await installedStarterPacks({ rolesDir: ROLES }), { packs: [], refused: [] }); + const real = await installedStarterPacks({ rolesDir: ROLES }); + assert.deepEqual(real.refused, []); + assert.ok(real.packs.length > 0); + assert.ok(real.packs.every((p) => p.origin === "shipped")); const f = tree(t); f.core("coding", moduleText(pack())); f.core("tiny", moduleText(pack((p) => { p.id = "tiny"; p.hires = []; }))); @@ -561,13 +568,13 @@ test("SP1 an installed pack lists with origin installed; a refused one is omitte const good = installed(t, "acmepack", moduleText(pack((p) => { p.id = "acmepack"; }))); const out = await installedStarterPacks({ dataRoot: good.dataRoot, rolesDir: ROLES }); assert.deepEqual(out.refused, []); - assert.deepEqual(out.packs.map((p) => [p.id, p.origin]), [["acmepack", "installed"]]); + assert.deepEqual(installedOnly(out).map((p) => [p.id, p.origin]), [["acmepack", "installed"]]); // An installed pack carrying packs, or a role without from, is omitted and logged. for (const mutate of [(p) => { p.packs = []; }, (p) => { delete p.definitions["release-writer"].from; }, (p) => { p.hires[0].level = "root"; }]) { const bad = installed(t, "badpack", moduleText(pack((p) => { p.id = "badpack"; mutate(p); }))); const log = []; const res = await installedStarterPacks({ dataRoot: bad.dataRoot, rolesDir: ROLES, log: (e) => log.push(e) }); - assert.deepEqual(res.packs, []); + assert.deepEqual(installedOnly(res), []); assert.equal(res.refused.length, 1); assert.deepEqual(log, res.refused); assert.deepEqual([res.refused[0].event, res.refused[0].kind, res.refused[0].name, res.refused[0].code], @@ -577,7 +584,7 @@ test("SP1 an installed pack lists with origin installed; a refused one is omitte const broken = installed(t, "brokenpack", "export const interfaceVersion = 1;\nexport const starterPack = 3;\n"); const log = []; const res = await installedStarterPacks({ dataRoot: broken.dataRoot, rolesDir: ROLES, log: (e) => log.push(e) }); - assert.deepEqual(res.packs, []); + assert.deepEqual(installedOnly(res), []); assert.deepEqual(log.map((e) => [e.name, e.code]), [["brokenpack", "missing-export"]]); // An async starterPack is refused, and its later rejection never reaches the process. const unhandled = []; @@ -586,7 +593,7 @@ test("SP1 an installed pack lists with origin installed; a refused one is omitte t.after(() => process.off("unhandledRejection", onUnhandled)); const later = installed(t, "asyncpack", "export const interfaceVersion = 1;\nexport async function starterPack() { await new Promise((r) => setTimeout(r, 20)); throw new Error('late'); }\n"); const res2 = await installedStarterPacks({ dataRoot: later.dataRoot, rolesDir: ROLES, log() {} }); - assert.deepEqual(res2.packs, []); + assert.deepEqual(installedOnly(res2), []); assert.deepEqual(res2.refused.map((e) => [e.name, e.code]), [["asyncpack", "starter-pack-invalid"]]); assert.match(res2.refused[0].message, /not a promise/); await new Promise((r) => setTimeout(r, 100)); @@ -601,7 +608,7 @@ test("SP1 an installed pack lists with origin installed; a refused one is omitte for (const [name, text] of Object.entries(thenables)) { const odd = installed(t, name, text); const res3 = await installedStarterPacks({ dataRoot: odd.dataRoot, rolesDir: ROLES, log() {} }); - assert.deepEqual(res3.packs, []); + assert.deepEqual(installedOnly(res3), []); assert.deepEqual(res3.refused.map((e) => [e.name, e.code]), [[name, "starter-pack-invalid"]]); } await new Promise((r) => setTimeout(r, 50));