@geraldmaron/construct 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -44
- package/agents/prompts/cx-accessibility.md +3 -3
- package/agents/prompts/cx-ai-engineer.md +7 -7
- package/agents/prompts/cx-architect.md +6 -6
- package/agents/prompts/cx-business-strategist.md +6 -6
- package/agents/prompts/cx-data-analyst.md +7 -7
- package/agents/prompts/cx-data-engineer.md +3 -3
- package/agents/prompts/cx-debugger.md +6 -6
- package/agents/prompts/cx-designer.md +5 -5
- package/agents/prompts/cx-devil-advocate.md +4 -4
- package/agents/prompts/cx-docs-keeper.md +5 -5
- package/agents/prompts/cx-engineer.md +5 -5
- package/agents/prompts/cx-evaluator.md +2 -2
- package/agents/prompts/cx-explorer.md +8 -8
- package/agents/prompts/cx-legal-compliance.md +2 -2
- package/agents/prompts/cx-operations.md +5 -5
- package/agents/prompts/cx-orchestrator.md +10 -10
- package/agents/prompts/cx-platform-engineer.md +5 -5
- package/agents/prompts/cx-product-manager.md +4 -4
- package/agents/prompts/cx-qa.md +8 -8
- package/agents/prompts/cx-rd-lead.md +9 -9
- package/agents/prompts/cx-release-manager.md +5 -5
- package/agents/prompts/cx-researcher.md +22 -22
- package/agents/prompts/cx-reviewer.md +7 -7
- package/agents/prompts/cx-security.md +10 -10
- package/agents/prompts/cx-sre.md +7 -7
- package/agents/prompts/cx-test-automation.md +3 -3
- package/agents/prompts/cx-trace-reviewer.md +8 -8
- package/agents/prompts/cx-ux-researcher.md +3 -3
- package/bin/construct +470 -4
- package/commands/build/feature.md +4 -4
- package/commands/build/fix.md +8 -8
- package/commands/design/access.md +3 -3
- package/commands/design/flow.md +3 -3
- package/commands/design/ui.md +4 -4
- package/commands/measure/experiment.md +5 -5
- package/commands/measure/metrics.md +3 -3
- package/commands/measure/results.md +4 -4
- package/commands/plan/api.md +3 -3
- package/commands/plan/challenge.md +3 -3
- package/commands/plan/decide.md +3 -3
- package/commands/plan/feature.md +7 -7
- package/commands/plan/requirements.md +3 -3
- package/commands/remember/context.md +5 -5
- package/commands/remember/handoff.md +3 -3
- package/commands/remember/runbook.md +3 -3
- package/commands/review/code.md +8 -8
- package/commands/review/quality.md +4 -4
- package/commands/review/security.md +3 -3
- package/commands/ship/ready.md +3 -3
- package/commands/ship/release.md +3 -3
- package/commands/ship/status.md +4 -4
- package/commands/understand/docs.md +3 -3
- package/commands/understand/research.md +5 -3
- package/commands/understand/this.md +4 -4
- package/commands/understand/why.md +8 -8
- package/commands/work/clean.md +14 -14
- package/commands/work/drive.md +10 -10
- package/commands/work/optimize-prompts.md +9 -9
- package/commands/work/parallel-review.md +8 -8
- package/db/schema/006_graph.sql +24 -0
- package/examples/provider-plugin/README.md +7 -7
- package/examples/seed-observations/README.md +6 -6
- package/examples/seed-observations/anti-patterns.md +14 -14
- package/examples/seed-observations/decisions.md +4 -4
- package/examples/seed-observations/patterns.md +14 -14
- package/lib/auto-docs.mjs +10 -5
- package/lib/cli-commands.mjs +45 -1
- package/lib/comment-lint.mjs +7 -1
- package/lib/config/schema.mjs +3 -0
- package/lib/flavors/loader.mjs +136 -0
- package/lib/hooks/agent-tracker.mjs +22 -3
- package/lib/hooks/pre-push-gate.mjs +14 -1
- package/lib/hooks/session-optimize.mjs +3 -2
- package/lib/hooks/session-reflect.mjs +68 -0
- package/lib/init-unified.mjs +25 -2
- package/lib/intake/classify.mjs +61 -183
- package/lib/intake/prepare.mjs +7 -0
- package/lib/intake/tables/creative.mjs +94 -0
- package/lib/intake/tables/operations.mjs +85 -0
- package/lib/intake/tables/research.mjs +85 -0
- package/lib/intake/tables/rnd.mjs +175 -0
- package/lib/knowledge/graph.mjs +213 -0
- package/lib/knowledge/research-store.mjs +109 -0
- package/lib/mcp/server.mjs +187 -1
- package/lib/mcp/tools/profile.mjs +270 -0
- package/lib/observation-store.mjs +19 -0
- package/lib/outcomes/aggregate.mjs +104 -0
- package/lib/outcomes/record.mjs +115 -0
- package/lib/parity.mjs +6 -9
- package/lib/profiles/lifecycle.mjs +388 -0
- package/lib/profiles/loader.mjs +123 -0
- package/lib/profiles/validate-custom.mjs +114 -0
- package/lib/reflect/extractor.mjs +193 -0
- package/lib/reflect.mjs +89 -2
- package/lib/sandbox.mjs +102 -0
- package/package.json +6 -1
- package/personas/construct.md +20 -20
- package/platforms/claude/CLAUDE.md +6 -6
- package/platforms/claude/settings.template.json +13 -0
- package/rules/common/agents.md +2 -2
- package/rules/common/beads-hygiene.md +11 -11
- package/rules/common/code-review.md +1 -1
- package/rules/common/coding-style.md +1 -1
- package/rules/common/comments.md +8 -8
- package/rules/common/commit-approval.md +4 -4
- package/rules/common/cx-agent-routing.md +2 -2
- package/rules/common/cx-skill-routing.md +2 -2
- package/rules/common/development-workflow.md +1 -1
- package/rules/common/doc-ownership.md +2 -2
- package/rules/common/efficiency.md +3 -3
- package/rules/common/framing.md +1 -1
- package/rules/common/git-workflow.md +1 -1
- package/rules/common/patterns.md +1 -1
- package/rules/common/performance.md +1 -1
- package/rules/common/release-gates.md +7 -7
- package/rules/common/research.md +4 -4
- package/rules/common/security.md +1 -1
- package/rules/common/skill-composition.md +8 -8
- package/rules/common/testing.md +1 -1
- package/rules/golang/coding-style.md +2 -2
- package/rules/golang/hooks.md +1 -1
- package/rules/golang/patterns.md +1 -1
- package/rules/golang/security.md +1 -1
- package/rules/golang/testing.md +1 -1
- package/rules/python/coding-style.md +1 -1
- package/rules/python/hooks.md +1 -1
- package/rules/python/patterns.md +1 -1
- package/rules/python/security.md +1 -1
- package/rules/python/testing.md +1 -1
- package/rules/swift/coding-style.md +3 -3
- package/rules/swift/hooks.md +2 -2
- package/rules/swift/patterns.md +2 -2
- package/rules/swift/security.md +4 -4
- package/rules/swift/testing.md +2 -2
- package/rules/typescript/coding-style.md +1 -1
- package/rules/typescript/hooks.md +1 -1
- package/rules/typescript/patterns.md +1 -1
- package/rules/typescript/security.md +1 -1
- package/rules/typescript/testing.md +1 -1
- package/rules/web/coding-style.md +1 -1
- package/rules/web/design-quality.md +1 -1
- package/rules/web/hooks.md +1 -1
- package/rules/web/patterns.md +1 -1
- package/rules/web/performance.md +1 -1
- package/rules/web/security.md +1 -1
- package/rules/web/testing.md +1 -1
- package/scripts/sync-agents.mjs +11 -0
- package/skills/ai/agent-dev.md +1 -1
- package/skills/ai/llm-security.md +1 -1
- package/skills/ai/ml-ops.md +6 -6
- package/skills/ai/orchestration-workflow.md +1 -1
- package/skills/ai/prompt-and-eval.md +1 -1
- package/skills/ai/prompt-optimizer.md +13 -13
- package/skills/ai/rag-system.md +1 -1
- package/skills/architecture/api-design.md +1 -1
- package/skills/architecture/caching.md +1 -1
- package/skills/architecture/cloud-native.md +1 -1
- package/skills/architecture/message-queue.md +1 -1
- package/skills/architecture/security-arch.md +1 -1
- package/skills/compliance/ai-disclosure.md +1 -1
- package/skills/compliance/data-privacy.md +1 -1
- package/skills/compliance/license-audit.md +2 -2
- package/skills/compliance/regulatory-review.md +1 -1
- package/skills/development/cpp.md +1 -1
- package/skills/development/go.md +1 -1
- package/skills/development/java.md +1 -1
- package/skills/development/kotlin.md +9 -9
- package/skills/development/mobile-crossplatform.md +13 -13
- package/skills/development/python.md +1 -1
- package/skills/development/rust.md +1 -1
- package/skills/development/shell.md +1 -1
- package/skills/development/swift.md +6 -6
- package/skills/development/typescript.md +1 -1
- package/skills/devops/ci-cd.md +5 -5
- package/skills/devops/containerization.md +9 -9
- package/skills/devops/cost-optimization.md +1 -1
- package/skills/devops/data-engineering.md +2 -2
- package/skills/devops/database.md +1 -1
- package/skills/devops/dependency-management.md +3 -3
- package/skills/devops/devsecops.md +1 -1
- package/skills/devops/git-workflow.md +1 -1
- package/skills/devops/incident-response.md +18 -18
- package/skills/devops/monorepo.md +5 -5
- package/skills/devops/observability.md +1 -1
- package/skills/devops/performance.md +1 -1
- package/skills/devops/testing.md +1 -1
- package/skills/docs/adr-workflow.md +2 -2
- package/skills/docs/backlog-proposal-workflow.md +1 -1
- package/skills/docs/customer-profile-workflow.md +1 -1
- package/skills/docs/document-ingest-workflow.md +1 -1
- package/skills/docs/evidence-ingest-workflow.md +1 -1
- package/skills/docs/init-docs.md +15 -15
- package/skills/docs/init-project.md +1 -1
- package/skills/docs/prd-workflow.md +3 -3
- package/skills/docs/prfaq-workflow.md +1 -1
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +1 -1
- package/skills/docs/product-signal-workflow.md +9 -9
- package/skills/docs/research-workflow.md +10 -10
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/strategy-workflow.md +3 -3
- package/skills/exploration/repo-map.md +11 -11
- package/skills/frameworks/django.md +15 -15
- package/skills/frameworks/nextjs.md +16 -16
- package/skills/frameworks/react.md +12 -12
- package/skills/frameworks/spring-boot.md +12 -12
- package/skills/frontend-design/accessibility.md +6 -6
- package/skills/frontend-design/component-patterns.md +1 -1
- package/skills/frontend-design/engineering.md +1 -1
- package/skills/frontend-design/state-management.md +1 -1
- package/skills/frontend-design/ui-aesthetics.md +1 -1
- package/skills/frontend-design/ux-principles.md +1 -1
- package/skills/operating/orchestration-reference.md +27 -27
- package/skills/quality-gates/review-work.md +3 -3
- package/skills/quality-gates/verify-change.md +1 -1
- package/skills/quality-gates/verify-module.md +1 -1
- package/skills/quality-gates/verify-quality.md +1 -1
- package/skills/quality-gates/verify-security.md +1 -1
- package/skills/roles/architect.ai-systems.md +4 -2
- package/skills/roles/architect.data.md +4 -2
- package/skills/roles/architect.enterprise.md +4 -2
- package/skills/roles/architect.integration.md +4 -2
- package/skills/roles/architect.md +7 -5
- package/skills/roles/architect.platform.md +4 -2
- package/skills/roles/data-analyst.experiment.md +4 -2
- package/skills/roles/data-analyst.md +9 -7
- package/skills/roles/data-analyst.product-intelligence.md +4 -2
- package/skills/roles/data-analyst.product.md +4 -2
- package/skills/roles/data-analyst.telemetry.md +4 -2
- package/skills/roles/data-engineer.pipeline.md +4 -2
- package/skills/roles/data-engineer.vector-retrieval.md +4 -2
- package/skills/roles/data-engineer.warehouse.md +4 -2
- package/skills/roles/debugger.md +7 -5
- package/skills/roles/designer.accessibility.md +4 -2
- package/skills/roles/designer.md +10 -8
- package/skills/roles/engineer.ai.md +4 -2
- package/skills/roles/engineer.data.md +5 -3
- package/skills/roles/engineer.md +14 -12
- package/skills/roles/engineer.platform.md +5 -3
- package/skills/roles/operator.docs.md +6 -4
- package/skills/roles/operator.md +6 -4
- package/skills/roles/operator.release.md +4 -2
- package/skills/roles/operator.sre.md +5 -3
- package/skills/roles/orchestrator.md +5 -3
- package/skills/roles/product-manager.ai-product.md +4 -2
- package/skills/roles/product-manager.business-strategy.md +4 -2
- package/skills/roles/product-manager.enterprise.md +4 -2
- package/skills/roles/product-manager.growth.md +4 -2
- package/skills/roles/product-manager.md +6 -4
- package/skills/roles/product-manager.platform.md +4 -2
- package/skills/roles/product-manager.product.md +4 -2
- package/skills/roles/qa.ai-eval.md +4 -2
- package/skills/roles/qa.api-contract.md +4 -2
- package/skills/roles/qa.data-pipeline.md +4 -2
- package/skills/roles/qa.md +7 -5
- package/skills/roles/qa.test-automation.md +5 -3
- package/skills/roles/qa.web-ui.md +4 -2
- package/skills/roles/researcher.explorer.md +4 -2
- package/skills/roles/researcher.md +11 -9
- package/skills/roles/researcher.ux.md +4 -2
- package/skills/roles/reviewer.devil-advocate.md +4 -2
- package/skills/roles/reviewer.evaluator.md +4 -2
- package/skills/roles/reviewer.md +14 -12
- package/skills/roles/reviewer.trace.md +4 -2
- package/skills/roles/security.ai.md +4 -2
- package/skills/roles/security.appsec.md +4 -2
- package/skills/roles/security.cloud.md +4 -2
- package/skills/roles/security.legal-compliance.md +4 -2
- package/skills/roles/security.md +7 -5
- package/skills/roles/security.privacy.md +4 -2
- package/skills/roles/security.supply-chain.md +4 -2
- package/skills/routing.md +14 -14
- package/skills/security/blue-team.md +1 -1
- package/skills/security/code-audit.md +1 -1
- package/skills/security/pentest.md +1 -1
- package/skills/security/red-team.md +1 -1
- package/skills/security/threat-intel.md +1 -1
- package/skills/security/vuln-research.md +1 -1
- package/skills/utility/clean-code.md +2 -2
- package/templates/docs/changelog-entry.md +1 -1
- package/templates/docs/construct_guide.md +13 -13
- package/templates/docs/meta-prd.md +16 -16
- package/templates/docs/one-pager.md +1 -1
- package/templates/docs/persona-artifact.md +36 -0
- package/templates/docs/prd-business.md +1 -1
- package/templates/docs/prd-platform.md +1 -1
- package/templates/docs/prd.md +17 -17
- package/templates/docs/research-brief.md +8 -8
- package/templates/docs/research-finding.md +26 -0
- package/templates/docs/rfc.md +1 -1
- package/templates/docs/skill-artifact.md +27 -0
- package/templates/docs/strategy.md +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
You have inherited enough flaky test suites to know that bad automation is worse than no automation
|
|
1
|
+
You have inherited enough flaky test suites to know that bad automation is worse than no automation: it creates false confidence while hiding real failures. The test that passes intermittently isn't catching bugs; it's teaching the team to ignore red builds.
|
|
2
2
|
|
|
3
3
|
**What you're instinctively suspicious of:**
|
|
4
4
|
- Tests that pass intermittently and are dismissed as "infrastructure issues"
|
|
@@ -7,7 +7,7 @@ You have inherited enough flaky test suites to know that bad automation is worse
|
|
|
7
7
|
- Tests with arbitrary sleeps instead of deterministic waits
|
|
8
8
|
- "The test infrastructure is complex" as an explanation for low coverage
|
|
9
9
|
|
|
10
|
-
**Your productive tension**: cx-qa
|
|
10
|
+
**Your productive tension**: cx-qa: QA plans the verification strategy; you implement it, and you know when the planned approach is untestable as written
|
|
11
11
|
|
|
12
12
|
**Your opening question**: Is this test deterministic, and does it actually fail when the behavior it's testing breaks?
|
|
13
13
|
|
|
@@ -17,7 +17,7 @@ You have inherited enough flaky test suites to know that bad automation is worse
|
|
|
17
17
|
|
|
18
18
|
Your scope: designing and implementing automated test suites (unit, integration, E2E, contract, visual regression, load), test framework selection and configuration, flaky test diagnosis, CI/CD test pipeline optimization, test data management, coverage tooling, and test parallelization.
|
|
19
19
|
|
|
20
|
-
You are distinct from cx-qa (who owns test strategy, planning, and quality assurance methodology)
|
|
20
|
+
You are distinct from cx-qa (who owns test strategy, planning, and quality assurance methodology): you own the automation implementation and infrastructure that executes that strategy.
|
|
21
21
|
|
|
22
22
|
When given a task:
|
|
23
23
|
1. Understand the existing test infrastructure before proposing new frameworks
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
You track whether agents are actually performing in production
|
|
1
|
+
You track whether agents are actually performing in production: not in demos, not in cherry-picked examples, but in the distribution of real usage. Trace patterns reveal what prompt reviews miss: the degradation that's invisible until you look at variance, the agent that's fine on median but catastrophically wrong on the 30th percentile.
|
|
2
2
|
|
|
3
3
|
**What you're instinctively suspicious of:**
|
|
4
4
|
- Stable median scores that hide high-variance agents
|
|
@@ -7,7 +7,7 @@ You track whether agents are actually performing in production — not in demos,
|
|
|
7
7
|
- "The prompt looks fine" without checking actual trace behavior
|
|
8
8
|
- Agents with no quality scoring that haven't been reviewed
|
|
9
9
|
|
|
10
|
-
**Your productive tension**: cx-ai-engineer
|
|
10
|
+
**Your productive tension**: cx-ai-engineer: AI engineer optimizes individual prompts; you see the fleet-level patterns and the agents that need attention
|
|
11
11
|
|
|
12
12
|
**Your opening question**: Which agents have degraded since the last cycle, and what does the trace evidence actually say about why?
|
|
13
13
|
|
|
@@ -15,11 +15,11 @@ You track whether agents are actually performing in production — not in demos,
|
|
|
15
15
|
|
|
16
16
|
**Role guidance**: call `get_skill("roles/reviewer.trace")` before drafting.
|
|
17
17
|
|
|
18
|
-
You support pluggable trace backends (configured via CONSTRUCT_TRACE_BACKEND env var). All trace access goes through the configured backend adapter
|
|
18
|
+
You support pluggable trace backends (configured via CONSTRUCT_TRACE_BACKEND env var). All trace access goes through the configured backend adapter: do not hardcode provider-specific API calls without checking CONSTRUCT_TRACE_BACKEND first.
|
|
19
19
|
|
|
20
20
|
Backend: Telemetry (`CONSTRUCT_TELEMETRY_URL`, `CONSTRUCT_TELEMETRY_PUBLIC_KEY`, `CONSTRUCT_TELEMETRY_SECRET_KEY`)
|
|
21
21
|
|
|
22
|
-
## Step 1
|
|
22
|
+
## Step 1: Triage
|
|
23
23
|
|
|
24
24
|
Fetch recent quality scores across all agents via the configured backend:
|
|
25
25
|
GET {CONSTRUCT_TELEMETRY_URL}/api/public/scores?name=quality&limit=200
|
|
@@ -27,16 +27,16 @@ Fetch recent quality scores across all agents via the configured backend:
|
|
|
27
27
|
Group by agent name (extracted from trace metadata). Compute median per agent. Flag any agent with:
|
|
28
28
|
- Median quality score < 0.65 over the past 7 days
|
|
29
29
|
- Downward trend (last-7-day median worse than prior-7-day median by more than 0.05)
|
|
30
|
-
- High variance (stddev > 0.25)
|
|
30
|
+
- High variance (stddev > 0.25): indicates inconsistent behavior
|
|
31
31
|
|
|
32
|
-
## Step 2
|
|
32
|
+
## Step 2: Deep analysis per flagged agent
|
|
33
33
|
|
|
34
34
|
For each flagged agent, fetch its low-scoring traces (score < 0.7) and high-scoring traces (score > 0.8) as a contrast pair. Extract:
|
|
35
35
|
- What inputs correlate with low scores?
|
|
36
36
|
- What tool usage patterns appear in high vs low traces?
|
|
37
37
|
- What output characteristics are present in high-scoring traces that are absent in low-scoring ones?
|
|
38
38
|
|
|
39
|
-
## Step 3
|
|
39
|
+
## Step 3: Optimization cycle
|
|
40
40
|
|
|
41
41
|
Follow skills/ai/prompt-optimizer.md. For each agent meeting the minimum trace threshold (20+):
|
|
42
42
|
1. Read current production prompt from agents/registry.json
|
|
@@ -45,7 +45,7 @@ Follow skills/ai/prompt-optimizer.md. For each agent meeting the minimum trace t
|
|
|
45
45
|
4. Log staging candidate via cx_trace with promptVersion attribute
|
|
46
46
|
5. Record the optimization in .cx/decisions/
|
|
47
47
|
|
|
48
|
-
## Step 4
|
|
48
|
+
## Step 4: Promotion decisions
|
|
49
49
|
|
|
50
50
|
For agents with staging versions that have accumulated 20+ new traces:
|
|
51
51
|
- Compare staging vs production median quality scores
|
|
@@ -7,14 +7,14 @@ You have watched enough users fail to know that what they say they want and what
|
|
|
7
7
|
- "Users will figure it out" as a UX strategy
|
|
8
8
|
- Assumptions in the product brief that were never tested against real behavior
|
|
9
9
|
|
|
10
|
-
**Your productive tension**: cx-designer
|
|
10
|
+
**Your productive tension**: cx-designer: designers have visual ideas; you insist on behavioral grounding before the design locks
|
|
11
11
|
|
|
12
|
-
**Your opening question**: Who specifically is the user, what is their actual context, and what would success feel like to them
|
|
12
|
+
**Your opening question**: Who specifically is the user, what is their actual context, and what would success feel like to them: not to us?
|
|
13
13
|
|
|
14
14
|
**Failure mode warning**: If your brief has no friction points, you haven't talked to users. Every product has places where users get stuck.
|
|
15
15
|
|
|
16
16
|
**Role guidance**: call `get_skill("roles/researcher.ux")` before drafting.
|
|
17
|
-
**Evidence policy**: for any external claims (benchmark data, published studies, platform statistics), follow `rules/common/research.md
|
|
17
|
+
**Evidence policy**: for any external claims (benchmark data, published studies, platform statistics), follow `rules/common/research.md`: most-recent-first, primary sources, verified URLs. UX findings based on direct user observation are primary evidence; stated preferences and self-reported data are secondary.
|
|
18
18
|
|
|
19
19
|
Produce a UX brief:
|
|
20
20
|
|
package/bin/construct
CHANGED
|
@@ -186,14 +186,49 @@ function runNodeScript(scriptPath, args = [], extraEnv = {}) {
|
|
|
186
186
|
async function cmdStatus() {
|
|
187
187
|
const jsonOutput = restArgsCache.includes('--json');
|
|
188
188
|
const status = await buildStatus({ rootDir: ROOT_DIR, cwd: process.cwd(), homeDir: HOME, env: process.env });
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
if (jsonOutput) {
|
|
191
191
|
println(JSON.stringify(status, null, 2));
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
println(formatStatusReport(status));
|
|
196
|
-
|
|
196
|
+
|
|
197
|
+
// Append the learning-loops summary so the operator gets a single answer
|
|
198
|
+
// to "is this thing actually learning?" without remembering a second
|
|
199
|
+
// command. Cheap: just reads .cx/observations + .cx/outcomes summaries.
|
|
200
|
+
try {
|
|
201
|
+
const { resolveActiveProfile } = await import('../lib/profiles/loader.mjs');
|
|
202
|
+
const { readSummary } = await import('../lib/outcomes/aggregate.mjs');
|
|
203
|
+
const fs = await import('node:fs');
|
|
204
|
+
const path = await import('node:path');
|
|
205
|
+
const cwd = process.cwd();
|
|
206
|
+
const active = resolveActiveProfile(cwd);
|
|
207
|
+
const obsIdxPath = path.join(cwd, '.cx', 'observations', 'index.json');
|
|
208
|
+
let obsTotal = 0;
|
|
209
|
+
let obs24h = 0;
|
|
210
|
+
if (fs.existsSync(obsIdxPath)) {
|
|
211
|
+
const idx = JSON.parse(fs.readFileSync(obsIdxPath, 'utf8'));
|
|
212
|
+
obsTotal = idx.length;
|
|
213
|
+
const since = Date.now() - 24 * 60 * 60 * 1000;
|
|
214
|
+
obs24h = idx.filter((e) => Date.parse(e.createdAt) >= since).length;
|
|
215
|
+
}
|
|
216
|
+
const researchDir = path.join(cwd, '.cx', 'knowledge', 'external', 'research');
|
|
217
|
+
const researchCount = fs.existsSync(researchDir)
|
|
218
|
+
? fs.readdirSync(researchDir).filter((f) => f.endsWith('.md')).length
|
|
219
|
+
: 0;
|
|
220
|
+
const summary = readSummary(cwd);
|
|
221
|
+
const outcomeRoles = summary?.roles ? Object.keys(summary.roles).length : 0;
|
|
222
|
+
|
|
223
|
+
println('');
|
|
224
|
+
println('Learning loops');
|
|
225
|
+
println('──────────────');
|
|
226
|
+
println(` Profile ${active.id} (${active.displayName || ''})`);
|
|
227
|
+
println(` Observations ${obsTotal} total, ${obs24h} in last 24h`);
|
|
228
|
+
println(` Research files ${researchCount}`);
|
|
229
|
+
println(` Outcome roles ${outcomeRoles}`);
|
|
230
|
+
println(` Detail: npm run learning:status`);
|
|
231
|
+
} catch { /* best-effort surface; never block status */ }
|
|
197
232
|
}
|
|
198
233
|
|
|
199
234
|
async function cmdShow() {
|
|
@@ -3233,6 +3268,27 @@ async function cmdHook(args) {
|
|
|
3233
3268
|
const [command, ...rest] = process.argv.slice(2);
|
|
3234
3269
|
const restArgsCache = rest;
|
|
3235
3270
|
|
|
3271
|
+
// Parse `--key=value` and repeatable `--source-url=<url>` for `construct knowledge add`.
|
|
3272
|
+
function parseKnowledgeAddArgs(args) {
|
|
3273
|
+
const opts = { slug: null, topic: null, confidence: 'inferred', sources: [] };
|
|
3274
|
+
for (const arg of args) {
|
|
3275
|
+
if (arg.startsWith('--slug=')) opts.slug = arg.slice('--slug='.length);
|
|
3276
|
+
else if (arg.startsWith('--topic=')) opts.topic = arg.slice('--topic='.length);
|
|
3277
|
+
else if (arg.startsWith('--confidence=')) opts.confidence = arg.slice('--confidence='.length);
|
|
3278
|
+
else if (arg.startsWith('--source-url=')) opts.sources.push({ url: arg.slice('--source-url='.length) });
|
|
3279
|
+
else if (arg.startsWith('--source=')) { /* tag, ignored for now */ }
|
|
3280
|
+
}
|
|
3281
|
+
return opts;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
// Read all of stdin as a UTF-8 string. Returns '' if nothing was piped.
|
|
3285
|
+
async function readStdin() {
|
|
3286
|
+
if (process.stdin.isTTY) return '';
|
|
3287
|
+
let raw = '';
|
|
3288
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
3289
|
+
return raw;
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3236
3292
|
const handlers = new Map([
|
|
3237
3293
|
// Core
|
|
3238
3294
|
['dev', cmdUp],
|
|
@@ -3268,6 +3324,24 @@ const handlers = new Map([
|
|
|
3268
3324
|
['handoffs', cmdHandoffs],
|
|
3269
3325
|
['headhunt', cmdHeadhunt],
|
|
3270
3326
|
['init', cmdInit],
|
|
3327
|
+
['drop', async (args) => {
|
|
3328
|
+
const { runDropCli } = await import('../lib/drop.mjs');
|
|
3329
|
+
return runDropCli(args, { cwd: process.cwd(), env: process.env });
|
|
3330
|
+
}],
|
|
3331
|
+
['docs', async (args) => {
|
|
3332
|
+
const sub = args[0];
|
|
3333
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
3334
|
+
println('Usage: construct docs <check|verify|update|site>');
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
const rest = args.slice(1);
|
|
3338
|
+
if (sub === 'verify') return cmdDocsVerify(rest);
|
|
3339
|
+
if (sub === 'update') return cmdDocsUpdate(rest);
|
|
3340
|
+
if (sub === 'check') return cmdDocsCheck(rest);
|
|
3341
|
+
if (sub === 'site') return cmdDocsSite();
|
|
3342
|
+
errorln(`Unknown docs subcommand: ${sub}. Available: check, verify, update, site`);
|
|
3343
|
+
process.exit(1);
|
|
3344
|
+
}],
|
|
3271
3345
|
['docs:verify', cmdDocsVerify],
|
|
3272
3346
|
['init:update', cmdInitUpdate],
|
|
3273
3347
|
['models', cmdModels],
|
|
@@ -3833,7 +3907,399 @@ const handlers = new Map([
|
|
|
3833
3907
|
}
|
|
3834
3908
|
return;
|
|
3835
3909
|
}
|
|
3836
|
-
|
|
3910
|
+
if (sub === 'add') {
|
|
3911
|
+
const opts = parseKnowledgeAddArgs(args.slice(1));
|
|
3912
|
+
if (!opts.slug || !opts.topic) {
|
|
3913
|
+
errorln('Usage: construct knowledge add --source=research --slug=<id> --topic="..." [--confidence=confirmed|inferred|weak] [--source-url=<url>] (body on stdin)');
|
|
3914
|
+
process.exit(1);
|
|
3915
|
+
}
|
|
3916
|
+
const body = await readStdin();
|
|
3917
|
+
if (!body || body.trim().length < 10) {
|
|
3918
|
+
errorln('construct knowledge add: stdin body is empty or too short');
|
|
3919
|
+
process.exit(1);
|
|
3920
|
+
}
|
|
3921
|
+
const { addResearchFinding } = await import('../lib/knowledge/research-store.mjs');
|
|
3922
|
+
try {
|
|
3923
|
+
const { path: outPath, bytes } = await addResearchFinding({
|
|
3924
|
+
cwd: process.cwd(),
|
|
3925
|
+
slug: opts.slug,
|
|
3926
|
+
topic: opts.topic,
|
|
3927
|
+
body,
|
|
3928
|
+
confidence: opts.confidence,
|
|
3929
|
+
sources: opts.sources,
|
|
3930
|
+
});
|
|
3931
|
+
process.stdout.write(`✓ wrote ${outPath} (${bytes} bytes)\n`);
|
|
3932
|
+
return;
|
|
3933
|
+
} catch (err) {
|
|
3934
|
+
errorln(`construct knowledge add: ${err.message}`);
|
|
3935
|
+
process.exit(1);
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
errorln(`Unknown knowledge subcommand: ${sub}. Available: trends, index, add`);
|
|
3939
|
+
process.exit(1);
|
|
3940
|
+
}],
|
|
3941
|
+
['sandbox', async (args) => {
|
|
3942
|
+
const sub = args[0] || 'create';
|
|
3943
|
+
const { createSandbox, listSandboxes, deleteSandbox, pruneSandboxes } = await import('../lib/sandbox.mjs');
|
|
3944
|
+
if (sub === 'create') {
|
|
3945
|
+
const profileArg = args.find((a) => a.startsWith('--profile='));
|
|
3946
|
+
const profile = profileArg ? profileArg.split('=')[1] : null;
|
|
3947
|
+
const { id, path: p } = createSandbox({ profile });
|
|
3948
|
+
process.stdout.write(`✓ created sandbox ${id}\n path: ${p}\n`);
|
|
3949
|
+
if (profile) process.stdout.write(` profile: ${profile}\n`);
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
if (sub === 'list') {
|
|
3953
|
+
const all = listSandboxes();
|
|
3954
|
+
if (all.length === 0) { process.stdout.write(' (no sandboxes)\n'); return; }
|
|
3955
|
+
for (const s of all) process.stdout.write(` ${s.id.padEnd(30)} ${s.path}\n`);
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
if (sub === 'delete') {
|
|
3959
|
+
const id = args[1];
|
|
3960
|
+
if (!id) { errorln('Usage: construct sandbox delete <id>'); process.exit(1); }
|
|
3961
|
+
process.stdout.write(deleteSandbox(id) ? `✓ removed ${id}\n` : `not found: ${id}\n`);
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
if (sub === 'prune') {
|
|
3965
|
+
const daysArg = args.find((a) => a.startsWith('--days='));
|
|
3966
|
+
const days = daysArg ? Number(daysArg.split('=')[1]) || 7 : 7;
|
|
3967
|
+
const removed = pruneSandboxes({ olderThanDays: days });
|
|
3968
|
+
process.stdout.write(`✓ pruned ${removed} sandbox${removed === 1 ? '' : 'es'} older than ${days} day${days === 1 ? '' : 's'}\n`);
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
errorln(`Unknown sandbox subcommand: ${sub}. Available: create, list, delete, prune`);
|
|
3972
|
+
process.exit(1);
|
|
3973
|
+
}],
|
|
3974
|
+
['profile', async (args) => {
|
|
3975
|
+
const sub = args[0] || 'show';
|
|
3976
|
+
const { listProfiles, loadProfile, resolveActiveProfile } = await import('../lib/profiles/loader.mjs');
|
|
3977
|
+
if (sub === 'list') {
|
|
3978
|
+
for (const id of listProfiles()) {
|
|
3979
|
+
const p = loadProfile(id);
|
|
3980
|
+
process.stdout.write(` ${id.padEnd(14)} ${p?.displayName || ''}\n`);
|
|
3981
|
+
}
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
if (sub === 'show') {
|
|
3985
|
+
const active = resolveActiveProfile(process.cwd());
|
|
3986
|
+
process.stdout.write(JSON.stringify({
|
|
3987
|
+
id: active.id,
|
|
3988
|
+
displayName: active.displayName,
|
|
3989
|
+
tagline: active.tagline,
|
|
3990
|
+
roles: active.roles?.length || 0,
|
|
3991
|
+
intakeTypes: active.intake?.types?.length || 0,
|
|
3992
|
+
}, null, 2) + '\n');
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
if (sub === 'set') {
|
|
3996
|
+
const id = args[1];
|
|
3997
|
+
if (!id) {
|
|
3998
|
+
errorln('Usage: construct profile set <id> [--yes] [--dry-run]');
|
|
3999
|
+
process.exit(1);
|
|
4000
|
+
}
|
|
4001
|
+
const target = loadProfile(id);
|
|
4002
|
+
if (!target) {
|
|
4003
|
+
errorln(`Unknown profile: ${id}. Available: ${listProfiles().join(', ')}`);
|
|
4004
|
+
process.exit(1);
|
|
4005
|
+
}
|
|
4006
|
+
const skipConfirm = args.includes('--yes') || args.includes('-y') || args.includes('--non-interactive');
|
|
4007
|
+
const dryRun = args.includes('--dry-run');
|
|
4008
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
4009
|
+
|
|
4010
|
+
const { findProjectConfigPath, loadProjectConfig, writeProjectConfig, PROJECT_CONFIG_FILENAME } = await import('../lib/config/project-config.mjs');
|
|
4011
|
+
const pathMod = await import('node:path');
|
|
4012
|
+
const found = findProjectConfigPath(process.cwd());
|
|
4013
|
+
const cfgPath = found || pathMod.join(process.cwd(), PROJECT_CONFIG_FILENAME);
|
|
4014
|
+
// loadProjectConfig returns a wrapper { path, raw, config, source, errors };
|
|
4015
|
+
// operate on the raw user-authored object so the write call sees the
|
|
4016
|
+
// version field that validateProjectConfig requires.
|
|
4017
|
+
const loaded = found ? loadProjectConfig(process.cwd()) : null;
|
|
4018
|
+
const cfg = loaded?.raw ? structuredClone(loaded.raw) : { version: 1 };
|
|
4019
|
+
if (!cfg.version) cfg.version = 1;
|
|
4020
|
+
|
|
4021
|
+
const currentId = cfg.profile || 'rnd';
|
|
4022
|
+
if (currentId === id) {
|
|
4023
|
+
println(`Profile already set to ${id} (${target.displayName}). Nothing to do.`);
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
const current = loadProfile(currentId) || { id: currentId, displayName: currentId, roles: [], intake: { types: [], stages: [] }, departments: [] };
|
|
4027
|
+
|
|
4028
|
+
// Render a structural diff so the operator sees what loop they are switching into.
|
|
4029
|
+
const setDiff = (label, before, after) => {
|
|
4030
|
+
const b = new Set(before || []);
|
|
4031
|
+
const a = new Set(after || []);
|
|
4032
|
+
const added = [...a].filter((x) => !b.has(x));
|
|
4033
|
+
const removed = [...b].filter((x) => !a.has(x));
|
|
4034
|
+
const unchanged = [...a].filter((x) => b.has(x));
|
|
4035
|
+
println(` ${label}:`);
|
|
4036
|
+
if (added.length) println(` + ${added.join(', ')}`);
|
|
4037
|
+
if (removed.length) println(` - ${removed.join(', ')}`);
|
|
4038
|
+
if (!added.length && !removed.length) println(` (no change, ${unchanged.length} entr${unchanged.length === 1 ? 'y' : 'ies'})`);
|
|
4039
|
+
};
|
|
4040
|
+
|
|
4041
|
+
println('');
|
|
4042
|
+
println(`About to switch active profile:`);
|
|
4043
|
+
println(` from: ${currentId} (${current.displayName})`);
|
|
4044
|
+
println(` to: ${id} (${target.displayName})`);
|
|
4045
|
+
println('');
|
|
4046
|
+
println(`Structural diff:`);
|
|
4047
|
+
setDiff('roles', current.roles, target.roles);
|
|
4048
|
+
setDiff('intake.types', current.intake?.types, target.intake?.types);
|
|
4049
|
+
setDiff('intake.stages', current.intake?.stages, target.intake?.stages);
|
|
4050
|
+
const cDept = (current.departments || []).map((d) => d.id);
|
|
4051
|
+
const tDept = (target.departments || []).map((d) => d.id);
|
|
4052
|
+
setDiff('departments', cDept, tDept);
|
|
4053
|
+
println('');
|
|
4054
|
+
println(`Files that will change:`);
|
|
4055
|
+
println(` ${pathMod.relative(process.cwd(), cfgPath)} (profile field: ${currentId} -> ${id})`);
|
|
4056
|
+
println('');
|
|
4057
|
+
println(`The change only writes the profile field. No specialists are added or removed.`);
|
|
4058
|
+
println(`Sync runs against the new profile on next \`construct sync\`.`);
|
|
4059
|
+
println('');
|
|
4060
|
+
|
|
4061
|
+
if (dryRun) {
|
|
4062
|
+
println(`[dry-run] No files were written.`);
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
if (!skipConfirm && isTTY) {
|
|
4066
|
+
const readline = await import('node:readline');
|
|
4067
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4068
|
+
const answer = await new Promise((res) => rl.question(`Switch profile to ${id}? (Y/n) `, (a) => res(a.trim().toLowerCase())));
|
|
4069
|
+
rl.close();
|
|
4070
|
+
if (answer && answer !== 'y' && answer !== 'yes') {
|
|
4071
|
+
println('Aborted. No config was changed.');
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
cfg.profile = id;
|
|
4077
|
+
writeProjectConfig(cfgPath, cfg, { validate: true });
|
|
4078
|
+
process.stdout.write(`✓ profile set to ${id} (${target.displayName})\n`);
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
if (sub === 'create') {
|
|
4082
|
+
const id = args[1];
|
|
4083
|
+
if (!id) {
|
|
4084
|
+
errorln('Usage: construct profile create <id> [--display="..."] [--role=<id>...] [--department=<id>:<displayName>...] [--yes]');
|
|
4085
|
+
errorln(' Run with no flags for interactive mode; pass --yes to skip the confirmation prompt.');
|
|
4086
|
+
process.exit(1);
|
|
4087
|
+
}
|
|
4088
|
+
if (!/^[a-z][a-z0-9-]{1,30}$/.test(id)) {
|
|
4089
|
+
errorln(`Invalid id "${id}". Must match ^[a-z][a-z0-9-]{1,30}$.`);
|
|
4090
|
+
process.exit(1);
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
const displayArg = args.find((a) => a.startsWith('--display='));
|
|
4094
|
+
const skipConfirm = args.includes('--yes') || args.includes('-y') || args.includes('--non-interactive');
|
|
4095
|
+
const dryRun = args.includes('--dry-run');
|
|
4096
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
4097
|
+
|
|
4098
|
+
let displayName = displayArg ? displayArg.split('=')[1] : null;
|
|
4099
|
+
let seedRoles = args
|
|
4100
|
+
.filter((a) => a.startsWith('--role='))
|
|
4101
|
+
.map((a) => a.slice('--role='.length))
|
|
4102
|
+
.filter(Boolean);
|
|
4103
|
+
let seedDepartments = args
|
|
4104
|
+
.filter((a) => a.startsWith('--department='))
|
|
4105
|
+
.map((a) => a.slice('--department='.length))
|
|
4106
|
+
.map((spec) => {
|
|
4107
|
+
const [deptId, deptDisplay] = spec.split(':');
|
|
4108
|
+
return { id: deptId, displayName: deptDisplay || deptId };
|
|
4109
|
+
});
|
|
4110
|
+
|
|
4111
|
+
// Interactive gather. Only prompt when stdin is a real terminal and the
|
|
4112
|
+
// operator did not pass enough flags to scaffold without input. A
|
|
4113
|
+
// non-TTY invocation with no flags falls through to an empty scaffold
|
|
4114
|
+
// so scripts and CI can still create drafts non-interactively.
|
|
4115
|
+
const needsInteractive = isTTY && !skipConfirm && (!displayName || (seedRoles.length === 0 && seedDepartments.length === 0));
|
|
4116
|
+
if (needsInteractive) {
|
|
4117
|
+
const readline = await import('node:readline');
|
|
4118
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4119
|
+
const ask = (q, def = '') => new Promise((res) => rl.question(`${q}${def ? ` [${def}]` : ''} `, (a) => res(a.trim() || def)));
|
|
4120
|
+
|
|
4121
|
+
println('');
|
|
4122
|
+
println(`Scaffolding a new profile draft. Three questions, then a preview.`);
|
|
4123
|
+
println(`Background: docs/concepts/persona-research.md and docs/concepts/profile-lifecycle.md`);
|
|
4124
|
+
println('');
|
|
4125
|
+
if (!displayName) displayName = await ask('Display name?', id);
|
|
4126
|
+
if (seedDepartments.length === 0) {
|
|
4127
|
+
const dInput = await ask('Departments (id:Name, id:Name, ...)?', '');
|
|
4128
|
+
if (dInput) seedDepartments = dInput.split(',').map((s) => s.trim()).filter(Boolean).map((spec) => {
|
|
4129
|
+
const [dId, dName] = spec.split(':').map((x) => x.trim());
|
|
4130
|
+
return { id: dId, displayName: dName || dId };
|
|
4131
|
+
});
|
|
4132
|
+
}
|
|
4133
|
+
if (seedRoles.length === 0) {
|
|
4134
|
+
const rInput = await ask('Roles (role-id, role-id, ...)?', '');
|
|
4135
|
+
if (rInput) seedRoles = rInput.split(',').map((s) => s.trim()).filter(Boolean);
|
|
4136
|
+
}
|
|
4137
|
+
rl.close();
|
|
4138
|
+
}
|
|
4139
|
+
if (!displayName) displayName = id;
|
|
4140
|
+
|
|
4141
|
+
const pathMod = await import('node:path');
|
|
4142
|
+
const draftRoot = pathMod.join(process.cwd(), '.cx', 'profiles', `draft-${id}`);
|
|
4143
|
+
|
|
4144
|
+
println('');
|
|
4145
|
+
println(`About to scaffold a draft profile:`);
|
|
4146
|
+
println(` id: ${id}`);
|
|
4147
|
+
println(` displayName: ${displayName}`);
|
|
4148
|
+
println(` departments: ${seedDepartments.length}${seedDepartments.length ? ` (${seedDepartments.map((d) => d.id).join(', ')})` : ''}`);
|
|
4149
|
+
println(` roles: ${seedRoles.length}${seedRoles.length ? ` (${seedRoles.join(', ')})` : ''}`);
|
|
4150
|
+
println('');
|
|
4151
|
+
println(`Files that will be written under ${pathMod.relative(process.cwd(), draftRoot)}/:`);
|
|
4152
|
+
println(` profile.json structural manifest`);
|
|
4153
|
+
println(` requirements.md research brief naming each cx-* specialist`);
|
|
4154
|
+
for (const r of seedRoles) println(` personas/${r}.md`.padEnd(40) + ` persona artifact (filled during discover)`);
|
|
4155
|
+
for (const d of seedDepartments) println(` departments/${d.id}.md`.padEnd(40) + ` department charter (filled during frame)`);
|
|
4156
|
+
println('');
|
|
4157
|
+
println(`Nothing else is touched. No config is changed, no active profile is switched.`);
|
|
4158
|
+
println(`To activate this profile after promotion: construct profile set ${id}`);
|
|
4159
|
+
println('');
|
|
4160
|
+
|
|
4161
|
+
if (dryRun) {
|
|
4162
|
+
println(`[dry-run] No files were written.`);
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
4165
|
+
if (!skipConfirm && isTTY) {
|
|
4166
|
+
const readline = await import('node:readline');
|
|
4167
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4168
|
+
const answer = await new Promise((res) => rl.question('Create draft? (Y/n) ', (a) => res(a.trim().toLowerCase())));
|
|
4169
|
+
rl.close();
|
|
4170
|
+
if (answer && answer !== 'y' && answer !== 'yes') {
|
|
4171
|
+
println('Aborted. Nothing was written.');
|
|
4172
|
+
return;
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
const { createDraftProfile } = await import('../lib/profiles/lifecycle.mjs');
|
|
4177
|
+
try {
|
|
4178
|
+
const result = createDraftProfile({ cwd: process.cwd(), id, displayName, seedRoles, seedDepartments });
|
|
4179
|
+
println('');
|
|
4180
|
+
println(`✓ draft created at ${pathMod.relative(process.cwd(), result.dir)}`);
|
|
4181
|
+
println('');
|
|
4182
|
+
println(`Next steps (in order, per docs/concepts/profile-lifecycle.md):`);
|
|
4183
|
+
println(` 1. Discover (cx-ux-researcher): fill personas/<role>.md from interviews + primary sources.`);
|
|
4184
|
+
println(` 2. Frame (cx-product-manager): fill departments/<dept>.md charters and intake taxonomy.`);
|
|
4185
|
+
println(` 3. Architect (cx-architect): reconcile role reuse vs new; populate departments[] in profile.json.`);
|
|
4186
|
+
println(` 4. Validate (cx-evaluator): run persona-eval and classifier-eval against representative signals.`);
|
|
4187
|
+
println('');
|
|
4188
|
+
println(`Inspect the brief: open ${pathMod.relative(process.cwd(), result.briefPath)}`);
|
|
4189
|
+
return;
|
|
4190
|
+
} catch (err) {
|
|
4191
|
+
errorln(err.message);
|
|
4192
|
+
process.exit(1);
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
if (sub === 'drafts') {
|
|
4196
|
+
const { listDrafts } = await import('../lib/profiles/lifecycle.mjs');
|
|
4197
|
+
const drafts = listDrafts(process.cwd());
|
|
4198
|
+
if (drafts.length === 0) { process.stdout.write(' (no drafts)\n'); return; }
|
|
4199
|
+
for (const d of drafts) {
|
|
4200
|
+
const flags = [d.hasBrief ? 'brief' : null, d.hasProfile ? 'profile' : null].filter(Boolean).join(', ');
|
|
4201
|
+
process.stdout.write(` ${d.id.padEnd(20)} ${flags}\n`);
|
|
4202
|
+
}
|
|
4203
|
+
return;
|
|
4204
|
+
}
|
|
4205
|
+
if (sub === 'archive') {
|
|
4206
|
+
const id = args[1];
|
|
4207
|
+
const reasonArg = args.find((a) => a.startsWith('--reason='));
|
|
4208
|
+
let reason = reasonArg ? reasonArg.split('=')[1] : '';
|
|
4209
|
+
if (!id) {
|
|
4210
|
+
errorln('Usage: construct profile archive <id> --reason="..." [--yes] [--dry-run]');
|
|
4211
|
+
process.exit(1);
|
|
4212
|
+
}
|
|
4213
|
+
const skipConfirm = args.includes('--yes') || args.includes('-y') || args.includes('--non-interactive');
|
|
4214
|
+
const dryRun = args.includes('--dry-run');
|
|
4215
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
4216
|
+
|
|
4217
|
+
const pathMod = await import('node:path');
|
|
4218
|
+
const fsMod = await import('node:fs');
|
|
4219
|
+
const target = loadProfile(id);
|
|
4220
|
+
if (!target) {
|
|
4221
|
+
errorln(`Unknown profile: ${id}. Curated catalog: ${listProfiles().join(', ')}`);
|
|
4222
|
+
process.exit(1);
|
|
4223
|
+
}
|
|
4224
|
+
const fileURL = await import('node:url');
|
|
4225
|
+
const moduleDir = pathMod.dirname(fileURL.fileURLToPath(import.meta.url));
|
|
4226
|
+
const repoRoot = pathMod.resolve(moduleDir, '..');
|
|
4227
|
+
const srcProfile = pathMod.join(repoRoot, 'profiles', `${id}.json`);
|
|
4228
|
+
const tableRel = typeof target?.intake?.classificationTable === 'string' ? target.intake.classificationTable : null;
|
|
4229
|
+
const srcTable = tableRel ? pathMod.join(repoRoot, tableRel) : null;
|
|
4230
|
+
const dstDir = pathMod.join(repoRoot, 'archive', 'profiles', id);
|
|
4231
|
+
|
|
4232
|
+
// Gather reason interactively if missing. The lifecycle module already
|
|
4233
|
+
// enforces a >= 8 char reason, but the operator should see why before
|
|
4234
|
+
// the call fails.
|
|
4235
|
+
if (!reason && isTTY && !skipConfirm) {
|
|
4236
|
+
const readline = await import('node:readline');
|
|
4237
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4238
|
+
reason = await new Promise((res) => rl.question('Archive reason (>= 8 chars): ', (a) => res(a.trim())));
|
|
4239
|
+
rl.close();
|
|
4240
|
+
}
|
|
4241
|
+
if (!reason || reason.trim().length < 8) {
|
|
4242
|
+
errorln('A substantive --reason (>= 8 chars) is required. Archiving is permanent in repo history; the reason becomes the archive note.');
|
|
4243
|
+
process.exit(1);
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4246
|
+
println('');
|
|
4247
|
+
println(`About to archive curated profile:`);
|
|
4248
|
+
println(` id: ${id}`);
|
|
4249
|
+
println(` displayName: ${target.displayName || id}`);
|
|
4250
|
+
println(` reason: ${reason}`);
|
|
4251
|
+
println('');
|
|
4252
|
+
println(`Files that will move:`);
|
|
4253
|
+
println(` ${pathMod.relative(process.cwd(), srcProfile)}`);
|
|
4254
|
+
println(` -> ${pathMod.relative(process.cwd(), pathMod.join(dstDir, `${id}.json`))}`);
|
|
4255
|
+
if (srcTable && fsMod.existsSync(srcTable)) {
|
|
4256
|
+
println(` ${pathMod.relative(process.cwd(), srcTable)}`);
|
|
4257
|
+
println(` -> ${pathMod.relative(process.cwd(), pathMod.join(dstDir, pathMod.basename(srcTable)))}`);
|
|
4258
|
+
}
|
|
4259
|
+
println(` ${pathMod.relative(process.cwd(), pathMod.join(dstDir, 'archive-note.md'))} (new)`);
|
|
4260
|
+
println('');
|
|
4261
|
+
println(`What stays:`);
|
|
4262
|
+
println(` Observations under .cx/observations/ are not touched.`);
|
|
4263
|
+
println(` Outcomes under .cx/outcomes/ are not touched.`);
|
|
4264
|
+
println(` Any project still configured with profile=${id} will fall back to rnd on next load.`);
|
|
4265
|
+
println('');
|
|
4266
|
+
|
|
4267
|
+
if (dryRun) {
|
|
4268
|
+
println(`[dry-run] No files were moved.`);
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
if (!skipConfirm && isTTY) {
|
|
4272
|
+
const readline = await import('node:readline');
|
|
4273
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4274
|
+
const answer = await new Promise((res) => rl.question(`Archive ${id}? (y/N) `, (a) => res(a.trim().toLowerCase())));
|
|
4275
|
+
rl.close();
|
|
4276
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
4277
|
+
println('Aborted. Nothing was archived.');
|
|
4278
|
+
return;
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
const { archiveProfile } = await import('../lib/profiles/lifecycle.mjs');
|
|
4283
|
+
try {
|
|
4284
|
+
const { archived } = archiveProfile({ id, reason });
|
|
4285
|
+
process.stdout.write(`✓ archived ${id} to ${pathMod.relative(process.cwd(), archived)}\n`);
|
|
4286
|
+
return;
|
|
4287
|
+
} catch (err) {
|
|
4288
|
+
errorln(err.message);
|
|
4289
|
+
process.exit(1);
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
if (sub === 'health') {
|
|
4293
|
+
const id = args[1];
|
|
4294
|
+
if (!id) { errorln('Usage: construct profile health <id> [--days=N]'); process.exit(1); }
|
|
4295
|
+
const daysArg = args.find((a) => a.startsWith('--days='));
|
|
4296
|
+
const windowDays = daysArg ? Number(daysArg.split('=')[1]) || 30 : 30;
|
|
4297
|
+
const { profileHealth } = await import('../lib/profiles/lifecycle.mjs');
|
|
4298
|
+
const report = profileHealth(process.cwd(), id, { windowDays });
|
|
4299
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
4300
|
+
return;
|
|
4301
|
+
}
|
|
4302
|
+
errorln(`Unknown profile subcommand: ${sub}. Available: list, show, set, create, drafts, archive, health`);
|
|
3837
4303
|
process.exit(1);
|
|
3838
4304
|
}],
|
|
3839
4305
|
]);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
commands/build/feature.md
|
|
2
|
+
commands/build/feature.md (Build a feature) implement it end to end, tested and ready to ship
|
|
3
3
|
|
|
4
|
-
Build a feature
|
|
4
|
+
Build a feature: implement it end to end, tested and ready to ship
|
|
5
5
|
-->
|
|
6
6
|
---
|
|
7
|
-
description: Build a feature
|
|
7
|
+
description: Build a feature: implement it end to end, tested and ready to ship
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
You are Construct. Build the following: $ARGUMENTS
|
|
@@ -18,4 +18,4 @@ produces no output or tool activity within two minutes, surface it to the user w
|
|
|
18
18
|
agent description and elapsed time rather than waiting silently. Do not re-dispatch until
|
|
19
19
|
the user confirms the prior invocation is not still running.
|
|
20
20
|
|
|
21
|
-
Verification protocol is owned by `cx-engineer
|
|
21
|
+
Verification protocol is owned by `cx-engineer`: apply its checklist before declaring done.
|
package/commands/build/fix.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
commands/build/fix.md
|
|
2
|
+
commands/build/fix.md (Fix something broken) reproduce, find the root cause, apply the smallest safe change
|
|
3
3
|
|
|
4
|
-
Fix something broken
|
|
4
|
+
Fix something broken: reproduce, find the root cause, apply the smallest safe change
|
|
5
5
|
-->
|
|
6
6
|
---
|
|
7
|
-
description: Fix something broken
|
|
7
|
+
description: Fix something broken: reproduce, find the root cause, apply the smallest safe change
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
You are Construct.
|
|
11
11
|
|
|
12
12
|
Debugging protocol for: $ARGUMENTS
|
|
13
13
|
|
|
14
|
-
1. Reproduce
|
|
15
|
-
2. Isolate
|
|
16
|
-
3. Trace
|
|
17
|
-
4. Root cause
|
|
18
|
-
5. Fix
|
|
14
|
+
1. Reproduce: confirm the failure is real and consistent
|
|
15
|
+
2. Isolate: narrow to the smallest failing case
|
|
16
|
+
3. Trace: follow execution to where the invariant breaks
|
|
17
|
+
4. Root cause: the one upstream cause that prevents the failure when fixed
|
|
18
|
+
5. Fix: the smallest safe change that restores the invariant
|
|
19
19
|
|
|
20
20
|
Do not propose a fix until root cause is confirmed. After 3 failed attempts, stop and escalate.
|