@duypham93/openkit 0.2.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/.opencode/README.md +47 -0
- package/.opencode/install-manifest.json +41 -0
- package/.opencode/lib/artifact-scaffolder.js +111 -0
- package/.opencode/lib/contract-consistency.js +218 -0
- package/.opencode/lib/parallel-execution-rules.js +261 -0
- package/.opencode/lib/runtime-paths.js +95 -0
- package/.opencode/lib/runtime-summary.js +82 -0
- package/.opencode/lib/state-guard.js +99 -0
- package/.opencode/lib/task-board-rules.js +375 -0
- package/.opencode/lib/work-item-store.js +280 -0
- package/.opencode/lib/workflow-state-controller.js +1739 -0
- package/.opencode/lib/workflow-state-rules.js +331 -0
- package/.opencode/opencode.json +93 -0
- package/.opencode/package.json +3 -0
- package/.opencode/tests/artifact-scaffolder.test.js +733 -0
- package/.opencode/tests/multi-work-item-runtime.test.js +369 -0
- package/.opencode/tests/parallel-execution-runtime.test.js +259 -0
- package/.opencode/tests/session-start-hook.test.js +357 -0
- package/.opencode/tests/state-guard.test.js +124 -0
- package/.opencode/tests/task-board-rules.test.js +204 -0
- package/.opencode/tests/work-item-store.test.js +380 -0
- package/.opencode/tests/workflow-behavior.test.js +149 -0
- package/.opencode/tests/workflow-contract-consistency.test.js +387 -0
- package/.opencode/tests/workflow-state-cli.test.js +1275 -0
- package/.opencode/tests/workflow-state-controller.test.js +1038 -0
- package/.opencode/work-items/feature-001/state.json +70 -0
- package/.opencode/work-items/index.json +13 -0
- package/.opencode/workflow-state.js +489 -0
- package/.opencode/workflow-state.json +70 -0
- package/AGENTS.md +265 -0
- package/README.md +401 -0
- package/agents/architect-agent.md +63 -0
- package/agents/ba-agent.md +56 -0
- package/agents/code-reviewer.md +77 -0
- package/agents/fullstack-agent.md +115 -0
- package/agents/master-orchestrator.md +60 -0
- package/agents/pm-agent.md +56 -0
- package/agents/qa-agent.md +124 -0
- package/agents/tech-lead-agent.md +60 -0
- package/assets/install-bundle/README.md +7 -0
- package/assets/install-bundle/opencode/README.md +11 -0
- package/assets/install-bundle/opencode/agents/ArchitectAgent.md +63 -0
- package/assets/install-bundle/opencode/agents/BAAgent.md +56 -0
- package/assets/install-bundle/opencode/agents/CodeReviewer.md +77 -0
- package/assets/install-bundle/opencode/agents/FullstackAgent.md +115 -0
- package/assets/install-bundle/opencode/agents/MasterOrchestrator.md +60 -0
- package/assets/install-bundle/opencode/agents/PMAgent.md +56 -0
- package/assets/install-bundle/opencode/agents/QAAgent.md +124 -0
- package/assets/install-bundle/opencode/agents/TechLeadAgent.md +60 -0
- package/assets/install-bundle/opencode/commands/brainstorm.md +44 -0
- package/assets/install-bundle/opencode/commands/delivery.md +45 -0
- package/assets/install-bundle/opencode/commands/execute-plan.md +44 -0
- package/assets/install-bundle/opencode/commands/migrate.md +61 -0
- package/assets/install-bundle/opencode/commands/quick-task.md +45 -0
- package/assets/install-bundle/opencode/commands/task.md +46 -0
- package/assets/install-bundle/opencode/commands/write-plan.md +50 -0
- package/assets/install-bundle/opencode/context/core/lane-selection.md +54 -0
- package/assets/install-bundle/opencode/skills/brainstorming/SKILL.md +51 -0
- package/assets/install-bundle/opencode/skills/code-review/SKILL.md +48 -0
- package/assets/install-bundle/opencode/skills/subagent-driven-development/SKILL.md +79 -0
- package/assets/install-bundle/opencode/skills/systematic-debugging/SKILL.md +61 -0
- package/assets/install-bundle/opencode/skills/test-driven-development/SKILL.md +48 -0
- package/assets/install-bundle/opencode/skills/using-skills/SKILL.md +39 -0
- package/assets/install-bundle/opencode/skills/verification-before-completion/SKILL.md +137 -0
- package/assets/install-bundle/opencode/skills/writing-plans/SKILL.md +68 -0
- package/assets/install-bundle/opencode/skills/writing-specs/SKILL.md +47 -0
- package/assets/opencode.json.template +11 -0
- package/assets/openkit-install.json.template +19 -0
- package/bin/openkit.js +9 -0
- package/commands/brainstorm.md +44 -0
- package/commands/delivery.md +45 -0
- package/commands/execute-plan.md +44 -0
- package/commands/migrate.md +61 -0
- package/commands/quick-task.md +45 -0
- package/commands/task.md +46 -0
- package/commands/write-plan.md +50 -0
- package/context/core/approval-gates.md +146 -0
- package/context/core/code-quality.md +42 -0
- package/context/core/issue-routing.md +85 -0
- package/context/core/lane-selection.md +54 -0
- package/context/core/project-config.md +143 -0
- package/context/core/session-resume.md +85 -0
- package/context/core/workflow-state-schema.md +224 -0
- package/context/core/workflow.md +442 -0
- package/context/navigation.md +94 -0
- package/docs/adr/README.md +6 -0
- package/docs/architecture/2026-03-20-task-intake-dashboard.md +54 -0
- package/docs/architecture/README.md +7 -0
- package/docs/briefs/2026-03-20-task-intake-dashboard.md +48 -0
- package/docs/briefs/README.md +7 -0
- package/docs/governance/README.md +25 -0
- package/docs/governance/adr-policy.md +27 -0
- package/docs/governance/definition-of-done.md +17 -0
- package/docs/governance/naming-conventions.md +21 -0
- package/docs/governance/severity-levels.md +12 -0
- package/docs/maintainer/README.md +51 -0
- package/docs/operations/README.md +79 -0
- package/docs/operations/internal-records/2026-03-24-release-checklist.md +79 -0
- package/docs/operations/internal-records/2026-03-24-simplified-install-ux.md +36 -0
- package/docs/operations/internal-records/README.md +18 -0
- package/docs/operations/runbooks/README.md +23 -0
- package/docs/operations/runbooks/openkit-daily-usage.md +288 -0
- package/docs/operations/runbooks/workflow-state-smoke-tests.md +302 -0
- package/docs/operator/README.md +50 -0
- package/docs/plans/2026-03-20-task-intake-dashboard.md +49 -0
- package/docs/plans/2026-03-21-openkit-full-delivery-multi-task-runtime.md +521 -0
- package/docs/plans/2026-03-23-openkit-global-install-runtime.md +157 -0
- package/docs/plans/README.md +7 -0
- package/docs/qa/2026-03-20-task-intake-dashboard.md +41 -0
- package/docs/qa/README.md +7 -0
- package/docs/specs/2026-03-20-task-intake-dashboard.md +50 -0
- package/docs/specs/2026-03-21-openkit-full-delivery-multi-task-runtime.md +462 -0
- package/docs/specs/README.md +7 -0
- package/docs/templates/README.md +36 -0
- package/docs/templates/adr-template.md +18 -0
- package/docs/templates/architecture-template.md +31 -0
- package/docs/templates/implementation-plan-template.md +32 -0
- package/docs/templates/migration-baseline-checklist.md +48 -0
- package/docs/templates/migration-plan-template.md +52 -0
- package/docs/templates/migration-report-template.md +74 -0
- package/docs/templates/migration-verify-checklist.md +39 -0
- package/docs/templates/product-brief-template.md +32 -0
- package/docs/templates/qa-report-template.md +37 -0
- package/docs/templates/quick-task-template.md +36 -0
- package/docs/templates/spec-template.md +31 -0
- package/hooks/hooks.json +16 -0
- package/hooks/session-start +162 -0
- package/package.json +24 -0
- package/registry.json +328 -0
- package/skills/brainstorming/SKILL.md +51 -0
- package/skills/code-review/SKILL.md +48 -0
- package/skills/subagent-driven-development/SKILL.md +79 -0
- package/skills/systematic-debugging/SKILL.md +61 -0
- package/skills/test-driven-development/SKILL.md +48 -0
- package/skills/using-skills/SKILL.md +39 -0
- package/skills/verification-before-completion/SKILL.md +137 -0
- package/skills/writing-plans/SKILL.md +68 -0
- package/skills/writing-specs/SKILL.md +47 -0
- package/src/audit/vietnamese-detection.js +259 -0
- package/src/cli/commands/detect-vietnamese.js +24 -0
- package/src/cli/commands/doctor.js +33 -0
- package/src/cli/commands/help.js +33 -0
- package/src/cli/commands/init.js +25 -0
- package/src/cli/commands/install-global.js +26 -0
- package/src/cli/commands/install.js +25 -0
- package/src/cli/commands/run.js +63 -0
- package/src/cli/commands/uninstall.js +32 -0
- package/src/cli/commands/upgrade.js +25 -0
- package/src/cli/conflict-output.js +19 -0
- package/src/cli/index.js +56 -0
- package/src/global/doctor.js +101 -0
- package/src/global/ensure-install.js +32 -0
- package/src/global/install-state.js +73 -0
- package/src/global/launcher.js +51 -0
- package/src/global/materialize.js +123 -0
- package/src/global/paths.js +85 -0
- package/src/global/uninstall.js +25 -0
- package/src/global/workspace-state.js +63 -0
- package/src/install/asset-manifest.js +284 -0
- package/src/install/conflicts.js +43 -0
- package/src/install/discovery.js +138 -0
- package/src/install/install-state.js +136 -0
- package/src/install/materialize.js +158 -0
- package/src/install/merge-policy.js +145 -0
- package/src/runtime/doctor.js +281 -0
- package/src/runtime/launcher.js +49 -0
- package/src/runtime/opencode-layering.js +135 -0
- package/src/runtime/openkit-managed-summary.js +27 -0
- package/tests/cli/openkit-cli.test.js +417 -0
- package/tests/global/doctor.test.js +130 -0
- package/tests/global/ensure-install.test.js +105 -0
- package/tests/install/discovery.test.js +124 -0
- package/tests/install/install-state.test.js +346 -0
- package/tests/install/materialize.test.js +244 -0
- package/tests/install/merge-policy.test.js +177 -0
- package/tests/runtime/doctor.test.js +430 -0
- package/tests/runtime/launcher.test.js +230 -0
- package/tests/runtime/module-boundary.test.js +16 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# .opencode Runtime Internals
|
|
2
|
+
|
|
3
|
+
This directory is the checked-in repository-local OpenKit runtime layer.
|
|
4
|
+
|
|
5
|
+
It remains live in this repository today.
|
|
6
|
+
|
|
7
|
+
For phase-1 navigation, treat this file as a local runtime boundary note, not as a replacement for the audience index layers in `README.md`, `docs/operator/README.md`, or `docs/maintainer/README.md`.
|
|
8
|
+
|
|
9
|
+
What lives here:
|
|
10
|
+
|
|
11
|
+
- `.opencode/opencode.json`: the current repository-local runtime manifest
|
|
12
|
+
- `.opencode/workflow-state.json`: the active compatibility mirror
|
|
13
|
+
- `.opencode/work-items/`: per-item backing store
|
|
14
|
+
- `.opencode/workflow-state.js`: lower-level runtime utility CLI
|
|
15
|
+
- `.opencode/lib/`: runtime internals
|
|
16
|
+
- `.opencode/tests/`: runtime regression tests
|
|
17
|
+
|
|
18
|
+
Related routing layers outside this directory:
|
|
19
|
+
|
|
20
|
+
- `README.md`: top-level repository boundary and current docs layout
|
|
21
|
+
- `docs/operator/README.md`: operator-facing routing for the live surfaces
|
|
22
|
+
- `docs/maintainer/README.md`: maintainer-facing routing for canonical docs and internals
|
|
23
|
+
- `docs/operations/README.md`: operational support routing for runbooks and internal records
|
|
24
|
+
|
|
25
|
+
What this directory is not:
|
|
26
|
+
|
|
27
|
+
- not the preferred top-level onboarding surface for everyday OpenKit usage now that the global install path exists
|
|
28
|
+
- not proof that end users should install the kit into each repository
|
|
29
|
+
- not a separate product from the repository itself
|
|
30
|
+
|
|
31
|
+
When the global OpenKit install exists elsewhere, the intended product path is:
|
|
32
|
+
|
|
33
|
+
- `npm install -g openkit`
|
|
34
|
+
- `openkit doctor`
|
|
35
|
+
- `openkit run`
|
|
36
|
+
- `openkit upgrade`
|
|
37
|
+
- `openkit uninstall`
|
|
38
|
+
|
|
39
|
+
`openkit install-global` remains available as a manual or compatibility setup command when you need it.
|
|
40
|
+
|
|
41
|
+
In this repository, `.opencode/` is still the concrete checked-in runtime that powers the lower-level workflow-state path and supports maintainer/runtime inspection.
|
|
42
|
+
|
|
43
|
+
Authority guardrail:
|
|
44
|
+
|
|
45
|
+
- `.opencode/` remains the live runtime layer
|
|
46
|
+
- `context/core/workflow.md` remains the canonical workflow-semantics document
|
|
47
|
+
- the phase-1 audience directories remain index layers only and do not relocate canonical runtime or workflow sources
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "openkit/install-manifest@1",
|
|
3
|
+
"manifestVersion": 1,
|
|
4
|
+
"kit": {
|
|
5
|
+
"name": "OpenKit AI Software Factory",
|
|
6
|
+
"version": "0.1.0"
|
|
7
|
+
},
|
|
8
|
+
"registry": {
|
|
9
|
+
"path": "registry.json",
|
|
10
|
+
"schema": "openkit/component-registry@1",
|
|
11
|
+
"registryVersion": 1
|
|
12
|
+
},
|
|
13
|
+
"installation": {
|
|
14
|
+
"type": "checked-in-local-metadata",
|
|
15
|
+
"activeProfile": "openkit-core",
|
|
16
|
+
"installedAt": "repository-root",
|
|
17
|
+
"managedRemotely": false,
|
|
18
|
+
"mode": "additive-non-destructive",
|
|
19
|
+
"installReadiness": "emerging",
|
|
20
|
+
"notes": "This manifest describes additive metadata checked into this repository. It does not fetch remote components, remove the repository-local runtime, or replace the preferred global OpenKit install path.",
|
|
21
|
+
"repositoryInternalRuntime": ".opencode/opencode.json",
|
|
22
|
+
"wrapperFacingMetadata": [
|
|
23
|
+
"registry.json",
|
|
24
|
+
".opencode/install-manifest.json"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"migration": {
|
|
28
|
+
"currentRuntimeManifest": ".opencode/opencode.json",
|
|
29
|
+
"status": "staged-not-complete",
|
|
30
|
+
"notes": "The checked-in repository runtime remains the authoring and compatibility surface. Everyday operator usage should prefer the global OpenKit install path."
|
|
31
|
+
},
|
|
32
|
+
"components": {
|
|
33
|
+
"agents": "present",
|
|
34
|
+
"skills": "present",
|
|
35
|
+
"commands": "present",
|
|
36
|
+
"artifacts": "present",
|
|
37
|
+
"runtime": "present",
|
|
38
|
+
"hooks": "present",
|
|
39
|
+
"docs": "present"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_SCAFFOLDS = {
|
|
5
|
+
task_card: {
|
|
6
|
+
templatePath: "docs/templates/quick-task-template.md",
|
|
7
|
+
outputDir: "docs/tasks",
|
|
8
|
+
},
|
|
9
|
+
plan: {
|
|
10
|
+
templatePath: "docs/templates/implementation-plan-template.md",
|
|
11
|
+
outputDir: "docs/plans",
|
|
12
|
+
},
|
|
13
|
+
migration_report: {
|
|
14
|
+
templatePath: "docs/templates/migration-report-template.md",
|
|
15
|
+
outputDir: "docs/plans",
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PLAN_TEMPLATE_BY_MODE = {
|
|
20
|
+
full: "docs/templates/implementation-plan-template.md",
|
|
21
|
+
migration: "docs/templates/migration-plan-template.md",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
25
|
+
|
|
26
|
+
function formatDate(date = new Date()) {
|
|
27
|
+
return date.toISOString().slice(0, 10)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function titleFromSlug(slug) {
|
|
31
|
+
return slug
|
|
32
|
+
.split("-")
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
35
|
+
.join(" ")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function replaceTemplatePlaceholders(template, values) {
|
|
39
|
+
return template
|
|
40
|
+
.replace(/TASK-000/g, values.featureId)
|
|
41
|
+
.replace(/FEATURE-000/g, values.featureId)
|
|
42
|
+
.replace(/example-task/g, values.featureSlug)
|
|
43
|
+
.replace(/example-feature/g, values.featureSlug)
|
|
44
|
+
.replace(/example-migration/g, values.featureSlug)
|
|
45
|
+
.replace(/docs\/architecture\/YYYY-MM-DD-[^\s]+\.md/g, values.sourceArchitecture)
|
|
46
|
+
.replace(/docs\/plans\/YYYY-MM-DD-[^\s]+\.md/g, values.sourcePlan)
|
|
47
|
+
.replace(/<Task Name>/g, values.title)
|
|
48
|
+
.replace(/<Feature Name>/g, values.title)
|
|
49
|
+
.replace(/<Migration Name>/g, values.title)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scaffoldArtifact({ projectRoot, kind, mode, slug, featureId, featureSlug, sourceArchitecture, sourcePlan }) {
|
|
53
|
+
const config = SUPPORTED_SCAFFOLDS[kind]
|
|
54
|
+
if (!config) {
|
|
55
|
+
throw new Error(`Unsupported scaffold kind '${kind}'`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!SLUG_PATTERN.test(slug)) {
|
|
59
|
+
throw new Error("artifact slug must use lowercase kebab-case")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const resolvedTemplatePath =
|
|
63
|
+
kind === "plan" ? PLAN_TEMPLATE_BY_MODE[mode] ?? config.templatePath : config.templatePath
|
|
64
|
+
const templatePath = path.join(projectRoot, resolvedTemplatePath)
|
|
65
|
+
const outputDir = path.join(projectRoot, config.outputDir)
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(templatePath)) {
|
|
68
|
+
throw new Error(`Template not found for scaffold kind '${kind}': '${resolvedTemplatePath}'`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!fs.existsSync(outputDir)) {
|
|
72
|
+
throw new Error(`Output directory does not exist for scaffold kind '${kind}': '${config.outputDir}'`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const date = formatDate()
|
|
76
|
+
const outputRelativePath = path.posix.join(config.outputDir, `${date}-${slug}.md`)
|
|
77
|
+
const outputPath = path.join(projectRoot, outputRelativePath)
|
|
78
|
+
const resolvedOutputDir = path.resolve(outputDir)
|
|
79
|
+
const resolvedOutputPath = path.resolve(outputPath)
|
|
80
|
+
|
|
81
|
+
if (!resolvedOutputPath.startsWith(`${resolvedOutputDir}${path.sep}`)) {
|
|
82
|
+
throw new Error(`artifact path escapes output directory for kind '${kind}'`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (fs.existsSync(resolvedOutputPath)) {
|
|
86
|
+
throw new Error(`Artifact already exists at '${outputRelativePath}'`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const template = fs.readFileSync(templatePath, "utf8")
|
|
90
|
+
const content = replaceTemplatePlaceholders(template, {
|
|
91
|
+
date,
|
|
92
|
+
featureId,
|
|
93
|
+
featureSlug,
|
|
94
|
+
sourceArchitecture: sourceArchitecture ?? "docs/architecture/YYYY-MM-DD-<feature>.md",
|
|
95
|
+
sourcePlan: sourcePlan ?? "docs/plans/YYYY-MM-DD-<migration>.md",
|
|
96
|
+
title: titleFromSlug(slug),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(resolvedOutputPath, content.endsWith("\n") ? content : `${content}\n`, "utf8")
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
artifactPath: outputRelativePath,
|
|
103
|
+
kind,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
SLUG_PATTERN,
|
|
109
|
+
SUPPORTED_SCAFFOLDS,
|
|
110
|
+
scaffoldArtifact,
|
|
111
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
ARTIFACT_KINDS,
|
|
6
|
+
MODE_VALUES,
|
|
7
|
+
MODE_STAGE_SEQUENCES,
|
|
8
|
+
MODE_APPROVAL_GATES,
|
|
9
|
+
} = require("./workflow-state-rules")
|
|
10
|
+
|
|
11
|
+
function listExistingMarkdownLiterals(markdown, values) {
|
|
12
|
+
return values.filter((value) => markdown.includes(`\`${value}\``))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function matchesAnyPattern(text, patterns) {
|
|
16
|
+
return patterns.some((pattern) => pattern.test(text))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeText(text) {
|
|
20
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, " ")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function includesAllTerms(text, terms) {
|
|
24
|
+
const normalized = normalizeText(text)
|
|
25
|
+
return terms.every((term) => normalized.includes(normalizeText(term).trim()))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readTextIfExists(filePath) {
|
|
29
|
+
if (!fs.existsSync(filePath)) {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return fs.readFileSync(filePath, "utf8")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getManifestSurfacePaths(projectRoot, manifest) {
|
|
37
|
+
const agents = manifest?.agents ?? {}
|
|
38
|
+
const commands = manifest?.commands ?? {}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
primaryAgentPath: agents.primary ? path.resolve(projectRoot, agents.primary) : null,
|
|
42
|
+
teamRolePaths: (agents.teamRoles ?? []).map((entry) => path.resolve(projectRoot, entry)),
|
|
43
|
+
commandPaths: (commands.available ?? []).map((entry) => path.resolve(projectRoot, entry)),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeCheck(label, ok) {
|
|
48
|
+
return { label, ok }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getContractConsistencyReport({ projectRoot, manifest }) {
|
|
52
|
+
const workflowPath = path.join(projectRoot, "context", "core", "workflow.md")
|
|
53
|
+
const schemaPath = path.join(projectRoot, "context", "core", "workflow-state-schema.md")
|
|
54
|
+
const sessionResumePath = path.join(projectRoot, "context", "core", "session-resume.md")
|
|
55
|
+
const fullDeliverySpecPath = path.join(
|
|
56
|
+
projectRoot,
|
|
57
|
+
"docs",
|
|
58
|
+
"specs",
|
|
59
|
+
"2026-03-21-openkit-full-delivery-multi-task-runtime.md",
|
|
60
|
+
)
|
|
61
|
+
const fullDeliveryPlanPath = path.join(
|
|
62
|
+
projectRoot,
|
|
63
|
+
"docs",
|
|
64
|
+
"plans",
|
|
65
|
+
"2026-03-21-openkit-full-delivery-multi-task-runtime.md",
|
|
66
|
+
)
|
|
67
|
+
const workflowText = readTextIfExists(workflowPath)
|
|
68
|
+
const schemaText = readTextIfExists(schemaPath)
|
|
69
|
+
const sessionResumeText = readTextIfExists(sessionResumePath) ?? ""
|
|
70
|
+
const fullDeliverySpecText = readTextIfExists(fullDeliverySpecPath) ?? ""
|
|
71
|
+
const fullDeliveryPlanText = readTextIfExists(fullDeliveryPlanPath) ?? ""
|
|
72
|
+
const surfacePaths = getManifestSurfacePaths(projectRoot, manifest)
|
|
73
|
+
const taskBoardContractText = [workflowText, sessionResumeText, fullDeliverySpecText, fullDeliveryPlanText]
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join("\n")
|
|
76
|
+
const compatibilityText = [schemaText, fullDeliverySpecText, fullDeliveryPlanText].filter(Boolean).join("\n")
|
|
77
|
+
|
|
78
|
+
const allStageNames = Object.values(MODE_STAGE_SEQUENCES).flat()
|
|
79
|
+
const allApprovalGates = Object.values(MODE_APPROVAL_GATES).flat()
|
|
80
|
+
|
|
81
|
+
const checks = [
|
|
82
|
+
makeCheck("workflow contract doc found", Boolean(workflowText)),
|
|
83
|
+
makeCheck("workflow schema doc found", Boolean(schemaText)),
|
|
84
|
+
makeCheck(
|
|
85
|
+
"declared primary agent exists",
|
|
86
|
+
!surfacePaths.primaryAgentPath || fs.existsSync(surfacePaths.primaryAgentPath),
|
|
87
|
+
),
|
|
88
|
+
makeCheck(
|
|
89
|
+
"declared team role files exist",
|
|
90
|
+
surfacePaths.teamRolePaths.every((entry) => fs.existsSync(entry)),
|
|
91
|
+
),
|
|
92
|
+
makeCheck(
|
|
93
|
+
"declared command files exist",
|
|
94
|
+
surfacePaths.commandPaths.every((entry) => fs.existsSync(entry)),
|
|
95
|
+
),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
if (!workflowText || !schemaText) {
|
|
99
|
+
return summarizeChecks(checks)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
checks.push(
|
|
103
|
+
makeCheck(
|
|
104
|
+
"workflow contract documents runtime mode enums",
|
|
105
|
+
MODE_VALUES.every((value) => workflowText.includes(`\`${value}\``)),
|
|
106
|
+
),
|
|
107
|
+
makeCheck(
|
|
108
|
+
"workflow contract states Quick Task+ is not a third lane",
|
|
109
|
+
/`?Quick Task\+`?/i.test(workflowText) &&
|
|
110
|
+
matchesAnyPattern(workflowText, [
|
|
111
|
+
/not a third lane/i,
|
|
112
|
+
/not a third operating mode/i,
|
|
113
|
+
/not a third mode/i,
|
|
114
|
+
]),
|
|
115
|
+
),
|
|
116
|
+
makeCheck(
|
|
117
|
+
"workflow contract keeps migration lane free of task boards",
|
|
118
|
+
includesAllTerms(taskBoardContractText, ["migration", "task", "board"]) &&
|
|
119
|
+
matchesAnyPattern(taskBoardContractText, [
|
|
120
|
+
/migration[^\n.]{0,120}(free of|without|must stay free of|must not carry|no) [^\n.]{0,80}task board/i,
|
|
121
|
+
/task board[^\n.]{0,120}(not allowed|forbidden|disallowed|full[- ]delivery only)[^\n.]{0,80}migration/i,
|
|
122
|
+
/migration[^\n.]{0,120}execution[- ]task[- ]board/i,
|
|
123
|
+
/migration[^\n.]{0,120}task[- ]board/i,
|
|
124
|
+
]),
|
|
125
|
+
),
|
|
126
|
+
makeCheck(
|
|
127
|
+
"workflow contract states migration lane is for upgrades",
|
|
128
|
+
includesAllTerms(workflowText, ["migration", "upgrade"]),
|
|
129
|
+
),
|
|
130
|
+
makeCheck(
|
|
131
|
+
"workflow contract states migration preserves behavior and decouples blockers first",
|
|
132
|
+
includesAllTerms(workflowText, ["migration", "behavior"]) &&
|
|
133
|
+
matchesAnyPattern(workflowText, [
|
|
134
|
+
/preserve[^\n.]{0,80}behavior/i,
|
|
135
|
+
/freeze[^\n.]{0,80}invariant/i,
|
|
136
|
+
/decouple[^\n.]{0,120}blocker/i,
|
|
137
|
+
/seam/i,
|
|
138
|
+
]),
|
|
139
|
+
),
|
|
140
|
+
makeCheck(
|
|
141
|
+
"workflow contract documents lane tie-breakers and examples",
|
|
142
|
+
includesAllTerms(workflowText, ["lane", "tie breaker"]) && includesAllTerms(workflowText, ["decision", "matrix"]),
|
|
143
|
+
),
|
|
144
|
+
makeCheck(
|
|
145
|
+
"workflow contract keeps quick lane free of task boards",
|
|
146
|
+
includesAllTerms(taskBoardContractText, ["quick", "task", "board"]) &&
|
|
147
|
+
matchesAnyPattern(taskBoardContractText, [
|
|
148
|
+
/quick[^\n.]{0,120}(free of|without|must stay free of|must not carry|no) [^\n.]{0,80}task board/i,
|
|
149
|
+
/task board[^\n.]{0,120}(not allowed|forbidden|disallowed|full[- ]delivery only)[^\n.]{0,80}quick/i,
|
|
150
|
+
/quick[^\n.]{0,120}execution[- ]task[- ]board/i,
|
|
151
|
+
/do not invent a quick task board/i,
|
|
152
|
+
]),
|
|
153
|
+
),
|
|
154
|
+
makeCheck(
|
|
155
|
+
"workflow contract states full delivery owns execution task boards",
|
|
156
|
+
includesAllTerms(taskBoardContractText, ["full", "task", "board"]) &&
|
|
157
|
+
matchesAnyPattern(taskBoardContractText, [
|
|
158
|
+
/(full delivery|full mode|full work)[^\n.]{0,120}(owns|uses|carries|gains|has|contains|belong)[^\n.]{0,80}task board/i,
|
|
159
|
+
/task board[^\n.]{0,120}(full delivery|full mode|full work)[^\n.]{0,80}(only|owns|belong)/i,
|
|
160
|
+
/execution task boards? belong only to full delivery/i,
|
|
161
|
+
/only `?full delivery`? work items gain an execution[- ]task board/i,
|
|
162
|
+
]),
|
|
163
|
+
),
|
|
164
|
+
makeCheck(
|
|
165
|
+
"workflow schema matches runtime mode enums",
|
|
166
|
+
listExistingMarkdownLiterals(schemaText, MODE_VALUES).length === MODE_VALUES.length,
|
|
167
|
+
),
|
|
168
|
+
makeCheck(
|
|
169
|
+
"workflow schema matches runtime stage sequences",
|
|
170
|
+
listExistingMarkdownLiterals(schemaText, allStageNames).length === allStageNames.length,
|
|
171
|
+
),
|
|
172
|
+
makeCheck(
|
|
173
|
+
"workflow schema matches runtime artifact slots",
|
|
174
|
+
listExistingMarkdownLiterals(schemaText, ARTIFACT_KINDS).length === ARTIFACT_KINDS.length,
|
|
175
|
+
),
|
|
176
|
+
makeCheck(
|
|
177
|
+
"workflow schema documents routing profile fields",
|
|
178
|
+
includesAllTerms(schemaText, ["routing_profile", "dominant_uncertainty", "behavior_delta"]),
|
|
179
|
+
),
|
|
180
|
+
makeCheck(
|
|
181
|
+
"workflow schema matches runtime approval keys",
|
|
182
|
+
listExistingMarkdownLiterals(schemaText, allApprovalGates).length === allApprovalGates.length,
|
|
183
|
+
),
|
|
184
|
+
makeCheck(
|
|
185
|
+
"workflow schema documents compatibility mirror behavior",
|
|
186
|
+
includesAllTerms(compatibilityText, ["work item"]) &&
|
|
187
|
+
matchesAnyPattern(compatibilityText, [
|
|
188
|
+
/(compatibility|mirrored|mirror)[^\n.]{0,120}(state file|workflow state|repo root)/i,
|
|
189
|
+
/(state file|workflow state|repo root)[^\n.]{0,120}(compatibility|mirrored|mirror)/i,
|
|
190
|
+
/mirrored compatibility surface/i,
|
|
191
|
+
/compatibility mirror/i,
|
|
192
|
+
/compatibility rule/i,
|
|
193
|
+
]),
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return summarizeChecks(checks)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function summarizeChecks(checks) {
|
|
201
|
+
const summary = checks.reduce(
|
|
202
|
+
(counts, check) => {
|
|
203
|
+
if (check.ok) {
|
|
204
|
+
counts.ok += 1
|
|
205
|
+
} else {
|
|
206
|
+
counts.error += 1
|
|
207
|
+
}
|
|
208
|
+
return counts
|
|
209
|
+
},
|
|
210
|
+
{ ok: 0, warn: 0, error: 0 },
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return { checks, summary }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
getContractConsistencyReport,
|
|
218
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
const ALLOWED_WORKTREE_PREFIX = ".worktrees/"
|
|
2
|
+
const PROTECTED_BRANCHES = new Set(["main", "master"])
|
|
3
|
+
const VALID_QA_REWORK_STAGES = new Set(["full_qa", "full_implementation"])
|
|
4
|
+
const VALID_QA_REWORK_DECIDERS = new Set(["MasterOrchestrator", "TechLeadAgent"])
|
|
5
|
+
const VALID_QA_REWORK_OWNERS = new Set(["QAAgent", "FullstackAgent"])
|
|
6
|
+
|
|
7
|
+
const VALID_ASSIGNMENT_AUTHORITIES = {
|
|
8
|
+
primary_owner: ["MasterOrchestrator", "TechLeadAgent"],
|
|
9
|
+
qa_owner: ["MasterOrchestrator", "TechLeadAgent"],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function fail(message) {
|
|
13
|
+
throw new Error(message)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isNonEmptyString(value) {
|
|
17
|
+
return typeof value === "string" && value.trim().length > 0
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateTaskReference(task, contextLabel = "Parallel execution") {
|
|
21
|
+
if (!task || typeof task !== "object" || Array.isArray(task)) {
|
|
22
|
+
fail(`${contextLabel} requires task objects`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!isNonEmptyString(task.task_id)) {
|
|
26
|
+
fail(`${contextLabel} requires each task to have a non-empty task_id`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return task
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validateTaskReferenceList(tasks) {
|
|
33
|
+
if (!Array.isArray(tasks)) {
|
|
34
|
+
fail("Parallel assignment validation requires a tasks array")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return tasks.map((task) => validateTaskReference(task, "Parallel assignment validation"))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validatePerTaskOwnerField(tasks, ownerField) {
|
|
41
|
+
for (const task of validateTaskReferenceList(tasks)) {
|
|
42
|
+
const owner = task[ownerField]
|
|
43
|
+
if (owner === null || owner === undefined) {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!isNonEmptyString(owner)) {
|
|
48
|
+
fail(`Parallel ${ownerField} for task '${task.task_id}' must be a non-empty string when set`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateParallelAssignments(tasks) {
|
|
54
|
+
validatePerTaskOwnerField(tasks, "primary_owner")
|
|
55
|
+
validatePerTaskOwnerField(tasks, "qa_owner")
|
|
56
|
+
return tasks
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateReassignmentAuthority({ task, ownerField, requestedBy, nextOwner }) {
|
|
60
|
+
validateTaskReference(task, "Reassignment validation")
|
|
61
|
+
|
|
62
|
+
if (!Object.prototype.hasOwnProperty.call(VALID_ASSIGNMENT_AUTHORITIES, ownerField)) {
|
|
63
|
+
fail(`Unknown assignment field '${ownerField}'`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!isNonEmptyString(requestedBy)) {
|
|
67
|
+
fail("Reassignment validation requires requestedBy")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!isNonEmptyString(nextOwner)) {
|
|
71
|
+
fail(`Reassignment validation requires a non-empty next owner for '${ownerField}'`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const currentOwner = task[ownerField]
|
|
75
|
+
const allowedAuthorities = VALID_ASSIGNMENT_AUTHORITIES[ownerField]
|
|
76
|
+
const isInitialAssignment = !isNonEmptyString(currentOwner)
|
|
77
|
+
|
|
78
|
+
if (isInitialAssignment) {
|
|
79
|
+
if (!allowedAuthorities.includes(requestedBy)) {
|
|
80
|
+
fail(`Only ${allowedAuthorities.join(" or ")} can assign ${ownerField}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
ownerField,
|
|
85
|
+
requestedBy,
|
|
86
|
+
nextOwner,
|
|
87
|
+
reassignment: false,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (currentOwner === nextOwner) {
|
|
92
|
+
return {
|
|
93
|
+
ownerField,
|
|
94
|
+
requestedBy,
|
|
95
|
+
nextOwner,
|
|
96
|
+
reassignment: false,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!allowedAuthorities.includes(requestedBy)) {
|
|
101
|
+
fail(`Only ${allowedAuthorities.join(" or ")} can reassign ${ownerField}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
ownerField,
|
|
106
|
+
requestedBy,
|
|
107
|
+
nextOwner,
|
|
108
|
+
reassignment: true,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateTaskScopedFinding(finding, task) {
|
|
113
|
+
if (!finding || typeof finding !== "object" || Array.isArray(finding)) {
|
|
114
|
+
fail("A task-scoped finding must be an object")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
validateTaskReference(task, "Task-scoped finding validation")
|
|
118
|
+
|
|
119
|
+
const requiredFields = [
|
|
120
|
+
"issue_id",
|
|
121
|
+
"task_id",
|
|
122
|
+
"title",
|
|
123
|
+
"summary",
|
|
124
|
+
"type",
|
|
125
|
+
"severity",
|
|
126
|
+
"rooted_in",
|
|
127
|
+
"recommended_owner",
|
|
128
|
+
"evidence",
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
for (const field of requiredFields) {
|
|
132
|
+
if (!isNonEmptyString(finding[field])) {
|
|
133
|
+
fail(`Task-scoped finding requires '${field}'`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (finding.task_id !== task.task_id) {
|
|
138
|
+
fail(`A task-scoped finding for '${task.task_id}' must reference the same task_id`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!Array.isArray(finding.artifact_refs) || finding.artifact_refs.length === 0 || !finding.artifact_refs.every(isNonEmptyString)) {
|
|
142
|
+
fail("Task-scoped finding requires non-empty artifact_refs")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!Array.isArray(finding.affects_tasks) || finding.affects_tasks.length !== 1 || finding.affects_tasks[0] !== task.task_id) {
|
|
146
|
+
fail(`Task-scoped finding affects_tasks must be isolated to exactly one task: '${task.task_id}'`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return finding
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateFailureIsolation(finding, task) {
|
|
153
|
+
validateTaskScopedFinding(finding, task)
|
|
154
|
+
|
|
155
|
+
if (finding.blocks_parallel_work === true) {
|
|
156
|
+
fail("Task-scoped local rework cannot proceed when the finding blocks parallel work")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (finding.affects_tasks.length !== 1) {
|
|
160
|
+
fail("Failure isolation requires a finding isolated to exactly one task")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (finding.severity === "critical") {
|
|
164
|
+
fail("critical failures require orchestrator escalation")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return finding
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateQaFailRerouteDecision(rerouteDecision) {
|
|
171
|
+
if (!rerouteDecision || typeof rerouteDecision !== "object" || Array.isArray(rerouteDecision)) {
|
|
172
|
+
fail("QA fail local rework routing requires a rerouteDecision")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!VALID_QA_REWORK_STAGES.has(rerouteDecision.stage)) {
|
|
176
|
+
fail(`QA fail local rework reroute field 'stage' must be 'full_qa' or 'full_implementation'; received '${String(rerouteDecision.stage)}'`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!VALID_QA_REWORK_OWNERS.has(rerouteDecision.owner)) {
|
|
180
|
+
fail(`QA fail local rework reroute field 'owner' must be 'QAAgent' or 'FullstackAgent'; received '${String(rerouteDecision.owner)}'`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!VALID_QA_REWORK_DECIDERS.has(rerouteDecision.decided_by)) {
|
|
184
|
+
fail(`QA fail local rework reroute field 'decided_by' must be 'MasterOrchestrator' or 'TechLeadAgent'; received '${String(rerouteDecision.decided_by)}'`)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!isNonEmptyString(rerouteDecision.reason)) {
|
|
188
|
+
fail("QA fail local rework reroute field 'reason' must be a non-empty string")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return rerouteDecision
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function decideQaFailLocalRework({ mode, task, finding, rerouteDecision }) {
|
|
195
|
+
if (mode !== "full") {
|
|
196
|
+
fail("QA fail local rework routing is only allowed in full mode")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
validateTaskReference(task, "QA fail local rework routing")
|
|
200
|
+
|
|
201
|
+
if (task.status !== "qa_in_progress") {
|
|
202
|
+
fail("QA fail local rework routing requires task status 'qa_in_progress'")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
validateFailureIsolation(finding, task)
|
|
206
|
+
|
|
207
|
+
if (finding.type !== "bug" || finding.rooted_in !== "implementation" || finding.recommended_owner !== "FullstackAgent") {
|
|
208
|
+
fail("QA design or requirements findings must not stay in local rework")
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const route = validateQaFailRerouteDecision(rerouteDecision)
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
allowed: true,
|
|
215
|
+
route,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function validateWorktreeMetadata(metadata) {
|
|
220
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
221
|
+
fail("Worktree metadata must be an object")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const { task_id: taskId, branch, worktree_path: worktreePath } = metadata
|
|
225
|
+
|
|
226
|
+
if (!isNonEmptyString(taskId)) {
|
|
227
|
+
fail("Worktree metadata requires task_id")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!isNonEmptyString(branch)) {
|
|
231
|
+
fail("Worktree metadata requires branch")
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!isNonEmptyString(worktreePath)) {
|
|
235
|
+
fail("Worktree metadata requires worktree_path")
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (PROTECTED_BRANCHES.has(branch)) {
|
|
239
|
+
fail("Parallel worktree branch must not target main or master")
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!branch.includes(taskId)) {
|
|
243
|
+
fail(`Parallel worktree branch must include task id '${taskId}'`)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!worktreePath.startsWith(ALLOWED_WORKTREE_PREFIX)) {
|
|
247
|
+
fail(`Parallel worktree_path must start with '${ALLOWED_WORKTREE_PREFIX}'`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return metadata
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
VALID_ASSIGNMENT_AUTHORITIES,
|
|
255
|
+
decideQaFailLocalRework,
|
|
256
|
+
validateFailureIsolation,
|
|
257
|
+
validateParallelAssignments,
|
|
258
|
+
validateReassignmentAuthority,
|
|
259
|
+
validateTaskScopedFinding,
|
|
260
|
+
validateWorktreeMetadata,
|
|
261
|
+
}
|