karajan-code 4.70.4 → 5.0.1
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 +2 -2
- package/package.json +6 -14
- package/packages/hu-board/package.json +1 -1
- package/packages/hu-board/public/app.js +4 -18
- package/packages/hu-board/public/index.html +5 -2
- package/packages/hu-board/public/styles.css +26 -0
- package/packages/hu-board/public/utils/api.js +0 -74
- package/packages/hu-board/public/utils/board-tour.js +62 -0
- package/packages/hu-board/public/utils/board-view.js +49 -144
- package/packages/hu-board/public/utils/command-launcher.js +8 -70
- package/packages/hu-board/public/utils/formatters.js +10 -0
- package/packages/hu-board/public/utils/governance-view.js +13 -1
- package/packages/hu-board/public/utils/hu-actions.js +17 -68
- package/packages/hu-board/public/utils/init-listeners.js +7 -8
- package/packages/hu-board/public/utils/log-panel.js +3 -16
- package/packages/hu-board/public/utils/maggle-mode.js +2 -12
- package/packages/hu-board/public/utils/modals.js +7 -0
- package/packages/hu-board/public/utils/project-picker-view.js +1 -1
- package/packages/hu-board/public/utils/server-push.js +3 -22
- package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
- package/packages/hu-board/public/utils/story-detail-view.js +5 -19
- package/packages/hu-board/src/cleanup-zombies.js +0 -1
- package/packages/hu-board/src/command-runner.js +36 -25
- package/packages/hu-board/src/db-connection.js +282 -0
- package/packages/hu-board/src/db-paths.js +113 -0
- package/packages/hu-board/src/db-queries.js +188 -0
- package/packages/hu-board/src/db-tombstones.js +51 -0
- package/packages/hu-board/src/db.js +28 -619
- package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
- package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
- package/packages/hu-board/src/plan-mutations.js +3 -153
- package/packages/hu-board/src/plans-service.js +55 -0
- package/packages/hu-board/src/routes/api/commands.js +122 -0
- package/packages/hu-board/src/routes/api/plans.js +55 -0
- package/packages/hu-board/src/routes/api/projects.js +267 -0
- package/packages/hu-board/src/routes/api/prompts.js +135 -0
- package/packages/hu-board/src/routes/api/sessions.js +95 -0
- package/packages/hu-board/src/routes/api/stories.js +183 -0
- package/packages/hu-board/src/routes/api/system.js +241 -0
- package/packages/hu-board/src/routes/api/tombstones.js +38 -0
- package/packages/hu-board/src/routes/api.js +24 -1371
- package/packages/hu-board/src/routes/governance.js +23 -1
- package/packages/hu-board/src/routes/lanes.js +40 -0
- package/packages/hu-board/src/routes/rag.js +4 -3
- package/packages/hu-board/src/server.js +20 -34
- package/packages/hu-board/src/sync-files.js +318 -0
- package/packages/hu-board/src/sync-project.js +99 -0
- package/packages/hu-board/src/sync.js +22 -566
- package/packages/hu-board/src/zombie-reaper.js +1 -1
- package/scripts/dual-publish.mjs +19 -3
- package/scripts/postinstall.js +15 -2
- package/scripts/verify-pack-mcp.mjs +11 -7
- package/scripts/verify-pack.mjs +4 -4
- package/src/audit/audit-history.js +3 -2
- package/src/audit/dead-exports.js +68 -59
- package/src/audit/deterministic-summary.js +33 -13
- package/src/audit/npm-signatures.js +41 -0
- package/src/audit/sbom.js +35 -0
- package/src/checks/agent-token.js +112 -0
- package/src/checks/dir-setup.js +34 -1
- package/src/checks/native-build.js +21 -39
- package/src/checks/runner.js +128 -142
- package/src/cli/advanced-commands.js +6 -6
- package/src/cli/register-meta.js +42 -82
- package/src/cli/register-pipeline.js +8 -143
- package/src/cli/register-plan.js +9 -100
- package/src/cli/register-roles-skills.js +1 -1
- package/src/cli/retired-commands.js +49 -0
- package/src/cli.js +18 -5
- package/src/commands/agent-run.js +40 -26
- package/src/commands/audit.js +127 -124
- package/src/commands/board.js +5 -9
- package/src/commands/check.js +29 -16
- package/src/commands/doctor.js +2 -0
- package/src/commands/env.js +176 -150
- package/src/commands/go.js +71 -53
- package/src/commands/harden.js +90 -81
- package/src/commands/hu-start.js +52 -0
- package/src/commands/hu.js +288 -221
- package/src/commands/identity.js +111 -78
- package/src/commands/init-atril.js +47 -0
- package/src/commands/init.js +11 -40
- package/src/commands/install-tools.js +70 -85
- package/src/commands/main-guard.js +78 -0
- package/src/commands/policy.js +226 -184
- package/src/commands/qmd.js +27 -21
- package/src/commands/rag.js +121 -55
- package/src/commands/review-gate.js +421 -341
- package/src/commands/skills.js +4 -1
- package/src/commands/start.js +4 -5
- package/src/commands/steward.js +96 -47
- package/src/commands/tournament.js +46 -37
- package/src/commands/worktree.js +107 -26
- package/src/config/defaults.js +1 -0
- package/src/config/loader.js +34 -17
- package/src/config/role-resolver.js +1 -0
- package/src/config/schema.js +1 -0
- package/src/config/secrets.js +36 -0
- package/src/config.js +1 -0
- package/src/environment/adr.js +16 -2
- package/src/git/karajan-label.js +20 -8
- package/src/guards/main-guard.js +54 -0
- package/src/harden/hook-templates.js +16 -1
- package/src/harden/sentinel/posttooluse.mjs +18 -6
- package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
- package/src/harden/sentinel/sentinel-lib.mjs +129 -26
- package/src/harden/sentinel/sentinel-shell.mjs +5 -16
- package/src/harden/sentinel/sessionstart.mjs +3 -2
- package/src/harden/sentinel/stop.mjs +5 -4
- package/src/harden/workflow-templates.js +1 -1
- package/src/hu/board-lookup.js +29 -0
- package/src/hu/gh-identity.js +23 -0
- package/src/hu/github-board.js +4 -2
- package/src/hu/lane-prune.js +83 -13
- package/src/hu/lane-setup.js +65 -0
- package/src/hu/lanes.js +66 -0
- package/src/hu/team-board-mirror.js +3 -2
- package/src/infrastructure/command-runner.js +1 -1
- package/src/infrastructure/file-system-service.js +1 -1
- package/src/mcp/handlers/direct-handlers.js +2 -292
- package/src/mcp/handlers/hu-handlers.js +1 -35
- package/src/mcp/handlers/management-handlers.js +2 -42
- package/src/mcp/handlers/rag-handler.js +9 -1
- package/src/mcp/server-handlers.js +16 -22
- package/src/mcp/tools.js +1 -196
- package/src/plan/lane-plans.js +54 -0
- package/src/plan/plan-store.js +7 -4
- package/src/policy/add.js +26 -12
- package/src/policy/adr-number-check.js +31 -0
- package/src/prompts/audit.js +299 -339
- package/src/prompts/card-context.js +10 -5
- package/src/prompts/reviewer.js +5 -2
- package/src/rag/auto-update.js +71 -44
- package/src/rag/bootstrap-index.js +46 -54
- package/src/rag/indexer.js +9 -4
- package/src/rag/migrate.js +1 -1
- package/src/rag/onnx-fallback.js +2 -2
- package/src/rag/pid-lease.js +181 -0
- package/src/rag/project-store.js +2 -12
- package/src/review/card-first.js +57 -46
- package/src/review/gate-gitignore.js +23 -4
- package/src/review/gate-hook.js +12 -4
- package/src/review/one-shot-review.js +45 -35
- package/src/review/publish-pr.js +14 -2
- package/src/review/repo-docs.js +73 -0
- package/src/review/repo-stack.js +59 -0
- package/src/review/reviewer-fallback.js +19 -4
- package/src/review/seal-commit.js +29 -0
- package/src/review/sonar-pregate.js +59 -44
- package/src/review/third-party-review.js +90 -16
- package/src/review/third-party-size.js +43 -0
- package/src/review/third-party-skills.js +54 -0
- package/src/review/verify-pr.js +1 -24
- package/src/roles/audit-role.js +106 -125
- package/src/skills/atril-cli.js +19 -1
- package/src/skills/atril-skills.js +5 -4
- package/src/skills/skills-search.js +72 -0
- package/src/skills/translate.js +62 -0
- package/src/sonar/scanner.js +116 -101
- package/src/steward/invariants.js +5 -4
- package/src/steward/phantom-sweep.js +66 -0
- package/src/tournament/judge.js +60 -53
- package/src/utils/gh-account.js +37 -0
- package/src/utils/git.js +0 -13
- package/src/utils/home-migration.js +7 -1
- package/src/utils/main-checkout.js +53 -0
- package/src/utils/qmd-collection.js +3 -1
- package/src/utils/welcome.js +3 -3
- package/templates/{skills → atril/commands}/kj-architect.md +5 -0
- package/templates/{skills → atril/commands}/kj-audit.md +5 -0
- package/templates/{skills → atril/commands}/kj-board.md +5 -0
- package/templates/{skills → atril/commands}/kj-code.md +5 -0
- package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
- package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
- package/templates/{skills → atril/commands}/kj-discover.md +5 -0
- package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
- package/templates/{skills → atril/commands}/kj-review.md +5 -0
- package/templates/{skills → atril/commands}/kj-security.md +5 -0
- package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
- package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
- package/templates/{skills → atril/commands}/kj-test.md +5 -0
- package/packages/hu-board/public/utils/run-launcher.js +0 -169
- package/packages/hu-board/src/run-tracker.js +0 -121
- package/src/activity-log.js +0 -59
- package/src/agents/host-agent.js +0 -38
- package/src/agents/model-registry.js +0 -142
- package/src/audit/plan-adherence.js +0 -224
- package/src/autonomy/arbiter-role.js +0 -71
- package/src/autonomy/arbiter.js +0 -22
- package/src/autonomy/decider.js +0 -18
- package/src/autonomy/decision-resolver.js +0 -58
- package/src/autonomy/outcome-report.js +0 -55
- package/src/autonomy/stage-ask.js +0 -30
- package/src/brain/decision-tracker.js +0 -125
- package/src/brain/decisor.js +0 -243
- package/src/brain/solomon-consult.js +0 -171
- package/src/brain/standby-scheduler.js +0 -8
- package/src/budget/comparison.js +0 -96
- package/src/canvas/parse-md.js +0 -238
- package/src/canvas/schema.js +0 -101
- package/src/canvas/to-flat.js +0 -85
- package/src/canvas/validate.js +0 -163
- package/src/ci/dispatch.js +0 -99
- package/src/ci/index.js +0 -3
- package/src/ci/pr-diff.js +0 -26
- package/src/cli/register-standby.js +0 -26
- package/src/commands/architect.js +0 -111
- package/src/commands/autorun.js +0 -49
- package/src/commands/researcher.js +0 -54
- package/src/commands/resume.js +0 -72
- package/src/commands/review.js +0 -89
- package/src/commands/run.js +0 -185
- package/src/commands/standby.js +0 -117
- package/src/commands/status.js +0 -36
- package/src/commands/triage.js +0 -78
- package/src/commands/undo.js +0 -62
- package/src/domains/domain-loader.js +0 -245
- package/src/domains/domain-registry.js +0 -140
- package/src/domains/domain-synthesizer.js +0 -129
- package/src/git/automation.js +0 -433
- package/src/git/coder-commit-guard.js +0 -40
- package/src/git/hu-automation.js +0 -243
- package/src/git/hu-snapshot.js +0 -10
- package/src/git/symlink-targets.js +0 -57
- package/src/golden/asserter.js +0 -92
- package/src/golden/loader.js +0 -52
- package/src/golden/runner.js +0 -92
- package/src/golden/schema.js +0 -41
- package/src/guards/duplicate-members.js +0 -86
- package/src/guards/intent-guard.js +0 -161
- package/src/guards/perf-guard.js +0 -126
- package/src/guards/policy-guard.js +0 -90
- package/src/guards/policy-resolver.js +0 -85
- package/src/guards/secret-redactor.js +0 -71
- package/src/hu/acceptance-runner.js +0 -138
- package/src/hu/lazy-planner.js +0 -109
- package/src/hu/parallel-executor.js +0 -94
- package/src/hu/routing-metrics.js +0 -17
- package/src/hu/split-validator.js +0 -143
- package/src/hu/splitting-detector.js +0 -158
- package/src/hu/splitting-generator.js +0 -175
- package/src/hu/store.js +0 -394
- package/src/infrastructure/mocks.js +0 -182
- package/src/mcp/handlers/run-handler.js +0 -318
- package/src/mcp/sovereignty-guard.js +0 -113
- package/src/mcp/tool-arg-normalizers.js +0 -16
- package/src/mutate/reviewer-signal.js +0 -76
- package/src/orchestrator/agent-fallback.js +0 -81
- package/src/orchestrator/brain-coordinator.js +0 -186
- package/src/orchestrator/ci-integration.js +0 -139
- package/src/orchestrator/config-init.js +0 -426
- package/src/orchestrator/direct-actions.js +0 -217
- package/src/orchestrator/drivers/error-recovery.js +0 -287
- package/src/orchestrator/drivers/init-context.js +0 -238
- package/src/orchestrator/drivers/iteration-loop.js +0 -382
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
- package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
- package/src/orchestrator/drivers/post-loop.js +0 -404
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
- package/src/orchestrator/drivers/pre-loop.js +0 -524
- package/src/orchestrator/drivers/run-hu-batch.js +0 -468
- package/src/orchestrator/feedback-enrichment.js +0 -140
- package/src/orchestrator/feedback-queue.js +0 -151
- package/src/orchestrator/flow-control.js +0 -170
- package/src/orchestrator/flow-runner.js +0 -365
- package/src/orchestrator/fs-leak-detector.js +0 -228
- package/src/orchestrator/hu-scheduler.js +0 -114
- package/src/orchestrator/hu-sub-pipeline.js +0 -642
- package/src/orchestrator/iteration-gate.js +0 -73
- package/src/orchestrator/iteration-stages.js +0 -9
- package/src/orchestrator/parallel-limiter.js +0 -74
- package/src/orchestrator/pipeline-context.js +0 -72
- package/src/orchestrator/post-loop-stages.js +0 -468
- package/src/orchestrator/pre-loop-stages.js +0 -11
- package/src/orchestrator/preflight-checks.js +0 -487
- package/src/orchestrator/repair/repair-detector.js +0 -117
- package/src/orchestrator/repair/repair-runner.js +0 -142
- package/src/orchestrator/reviewer-fallback.js +0 -45
- package/src/orchestrator/role-output-compressor.js +0 -189
- package/src/orchestrator/session-journal.js +0 -308
- package/src/orchestrator/solomon-escalation.js +0 -331
- package/src/orchestrator/solomon-rules.js +0 -212
- package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
- package/src/orchestrator/stages/architect-stage.js +0 -172
- package/src/orchestrator/stages/coder-stage.js +0 -506
- package/src/orchestrator/stages/domain-curator-stage.js +0 -82
- package/src/orchestrator/stages/format-stage.js +0 -61
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
- package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
- package/src/orchestrator/stages/perf-stage.js +0 -140
- package/src/orchestrator/stages/planner-stage.js +0 -85
- package/src/orchestrator/stages/rag-context-stage.js +0 -80
- package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
- package/src/orchestrator/stages/research-stage.js +0 -69
- package/src/orchestrator/stages/reviewer-stage.js +0 -431
- package/src/orchestrator/stages/sonar-stage.js +0 -439
- package/src/orchestrator/stages/stage-classes.js +0 -84
- package/src/orchestrator/stages/stage-executor.js +0 -155
- package/src/orchestrator/stages/suite-green-stage.js +0 -62
- package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
- package/src/orchestrator/stages/tool-judge-stage.js +0 -53
- package/src/orchestrator/stages/triage-stage.js +0 -172
- package/src/orchestrator/standby.js +0 -71
- package/src/orchestrator/verification-gate.js +0 -183
- package/src/orchestrator.js +0 -22
- package/src/plan/adr-generator.js +0 -170
- package/src/plan/adr-loader.js +0 -60
- package/src/plan/plan-executor.js +0 -100
- package/src/plan/plan-validation.js +0 -4
- package/src/prompts/acceptance-coverage.js +0 -32
- package/src/prompts/arbiter.js +0 -38
- package/src/prompts/architect.js +0 -103
- package/src/prompts/hu-reviewer.js +0 -244
- package/src/prompts/tool-correctness-judge.js +0 -31
- package/src/prompts/triage.js +0 -57
- package/src/rag/embedder.js +0 -2
- package/src/rag/embedders/_cloud-base.js +0 -2
- package/src/rag/embedders/cohere.js +0 -2
- package/src/rag/embedders/mistral.js +0 -2
- package/src/rag/embedders/onnx.js +0 -2
- package/src/rag/embedders/openai.js +0 -2
- package/src/rag/embedders/voyage.js +0 -2
- package/src/rag/rerank.js +0 -4
- package/src/rag/where-parser.js +0 -4
- package/src/repeat-detector.js +0 -88
- package/src/review/diff-generator.js +0 -112
- package/src/review/git-stash-helper.js +0 -104
- package/src/review/schema.js +0 -31
- package/src/review/scope-filter.js +0 -152
- package/src/review/snapshot-diff.js +0 -122
- package/src/review/tdd-discipline.js +0 -90
- package/src/review/tdd-policy.js +0 -69
- package/src/roles/acceptance-coverage-role.js +0 -78
- package/src/roles/architect-role.js +0 -77
- package/src/roles/coder-role.js +0 -113
- package/src/roles/commiter-role.js +0 -113
- package/src/roles/domain-curator-role.js +0 -134
- package/src/roles/hu-reviewer-role.js +0 -64
- package/src/roles/impeccable-role.js +0 -85
- package/src/roles/index.js +0 -18
- package/src/roles/karajan-brain-role.js +0 -134
- package/src/roles/perf-role.js +0 -122
- package/src/roles/planner-role.js +0 -151
- package/src/roles/refactorer-role.js +0 -30
- package/src/roles/repairer-role.js +0 -181
- package/src/roles/researcher-role.js +0 -70
- package/src/roles/reviewer-role.js +0 -115
- package/src/roles/security-role.js +0 -73
- package/src/roles/solomon-role.js +0 -271
- package/src/roles/sonar-role.js +0 -72
- package/src/roles/tester-role.js +0 -235
- package/src/roles/tool-correctness-judge-role.js +0 -78
- package/src/roles/triage-role.js +0 -99
- package/src/session/journal/decisions-writer.js +0 -200
- package/src/session/journal/iteration-logger.js +0 -194
- package/src/session/journal/summary-writer.js +0 -320
- package/src/session/journal/tree-writer.js +0 -164
- package/src/session/journal/write-all.js +0 -89
- package/src/session-cleanup.js +0 -116
- package/src/skills/semantic-detector.js +0 -167
- package/src/skills/skill-detector.js +0 -385
- package/src/sonar/cloud-scanner.js +0 -78
- package/src/tracker-bootstrap.js +0 -44
- package/src/types/index.js +0 -38
- package/src/types/policy.js +0 -43
- package/src/types/solomon.js +0 -87
- package/src/utils/async-lock.js +0 -31
- package/src/utils/budget.js +0 -229
- package/src/utils/cwd-confirm.js +0 -39
- package/src/utils/display/event-handlers.js +0 -403
- package/src/utils/display/formatters.js +0 -132
- package/src/utils/display/header.js +0 -47
- package/src/utils/display/session-summary.js +0 -118
- package/src/utils/display/solomon.js +0 -42
- package/src/utils/pricing.js +0 -29
- package/src/utils/quota-simulator.js +0 -56
- package/src/utils/role-assigner.js +0 -137
- package/src/utils/rtk-wrapper.js +0 -87
- package/src/utils/run-registry.js +0 -9
- package/src/utils/status-dashboard.js +0 -254
- package/src/utils/tool-call-extractor.js +0 -56
- package/src/webperf/devtools-detect.js +0 -112
- package/src/webperf/webperf-role-integration.js +0 -66
- package/templates/skills/kj-run.md +0 -73
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Single source for every sentinel script: state, branch, classification,
|
|
3
3
|
// and violations.
|
|
4
4
|
import process from "node:process";
|
|
5
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { execSync, spawnSync } from "node:child_process";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
7
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
8
8
|
import { basename, dirname, join } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -35,24 +35,124 @@ export const session = (state, sid) => {
|
|
|
35
35
|
state.sessions ||= {};
|
|
36
36
|
return (state.sessions[sid] ||= { edited_sources: [], edited_tests: [], escapes: [], errors: [], blocks: 0 });
|
|
37
37
|
};
|
|
38
|
-
export const branchOf = () => {
|
|
39
|
-
try { return execSync("git rev-parse --abbrev-ref HEAD", { cwd: ROOT, stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); }
|
|
40
|
-
catch { return null; }
|
|
41
|
-
};
|
|
42
38
|
const _git = (args, cwd) => {
|
|
43
39
|
try { return execSync("git " + args, { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); } catch { return ""; }
|
|
44
40
|
};
|
|
41
|
+
// MONO-0 (ADR 0002, amended by #2566): lane identity = worktree toplevel; repo
|
|
42
|
+
// identity = git-common-dir. The session's lane is where the HOST says it works
|
|
43
|
+
// (the hook payload's cwd, which EnterWorktree moves) when that is a worktree of
|
|
44
|
+
// this same repo; otherwise the dir the guard is installed in. ROOT keeps the
|
|
45
|
+
// harness (state, config); laneTop() is the git tree the session works in.
|
|
46
|
+
// ADR 0033 (KJC-TSK-1236): a session switches into a lane only when no other live session
|
|
47
|
+
// is there. Each session keeps one presence mark (its lane and when it last resolved it) in
|
|
48
|
+
// the repo's git common dir. Entering is intent first, then a look: the session writes the
|
|
49
|
+
// new lane, then reads the others' marks and steps back if another live one is there. Two
|
|
50
|
+
// sessions entering together never both stay: the later write is seen by the later look.
|
|
51
|
+
const LANE_TTL_MS = 30 * 60 * 1000;
|
|
52
|
+
let _cwd = null;
|
|
53
|
+
let _sid = null;
|
|
45
54
|
let _home;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
let _refusal = null;
|
|
56
|
+
const homeOf = (dir) => {
|
|
57
|
+
const [top = "", common = ""] = _git("rev-parse --show-toplevel --path-format=absolute --git-common-dir", dir).split("\n");
|
|
58
|
+
return { top, common };
|
|
59
|
+
};
|
|
60
|
+
export const enterLane = (_state, sid, cwd) => {
|
|
61
|
+
_cwd = cwd;
|
|
62
|
+
_sid = sid;
|
|
63
|
+
_home = undefined;
|
|
64
|
+
_refusal = null;
|
|
65
|
+
};
|
|
66
|
+
/** Why the session was kept out of the lane its cwd is in, or null. */
|
|
67
|
+
export const laneRefusal = () => _refusal;
|
|
68
|
+
const laneKey = (s) => createHash("sha256").update(String(s)).digest("hex").slice(0, 16);
|
|
69
|
+
const readJson = (f) => { try { return JSON.parse(readFileSync(f, "utf8")); } catch { return null; } };
|
|
70
|
+
const live = (at, now) => typeof at === "number" && now - at < LANE_TTL_MS;
|
|
71
|
+
// ADR 0034: a mark also lists the lanes the session wrote in (`held`); another session holds
|
|
72
|
+
// a lane when it is in it or wrote there within the period. Another session's mark that cannot
|
|
73
|
+
// be read or parsed throws: the caller refuses rather than read it as a free lane.
|
|
74
|
+
const heldByOther = (dir, top, now) => readdirSync(dir).filter((f) => f.startsWith("session-"))
|
|
75
|
+
.map((f) => JSON.parse(readFileSync(join(dir, f), "utf8")))
|
|
76
|
+
.some((r) => r?.sid !== _sid && ((r?.lane === top && live(r.at, now)) || live(r?.held?.[top], now)));
|
|
77
|
+
const liveHeld = (mark, now) => Object.fromEntries(Object.entries(mark?.held ?? {}).filter(([, at]) => live(at, now)));
|
|
78
|
+
// The lane the session may take: `lane` when it is home, already its live lane, or free.
|
|
79
|
+
const claimLane = (root, lane, now = Date.now()) => {
|
|
80
|
+
if (!_sid || !root.common) return lane;
|
|
81
|
+
const dir = join(root.common, "kj-lanes");
|
|
82
|
+
const mark = join(dir, "session-" + laneKey(_sid) + ".json");
|
|
83
|
+
const write = (top) => writeFileSync(mark, JSON.stringify({ sid: _sid, lane: top, at: now, held: liveHeld(readJson(mark), now) }));
|
|
84
|
+
try {
|
|
85
|
+
mkdirSync(dir, { recursive: true });
|
|
86
|
+
const prev = readJson(mark);
|
|
87
|
+
if (lane.top !== root.top && !(prev?.lane === lane.top && live(prev.at, now))) {
|
|
88
|
+
write(lane.top);
|
|
89
|
+
if (heldByOther(dir, lane.top, now)) {
|
|
90
|
+
_refusal = "(" + lane.top + " tiene otra sesión viva: un carril, una sesión, ADR 0033)";
|
|
91
|
+
lane = root;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
write(lane.top);
|
|
95
|
+
} catch {
|
|
96
|
+
if (lane.top !== root.top) {
|
|
97
|
+
_refusal = "(no se pudo reclamar " + lane.top + ")";
|
|
98
|
+
lane = root;
|
|
99
|
+
// Best effort: a mark left in the refused lane would keep others out for 30 min.
|
|
100
|
+
try { write(root.top); } catch { /* the session stays home either way */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return lane;
|
|
104
|
+
};
|
|
105
|
+
// Resolved on first use: a hook that never asks git pays no spawn.
|
|
106
|
+
const home = () => {
|
|
107
|
+
if (_home) return _home;
|
|
108
|
+
const root = homeOf(ROOT);
|
|
109
|
+
const here = _cwd ? homeOf(_cwd) : null;
|
|
110
|
+
// git that cannot answer ("" common dir) never matches: fail closed to the install lane.
|
|
111
|
+
return (_home = claimLane(root, here?.top && root.common && here.common === root.common ? here : root));
|
|
112
|
+
};
|
|
113
|
+
export const laneTop = () => home().top || ROOT;
|
|
114
|
+
/**
|
|
115
|
+
* ADR 0034 (KJC-TSK-1244): why the session may not write in lane `top`, or null once it holds it.
|
|
116
|
+
* Intent first, then a look, as a switch does (ADR 0033): the hold is written, then another
|
|
117
|
+
* live session in that lane takes it back out. Anything unreadable refuses.
|
|
118
|
+
*/
|
|
119
|
+
export const holdLane = (top, now = Date.now()) => {
|
|
120
|
+
const own = home();
|
|
121
|
+
if (top === laneTop()) return null;
|
|
122
|
+
if (!_sid || !own.common) return "(sin sesión identificada no se reclama " + top + ")";
|
|
123
|
+
const dir = join(own.common, "kj-lanes");
|
|
124
|
+
const mark = join(dir, "session-" + laneKey(_sid) + ".json");
|
|
125
|
+
try {
|
|
126
|
+
mkdirSync(dir, { recursive: true });
|
|
127
|
+
const mine = { sid: _sid, lane: own.top, at: now, ...readJson(mark) };
|
|
128
|
+
const held = liveHeld(mine, now);
|
|
129
|
+
writeFileSync(mark, JSON.stringify({ ...mine, held: { ...held, [top]: now } }));
|
|
130
|
+
if (!heldByOther(dir, top, now)) return null;
|
|
131
|
+
writeFileSync(mark, JSON.stringify({ ...mine, held }));
|
|
132
|
+
return "(" + top + " tiene otra sesión viva: un carril, una sesión, ADR 0033/0034)";
|
|
133
|
+
} catch {
|
|
134
|
+
return "(no se pudo reclamar " + top + ")";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
export const branchOf = (top = laneTop()) => {
|
|
138
|
+
try { return execSync("git rev-parse --abbrev-ref HEAD", { cwd: top, stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); }
|
|
139
|
+
catch { return null; }
|
|
140
|
+
};
|
|
141
|
+
// ADR 0034 (KJC-TSK-1243): the lane a write lands in is the worktree holding its path when
|
|
142
|
+
// that worktree is of this repo; any other path is judged in the session's lane. It walks up
|
|
143
|
+
// to the first existing dir (a subdir not created yet escaped) and canonicalizes it (a local
|
|
144
|
+
// symlink into another lane was classified by its literal path).
|
|
145
|
+
export const laneFor = (p) => {
|
|
146
|
+
let d = String(p);
|
|
147
|
+
while (d && !(existsSync(d) && statSync(d).isDirectory())) {
|
|
148
|
+
const up = dirname(d);
|
|
149
|
+
if (up === d) return laneTop();
|
|
150
|
+
d = up;
|
|
151
|
+
}
|
|
152
|
+
try { d = realpathSync(d); } catch { /* classified as is */ }
|
|
153
|
+
const target = homeOf(d);
|
|
154
|
+
const own = home();
|
|
155
|
+
return target.top && own.top && target.common && target.common === own.common ? target.top : laneTop();
|
|
56
156
|
};
|
|
57
157
|
// KJC-TSK-0765 board-sync: cards merged in this session but not yet moved in
|
|
58
158
|
// the tracker. The method does not advance until the board is true.
|
|
@@ -72,9 +172,8 @@ export const bootstrapPhase = () => {
|
|
|
72
172
|
// state: approved | rejected | missing | empty (nothing staged) | unknown (git failed).
|
|
73
173
|
export const stagedVerdict = (dir) => {
|
|
74
174
|
const git = (args) => spawnSync("git", ["-C", dir, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
75
|
-
// No commit yet (or no repo): no committed marker
|
|
175
|
+
// No commit yet (or no repo): no committed marker exists. A git error fails closed.
|
|
76
176
|
if (git(["rev-parse", "--verify", "-q", "HEAD"]).status !== 0) return { gate: false };
|
|
77
|
-
// A git error is not a missing marker: it fails closed.
|
|
78
177
|
const marker = git(["ls-tree", "--name-only", "HEAD", ".karajan/review-gate"]);
|
|
79
178
|
if (marker.status !== 0) return { gate: true, state: "unknown" };
|
|
80
179
|
if (!marker.stdout.trim()) return { gate: false };
|
|
@@ -104,31 +203,35 @@ export const boardGate = (sid, pend) => {
|
|
|
104
203
|
// files dirty, or local commits the remote does not have), not the ledger: after a
|
|
105
204
|
// merged PR and a sync of main the ledger stays full while the base holds nothing.
|
|
106
205
|
// Anything git cannot answer (no upstream, odd path) counts as work: fail closed.
|
|
107
|
-
const baseHoldsWork = (files) => {
|
|
206
|
+
const baseHoldsWork = (files, top) => {
|
|
108
207
|
if (files.some((f) => !/^[A-Za-z0-9._/-]+$/.test(f))) return true;
|
|
109
208
|
try {
|
|
110
|
-
if (execSync("git status --porcelain -- " + files.join(" "), { cwd:
|
|
111
|
-
return execSync("git rev-list @{u}..HEAD", { cwd:
|
|
209
|
+
if (execSync("git status --porcelain -- " + files.join(" "), { cwd: top, stdio: ["ignore", "pipe", "ignore"] }).toString().trim()) return true;
|
|
210
|
+
return execSync("git rev-list @{u}..HEAD", { cwd: top, stdio: ["ignore", "pipe", "ignore"] }).toString().trim() !== "";
|
|
112
211
|
} catch { return true; }
|
|
113
212
|
};
|
|
114
|
-
|
|
213
|
+
// `top` is the tree the recorded paths are relative to: ROOT, or another lane (ADR 0034).
|
|
214
|
+
// laneViolations: those of every other lane the session wrote in, each named by its lane.
|
|
215
|
+
export const laneViolations = (s) => Object.entries(s?.lanes ?? {})
|
|
216
|
+
.flatMap(([top, rec]) => violations(rec, branchOf(top), [], top).map((x) => "[" + top + "] " + x));
|
|
217
|
+
export const violations = (s, branch, pending = pendingMoves(s), top = ROOT) => {
|
|
115
218
|
const v = pending.map(pendingText);
|
|
116
219
|
if (!s || !(s.edited_sources || []).length) return v;
|
|
117
220
|
if (bootstrapPhase()) return v;
|
|
118
|
-
if (BASE_BRANCHES.has(branch)) { if (baseHoldsWork(s.edited_sources)) v.push("Fuentes editadas en la rama base '" + branch + "' — crea una rama: git checkout -b feat/<CARD-ID>-descripcion"); }
|
|
221
|
+
if (BASE_BRANCHES.has(branch)) { if (baseHoldsWork(s.edited_sources, top)) v.push("Fuentes editadas en la rama base '" + branch + "' — crea una rama: git checkout -b feat/<CARD-ID>-descripcion"); }
|
|
119
222
|
else if (branch && !CARD.test(branch)) v.push("La rama '" + branch + "' no referencia ninguna card — usa feat/<CARD-ID>-descripcion (y una card VIVA en el board)");
|
|
120
|
-
if (!(s.edited_tests || []).length && sessionAddsCode(s)) v.push("Fuentes editadas sin tocar un solo test (" + s.edited_sources.join(", ") + ") — escribe o actualiza el test que prueba el cambio");
|
|
223
|
+
if (!(s.edited_tests || []).length && sessionAddsCode(s, top)) v.push("Fuentes editadas sin tocar un solo test (" + s.edited_sources.join(", ") + ") — escribe o actualiza el test que prueba el cambio");
|
|
121
224
|
return v;
|
|
122
225
|
};
|
|
123
226
|
// KJC-TSK-0795 AC1: deleting code adds no behavior to test — a session whose
|
|
124
227
|
// source edits only REMOVED lines owes no test. Unreadable git = conservative
|
|
125
228
|
// (the violation stands): a gate must never stand down on a shrug.
|
|
126
|
-
const sessionAddsCode = (s) => {
|
|
229
|
+
const sessionAddsCode = (s, top) => {
|
|
127
230
|
const files = s.edited_sources || [];
|
|
128
231
|
// _git builds a shell string: any path outside a strict charset stays out of
|
|
129
232
|
// it — and the gate stays ARMED for it (conservative, and injection-proof).
|
|
130
233
|
if (files.some((f) => !/^[A-Za-z0-9._/-]+$/.test(f))) return true;
|
|
131
|
-
const stat = _git("diff HEAD --numstat -- " + files.join(" "),
|
|
234
|
+
const stat = _git("diff HEAD --numstat -- " + files.join(" "), top);
|
|
132
235
|
if (!stat) return true;
|
|
133
236
|
return stat.split(String.fromCharCode(10)).some((l) => {
|
|
134
237
|
const a = l.trim().replaceAll(String.fromCharCode(9), " ").split(" ").filter(Boolean)[0];
|
|
@@ -301,8 +301,7 @@ export const isReadOnlyCommand = (cmd) => {
|
|
|
301
301
|
/** Short flags of a cluster stop at "e": the rest is -e's value (-fen = -f -e n). */
|
|
302
302
|
export const shortOpts = (a) => (/^-[a-zA-Z]/.test(a) ? a.slice(1).split("e")[0] : "");
|
|
303
303
|
|
|
304
|
-
// KJC-BUG-0455 (#2933):
|
|
305
|
-
// change the index. A commit that stages in the same command commits a diff nobody
|
|
304
|
+
// KJC-BUG-0455 (#2933): a commit that stages in the same command commits a diff nobody
|
|
306
305
|
// could have reviewed yet: the verdict is checked against what is staged BEFORE it runs.
|
|
307
306
|
const COMMIT_VALUE_OPTS = new Set(["-m", "-F", "-C", "-c", "-t", "--message", "--file", "--reuse-message", "--reedit-message", "--template", "--author", "--date", "--cleanup", "--fixup", "--squash", "--trailer", "--pathspec-from-file"]);
|
|
308
307
|
const COMMIT_VALUE_SHORT = new Set(["m", "F", "C", "c", "t"]);
|
|
@@ -316,9 +315,7 @@ const gitVerbOf = (words) => {
|
|
|
316
315
|
while (i < words.length) {
|
|
317
316
|
const w = words[i];
|
|
318
317
|
if (GIT_GLOBAL_WITH_VALUE.has(w)) {
|
|
319
|
-
if (w === "-C")
|
|
320
|
-
dir = words[i + 1] ?? null;
|
|
321
|
-
}
|
|
318
|
+
if (w === "-C") dir = words[i + 1] ?? null;
|
|
322
319
|
i += 2; // the option and its value
|
|
323
320
|
} else if (GIT_GLOBAL_FLAG.test(w)) {
|
|
324
321
|
i += 1;
|
|
@@ -351,23 +348,15 @@ const commitArg = (a) => {
|
|
|
351
348
|
const commitStagesItself = (args) => {
|
|
352
349
|
let i = 0;
|
|
353
350
|
while (i < args.length) {
|
|
354
|
-
if (args[i] === "--")
|
|
355
|
-
return i + 1 < args.length;
|
|
356
|
-
}
|
|
351
|
+
if (args[i] === "--") return i + 1 < args.length;
|
|
357
352
|
const { stages, words } = commitArg(args[i]);
|
|
358
|
-
if (stages)
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
353
|
+
if (stages) return true;
|
|
361
354
|
i += words;
|
|
362
355
|
}
|
|
363
356
|
return false;
|
|
364
357
|
};
|
|
365
358
|
|
|
366
|
-
/**
|
|
367
|
-
* Does the command run git commit, and does it stage in the same command?
|
|
368
|
-
* @param {string} cmd
|
|
369
|
-
* @returns {{commits: boolean, stages: boolean, dir: string|null}}
|
|
370
|
-
*/
|
|
359
|
+
/** Does the command run git commit, and does it stage in the same command? → { commits, stages, dir } */
|
|
371
360
|
export const commitStaging = (cmd) => {
|
|
372
361
|
const gits = shellSegments(cmd).map((words) => ({ words, g: gitVerbOf(words) })).filter((s) => s.g);
|
|
373
362
|
const commits = gits.filter((s) => s.g.verb === "commit");
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Copied byte for byte into .karajan/harness; never fails a session (exit 0).
|
|
7
7
|
import process from "node:process";
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
|
-
import { CARD, ROOT, branchOf, load, pendingMoves } from "./sentinel-lib.mjs";
|
|
9
|
+
import { CARD, ROOT, branchOf, enterLane, load, pendingMoves } from "./sentinel-lib.mjs";
|
|
10
10
|
import { rulesDriftNotice } from "./sentinel-rules.mjs";
|
|
11
11
|
|
|
12
12
|
const RULES = [
|
|
@@ -25,7 +25,8 @@ let raw = "";
|
|
|
25
25
|
process.stdin.on("data", (d) => { raw += d; });
|
|
26
26
|
process.stdin.on("end", () => {
|
|
27
27
|
try {
|
|
28
|
-
const { session_id: sid = "default", source } = JSON.parse(raw || "{}");
|
|
28
|
+
const { session_id: sid = "default", source, cwd = null } = JSON.parse(raw || "{}");
|
|
29
|
+
enterLane(load(), sid, cwd);
|
|
29
30
|
// KJC-TSK-0949: an MD changed and its compiled rules fell behind. Said at
|
|
30
31
|
// every start, a new session included: that one gets this line and nothing else.
|
|
31
32
|
const drift = rulesDriftNotice({ root: ROOT, run: spawnSync });
|
|
@@ -10,7 +10,7 @@ import { spawnSync } from "node:child_process";
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { doc, load, save, branchOf, violations, ROOT } from "./sentinel-lib.mjs";
|
|
13
|
+
import { doc, load, save, branchOf, enterLane, laneViolations, violations, ROOT } from "./sentinel-lib.mjs";
|
|
14
14
|
// KJC-TSK-0774: when the turn ends green with cards closed, remind the user
|
|
15
15
|
// where to look — the board URL if it runs (pid file alive), else how to start it.
|
|
16
16
|
const boardHint = () => {
|
|
@@ -27,7 +27,7 @@ if (process.argv.includes("--status")) {
|
|
|
27
27
|
if (!sessions.length) console.log("sentinel: sin actividad registrada en esta sesion");
|
|
28
28
|
for (const [sid, s] of sessions) {
|
|
29
29
|
console.log("session " + sid + ": sources=[" + (s.edited_sources || []).join(", ") + "] tests=[" + (s.edited_tests || []).join(", ") + "] escapes=[" + (s.escapes || []).join(", ") + "] blocks=" + (s.blocks || 0) + ((s.errors || []).length ? " errors=" + s.errors.length : "") + " rag=" + (s.rag_queries || []).length + " queries/" + (s.rag_hits || []).length + " sources");
|
|
30
|
-
for (const x of violations(s, branch)) console.log(" ROJO: " + x);
|
|
30
|
+
for (const x of [...violations(s, branch), ...laneViolations(s)]) console.log(" ROJO: " + x);
|
|
31
31
|
}
|
|
32
32
|
process.exit(0);
|
|
33
33
|
}
|
|
@@ -36,8 +36,9 @@ process.stdin.on("data", (d) => { raw += d; });
|
|
|
36
36
|
process.stdin.on("end", () => {
|
|
37
37
|
try {
|
|
38
38
|
if (process.env.KJ_SENTINEL_OFF === "1") process.exit(0);
|
|
39
|
-
const { session_id: sid = "default", transcript_path: transcript = null } = JSON.parse(raw);
|
|
39
|
+
const { session_id: sid = "default", transcript_path: transcript = null, cwd = null } = JSON.parse(raw);
|
|
40
40
|
const state = load();
|
|
41
|
+
enterLane(state, sid, cwd);
|
|
41
42
|
// Tamper check runs regardless of session state: shell indirection leaves
|
|
42
43
|
// no edit-tool record, but it cannot survive `kj sentinel verify`, whose
|
|
43
44
|
// root of trust is the INSTALLED kj package (outside this project tree) —
|
|
@@ -87,7 +88,7 @@ process.stdin.on("end", () => {
|
|
|
87
88
|
if ((p.stdout || "").trim()) console.error(p.stdout.trim());
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
const v = violations(s, branchOf());
|
|
91
|
+
const v = [...violations(s, branchOf()), ...laneViolations(s)];
|
|
91
92
|
if (claimsBlock) v.push(claimsBlock);
|
|
92
93
|
if (!v.length) {
|
|
93
94
|
s.blocks = 0;
|
|
@@ -19,7 +19,7 @@ export const PINNED_ACTIONS = {
|
|
|
19
19
|
setupNode: "actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4",
|
|
20
20
|
setupPython: "actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5",
|
|
21
21
|
setupGo: "actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5",
|
|
22
|
-
setupPhp: "shivammathur/setup-php@
|
|
22
|
+
setupPhp: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2",
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
export const NO_AI_ATTRIBUTION_WORKFLOW = [
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-BUG-0454: one card of the HU Board, read where `kj hu` keeps it. `kj code --card`
|
|
3
|
+
* looked in the old kj run store, so every card `kj hu add` created was "not found".
|
|
4
|
+
* Shared board: the GitHub issue HU-<n>, as `kj hu list` reads it. Local board: the
|
|
5
|
+
* plan backlog, by id or short id.
|
|
6
|
+
*/
|
|
7
|
+
import { listPlans, loadPlan } from "../plan/plan-store.js";
|
|
8
|
+
import { readBoardConfig } from "./board-config.js";
|
|
9
|
+
import { listHus } from "./github-board.js";
|
|
10
|
+
import { pinGhIdentity } from "./gh-identity.js";
|
|
11
|
+
import { listWithCache } from "./team-board-cache.js";
|
|
12
|
+
|
|
13
|
+
const SHARED_ID = /^HU-[1-9]\d*$/i;
|
|
14
|
+
|
|
15
|
+
export async function findBoardHu(projectDir, ref, deps = {}) {
|
|
16
|
+
const board = await (deps.boardConfig ?? readBoardConfig)(projectDir);
|
|
17
|
+
if (board.backend === "github" && SHARED_ID.test(ref)) {
|
|
18
|
+
(deps.pinGhIdentity ?? pinGhIdentity)({ projectDir });
|
|
19
|
+
const { rows } = await (deps.cache?.list ?? listWithCache)({ projectDir, repo: board.repo, list: deps.listTeamHus ?? listHus });
|
|
20
|
+
const hu = rows.find((h) => h.short_id.toUpperCase() === ref.toUpperCase());
|
|
21
|
+
if (hu) return hu;
|
|
22
|
+
}
|
|
23
|
+
for (const meta of await (deps.listPlans ?? listPlans)(projectDir)) {
|
|
24
|
+
const plan = await (deps.loadPlan ?? loadPlan)(projectDir, meta.planId);
|
|
25
|
+
const hu = (plan.hus || []).find((h) => h.id === ref || h.short_id === ref);
|
|
26
|
+
if (hu) return hu;
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`HU ${ref} is not on the HU Board — kj hu list shows its cards`);
|
|
29
|
+
}
|
package/src/hu/gh-identity.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// accounts on one machine, another session switching the active one turned `kj hu move` into an
|
|
3
3
|
// opaque GraphQL permission error, three times in a day. The clone declares its account in
|
|
4
4
|
// .karajan/identity.local.yml (ADR 0005): with a declaration, the active account must be it.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { readFileSync } from "node:fs";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
8
|
import { join } from "node:path";
|
|
@@ -42,3 +43,25 @@ export function assertGhIdentity({ projectDir, env = process.env }) {
|
|
|
42
43
|
}
|
|
43
44
|
return { declared, active };
|
|
44
45
|
}
|
|
46
|
+
|
|
47
|
+
/** The token gh keeps for `user`, read without switching the active account; null when it has none. */
|
|
48
|
+
function ghTokenOf(user, env) {
|
|
49
|
+
const res = spawnSync("gh", ["auth", "token", "--hostname", "github.com", "--user", user], { env, encoding: "utf8" });
|
|
50
|
+
return res.status === 0 ? res.stdout.trim() || null : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* KJC-BUG-0406: checking the active account and then calling gh was a race; another session could
|
|
55
|
+
* switch it in between. With an identity declared, GH_TOKEN is pinned to that account's token for
|
|
56
|
+
* the rest of the command, so every gh call goes out as it. A token already in the environment is
|
|
57
|
+
* the user's explicit choice and is left alone.
|
|
58
|
+
* @returns {{ declared: string | null, pinned: boolean }}
|
|
59
|
+
*/
|
|
60
|
+
export function pinGhIdentity({ projectDir, env = process.env, tokenOf = ghTokenOf }) {
|
|
61
|
+
const declared = readIdentity(projectDir)?.gh_user ?? null;
|
|
62
|
+
if (!declared || env.GH_TOKEN || env.GITHUB_TOKEN) return { declared, pinned: false };
|
|
63
|
+
const token = tokenOf(declared, env);
|
|
64
|
+
if (!token) throw new BoardRejectedError(`gh has no login for ${declared}, the account this clone declares: run gh auth login and retry`);
|
|
65
|
+
env.GH_TOKEN = token;
|
|
66
|
+
return { declared, pinned: true };
|
|
67
|
+
}
|
package/src/hu/github-board.js
CHANGED
|
@@ -70,8 +70,10 @@ export async function createHu({ repo, title, acceptanceCriteria = [], tests = [
|
|
|
70
70
|
|
|
71
71
|
/** Every HU of the repo, with its status and who holds it. */
|
|
72
72
|
export async function listHus({ repo, run = runCommand }) {
|
|
73
|
-
const issues = listOf(await gh(run, ["issue", "list", "-R", repo, "--label", HU_LABEL, "--state", "all", "--limit", "1000", "--json", "number,title,state,labels,assignees"]));
|
|
74
|
-
|
|
73
|
+
const issues = listOf(await gh(run, ["issue", "list", "-R", repo, "--label", HU_LABEL, "--state", "all", "--limit", "1000", "--json", "number,title,state,labels,assignees,body"]));
|
|
74
|
+
// KJC-BUG-0443: the tracker reference kj hu add --ref wrote in the body.
|
|
75
|
+
const refOf = (body) => /^Ref: (\S.*)$/m.exec(body ?? "")?.[1].trim() ?? null;
|
|
76
|
+
return issues.map((i) => ({ short_id: `HU-${i.number}`, number: i.number, title: i.title, status: statusOf(i), claimed_by: i.assignees[0]?.login ?? null, ref: refOf(i.body) }));
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/** The one kj:<status> of an issue; none or several is a board someone broke by hand. */
|
package/src/hu/lane-prune.js
CHANGED
|
@@ -5,30 +5,61 @@
|
|
|
5
5
|
* touched, so a session may run this without risking anyone's work.
|
|
6
6
|
*/
|
|
7
7
|
import fs from "node:fs";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
8
9
|
import path from "node:path";
|
|
10
|
+
import { isLaneSetupPath, laneWork } from "./lane-setup.js";
|
|
11
|
+
import { readBootstrap } from "../rag/bootstrap-index.js";
|
|
9
12
|
|
|
10
13
|
const IDLE_HOURS = 24;
|
|
14
|
+
// HU-3092: the lane's index and what rides along it (-wal, -shm, .bootstrap, .embed-schema) is regenerable state.
|
|
15
|
+
const RAG_STATE = /^\.karajan\/rag\.db([.-]|$)/;
|
|
11
16
|
const REGENERABLE = new Set(["node_modules", "dist", "build", "coverage", ".venv", "venv", "__pycache__", ".cache", "target"]);
|
|
12
17
|
|
|
18
|
+
const LSOF = "/usr/sbin/lsof";
|
|
19
|
+
const LSOF_TIMEOUT_MS = 10_000;
|
|
20
|
+
|
|
21
|
+
/** HU-3117: `lsof -Fpn` output is `p<pid>` then `n<path>` lines; a pid with no path line is skipped. */
|
|
22
|
+
export function parseLsofCwds(out) {
|
|
23
|
+
const procs = [];
|
|
24
|
+
let pid = null;
|
|
25
|
+
for (const line of out.split("\n")) {
|
|
26
|
+
if (line.startsWith("p") && /^\d+$/.test(line.slice(1))) pid = Number(line.slice(1));
|
|
27
|
+
else if (line.startsWith("n") && pid !== null) { procs.push({ pid, cwd: line.slice(1) }); pid = null; }
|
|
28
|
+
}
|
|
29
|
+
return procs;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** darwin has no /proc: lsof lists this user's cwds. null when it is missing, fails or times out (never "no processes"). */
|
|
33
|
+
function lsofProcesses({ run, uid }) {
|
|
34
|
+
const r = run(LSOF, ["-a", "-d", "cwd", "-u", String(uid), "-Fpn"], { encoding: "utf8", timeout: LSOF_TIMEOUT_MS });
|
|
35
|
+
if (r.error || r.status !== 0 || typeof r.stdout !== "string") return null;
|
|
36
|
+
const procs = parseLsofCwds(r.stdout);
|
|
37
|
+
return { cwds: new Set(procs.map((p) => p.cwd)), unseen: 0, procs };
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
/**
|
|
14
41
|
* This user's live processes (another user's cannot write in our worktrees): the
|
|
15
42
|
* cwds that can be read and how many cannot (non-dumpable ones: ssh-agent, systemd,
|
|
16
|
-
* some JVMs). null when the platform
|
|
43
|
+
* some JVMs). null when the platform's listing is unavailable (no /proc, no lsof).
|
|
17
44
|
*/
|
|
18
|
-
export function liveProcesses() {
|
|
45
|
+
export function liveProcesses({ platform = process.platform, run = spawnSync, uid = process.getuid() } = {}) {
|
|
46
|
+
if (platform === "darwin") return lsofProcesses({ run, uid });
|
|
19
47
|
let pids;
|
|
20
48
|
try { pids = fs.readdirSync("/proc").filter((p) => /^\d+$/.test(p)); } catch { return null; }
|
|
21
49
|
const cwds = new Set();
|
|
50
|
+
const procs = [];
|
|
22
51
|
let unseen = 0;
|
|
23
52
|
for (const pid of pids) {
|
|
24
53
|
try {
|
|
25
54
|
if (fs.statSync(`/proc/${pid}`).uid !== process.getuid()) continue;
|
|
26
|
-
|
|
55
|
+
const cwd = fs.readlinkSync(`/proc/${pid}/cwd`);
|
|
56
|
+
cwds.add(cwd);
|
|
57
|
+
procs.push({ pid: Number(pid), cwd });
|
|
27
58
|
} catch (err) {
|
|
28
59
|
if (err.code !== "ENOENT" && err.code !== "ESRCH") unseen += 1;
|
|
29
60
|
}
|
|
30
61
|
}
|
|
31
|
-
return { cwds, unseen };
|
|
62
|
+
return { cwds, unseen, procs };
|
|
32
63
|
}
|
|
33
64
|
|
|
34
65
|
/** Hours since anything in the lane moved: its dir, and its git index and HEAD. */
|
|
@@ -42,6 +73,32 @@ async function idleHours(git, lanePath, now) {
|
|
|
42
73
|
|
|
43
74
|
const inside = (cwd, dir) => cwd === dir || cwd.startsWith(dir + path.sep);
|
|
44
75
|
|
|
76
|
+
/** A folder git lists whole (an ignored `.karajan/`) holds only index state and setup when every file in it does. */
|
|
77
|
+
function onlyRagState(lanePath, dir) {
|
|
78
|
+
try {
|
|
79
|
+
return fs.readdirSync(path.join(lanePath, dir), { recursive: true, withFileTypes: true }).filter((d) => !d.isDirectory()) // symlinks and the like are the user's, never state
|
|
80
|
+
.every((d) => { const rel = path.relative(lanePath, path.join(d.parentPath, d.name)); return RAG_STATE.test(rel) || isLaneSetupPath(rel); });
|
|
81
|
+
} catch { return false; }
|
|
82
|
+
}
|
|
83
|
+
const ragOnly = (lanePath, p) => RAG_STATE.test(p) || (p.endsWith("/") && onlyRagState(lanePath, p));
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The pid of the lane's background indexer: the one its marker names, while the marker notes no stop.
|
|
87
|
+
* null when KJ_RAG_DB puts the index elsewhere: the marker read is then not this lane's.
|
|
88
|
+
*/
|
|
89
|
+
function indexerOf(lanePath) {
|
|
90
|
+
if (process.env.KJ_RAG_DB) return null;
|
|
91
|
+
const marker = readBootstrap(lanePath);
|
|
92
|
+
return Number.isInteger(marker?.pid) && !marker.stoppedAt ? marker.pid : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** D6: stops each removable lane's indexer; a lane whose indexer survives (marker still running) is kept, saying so. */
|
|
96
|
+
export async function stopLaneIndexers(lanes, { stop, stillRunning }) {
|
|
97
|
+
for (const l of lanes.filter((x) => x.indexer && x.reasons.length === 0)) {
|
|
98
|
+
if (!(await stop(l)) && (await stillRunning(l))) l.reasons.push(`its background index (pid ${l.indexer.pid}) could not be stopped`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
45
102
|
/** @returns {Promise<{slug: string, path: string, branch: string|null, reasons: string[]}[]>} */
|
|
46
103
|
export async function classifyLanes({ git, laneRoot, base, processes, now = Date.now() }) {
|
|
47
104
|
const { stdout } = await git(["worktree", "list", "--porcelain"]);
|
|
@@ -55,21 +112,28 @@ export async function classifyLanes({ git, laneRoot, base, processes, now = Date
|
|
|
55
112
|
}
|
|
56
113
|
return Promise.all(lanes.map(async (lane) => {
|
|
57
114
|
const reasons = [];
|
|
58
|
-
|
|
59
|
-
const
|
|
115
|
+
// KJC-TSK-1371: the identity and Sentinel files lane setup wrote are not the lane's work.
|
|
116
|
+
const { stdout: status } = await git(["status", "--porcelain", "--ignored=matching", "--untracked-files=all"], lane.path);
|
|
117
|
+
const entries = laneWork(status, lane.path).filter((e) => !ragOnly(lane.path, e.slice(3)));
|
|
60
118
|
if (entries.some((e) => !e.startsWith("!! "))) reasons.push("uncommitted changes");
|
|
61
119
|
// Ignored files can be valuable (.env, local artifacts); only regenerable dirs are not.
|
|
62
120
|
const precious = entries.filter((e) => e.startsWith("!! ") && !REGENERABLE.has(e.slice(3).split("/")[0]));
|
|
63
121
|
if (precious.length > 0) reasons.push(`ignored files that are not regenerable (${precious[0].slice(3)}${precious.length > 1 ? ", …" : ""})`);
|
|
64
122
|
// The lane's HEAD, branch or detached: a detached lane's commits have no other name.
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
123
|
+
const ahead = await commitsNotIn({ git, base, cwd: lane.path });
|
|
124
|
+
if (ahead > 0) reasons.push(`${ahead} commit(s) not in ${base}`);
|
|
125
|
+
// The lane's own indexer alone is no reason to keep it (HU-3092); without pids (older callers) any process still is.
|
|
126
|
+
const inLane = (processes?.procs ?? []).filter((p) => inside(p.cwd, lane.path));
|
|
127
|
+
const indexer = processes?.procs ? indexerOf(lane.path) : null;
|
|
128
|
+
const busy = processes !== null && [...processes.cwds].some((cwd) => inside(cwd, lane.path));
|
|
129
|
+
const owned = busy && indexer !== null && inLane.every((p) => p.pid === indexer) ? { pid: indexer } : null;
|
|
67
130
|
if (processes === null) reasons.push("cannot check for live processes on this platform");
|
|
68
|
-
else if (
|
|
131
|
+
else if (busy && !owned) reasons.push("a live process works in it");
|
|
132
|
+
// The owned indexer excluded, processes that cannot be inspected still protect a recently active lane.
|
|
69
133
|
else if (processes.unseen > 0 && (await idleHours(git, lane.path, now)) < IDLE_HOURS) {
|
|
70
134
|
reasons.push(`${processes.unseen} process(es) of yours cannot be inspected and the lane changed in the last ${IDLE_HOURS} h`);
|
|
71
135
|
}
|
|
72
|
-
return { slug: path.basename(lane.path), path: lane.path, branch: lane.branch, reasons };
|
|
136
|
+
return { slug: path.basename(lane.path), path: lane.path, branch: lane.branch, reasons, indexer: owned };
|
|
73
137
|
}));
|
|
74
138
|
}
|
|
75
139
|
|
|
@@ -83,6 +147,12 @@ export async function resolveBase({ git, base }) {
|
|
|
83
147
|
}
|
|
84
148
|
}
|
|
85
149
|
|
|
150
|
+
/** Commits of `ref` (default HEAD, run in `cwd`) that `base` lacks; 0 means merged. */
|
|
151
|
+
export async function commitsNotIn({ git, base, ref = "HEAD", cwd }) {
|
|
152
|
+
const { stdout } = await git(["rev-list", "--count", `${base}..${ref}`], cwd);
|
|
153
|
+
return Number(stdout);
|
|
154
|
+
}
|
|
155
|
+
|
|
86
156
|
/** KJC-TSK-1037: loose kj-hu-* branches (no worktree): the empty ones, and the ones with work and how much. */
|
|
87
157
|
export async function looseLaneBranches({ git, base }) {
|
|
88
158
|
const { stdout: listed } = await git(["for-each-ref", "--format=%(refname:short) %(worktreepath)", "refs/heads/kj-hu-*"]);
|
|
@@ -90,9 +160,9 @@ export async function looseLaneBranches({ git, base }) {
|
|
|
90
160
|
const empty = [];
|
|
91
161
|
const withWork = [];
|
|
92
162
|
for (const branch of loose) {
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
else withWork.push({ branch, reason: `${
|
|
163
|
+
const ahead = await commitsNotIn({ git, base, ref: branch });
|
|
164
|
+
if (ahead === 0) empty.push(branch);
|
|
165
|
+
else withWork.push({ branch, reason: `${ahead} commit(s) not in ${base}` });
|
|
96
166
|
}
|
|
97
167
|
return { empty, withWork };
|
|
98
168
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane setup (KJC-TSK-1371, #2852, step 6 of #2566): a lane `kj worktree start` creates is
|
|
3
|
+
* ready on creation. Two things a fresh git worktree lacks, because git does not track them:
|
|
4
|
+
* - the clone identity (.karajan/identity.local.yml), which the hooks read from the lane root
|
|
5
|
+
* so the identity lock knows who pushes; copied from the main checkout when it has one;
|
|
6
|
+
* - the Sentinel wiring (.claude/settings.json hooks and .karajan/harness), installed in the
|
|
7
|
+
* lane with the same installers `kj harden` uses, so the first write there is governed.
|
|
8
|
+
* A step that fails stops the start loudly, naming the step: never a half-set lane in silence.
|
|
9
|
+
*/
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
import { installSentinelHooks } from "../harden/sentinel-hooks.js";
|
|
14
|
+
import { installHarnessHooks } from "../harden/harness-hooks.js";
|
|
15
|
+
|
|
16
|
+
const IDENTITY = path.join(".karajan", "identity.local.yml");
|
|
17
|
+
const INSTALLERS = { sentinel: installSentinelHooks, harness: installHarnessHooks };
|
|
18
|
+
|
|
19
|
+
/** What setupLane writes into a lane (and the Sentinel's runtime state): never the lane's work. */
|
|
20
|
+
export const isLaneSetupPath = (p) => p === IDENTITY || p === path.join(".claude", "settings.json") || p.startsWith(path.join(".karajan", "harness") + path.sep);
|
|
21
|
+
|
|
22
|
+
/** A folder git lists whole (an ignored `.claude/`) is setup only when every file in it is. */
|
|
23
|
+
function onlySetup(lanePath, dir) {
|
|
24
|
+
try {
|
|
25
|
+
const files = fs.readdirSync(path.join(lanePath, dir), { recursive: true, withFileTypes: true }).filter((d) => d.isFile());
|
|
26
|
+
return files.every((d) => isLaneSetupPath(path.relative(lanePath, path.join(d.parentPath, d.name))));
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `git status --porcelain` lines that are the lane's own work, setup files left out. */
|
|
33
|
+
export const laneWork = (porcelain, lanePath) => porcelain.split("\n").filter(Boolean).filter((line) => {
|
|
34
|
+
const p = line.slice(3);
|
|
35
|
+
return !(isLaneSetupPath(p) || (p.endsWith("/") && lanePath && onlySetup(lanePath, p)));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function step(name, fn) {
|
|
39
|
+
try {
|
|
40
|
+
return fn();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw new Error(`lane setup failed at ${name}: ${err?.message || err}`, { cause: err });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @returns {Promise<{ identity: boolean, wired: boolean }>} */
|
|
47
|
+
export async function setupLane({ mainDir, lanePath, logger = console, install = INSTALLERS }) {
|
|
48
|
+
const source = path.join(mainDir, IDENTITY);
|
|
49
|
+
const identity = step("the clone identity", () => {
|
|
50
|
+
if (!fs.existsSync(source)) return false;
|
|
51
|
+
fs.mkdirSync(path.join(lanePath, ".karajan"), { recursive: true });
|
|
52
|
+
fs.copyFileSync(source, path.join(lanePath, IDENTITY));
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
if (identity) logger.info?.(` identity: ${IDENTITY} copied from the main checkout`);
|
|
56
|
+
else logger.warn?.(`! no ${IDENTITY} in the main checkout: run kj identity set in the lane before pushing`);
|
|
57
|
+
|
|
58
|
+
step("the Sentinel wiring", () => {
|
|
59
|
+
const quiet = { info: () => {}, warn: (m) => logger.warn?.(m) };
|
|
60
|
+
const results = [install.sentinel({ projectDir: lanePath, logger: quiet }), install.harness({ projectDir: lanePath, logger: quiet })];
|
|
61
|
+
if (results.some((r) => !r?.wired)) throw new Error("the hooks were not wired (unreadable .claude/settings.json?)");
|
|
62
|
+
});
|
|
63
|
+
logger.info?.(" Sentinel: hooks wired in the lane");
|
|
64
|
+
return { identity, wired: true };
|
|
65
|
+
}
|