@geminix/gxpm 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/AGENTS.md +148 -0
- package/CANON.md +53 -0
- package/CLAUDE.md +60 -0
- package/CONTEXT.md +49 -0
- package/DEBUG.md +59 -0
- package/ISSUE_CONTEXT.md +25 -0
- package/README.md +143 -0
- package/VERSION +1 -0
- package/agents/cleanup-auditor/cleanup-auditor.md +56 -0
- package/agents/grill-master.md +26 -0
- package/agents/implementer.md +32 -0
- package/agents/review-army/accessibility-reviewer.md +54 -0
- package/agents/review-army/code-quality-reviewer.md +54 -0
- package/agents/review-army/security-reviewer.md +56 -0
- package/agents/review-army/spec-compliance-reviewer.md +51 -0
- package/agents/review-army/test-reviewer.md +55 -0
- package/agents/reviewer.md +59 -0
- package/agents/ship-audit-army/docs-auditor.md +53 -0
- package/agents/ship-audit-army/performance-auditor.md +52 -0
- package/agents/ship-audit-army/security-auditor.md +52 -0
- package/agents/specifier.md +55 -0
- package/agents/triage-officer.md +27 -0
- package/bin/gxpm +17 -0
- package/bin/gxpm-browser +17 -0
- package/bin/gxpm-config +15 -0
- package/bin/gxpm-eval +13 -0
- package/bin/gxpm-global-discover +15 -0
- package/bin/gxpm-init +38 -0
- package/bin/gxpm-investigate +194 -0
- package/bin/gxpm-uninstall +15 -0
- package/bin/gxpm-update-check +165 -0
- package/commands/build.md +40 -0
- package/commands/help.md +53 -0
- package/commands/plan.md +34 -0
- package/commands/refine.md +46 -0
- package/commands/review.md +34 -0
- package/commands/ship.md +37 -0
- package/core/ac-check.ts +20 -0
- package/core/agent-runtime.ts +363 -0
- package/core/artifact-validator.ts +151 -0
- package/core/artifacts.ts +313 -0
- package/core/autopilot.ts +250 -0
- package/core/capabilities.ts +779 -0
- package/core/checkpoint.ts +370 -0
- package/core/cleanup.ts +32 -0
- package/core/command-probe.ts +82 -0
- package/core/config.ts +533 -0
- package/core/contracts/behavior-spec.schema.ts +38 -0
- package/core/contracts/converter.ts +61 -0
- package/core/contracts/host.ts +43 -0
- package/core/converters/converter.ts +93 -0
- package/core/converters/index.ts +8 -0
- package/core/converters/managed-artifact.ts +119 -0
- package/core/converters/parser.ts +159 -0
- package/core/converters/template-renderer.ts +35 -0
- package/core/converters/writer.ts +61 -0
- package/core/dag-executor.ts +426 -0
- package/core/dag-loader.ts +292 -0
- package/core/dag-schemas.ts +150 -0
- package/core/dispatch.ts +125 -0
- package/core/evidence.ts +148 -0
- package/core/gate.ts +269 -0
- package/core/hook-engine.ts +566 -0
- package/core/host-probe.ts +64 -0
- package/core/implement.ts +16 -0
- package/core/isolation-errors.ts +174 -0
- package/core/isolation-resolver.ts +921 -0
- package/core/issue-context.ts +381 -0
- package/core/issue-readiness.ts +457 -0
- package/core/issue-sync.ts +427 -0
- package/core/issues.ts +132 -0
- package/core/land.ts +108 -0
- package/core/orchestrator.ts +54 -0
- package/core/phase-artifact.ts +32 -0
- package/core/phase-gates.ts +130 -0
- package/core/phase-rewind.ts +94 -0
- package/core/plan-lint.ts +61 -0
- package/core/plan.ts +77 -0
- package/core/port-allocation.ts +50 -0
- package/core/pr-check.ts +15 -0
- package/core/preset-system/preset-resolver.ts +221 -0
- package/core/project-init-status.ts +127 -0
- package/core/qa.ts +15 -0
- package/core/resilience.ts +165 -0
- package/core/runs.ts +288 -0
- package/core/safe-path.test.ts +80 -0
- package/core/safe-path.ts +60 -0
- package/core/sdd-gate.test.ts +98 -0
- package/core/sdd-gate.ts +134 -0
- package/core/self-review.ts +62 -0
- package/core/session.ts +70 -0
- package/core/ship.ts +86 -0
- package/core/specify.ts +173 -0
- package/core/state.ts +1002 -0
- package/core/template-engine.ts +152 -0
- package/core/template-resolver.test.ts +70 -0
- package/core/template-resolver.ts +156 -0
- package/core/triage.ts +26 -0
- package/core/verify.ts +15 -0
- package/core/wiki-native.ts +2423 -0
- package/core/wiki.ts +27 -0
- package/core/workflow-event-emitter.ts +163 -0
- package/core/workflows/engine.ts +273 -0
- package/core/workflows/expressions.ts +76 -0
- package/core/workflows/index.ts +38 -0
- package/core/workflows/steps/command.ts +43 -0
- package/core/workflows/steps/gate.ts +47 -0
- package/core/workflows/steps/gxpm.ts +44 -0
- package/core/workflows/steps/linear.ts +31 -0
- package/core/workflows/steps/shell.ts +65 -0
- package/core/workflows/types.ts +62 -0
- package/core/workspace-runtime.ts +227 -0
- package/core/worktree-init-steps.ts +647 -0
- package/core/worktree-init.ts +330 -0
- package/core/worktree-owner.ts +143 -0
- package/docs/GXPM_VERIFY.md +98 -0
- package/docs/INSTALL_FOR_AGENTS.md +113 -0
- package/docs/README.md +57 -0
- package/docs/adr/adr-005-multi-platform-skill-converter.md +72 -0
- package/docs/agents/domain.md +30 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +32 -0
- package/docs/architecture/gxpm-architecture-diagram.md +265 -0
- package/docs/architecture/gxpm-current-architecture.md +175 -0
- package/docs/architecture/gxpm-current-flow.md +278 -0
- package/docs/architecture/gxpm-replacement-architecture.md +211 -0
- package/docs/architecture/gxpm-target-architecture.md +449 -0
- package/docs/architecture/gxpm-v0-contract.md +311 -0
- package/docs/architecture/layered-workflow-boundaries.md +193 -0
- package/docs/architecture/preset-system.md +126 -0
- package/docs/architecture/scaffold-northstar.md +23 -0
- package/docs/brainstorms/2026-05-14-bdd-then-tdd-design.md +320 -0
- package/docs/brainstorms/README.md +22 -0
- package/docs/brainstorms/docs-knowledge-system-requirements.md +29 -0
- package/docs/governance/beta-skill-promotion.md +39 -0
- package/docs/governance/development-contract.md +144 -0
- package/docs/governance/gherkin-style.md +90 -0
- package/docs/governance/host-adapter.md +56 -0
- package/docs/governance/skill-authoring.md +87 -0
- package/docs/governance/skill-testing.md +356 -0
- package/docs/governance/template-authoring.md +53 -0
- package/docs/migrations/v0.2.md +51 -0
- package/docs/plans/README.md +23 -0
- package/docs/plans/bdd-then-tdd-plan.md +1767 -0
- package/docs/plans/docs-knowledge-system-plan.md +31 -0
- package/docs/plans/spec-kit-sdd-adoption-plan.md +305 -0
- package/docs/research/agents-md-best-practices.md +207 -0
- package/docs/research/archon-study.md +351 -0
- package/docs/research/claude-hooks-study.md +440 -0
- package/docs/research/codex-hooks-study.md +624 -0
- package/docs/research/everything-claude-code-study.md +252 -0
- package/docs/research/from-skills-to-layered-workflow.md +322 -0
- package/docs/research/gsd-study.md +69 -0
- package/docs/research/kimi-hooks-study.md +274 -0
- package/docs/research/mattpocock-skills-comparison.md +429 -0
- package/docs/research/mattpocock-skills-study.md +275 -0
- package/docs/research/oh-my-codex-study.md +279 -0
- package/docs/research/perplexity-agent-skills-design.md +168 -0
- package/docs/research/pmc-gstack-skill-study.md +122 -0
- package/docs/research/spec-kit-study.md +224 -0
- package/docs/research/superpowers-study.md +209 -0
- package/docs/roadmap/initial-roadmap.md +53 -0
- package/docs/solutions/README.md +45 -0
- package/docs/solutions/artifact-nesting-recovery.md +58 -0
- package/docs/solutions/session-context-restore-practice.md +67 -0
- package/docs/solutions/workflow/version-drift-recovery.md +49 -0
- package/docs/solutions/worktree-gate-recovery.md +62 -0
- package/docs/specs/README.md +28 -0
- package/docs/specs/claude.md +45 -0
- package/docs/specs/codex.md +44 -0
- package/docs/specs/cursor.md +44 -0
- package/hosts/adapters/claude.ts +29 -0
- package/hosts/adapters/codex.ts +27 -0
- package/hosts/adapters/cursor.ts +27 -0
- package/hosts/adapters/kimi.ts +27 -0
- package/hosts/claude.ts +23 -0
- package/hosts/codex.ts +26 -0
- package/hosts/cursor.ts +19 -0
- package/hosts/index.ts +33 -0
- package/hosts/registry.test.ts +52 -0
- package/hosts/registry.ts +57 -0
- package/hosts/schema.ts +58 -0
- package/package.json +52 -0
- package/scripts/browser.ts +185 -0
- package/scripts/cleanup.ts +142 -0
- package/scripts/commands/artifact.ts +115 -0
- package/scripts/commands/autopilot.ts +143 -0
- package/scripts/commands/capability.ts +57 -0
- package/scripts/commands/config.ts +69 -0
- package/scripts/commands/dag.ts +126 -0
- package/scripts/commands/feedback.ts +123 -0
- package/scripts/commands/gate.ts +291 -0
- package/scripts/commands/helpers.ts +126 -0
- package/scripts/commands/hook.ts +66 -0
- package/scripts/commands/init.ts +515 -0
- package/scripts/commands/issue.ts +825 -0
- package/scripts/commands/phase.ts +61 -0
- package/scripts/commands/preset.ts +159 -0
- package/scripts/commands/runtime.ts +199 -0
- package/scripts/commands/specify.ts +71 -0
- package/scripts/commands/upgrade.ts +243 -0
- package/scripts/commands/verify.ts +183 -0
- package/scripts/commands/wiki.ts +242 -0
- package/scripts/commands/workflow.ts +131 -0
- package/scripts/dev-skill.ts +55 -0
- package/scripts/discover-skills.ts +116 -0
- package/scripts/doctor.ts +410 -0
- package/scripts/dogfood-check.ts +125 -0
- package/scripts/eval-functional.ts +218 -0
- package/scripts/eval.ts +246 -0
- package/scripts/gen-skill-docs.ts +201 -0
- package/scripts/global-discover.ts +217 -0
- package/scripts/governance-check.ts +75 -0
- package/scripts/gxpm-check.ts +12 -0
- package/scripts/gxpm.ts +216 -0
- package/scripts/host-config.ts +62 -0
- package/scripts/install-claude-hooks.ts +138 -0
- package/scripts/install-codex-hooks.ts +271 -0
- package/scripts/install-hooks.ts +128 -0
- package/scripts/install-kimi-hooks.ts +92 -0
- package/scripts/install-skill.ts +184 -0
- package/scripts/phase-artifact-commands.ts +100 -0
- package/scripts/post-land-sync.ts +46 -0
- package/scripts/scaffold-check.ts +85 -0
- package/scripts/skill-naming-check.ts +78 -0
- package/scripts/skill-structure-check.ts +157 -0
- package/scripts/skills-lock-check.ts +60 -0
- package/scripts/sync-markdown-artifacts.ts +172 -0
- package/scripts/uninstall.ts +162 -0
- package/scripts/version.ts +47 -0
- package/scripts/wait-pr-ready.ts +407 -0
- package/skills/gxpm/SKILL.md +485 -0
- package/skills/gxpm/SKILL.md.tmpl +422 -0
- package/skills/gxpm/references/CANON.md +53 -0
- package/skills/gxpm/references/key-rules.md +130 -0
- package/skills/gxpm-architecture/SKILL.md +106 -0
- package/skills/gxpm-architecture/references/DEEPENING.md +37 -0
- package/skills/gxpm-architecture/references/INTERFACE-DESIGN.md +44 -0
- package/skills/gxpm-autopilot/SKILL.md +116 -0
- package/skills/gxpm-autopilot/SKILL.md.tmpl +107 -0
- package/skills/gxpm-browser/SKILL.md +105 -0
- package/skills/gxpm-browser/SKILL.md.tmpl +41 -0
- package/skills/gxpm-browser/references/commands.md +43 -0
- package/skills/gxpm-browser/references/evidence-path.md +20 -0
- package/skills/gxpm-build/SKILL.md +78 -0
- package/skills/gxpm-cleanup/SKILL.md +76 -0
- package/skills/gxpm-debug-issue/SKILL.md +39 -0
- package/skills/gxpm-diagnose/SKILL.md +220 -0
- package/skills/gxpm-diagnose/SKILL.md.tmpl +31 -0
- package/skills/gxpm-diagnose/references/feedback-loop.md +34 -0
- package/skills/gxpm-diagnose/references/feedback-loops.md +43 -0
- package/skills/gxpm-diagnose/references/phases.md +60 -0
- package/skills/gxpm-eval/SKILL.md +78 -0
- package/skills/gxpm-explore-codebase/SKILL.md +36 -0
- package/skills/gxpm-explore-codebase/scripts/summarize-communities.ts +51 -0
- package/skills/gxpm-feedback/SKILL.md +122 -0
- package/skills/gxpm-grill/SKILL.md +159 -0
- package/skills/gxpm-grill/SKILL.md.tmpl +77 -0
- package/skills/gxpm-grill/references/documentation-templates.md +56 -0
- package/skills/gxpm-grill/references/process.md +25 -0
- package/skills/gxpm-handoff/SKILL.md +112 -0
- package/skills/gxpm-hygiene/SKILL.md +69 -0
- package/skills/gxpm-implementer/SKILL.md +142 -0
- package/skills/gxpm-implementer/SKILL.md.tmpl +141 -0
- package/skills/gxpm-linear/SKILL.md +282 -0
- package/skills/gxpm-linear/SKILL.md.tmpl +86 -0
- package/skills/gxpm-linear/references/commands.md +75 -0
- package/skills/gxpm-linear/references/workflows.md +120 -0
- package/skills/gxpm-planning/SKILL.md +134 -0
- package/skills/gxpm-prototype/SKILL.md +64 -0
- package/skills/gxpm-refactor-safely/SKILL.md +62 -0
- package/skills/gxpm-review-army/SKILL.md +117 -0
- package/skills/gxpm-review-changes/SKILL.md +36 -0
- package/skills/gxpm-setup/SKILL.md +101 -0
- package/skills/gxpm-specifier/SKILL.md +135 -0
- package/skills/gxpm-tdd/SKILL.md +187 -0
- package/skills/gxpm-tdd/references/interface-design.md +23 -0
- package/skills/gxpm-tdd/references/mocking.md +27 -0
- package/skills/gxpm-tdd/references/red-green-refactor.md +61 -0
- package/skills/gxpm-tdd/references/troubleshooting.md +28 -0
- package/skills/gxpm-tdd/references/workflow.md +50 -0
- package/skills/gxpm-tdd/testing-anti-patterns.tmpl +304 -0
- package/skills/gxpm-triage/SKILL.md +160 -0
- package/skills/gxpm-verify/SKILL.md +107 -0
- package/skills/gxpm-write-skill/SKILL.md +131 -0
- package/skills/gxpm-zoom-out/SKILL.md +69 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md +54 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md.tmpl +53 -0
- package/templates/constitution-template.md +63 -0
- package/templates/hooks/gxpm-commit-msg +16 -0
- package/templates/hooks/gxpm-post-checkout +19 -0
- package/templates/hooks/gxpm-post-commit +7 -0
- package/templates/hooks/gxpm-post-merge +29 -0
- package/templates/hooks/gxpm-pre-commit +39 -0
- package/templates/hooks/gxpm-pre-push +33 -0
- package/templates/plan-template.md.tmpl +46 -0
- package/templates/spec-template.md.tmpl +63 -0
- package/templates/specify-stub.tmpl +22 -0
- package/templates/tasks-template.md.tmpl +32 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified hook engine for gxpm.
|
|
3
|
+
*
|
|
4
|
+
* All host-specific hooks (Claude Code, Codex CLI, Kimi, Cursor) call
|
|
5
|
+
* `gxpm hook <event> --host <host>` which delegates to this engine.
|
|
6
|
+
* Business logic is host-agnostic; output is formatted per-host.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
formatProjectInitializationContext,
|
|
14
|
+
getProjectInitializationStatus,
|
|
15
|
+
} from "./project-init-status";
|
|
16
|
+
import { readWorktreeOwner } from "./worktree-owner";
|
|
17
|
+
|
|
18
|
+
export type HookHostName = "claude" | "codex" | "cursor" | "kimi";
|
|
19
|
+
|
|
20
|
+
export interface HookInput {
|
|
21
|
+
session_id: string;
|
|
22
|
+
transcript_path?: string;
|
|
23
|
+
cwd: string;
|
|
24
|
+
hook_event_name: string;
|
|
25
|
+
permission_mode?: string;
|
|
26
|
+
agent_id?: string;
|
|
27
|
+
agent_type?: string;
|
|
28
|
+
// Tool events
|
|
29
|
+
tool_name?: string;
|
|
30
|
+
tool_input?: Record<string, unknown>;
|
|
31
|
+
tool_use_id?: string;
|
|
32
|
+
tool_response?: Record<string, unknown>;
|
|
33
|
+
arguments?: Record<string, unknown>;
|
|
34
|
+
// Prompt events
|
|
35
|
+
prompt?: string;
|
|
36
|
+
turn_id?: string;
|
|
37
|
+
// Stop events
|
|
38
|
+
stop_hook_active?: boolean;
|
|
39
|
+
last_assistant_message?: string | null;
|
|
40
|
+
// SessionStart events
|
|
41
|
+
source?: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface HookResult {
|
|
46
|
+
action: "allow" | "block";
|
|
47
|
+
reason?: string;
|
|
48
|
+
additionalContext?: string;
|
|
49
|
+
exitCode: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Detect host from well-known environment variables. */
|
|
53
|
+
export function detectHostFromEnv(): HookHostName | null {
|
|
54
|
+
if (process.env.CLAUDE_PROJECT_DIR) return "claude";
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isValidHookHost(name: string): name is HookHostName {
|
|
59
|
+
return ["claude", "codex", "cursor", "kimi"].includes(name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Main dispatcher — routes by event type to the appropriate processor. */
|
|
63
|
+
export async function processHook(
|
|
64
|
+
host: HookHostName,
|
|
65
|
+
event: string,
|
|
66
|
+
input: HookInput,
|
|
67
|
+
): Promise<HookResult> {
|
|
68
|
+
switch (event) {
|
|
69
|
+
case "SessionStart":
|
|
70
|
+
return processSessionStart(host, input);
|
|
71
|
+
case "UserPromptSubmit":
|
|
72
|
+
return processUserPromptSubmit(host, input);
|
|
73
|
+
case "PreToolUse":
|
|
74
|
+
return processPreToolUse(host, input);
|
|
75
|
+
case "Stop":
|
|
76
|
+
return processStop(host, input);
|
|
77
|
+
case "PostToolUse":
|
|
78
|
+
return processPostToolUse(host, input);
|
|
79
|
+
default:
|
|
80
|
+
return { action: "allow", exitCode: 0 };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Format a HookResult into the JSON / text shape expected by the target host.
|
|
86
|
+
*
|
|
87
|
+
* Rules per host + event:
|
|
88
|
+
* - Codex SessionStart/UserPromptSubmit: hookSpecificOutput.additionalContext
|
|
89
|
+
* - Codex PreToolUse/PermissionRequest: hookSpecificOutput.permissionDecision
|
|
90
|
+
* - Codex Stop: decision: block + reason (continues)
|
|
91
|
+
* - Claude SessionStart: plain text stdout
|
|
92
|
+
* - Claude UserPromptSubmit/others: JSON additionalContext
|
|
93
|
+
* - Claude PreToolUse: JSON permissionDecision
|
|
94
|
+
* - Kimi (block only): JSON permissionDecision (context injection unsupported)
|
|
95
|
+
*/
|
|
96
|
+
export function formatHookOutput(
|
|
97
|
+
host: HookHostName,
|
|
98
|
+
event: string,
|
|
99
|
+
result: HookResult,
|
|
100
|
+
): string {
|
|
101
|
+
if (result.action === "allow" && !result.additionalContext) {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Codex ---------------------------------------------------------------
|
|
106
|
+
if (host === "codex") {
|
|
107
|
+
if (result.additionalContext) {
|
|
108
|
+
return JSON.stringify({
|
|
109
|
+
hookSpecificOutput: {
|
|
110
|
+
hookEventName: event,
|
|
111
|
+
additionalContext: result.additionalContext,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (result.action === "block" && result.reason) {
|
|
116
|
+
if (event === "Stop") {
|
|
117
|
+
return JSON.stringify({ decision: "block", reason: result.reason });
|
|
118
|
+
}
|
|
119
|
+
return JSON.stringify({
|
|
120
|
+
hookSpecificOutput: {
|
|
121
|
+
hookEventName: event,
|
|
122
|
+
permissionDecision: "deny",
|
|
123
|
+
permissionDecisionReason: result.reason,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Claude --------------------------------------------------------------
|
|
131
|
+
if (host === "claude") {
|
|
132
|
+
if (event === "SessionStart" && result.additionalContext) {
|
|
133
|
+
return result.additionalContext;
|
|
134
|
+
}
|
|
135
|
+
if (result.additionalContext) {
|
|
136
|
+
return JSON.stringify({ additionalContext: result.additionalContext });
|
|
137
|
+
}
|
|
138
|
+
if (result.action === "block") {
|
|
139
|
+
return JSON.stringify({
|
|
140
|
+
hookSpecificOutput: {
|
|
141
|
+
hookEventName: event,
|
|
142
|
+
permissionDecision: "deny",
|
|
143
|
+
permissionDecisionReason: result.reason,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Kimi ----------------------------------------------------------------
|
|
151
|
+
if (host === "kimi") {
|
|
152
|
+
if (result.action === "block" && result.reason) {
|
|
153
|
+
return JSON.stringify({
|
|
154
|
+
hookSpecificOutput: {
|
|
155
|
+
hookEventName: event,
|
|
156
|
+
permissionDecision: "deny",
|
|
157
|
+
permissionDecisionReason: result.reason,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// Kimi does not support context injection for SessionStart/UserPromptSubmit,
|
|
162
|
+
// but we emit anyway; the host ignores it.
|
|
163
|
+
if (result.additionalContext) {
|
|
164
|
+
return result.additionalContext;
|
|
165
|
+
}
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Cursor / fallback ----------------------------------------------------
|
|
170
|
+
return "";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// =======================================================================
|
|
174
|
+
// Event processors
|
|
175
|
+
// =======================================================================
|
|
176
|
+
|
|
177
|
+
async function processSessionStart(
|
|
178
|
+
_host: HookHostName,
|
|
179
|
+
input: HookInput,
|
|
180
|
+
): Promise<HookResult> {
|
|
181
|
+
const cwd = input.cwd;
|
|
182
|
+
if (!cwd) {
|
|
183
|
+
return { action: "allow", exitCode: 0 };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const initStatus = getProjectInitializationStatus(cwd);
|
|
187
|
+
if (initStatus.kind !== "initialized") {
|
|
188
|
+
if (initStatus.kind === "partial" || hasRepoScopedGxpmHookConfig(cwd)) {
|
|
189
|
+
const context = formatProjectInitializationContext(initStatus);
|
|
190
|
+
if (context) {
|
|
191
|
+
return { action: "allow", additionalContext: context, exitCode: 0 };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { action: "allow", exitCode: 0 };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const parts: string[] = [];
|
|
198
|
+
|
|
199
|
+
const worktreeCtx = getWorktreeContext(cwd);
|
|
200
|
+
if (worktreeCtx) parts.push(worktreeCtx);
|
|
201
|
+
|
|
202
|
+
const schema = readSchemaVersion(cwd);
|
|
203
|
+
const version = readVersion(cwd);
|
|
204
|
+
parts.push(
|
|
205
|
+
`This repo uses gxpm (schema v${schema}, version ${version}). Run \`gxpm issue list\` to see active work, \`gxpm issue status <id>\` to load context.`,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const updateCtx = checkUpdate(cwd);
|
|
209
|
+
if (updateCtx) parts.push(updateCtx);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
action: "allow",
|
|
213
|
+
additionalContext: parts.join("\n\n"),
|
|
214
|
+
exitCode: 0,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function processUserPromptSubmit(
|
|
219
|
+
_host: HookHostName,
|
|
220
|
+
input: HookInput,
|
|
221
|
+
): Promise<HookResult> {
|
|
222
|
+
const prompt = input.prompt ?? "";
|
|
223
|
+
const cwd = input.cwd;
|
|
224
|
+
|
|
225
|
+
const match = prompt.match(/\b(GXG|GXPM)-\d+\b/i);
|
|
226
|
+
if (!cwd) {
|
|
227
|
+
return { action: "allow", exitCode: 0 };
|
|
228
|
+
}
|
|
229
|
+
if (!match && !existsSync(join(cwd, ".gxpm", "issues"))) {
|
|
230
|
+
return { action: "allow", exitCode: 0 };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const lines: string[] = [];
|
|
234
|
+
|
|
235
|
+
// Scope drift detection: if current directory has a worktree owner marker,
|
|
236
|
+
// warn when prompt mentions an unrelated issue.
|
|
237
|
+
if (match && cwd) {
|
|
238
|
+
const mentionedIssue = match[0].toUpperCase();
|
|
239
|
+
const owner = readWorktreeOwner(cwd);
|
|
240
|
+
if (owner && !owner.linkedIssues.includes(mentionedIssue)) {
|
|
241
|
+
lines.push(`⚠️ SCOPE DRIFT DETECTED ⚠️`);
|
|
242
|
+
lines.push(`Current worktree is owned by ${owner.ownerIssueId} (linked: ${owner.linkedIssues.join(", ")}).`);
|
|
243
|
+
lines.push(`You mentioned ${mentionedIssue} which is NOT in this batch.`);
|
|
244
|
+
lines.push(`RULE: If you need to work on ${mentionedIssue}, create it as a child issue first:`);
|
|
245
|
+
lines.push(` gxpm issue create --auto-id --parent ${owner.ownerIssueId}`);
|
|
246
|
+
lines.push(`Or switch to a different worktree/session.`);
|
|
247
|
+
lines.push("");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (match) {
|
|
252
|
+
const issueId = match[0].toUpperCase();
|
|
253
|
+
const statePath = join(cwd, ".gxpm", "issues", issueId, "state.json");
|
|
254
|
+
if (existsSync(statePath)) {
|
|
255
|
+
lines.push(`gxpm context for ${issueId} (referenced in prompt):`);
|
|
256
|
+
|
|
257
|
+
const sessionId = getSessionId(cwd);
|
|
258
|
+
const currentOwner = getCurrentOwner(cwd, issueId);
|
|
259
|
+
if (sessionId && currentOwner && currentOwner !== sessionId) {
|
|
260
|
+
const wasOwner = checkOwnershipHistory(cwd, issueId, sessionId);
|
|
261
|
+
if (wasOwner) {
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push(`ownership transferred: current owner is ${currentOwner}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lines.push("");
|
|
268
|
+
|
|
269
|
+
const contextOutput = getIssueContext(cwd, issueId);
|
|
270
|
+
if (contextOutput) {
|
|
271
|
+
lines.push(contextOutput);
|
|
272
|
+
} else {
|
|
273
|
+
const statusOutput = getIssueStatus(cwd, issueId);
|
|
274
|
+
const nextOutput = getIssueNext(cwd, issueId);
|
|
275
|
+
if (statusOutput) lines.push(statusOutput);
|
|
276
|
+
if (nextOutput) lines.push(nextOutput);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const { formatAutopilotGrantContext, listActiveAutopilotGrants } = await import("./autopilot");
|
|
282
|
+
const activeGrants = listActiveAutopilotGrants({
|
|
283
|
+
root: cwd,
|
|
284
|
+
issueId: match ? match[0].toUpperCase() : undefined,
|
|
285
|
+
limit: match ? 1 : 3,
|
|
286
|
+
});
|
|
287
|
+
const grantContext = formatAutopilotGrantContext(activeGrants);
|
|
288
|
+
if (grantContext) {
|
|
289
|
+
if (lines.length > 0) lines.push("");
|
|
290
|
+
lines.push(grantContext);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (lines.length === 0) {
|
|
294
|
+
return { action: "allow", exitCode: 0 };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
action: "allow",
|
|
299
|
+
additionalContext: lines.join("\n"),
|
|
300
|
+
exitCode: 0,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function processPreToolUse(
|
|
305
|
+
_host: HookHostName,
|
|
306
|
+
input: HookInput,
|
|
307
|
+
): Promise<HookResult> {
|
|
308
|
+
const toolName = input.tool_name;
|
|
309
|
+
const cwd = input.cwd;
|
|
310
|
+
|
|
311
|
+
const RECORDABLE_TOOLS = ["update_plan", "ExitPlanMode"];
|
|
312
|
+
if (!toolName || !RECORDABLE_TOOLS.includes(toolName) || !cwd) {
|
|
313
|
+
return { action: "allow", exitCode: 0 };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (getProjectInitializationStatus(cwd).kind !== "initialized") {
|
|
317
|
+
return { action: "allow", exitCode: 0 };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const args = JSON.stringify({
|
|
321
|
+
tool_name: toolName,
|
|
322
|
+
arguments: input.tool_input ?? input.arguments,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const issueId = getActiveIssueId(cwd);
|
|
326
|
+
const logDir = issueId
|
|
327
|
+
? join(cwd, ".gxpm", "issues", issueId)
|
|
328
|
+
: join(cwd, ".gxpm");
|
|
329
|
+
const logPath = issueId
|
|
330
|
+
? join(logDir, "codex-plans.jsonl")
|
|
331
|
+
: join(logDir, "codex-plans-orphan.jsonl");
|
|
332
|
+
|
|
333
|
+
mkdirSync(logDir, { recursive: true });
|
|
334
|
+
writeFileSync(logPath, args + "\n", { flag: "a" });
|
|
335
|
+
|
|
336
|
+
return { action: "allow", exitCode: 0 };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function processStop(
|
|
340
|
+
_host: HookHostName,
|
|
341
|
+
input: HookInput,
|
|
342
|
+
): Promise<HookResult> {
|
|
343
|
+
if (input.stop_hook_active) {
|
|
344
|
+
return { action: "allow", exitCode: 0 };
|
|
345
|
+
}
|
|
346
|
+
const cwd = input.cwd;
|
|
347
|
+
if (!cwd) {
|
|
348
|
+
return { action: "allow", exitCode: 0 };
|
|
349
|
+
}
|
|
350
|
+
const { buildAutopilotStopContinuation, listActiveAutopilotGrants } = await import("./autopilot");
|
|
351
|
+
const continuation = buildAutopilotStopContinuation(
|
|
352
|
+
listActiveAutopilotGrants({ root: cwd, limit: 3 }),
|
|
353
|
+
);
|
|
354
|
+
if (continuation) {
|
|
355
|
+
return { action: "block", reason: continuation, exitCode: 2 };
|
|
356
|
+
}
|
|
357
|
+
return { action: "allow", exitCode: 0 };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function processPostToolUse(
|
|
361
|
+
_host: HookHostName,
|
|
362
|
+
_input: HookInput,
|
|
363
|
+
): Promise<HookResult> {
|
|
364
|
+
return { action: "allow", exitCode: 0 };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// =======================================================================
|
|
368
|
+
// Helpers
|
|
369
|
+
// =======================================================================
|
|
370
|
+
|
|
371
|
+
function getWorktreeContext(cwd: string): string | null {
|
|
372
|
+
try {
|
|
373
|
+
execSync("git rev-parse --git-dir", { cwd, stdio: ["ignore", "ignore", "ignore"] });
|
|
374
|
+
} catch {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let branch: string;
|
|
379
|
+
try {
|
|
380
|
+
branch = execSync("git symbolic-ref --short HEAD", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!branch || branch === "main" || branch === "master") {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let gitPath: string;
|
|
390
|
+
try {
|
|
391
|
+
gitPath = execSync("git rev-parse --git-path HEAD", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (gitPath.includes("/worktrees/")) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return `WARNING: You are on branch '${branch}' in the canonical main checkout. gxpm worktree.enforcement is required. Create a worktree before editing code: gxpm workspace ensure <issue-id>`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function hasRepoScopedGxpmHookConfig(cwd: string): boolean {
|
|
404
|
+
for (const file of [
|
|
405
|
+
join(cwd, ".codex", "hooks.json"),
|
|
406
|
+
join(cwd, ".claude", "settings.json"),
|
|
407
|
+
join(cwd, ".kimi", "config.toml"),
|
|
408
|
+
]) {
|
|
409
|
+
try {
|
|
410
|
+
if (readFileSync(file, "utf8").includes("gxpm hook")) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
// absent or unreadable host config; fail open
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function readSchemaVersion(cwd: string): number {
|
|
421
|
+
const stateFile = join(cwd, "core", "state.ts");
|
|
422
|
+
if (!existsSync(stateFile)) return 1;
|
|
423
|
+
try {
|
|
424
|
+
const content = readFileSync(stateFile, "utf8");
|
|
425
|
+
const m = content.match(/\bCURRENT_SCHEMA_VERSION\s*=\s*([0-9]+)\b/);
|
|
426
|
+
return m ? parseInt(m[1], 10) : 1;
|
|
427
|
+
} catch {
|
|
428
|
+
return 1;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function readVersion(cwd: string): string {
|
|
433
|
+
const vf = join(cwd, "VERSION");
|
|
434
|
+
if (!existsSync(vf)) return "dev";
|
|
435
|
+
try {
|
|
436
|
+
const v = readFileSync(vf, "utf8").trim();
|
|
437
|
+
return v.slice(0, 40) || "dev";
|
|
438
|
+
} catch {
|
|
439
|
+
return "dev";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function checkUpdate(cwd: string): string | null {
|
|
444
|
+
const envBin = process.env.GXPM_UPDATE_CHECK_BIN;
|
|
445
|
+
const candidates = envBin ? [envBin] : [];
|
|
446
|
+
try {
|
|
447
|
+
const found = execSync("command -v gxpm-update-check 2>/dev/null", {
|
|
448
|
+
encoding: "utf8",
|
|
449
|
+
shell: "/bin/bash",
|
|
450
|
+
cwd,
|
|
451
|
+
}).trim();
|
|
452
|
+
if (found) candidates.push(found);
|
|
453
|
+
} catch {
|
|
454
|
+
// ignore
|
|
455
|
+
}
|
|
456
|
+
for (const bin of candidates) {
|
|
457
|
+
try {
|
|
458
|
+
const out = execSync(bin, { encoding: "utf8", cwd }).trim();
|
|
459
|
+
if (out.startsWith("UPGRADE_AVAILABLE")) {
|
|
460
|
+
const parts = out.split(/\s+/);
|
|
461
|
+
if (parts[1] && parts[2]) {
|
|
462
|
+
return `gxpm update available: ${parts[1]} -> ${parts[2]}.`;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// ignore
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function getSessionId(cwd: string): string | null {
|
|
473
|
+
try {
|
|
474
|
+
const out = execSync("gxpm session-id", {
|
|
475
|
+
encoding: "utf8",
|
|
476
|
+
cwd,
|
|
477
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
478
|
+
}).trim();
|
|
479
|
+
return out || null;
|
|
480
|
+
} catch {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function getCurrentOwner(cwd: string, issueId: string): string | null {
|
|
486
|
+
try {
|
|
487
|
+
const out = execSync(`gxpm issue ownership "${issueId}" --field currentSession`, {
|
|
488
|
+
encoding: "utf8",
|
|
489
|
+
cwd,
|
|
490
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
491
|
+
}).trim();
|
|
492
|
+
return out || null;
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function checkOwnershipHistory(cwd: string, issueId: string, sessionId: string): boolean {
|
|
499
|
+
try {
|
|
500
|
+
execSync(`gxpm issue ownership "${issueId}" --history-contains "${sessionId}"`, {
|
|
501
|
+
cwd,
|
|
502
|
+
stdio: "ignore",
|
|
503
|
+
});
|
|
504
|
+
return true;
|
|
505
|
+
} catch {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function getIssueContext(cwd: string, issueId: string): string | null {
|
|
511
|
+
try {
|
|
512
|
+
const out = execSync(`gxpm issue context "${issueId}"`, {
|
|
513
|
+
encoding: "utf8",
|
|
514
|
+
cwd,
|
|
515
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
516
|
+
}).trim();
|
|
517
|
+
return out || null;
|
|
518
|
+
} catch {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function getIssueStatus(cwd: string, issueId: string): string | null {
|
|
524
|
+
try {
|
|
525
|
+
const out = execSync(`gxpm issue status "${issueId}"`, {
|
|
526
|
+
encoding: "utf8",
|
|
527
|
+
cwd,
|
|
528
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
529
|
+
}).trim();
|
|
530
|
+
return out || null;
|
|
531
|
+
} catch {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function getIssueNext(cwd: string, issueId: string): string | null {
|
|
537
|
+
try {
|
|
538
|
+
const out = execSync(`gxpm issue next "${issueId}"`, {
|
|
539
|
+
encoding: "utf8",
|
|
540
|
+
cwd,
|
|
541
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
542
|
+
}).trim();
|
|
543
|
+
return out || null;
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function getActiveIssueId(cwd: string): string | null {
|
|
550
|
+
try {
|
|
551
|
+
const out = execSync("gxpm issue list --json", {
|
|
552
|
+
encoding: "utf8",
|
|
553
|
+
cwd,
|
|
554
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
555
|
+
}).trim();
|
|
556
|
+
const issues = JSON.parse(out);
|
|
557
|
+
if (Array.isArray(issues) && issues.length === 1) {
|
|
558
|
+
return (issues[0] as Record<string, unknown>)?.issueId as string ?? null;
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
} catch {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ALL_HOST_CONFIGS } from "../hosts";
|
|
6
|
+
import type { HostName } from "../hosts";
|
|
7
|
+
|
|
8
|
+
export interface HostProbeResult {
|
|
9
|
+
host: HostName;
|
|
10
|
+
displayName: string;
|
|
11
|
+
cliInstalled: boolean;
|
|
12
|
+
repoConfigExists: boolean;
|
|
13
|
+
userConfigExists: boolean;
|
|
14
|
+
/** True if any signal suggests the host is actively used */
|
|
15
|
+
detected: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Probe which agent CLIs are installed and active for a given repo.
|
|
20
|
+
*
|
|
21
|
+
* Detection strategy (per host):
|
|
22
|
+
* 1. CLI command in PATH → cliInstalled
|
|
23
|
+
* 2. <repo>/<hostSubdir> exists → repoConfigExists
|
|
24
|
+
* 3. ~/<hostSubdir> exists → userConfigExists
|
|
25
|
+
*
|
|
26
|
+
* A host is "detected" if the CLI is in PATH OR repo config exists.
|
|
27
|
+
* User config alone does not count (user may have installed it globally
|
|
28
|
+
* but not use it for this project).
|
|
29
|
+
*/
|
|
30
|
+
export function probeHosts(target: string): HostProbeResult[] {
|
|
31
|
+
const home = homedir();
|
|
32
|
+
|
|
33
|
+
return ALL_HOST_CONFIGS.map((config) => {
|
|
34
|
+
const cliInstalled = commandExists(config.cliCommand);
|
|
35
|
+
const repoConfigExists = existsSync(join(target, config.hostSubdir));
|
|
36
|
+
const userConfigExists = existsSync(join(home, config.hostSubdir));
|
|
37
|
+
const detected = cliInstalled || repoConfigExists;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
host: config.name,
|
|
41
|
+
displayName: config.displayName,
|
|
42
|
+
cliInstalled,
|
|
43
|
+
repoConfigExists,
|
|
44
|
+
userConfigExists,
|
|
45
|
+
detected,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Return only hosts that appear to be installed / active. */
|
|
51
|
+
export function detectedHostNames(target: string): HostName[] {
|
|
52
|
+
return probeHosts(target)
|
|
53
|
+
.filter((r) => r.detected)
|
|
54
|
+
.map((r) => r.host);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function commandExists(cmd: string): boolean {
|
|
58
|
+
try {
|
|
59
|
+
execSync(`command -v ${cmd}`, { stdio: "ignore", timeout: 3000 });
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createPhaseArtifactInitializer } from "./phase-artifact";
|
|
2
|
+
|
|
3
|
+
export const initializeLocalVerify = createPhaseArtifactInitializer({
|
|
4
|
+
artifactType: "local-verify",
|
|
5
|
+
label: "Local verify",
|
|
6
|
+
payload: {
|
|
7
|
+
changedFiles: [],
|
|
8
|
+
commands: [],
|
|
9
|
+
evidence: [],
|
|
10
|
+
results: [],
|
|
11
|
+
risks: [],
|
|
12
|
+
status: "draft",
|
|
13
|
+
verificationLog: [],
|
|
14
|
+
},
|
|
15
|
+
requiredPhase: "implement",
|
|
16
|
+
});
|