@donghyeonlee/jjamppong-harness 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/AGENTS.md +77 -0
  2. package/CONTEXT.md +51 -0
  3. package/README.md +85 -0
  4. package/bin/jjamppong.js +123 -0
  5. package/handoff.md +13 -0
  6. package/harness/contracts/capability-catalog.yaml +128 -0
  7. package/harness/contracts/gate-contract-matrix.yaml +188 -0
  8. package/harness/contracts/installer-contract.yaml +79 -0
  9. package/harness/contracts/ledger-event.schema.yaml +79 -0
  10. package/harness/contracts/path-policy.schema.yaml +51 -0
  11. package/harness/contracts/permission-decision.schema.yaml +95 -0
  12. package/harness/contracts/task.schema.yaml +88 -0
  13. package/harness/docs/adr/.gitkeep +1 -0
  14. package/harness/docs/adr/2026-06-02-jjamppong-planning-gate.md +33 -0
  15. package/harness/docs/agents/domain.md +14 -0
  16. package/harness/docs/agents/issue-tracker.md +9 -0
  17. package/harness/docs/agents/matt-pocock-skills.md +60 -0
  18. package/harness/docs/agents/triage-labels.md +11 -0
  19. package/harness/docs/solutions/.gitkeep +1 -0
  20. package/harness/docs/tasks/active/.gitkeep +1 -0
  21. package/harness/docs/tasks/archive/.gitkeep +1 -0
  22. package/harness/docs/tasks/archive/index.md +5 -0
  23. package/harness/docs/tasks/index.md +13 -0
  24. package/harness/doctor/doctor.js +114 -0
  25. package/harness/installer/install.js +235 -0
  26. package/harness/lifecycle/lifecycle.js +133 -0
  27. package/harness/permission/permission-decision.js +377 -0
  28. package/harness/release/CHECKSUMS.sha256 +84 -0
  29. package/harness/release/RELEASE-NOTES.md +33 -0
  30. package/harness/release/SOURCE-MANIFEST.md +31 -0
  31. package/harness/rules/module-types.md +98 -0
  32. package/harness/rules/rules.md +220 -0
  33. package/harness/rules/workflow.md +252 -0
  34. package/harness/state/compound.md +7 -0
  35. package/harness/state/intake.md +11 -0
  36. package/harness/state/module-structure.md +13 -0
  37. package/harness/state/planning.md +21 -0
  38. package/harness/templates/module/module-state.md +13 -0
  39. package/harness/templates/task/archive-summary.md +19 -0
  40. package/harness/templates/task/events.jsonl.template +1 -0
  41. package/harness/templates/task/gate-ledger.md +21 -0
  42. package/harness/templates/task/implementation-approval.md +11 -0
  43. package/harness/templates/task/planning/00-current-planning-context.md +3 -0
  44. package/harness/templates/task/planning/01-grill-summary.md +3 -0
  45. package/harness/templates/task/planning/02-research-summary.md +3 -0
  46. package/harness/templates/task/planning/02b-compound-lookup.md +3 -0
  47. package/harness/templates/task/planning/02c-architecture-orientation.md +3 -0
  48. package/harness/templates/task/planning/03-prd.md +3 -0
  49. package/harness/templates/task/planning/04-issues.md +3 -0
  50. package/harness/templates/task/planning/05-module-structure.md +3 -0
  51. package/harness/templates/task/planning/06-writing-plan.md +3 -0
  52. package/harness/templates/task/planning/07-plan-review.md +3 -0
  53. package/harness/templates/task/planning-pack.md +23 -0
  54. package/harness/templates/task/task.yaml +10 -0
  55. package/harness/templates/task/verification.md +12 -0
  56. package/harness/verify/verify.js +271 -0
  57. package/module-template/MODULE.md +25 -0
  58. package/module-template/README.md +9 -0
  59. package/modules/.gitkeep +0 -0
  60. package/package.json +40 -0
  61. package/proposals/README.md +16 -0
  62. package/scripts/install-jjamppong-harness.ps1 +62 -0
@@ -0,0 +1,79 @@
1
+ version: 0.1.0
2
+ name: installer-contract
3
+
4
+ modes:
5
+ install:
6
+ behavior: install_verify_stop
7
+ may_modify: [harness_core_files, state_neutral_files, harness_lock]
8
+ must_not: [start_planning, create_product_task, create_code, run_live_access, commit, push]
9
+ verify:
10
+ behavior: read_only_pass_fail
11
+ doctor:
12
+ behavior: read_only_diagnose_plus_proposal
13
+ live_edit_default: false
14
+ update:
15
+ behavior: proposal_only_if_managed_files_modified
16
+ repair:
17
+ behavior: proposal_only_if_managed_files_modified
18
+
19
+ ownership_classes:
20
+ harness_core_owned:
21
+ examples: [AGENTS.md, harness/rules/**, harness/contracts/**, scripts/install-jjamppong-harness.ps1]
22
+ update_strategy: replace_with_backup_if_clean_else_proposal
23
+ project_overlay_owned:
24
+ examples: [project-local policy files]
25
+ update_strategy: merge_proposal
26
+ user_owned:
27
+ update_strategy: never_overwrite
28
+ generated_derived:
29
+ examples: [task.yaml, gate-ledger.md projection, index files]
30
+ update_strategy: regenerate_from_canonical_when_explicit
31
+ append_only_canonical:
32
+ examples: [events.jsonl]
33
+ update_strategy: append_only_never_rewrite_without_repair_record
34
+ local_artifact:
35
+ examples: [harness/artifacts/local/**]
36
+ update_strategy: gitignored_do_not_publish
37
+
38
+ fresh_install_requirements:
39
+ root_items:
40
+ - AGENTS.md
41
+ - README.md
42
+ - CONTEXT.md
43
+ - handoff.md
44
+ - harness/
45
+ - modules/
46
+ - module-template/
47
+ - proposals/
48
+ - harness.lock.yaml
49
+ forbidden_nested:
50
+ - jjamppong-harness/
51
+ - ourosuper-harness/
52
+ state:
53
+ active_tasks_empty: true
54
+ archive_empty_or_gitkeep_only: true
55
+ planning_state: neutral
56
+ handoff: neutral
57
+ git:
58
+ origin_must_not_be_template_repo: true
59
+ create_remote_repo_default: false
60
+ commit_default: false
61
+ push_default: false
62
+
63
+ backup_rollback:
64
+ backup_before_overwrite: true
65
+ rollback_manifest_required: true
66
+ idempotent_install_required: true
67
+
68
+ harness_lock_required_fields:
69
+ - harness.name
70
+ - harness.version
71
+ - installer.package
72
+ - installer.version
73
+ - installed_at
74
+ - installed_from
75
+ - planning_started
76
+ - github_repo_created
77
+ - commit_created
78
+ - push_performed
79
+ - managed_files
@@ -0,0 +1,79 @@
1
+ version: 0.1.0
2
+ name: ledger-event-schema
3
+ canonical_log: events.jsonl
4
+ append_only: true
5
+ hash_chain_required: true
6
+
7
+ required_fields:
8
+ - event_id
9
+ - schema_version
10
+ - task_id
11
+ - event_type
12
+ - created_at
13
+ - actor_type
14
+ - previous_hash
15
+ - event_hash
16
+
17
+ fields:
18
+ event_id: {type: string, format: uuid_or_ulid}
19
+ schema_version: {type: string}
20
+ task_id: {type: string}
21
+ event_type:
22
+ enum:
23
+ - task_opened
24
+ - gate_question
25
+ - user_answer
26
+ - approval_decision
27
+ - gate_status_change
28
+ - artifact_written
29
+ - permission_decision
30
+ - capability_used
31
+ - verification_result
32
+ - invalidation
33
+ - projection_generated
34
+ - task_archived
35
+ created_at: {type: string, format: iso8601}
36
+ actor_type:
37
+ enum: [user, assistant, tool, verifier, doctor, installer, reviewer]
38
+ actor_id: {type: string, required: false}
39
+ previous_hash: {type: string}
40
+ event_hash: {type: string}
41
+ payload: {type: object}
42
+
43
+ payload_schemas:
44
+ gate_question:
45
+ required: [gate_id, question_text, explicit_scope, still_forbidden]
46
+ fields:
47
+ gate_id: {type: string}
48
+ question_text: {type: string}
49
+ explicit_scope: {type: object}
50
+ still_forbidden: {type: array}
51
+ target_paths: {type: array, required: false}
52
+ capability_flags: {type: object, required: false}
53
+ user_answer:
54
+ required: [answer_quote, responds_to_event_id]
55
+ fields:
56
+ answer_quote: {type: string}
57
+ responds_to_event_id: {type: string}
58
+ approval_decision:
59
+ required: [gate_id, user_answer_event_id, gate_question_event_id, interpreted_scope, effects, invalidates_on]
60
+ fields:
61
+ gate_id: {type: string}
62
+ status: {enum: [approved, rejected, blocked, not_applicable, invalidated]}
63
+ interpreted_scope: {type: object}
64
+ effects: {type: object}
65
+ based_on_artifact_hashes: {type: object}
66
+ target_paths: {type: object}
67
+ still_forbidden: {type: array}
68
+ invalidates_on: {type: array}
69
+ permission_decision:
70
+ required: [decision_id, capability, decision, reason, based_on_events]
71
+ fields:
72
+ decision_id: {type: string}
73
+ capability: {type: string}
74
+ path: {type: string, required: false}
75
+ target: {type: string, required: false}
76
+ decision: {enum: [allow, deny, block, proposal_required]}
77
+ reason: {type: string}
78
+ required_next_action: {type: object, required: false}
79
+ based_on_events: {type: array}
@@ -0,0 +1,51 @@
1
+ version: 0.1.0
2
+ name: path-policy-schema
3
+
4
+ normalization_required:
5
+ - expand_user_visible_windows_drive_shorthand
6
+ - normalize_separators
7
+ - resolve_dot_segments
8
+ - convert_to_absolute_path
9
+ - resolve_realpath
10
+ - case_normalize_when_filesystem_is_case_insensitive
11
+ - detect_symlink
12
+ - detect_junction
13
+ - detect_hardlink_when_possible
14
+ - reject_alternate_data_streams_on_windows
15
+ - reject_reserved_device_names_on_windows
16
+
17
+ windows_cases:
18
+ drive_shorthand:
19
+ example: F:mptech
20
+ policy: normalize_to_explicit_drive_root_or_ask_if_ambiguous
21
+ unc_paths:
22
+ example: "\\\\server\\share"
23
+ policy: deny_unless_project_root_explicitly_unc
24
+ alternate_data_stream:
25
+ example: file.txt:stream
26
+ policy: deny
27
+ reserved_names:
28
+ examples: [CON, NUL, AUX, PRN]
29
+ policy: deny
30
+ symlink_junction_escape:
31
+ policy: deny_if_realpath_outside_project_root_or_outside_approved_module
32
+
33
+ glob_policy:
34
+ raw_globs_do_not_grant_permission: true
35
+ glob_must_expand_to_exact_paths_before_decision: true
36
+ empty_glob: deny
37
+ glob_with_escape: deny
38
+
39
+ containment:
40
+ project_root:
41
+ required_for_all_repo_writes: true
42
+ modules_root:
43
+ default_product_code_root: modules/
44
+ path_decision_uses: realpath
45
+ harness_core_paths:
46
+ - AGENTS.md
47
+ - harness/rules/**
48
+ - harness/contracts/**
49
+ - scripts/install-jjamppong-harness.ps1
50
+ - plugins/**
51
+ - package installer files
@@ -0,0 +1,95 @@
1
+ version: 0.1.0
2
+ name: permission-decision-schema
3
+
4
+ required_fields:
5
+ - decision_id
6
+ - schema_version
7
+ - task_id
8
+ - requested_action
9
+ - capability
10
+ - decision
11
+ - reason
12
+ - based_on
13
+ - created_at
14
+
15
+ decisions:
16
+ - allow
17
+ - deny
18
+ - block
19
+ - proposal_required
20
+
21
+ requested_action_fields:
22
+ action_type:
23
+ enum:
24
+ - file_read
25
+ - file_write
26
+ - file_delete
27
+ - command_exec
28
+ - network_access
29
+ - package_install
30
+ - git_operation
31
+ - installer_operation
32
+ - harness_core_change
33
+ - parallel_write
34
+ path: {type: string, required: false}
35
+ command: {type: string, required: false}
36
+ network_target: {type: string, required: false}
37
+ package_name: {type: string, required: false}
38
+
39
+ capability_examples:
40
+ file_write_module: file.write.module
41
+ live_target: network.live_target
42
+ package_install: package.install
43
+ git_push: git.push
44
+ secret_read: file.read.secret
45
+
46
+ based_on:
47
+ required:
48
+ - canonical_event_log_hash
49
+ - relevant_event_ids
50
+ - artifact_hashes
51
+ - contract_versions
52
+ fields:
53
+ canonical_event_log_hash: string
54
+ relevant_event_ids: array
55
+ artifact_hashes: object
56
+ contract_versions: object
57
+
58
+ path_policy_result:
59
+ required_when: path_present
60
+ fields:
61
+ raw_path: string
62
+ normalized_path: string
63
+ absolute_path: string
64
+ realpath: string
65
+ project_root: string
66
+ is_within_project_root: boolean
67
+ is_within_modules: boolean
68
+ has_symlink_or_junction: boolean
69
+ matched_allowed_pattern: string
70
+ escape_detected: boolean
71
+
72
+ required_next_action:
73
+ fields:
74
+ type:
75
+ enum: [ask_user, open_gate, create_proposal, run_verify, stop]
76
+ gate_id: string
77
+ suggested_user_question: string
78
+ missing_capabilities: array
79
+
80
+ invalidation:
81
+ approval_must_bind_to:
82
+ - gate_question_event_id
83
+ - user_answer_event_id
84
+ - target_paths
85
+ - capability_flags
86
+ - artifact_hashes
87
+ invalidates_when:
88
+ - prd_hash_changed
89
+ - issues_hash_changed
90
+ - module_structure_hash_changed
91
+ - writing_plan_hash_changed
92
+ - target_paths_changed
93
+ - capability_need_added
94
+ - user_added_new_constraint
95
+ - contract_version_changed
@@ -0,0 +1,88 @@
1
+ version: 0.1.0
2
+ name: task-schema
3
+
4
+ canonical_event_log: events.jsonl
5
+ human_projection: gate-ledger.md
6
+ cache_projection: task.yaml
7
+
8
+ allowed_task_types:
9
+ - install
10
+ - module_bootstrap
11
+ - product_feature
12
+ - product_bugfix
13
+ - template_maintenance
14
+ - project_policy_change
15
+ - harness_update
16
+ - knowledge_maintenance
17
+ - archive_maintenance
18
+
19
+ task_types:
20
+ module_bootstrap:
21
+ purpose: Create module workspace only.
22
+ allowed_capabilities:
23
+ - file.write.folder_skeleton
24
+ - file.write.task_artifact
25
+ denied_capabilities:
26
+ - file.write.module_executable_source
27
+ - tests.create
28
+ - fixtures.create
29
+ - package.install
30
+ - network.live_target
31
+ - git.commit
32
+ - git.push
33
+ allowed_files:
34
+ - directories
35
+ - .gitkeep
36
+ - MODULE.md
37
+ - README.md
38
+ - module-state.md
39
+ denied_files:
40
+ - "*.py"
41
+ - "*.ts"
42
+ - "*.js"
43
+ - "*.ps1"
44
+ - "*.sh"
45
+ - tests/**
46
+ - fixtures/**
47
+ - package.json
48
+ - pyproject.toml
49
+ - executable runtime config
50
+
51
+ product_feature:
52
+ purpose: Implement approved vertical slice inside approved module paths.
53
+ requires:
54
+ - grill
55
+ - research
56
+ - prd
57
+ - issues
58
+ - module_structure
59
+ - writing_plan
60
+ - plan_review
61
+ - implementation
62
+ default_root: modules/
63
+
64
+ template_maintenance:
65
+ purpose: Change harness-core rules/contracts/installer/plugins.
66
+ requires:
67
+ - proposal_approved
68
+ - separate_template_maintenance_checkout
69
+ forbidden:
70
+ - product_code
71
+ - project_domain_solution_as_core_rule_without_proposal
72
+
73
+ project_policy_change:
74
+ purpose: Change project-local policy only.
75
+ requires:
76
+ - project_policy_proposal
77
+ forbidden:
78
+ - harness_core_rule_change
79
+
80
+ projections:
81
+ task_yaml:
82
+ status: derived_cache
83
+ may_be_regenerated: true
84
+ cannot_grant_permission: true
85
+ gate_ledger_md:
86
+ status: human_projection
87
+ may_be_regenerated: true
88
+ cannot_grant_permission_without_events: true
@@ -0,0 +1 @@
1
+ # keep
@@ -0,0 +1,33 @@
1
+ # ADR: Use 짬뽕하네스 Planning Gate
2
+
3
+ ## Status
4
+
5
+ Accepted after proposal approval.
6
+
7
+ ## Context
8
+
9
+ The previous harness centered planning on OuroSuper interview, Seed YAML, and handoff-packet concepts. That flow made the harness depend on a planning runtime and produced confusion around missing ambiguity scores, duplicated handoff meanings, and scattered AI artifacts.
10
+
11
+ ## Decision
12
+
13
+ Use 짬뽕하네스 as the harness workflow. The planning gate combines Matt Pocock planning skills, Superpowers writing plans, gstack review, Compound Engineering learning, and vowline discipline.
14
+
15
+ AI task artifacts live under `harness/docs/tasks/active/<slug>/` while in progress and move to `harness/docs/tasks/archive/<slug>/` after completion by default. Long-lived reusable learning lives under `harness/docs/solutions/`. `harness/state/` stores only small pointer/status files.
16
+
17
+ ## Alternatives Considered
18
+
19
+ - Keep OuroSuper as the planning engine.
20
+ - Store AI artifacts under root `docs/`.
21
+ - Put reusable solutions under `harness/state/`.
22
+ - Restore a small-task bypass.
23
+
24
+ ## Consequences
25
+
26
+ - The workflow is more explicit and easier to audit.
27
+ - Every task creates more planning artifacts.
28
+ - The user reviews one active task folder during work, then mostly works from `modules/` after approval.
29
+ - Root `handoff.md` keeps its original next-chat role.
30
+
31
+ ## Rollback
32
+
33
+ Reintroduce a proposal that restores the previous workflow and updates `harness/rules/`, README, and AGENTS.md together. Do not partially restore OuroSuper terms without restoring the full old workflow.
@@ -0,0 +1,14 @@
1
+ # Domain Docs
2
+
3
+ This harness uses a single-context documentation layout.
4
+
5
+ Required files:
6
+
7
+ - Root glossary: `CONTEXT.md`
8
+ - Architecture decisions: `harness/docs/adr/`
9
+
10
+ Consumer rules:
11
+
12
+ - `CONTEXT.md` is a glossary, not a PRD or scratch pad.
13
+ - ADRs are for decisions that are hard to reverse, surprising without context, and chosen after a real trade-off.
14
+ - Do not create ADRs for routine implementation details.
@@ -0,0 +1,9 @@
1
+ # Issue Tracker
2
+
3
+ This harness uses local markdown task issues by default.
4
+
5
+ Issues live under `harness/docs/tasks/active/<YYYY-MM-DD-short-topic>/issues/`.
6
+
7
+ Each issue is a vertical slice that can be reviewed or implemented independently.
8
+
9
+ Do not publish issues to GitHub Issues unless the user explicitly asks for that in the current chat.
@@ -0,0 +1,60 @@
1
+ # Matt Pocock Skills
2
+
3
+ 짬뽕하네스 requires these external Matt Pocock skills:
4
+
5
+ - `setup-matt-pocock-skills`
6
+ - `grill-with-docs`
7
+ - `to-prd`
8
+ - `to-issues`
9
+
10
+ ## Readiness Check
11
+
12
+ Before the planning gate continues, the agent must verify that the required skills are available in the current Codex skill list or installed skill directories.
13
+
14
+ Use this readiness command in PowerShell when the visible skill list is unavailable:
15
+
16
+ ```powershell
17
+ $required = @(
18
+ 'setup-matt-pocock-skills',
19
+ 'grill-with-docs',
20
+ 'to-prd',
21
+ 'to-issues'
22
+ )
23
+ $roots = @(
24
+ (Join-Path $env:USERPROFILE '.codex/skills'),
25
+ (Join-Path $env:USERPROFILE '.agents/skills')
26
+ )
27
+ $skillFiles = foreach ($root in $roots) {
28
+ if (Test-Path -LiteralPath $root) {
29
+ Get-ChildItem -LiteralPath $root -Recurse -Filter 'SKILL.md' -ErrorAction SilentlyContinue
30
+ }
31
+ }
32
+ $missing = foreach ($skill in $required) {
33
+ $found = $skillFiles | Where-Object {
34
+ Select-String -LiteralPath $_.FullName -Pattern "name: $skill" -Quiet
35
+ }
36
+ if (-not $found) { $skill }
37
+ }
38
+ if ($missing) {
39
+ $missing
40
+ throw "Missing required Matt Pocock planning skills"
41
+ }
42
+ "OK: all Matt Pocock planning skills available"
43
+ ```
44
+
45
+ If any required skill is unavailable, stop and tell the user:
46
+
47
+ ```text
48
+ Matt Pocock planning skills are required before this harness can continue.
49
+ Install them first, then rerun this task.
50
+ ```
51
+
52
+ ## Install Reference
53
+
54
+ Use the official installer when available:
55
+
56
+ ```bash
57
+ npx skills@latest add mattpocock/skills
58
+ ```
59
+
60
+ Do not vendor-copy the skill files into this repository.
@@ -0,0 +1,11 @@
1
+ # Triage Labels
2
+
3
+ Local markdown issues use these canonical roles:
4
+
5
+ - `needs-triage` - maintainer needs to evaluate
6
+ - `needs-info` - waiting on user input
7
+ - `ready-for-agent` - ready for AFK agent work
8
+ - `ready-for-human` - requires human judgment or external access
9
+ - `wontfix` - will not be actioned
10
+
11
+ For local markdown issues, record the role in the issue body instead of applying a GitHub label.
@@ -0,0 +1 @@
1
+ # keep
@@ -0,0 +1 @@
1
+ # keep
@@ -0,0 +1 @@
1
+ # keep
@@ -0,0 +1,5 @@
1
+ # Archive Index
2
+
3
+ Archive is cold context.
4
+
5
+ Read `archive-summary.md` first before opening detailed artifacts.
@@ -0,0 +1,13 @@
1
+ # Task Index
2
+
3
+ Active tasks are read directly from `harness/docs/tasks/active/`.
4
+
5
+ Archived tasks are cold context. Read archive indexes and summaries before opening detailed artifacts.
6
+
7
+ ## Active
8
+
9
+ - See `active/`
10
+
11
+ ## Archive
12
+
13
+ - See `archive/`
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { verifyRoot } = require('../verify/verify');
7
+
8
+ function parseArgs(argv) {
9
+ const args = {};
10
+ for (let i = 0; i < argv.length; i += 1) {
11
+ const token = argv[i];
12
+ if (!token.startsWith('--')) continue;
13
+ const key = token.slice(2);
14
+ const next = argv[i + 1];
15
+ if (!next || next.startsWith('--')) {
16
+ args[key] = true;
17
+ } else {
18
+ args[key] = next;
19
+ i += 1;
20
+ }
21
+ }
22
+ return args;
23
+ }
24
+
25
+ function nextActionFor(failure) {
26
+ const table = {
27
+ missing_root_item: 'Run installer or restore the missing root file before planning.',
28
+ missing_contract: 'Restore harness/contracts from the release package.',
29
+ nested_harness_folder: 'Move harness files to the project root; do not keep a nested template clone.',
30
+ secret_file_present: 'Do not read the value. Move/ignore the secret outside publishable harness scope.',
31
+ harness_lock_missing_field: 'Regenerate harness.lock.yaml from installer receipt data.',
32
+ planning_started_during_install: 'Treat install as contaminated; create a repair proposal and reset install-only state.',
33
+ github_repo_created_during_install: 'Record as policy violation; future installer defaults must not create remote repos.',
34
+ commit_created_during_install: 'Stop and ask for git cleanup instructions; do not rewrite history automatically.',
35
+ push_performed_during_install: 'Stop and ask for remote cleanup instructions; do not mutate remote automatically.',
36
+ active_task_single_default: 'Archive, cancel, or explicitly approve parallel active task handling.',
37
+ projection_without_canonical_event: 'Regenerate projections from events.jsonl or ask the user for explicit approval again.',
38
+ event_hash_chain_broken: 'Stop using this task state until the event log is audited; do not invent missing approvals.',
39
+ event_required_fields: 'Repair event log only from trusted source events.',
40
+ };
41
+ return table[failure.id] || 'Create a proposal explaining the required safe repair.';
42
+ }
43
+
44
+ function createProposal(root, verifyResult) {
45
+ const proposalDir = path.join(root, 'proposals', 'active');
46
+ fs.mkdirSync(proposalDir, { recursive: true });
47
+ const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, 'Z');
48
+ const proposalPath = path.join(proposalDir, `doctor-fix-${stamp}.md`);
49
+ const lines = [
50
+ '# Doctor Repair Proposal',
51
+ '',
52
+ 'This proposal was generated by `harness/doctor/doctor.js --proposal`.',
53
+ 'It does not apply live repairs.',
54
+ '',
55
+ '## Failures',
56
+ '',
57
+ ];
58
+ for (const failure of verifyResult.failures) {
59
+ lines.push(`- ${failure.id}: ${failure.message}`);
60
+ lines.push(` - Suggested next action: ${nextActionFor(failure)}`);
61
+ }
62
+ lines.push('');
63
+ lines.push('## Required Approval');
64
+ lines.push('');
65
+ lines.push('A user must approve any live file change separately. This proposal does not grant implementation, git, package, network, or harness-core write permission.');
66
+ fs.writeFileSync(proposalPath, `${lines.join('\n')}\n`, 'utf8');
67
+ return proposalPath;
68
+ }
69
+
70
+ function diagnose(root, options = {}) {
71
+ const result = verifyRoot(root);
72
+ const diagnosis = {
73
+ ok: result.ok,
74
+ root: result.root,
75
+ failures: result.failures.map((failure) => ({
76
+ ...failure,
77
+ next_action: nextActionFor(failure),
78
+ })),
79
+ warnings: result.warnings,
80
+ proposal_path: null,
81
+ };
82
+ if (!result.ok && options.proposal) {
83
+ diagnosis.proposal_path = createProposal(result.root, result);
84
+ }
85
+ return diagnosis;
86
+ }
87
+
88
+ function main() {
89
+ const args = parseArgs(process.argv.slice(2));
90
+ const root = path.resolve(args.root || process.cwd());
91
+ const result = diagnose(root, { proposal: Boolean(args.proposal) });
92
+ if (args.json) {
93
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
94
+ } else if (result.ok) {
95
+ process.stdout.write(`doctor found no P0 issues for ${result.root}\n`);
96
+ } else {
97
+ process.stdout.write(`doctor found ${result.failures.length} issue(s) for ${result.root}\n`);
98
+ for (const failure of result.failures) {
99
+ process.stdout.write(`- ${failure.id}: ${failure.next_action}\n`);
100
+ }
101
+ if (result.proposal_path) {
102
+ process.stdout.write(`proposal: ${result.proposal_path}\n`);
103
+ }
104
+ }
105
+ process.exitCode = result.ok ? 0 : 1;
106
+ }
107
+
108
+ if (require.main === module) {
109
+ main();
110
+ }
111
+
112
+ module.exports = {
113
+ diagnose,
114
+ };