@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,12 @@
1
+ # 검증 기록
2
+
3
+ 검증 명령, 결과, 남은 위험을 적습니다.
4
+
5
+ ## 실행한 검증
6
+
7
+ | 명령 | 기대 결과 | 실제 결과 | 통과 여부 |
8
+ | --- | --- | --- | --- |
9
+
10
+ ## 남은 위험
11
+
12
+ - 아직 없음
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const REQUIRED_ROOT_ITEMS = [
8
+ 'AGENTS.md',
9
+ 'README.md',
10
+ 'CONTEXT.md',
11
+ 'handoff.md',
12
+ 'harness',
13
+ 'modules',
14
+ 'module-template',
15
+ 'proposals',
16
+ 'harness.lock.yaml',
17
+ ];
18
+
19
+ const REQUIRED_CONTRACTS = [
20
+ 'harness/contracts/capability-catalog.yaml',
21
+ 'harness/contracts/gate-contract-matrix.yaml',
22
+ 'harness/contracts/ledger-event.schema.yaml',
23
+ 'harness/contracts/permission-decision.schema.yaml',
24
+ 'harness/contracts/path-policy.schema.yaml',
25
+ 'harness/contracts/task.schema.yaml',
26
+ 'harness/contracts/installer-contract.yaml',
27
+ ];
28
+
29
+ const REQUIRED_LOCK_FIELDS = [
30
+ 'planning_started',
31
+ 'github_repo_created',
32
+ 'commit_created',
33
+ 'push_performed',
34
+ 'managed_files',
35
+ ];
36
+
37
+ function parseArgs(argv) {
38
+ const args = {};
39
+ for (let i = 0; i < argv.length; i += 1) {
40
+ const token = argv[i];
41
+ if (!token.startsWith('--')) continue;
42
+ const key = token.slice(2);
43
+ const next = argv[i + 1];
44
+ if (!next || next.startsWith('--')) {
45
+ args[key] = true;
46
+ } else {
47
+ args[key] = next;
48
+ i += 1;
49
+ }
50
+ }
51
+ return args;
52
+ }
53
+
54
+ function readText(filePath) {
55
+ if (!fs.existsSync(filePath)) return '';
56
+ return fs.readFileSync(filePath, 'utf8');
57
+ }
58
+
59
+ function readJsonl(filePath, failures) {
60
+ if (!fs.existsSync(filePath)) return [];
61
+ const lines = readText(filePath).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
62
+ const events = [];
63
+ lines.forEach((line, index) => {
64
+ try {
65
+ events.push(JSON.parse(line));
66
+ } catch (error) {
67
+ failures.push({
68
+ id: 'invalid_jsonl',
69
+ severity: 'P0',
70
+ message: `Invalid JSONL in ${filePath}:${index + 1}: ${error.message}`,
71
+ });
72
+ }
73
+ });
74
+ return events;
75
+ }
76
+
77
+ function yamlScalar(text, key) {
78
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
79
+ const match = text.match(new RegExp(`^\\s*${escaped}:\\s*(.+?)\\s*$`, 'm'));
80
+ return match ? match[1].replace(/^["']|["']$/g, '') : null;
81
+ }
82
+
83
+ function yamlBoolean(text, key) {
84
+ const value = yamlScalar(text, key);
85
+ if (value === null) return null;
86
+ if (/^true$/i.test(value)) return true;
87
+ if (/^false$/i.test(value)) return false;
88
+ return value;
89
+ }
90
+
91
+ function listDirectories(dirPath) {
92
+ if (!fs.existsSync(dirPath)) return [];
93
+ return fs.readdirSync(dirPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
94
+ }
95
+
96
+ function verifyEventHashChain(events, taskName, failures) {
97
+ for (let i = 0; i < events.length; i += 1) {
98
+ const event = events[i];
99
+ if (!event.event_id || !event.event_type || !event.event_hash) {
100
+ failures.push({
101
+ id: 'event_required_fields',
102
+ severity: 'P0',
103
+ message: `Task ${taskName} has an event missing event_id/event_type/event_hash.`,
104
+ });
105
+ }
106
+ if (i > 0 && event.previous_hash !== events[i - 1].event_hash) {
107
+ failures.push({
108
+ id: 'event_hash_chain_broken',
109
+ severity: 'P0',
110
+ message: `Task ${taskName} has broken event hash chain at event ${event.event_id || i}.`,
111
+ });
112
+ }
113
+ }
114
+ }
115
+
116
+ function hasApprovalDecision(events) {
117
+ return events.some((event) => {
118
+ const payload = event.payload || {};
119
+ return event.event_type === 'approval_decision' && payload.status === 'approved';
120
+ });
121
+ }
122
+
123
+ function verifyActiveTasks(root, failures, warnings) {
124
+ const activeDir = path.join(root, 'harness', 'docs', 'tasks', 'active');
125
+ if (!fs.existsSync(activeDir)) return;
126
+
127
+ const tasks = listDirectories(activeDir).filter((name) => name !== '.gitkeep');
128
+ if (tasks.length > 1) {
129
+ failures.push({
130
+ id: 'active_task_single_default',
131
+ severity: 'P0',
132
+ message: `Multiple active tasks found without parallel approval: ${tasks.join(', ')}`,
133
+ });
134
+ }
135
+
136
+ for (const task of tasks) {
137
+ const taskDir = path.join(activeDir, task);
138
+ const eventsPath = path.join(taskDir, 'events.jsonl');
139
+ const taskYamlPath = path.join(taskDir, 'task.yaml');
140
+ const events = readJsonl(eventsPath, failures);
141
+ verifyEventHashChain(events, task, failures);
142
+
143
+ if (fs.existsSync(taskYamlPath)) {
144
+ const taskYaml = readText(taskYamlPath);
145
+ if (/(approved|allowed|permission|capabilit)/i.test(taskYaml) && !hasApprovalDecision(events)) {
146
+ failures.push({
147
+ id: 'projection_without_canonical_event',
148
+ severity: 'P0',
149
+ message: `Task ${task} has task.yaml permission-like state without approval_decision in events.jsonl.`,
150
+ });
151
+ }
152
+ } else {
153
+ warnings.push({
154
+ id: 'active_task_missing_task_yaml',
155
+ severity: 'warning',
156
+ message: `Task ${task} has no task.yaml derived cache.`,
157
+ });
158
+ }
159
+ }
160
+ }
161
+
162
+ function verifyHarnessLock(root, failures) {
163
+ const lockPath = path.join(root, 'harness.lock.yaml');
164
+ const text = readText(lockPath);
165
+ if (!text) return;
166
+
167
+ for (const field of REQUIRED_LOCK_FIELDS) {
168
+ if (!text.includes(field)) {
169
+ failures.push({
170
+ id: 'harness_lock_missing_field',
171
+ severity: 'P0',
172
+ message: `harness.lock.yaml missing required field: ${field}`,
173
+ });
174
+ }
175
+ }
176
+
177
+ for (const field of ['planning_started', 'github_repo_created', 'commit_created', 'push_performed']) {
178
+ const value = yamlBoolean(text, field);
179
+ if (value !== false) {
180
+ failures.push({
181
+ id: `${field}_during_install`,
182
+ severity: 'P0',
183
+ message: `harness.lock.yaml must record ${field}: false for install-only safety.`,
184
+ });
185
+ }
186
+ }
187
+ }
188
+
189
+ function verifyRoot(root) {
190
+ const projectRoot = path.resolve(root);
191
+ const failures = [];
192
+ const warnings = [];
193
+
194
+ for (const item of REQUIRED_ROOT_ITEMS) {
195
+ if (!fs.existsSync(path.join(projectRoot, item))) {
196
+ failures.push({
197
+ id: 'missing_root_item',
198
+ severity: 'P0',
199
+ message: `Missing required root item: ${item}`,
200
+ });
201
+ }
202
+ }
203
+
204
+ for (const contract of REQUIRED_CONTRACTS) {
205
+ if (!fs.existsSync(path.join(projectRoot, contract))) {
206
+ failures.push({
207
+ id: 'missing_contract',
208
+ severity: 'P0',
209
+ message: `Missing contract file: ${contract}`,
210
+ });
211
+ }
212
+ }
213
+
214
+ for (const nested of ['jjamppong-harness', 'ourosuper-harness']) {
215
+ if (fs.existsSync(path.join(projectRoot, nested, 'AGENTS.md'))) {
216
+ failures.push({
217
+ id: 'nested_harness_folder',
218
+ severity: 'P0',
219
+ message: `Forbidden nested harness folder found: ${nested}/`,
220
+ });
221
+ }
222
+ }
223
+
224
+ for (const secretName of ['.env', '.env.local', 'credentials.json', 'service-account.json', 'cookies.txt', 'token.txt']) {
225
+ if (fs.existsSync(path.join(projectRoot, secretName))) {
226
+ failures.push({
227
+ id: 'secret_file_present',
228
+ severity: 'P0',
229
+ message: `Secret-like file present at root and must not be read or published: ${secretName}`,
230
+ });
231
+ }
232
+ }
233
+
234
+ verifyHarnessLock(projectRoot, failures);
235
+ verifyActiveTasks(projectRoot, failures, warnings);
236
+
237
+ return {
238
+ ok: failures.length === 0,
239
+ root: projectRoot,
240
+ failures,
241
+ warnings,
242
+ };
243
+ }
244
+
245
+ function main() {
246
+ const args = parseArgs(process.argv.slice(2));
247
+ const root = args.root || process.cwd();
248
+ const result = verifyRoot(root);
249
+ if (args.json) {
250
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
251
+ } else if (result.ok) {
252
+ process.stdout.write(`verify passed for ${result.root}\n`);
253
+ } else {
254
+ process.stdout.write(`verify failed for ${result.root}\n`);
255
+ for (const failure of result.failures) {
256
+ process.stdout.write(`FAIL ${failure.id}: ${failure.message}\n`);
257
+ }
258
+ for (const warning of result.warnings) {
259
+ process.stdout.write(`WARN ${warning.id}: ${warning.message}\n`);
260
+ }
261
+ }
262
+ process.exitCode = result.ok ? 0 : 1;
263
+ }
264
+
265
+ if (require.main === module) {
266
+ main();
267
+ }
268
+
269
+ module.exports = {
270
+ verifyRoot,
271
+ };
@@ -0,0 +1,25 @@
1
+ # Module Contract
2
+
3
+ This file is copied when a module is created.
4
+
5
+ Before using it in a real module, the module must already be allowed by:
6
+
7
+ ```text
8
+ harness/state/module-structure.md
9
+ ```
10
+
11
+ ## Purpose
12
+
13
+ No module-specific purpose is recorded in the template.
14
+
15
+ ## Boundaries
16
+
17
+ No module-specific boundaries are recorded in the template.
18
+
19
+ ## Allowed Dependencies
20
+
21
+ No module-specific dependencies are recorded in the template.
22
+
23
+ ## Verification
24
+
25
+ No module-specific verification commands are recorded in the template.
@@ -0,0 +1,9 @@
1
+ # Module
2
+
3
+ This README is copied when a module is created.
4
+
5
+ Before writing module content, confirm the module type and folder set in:
6
+
7
+ ```text
8
+ harness/state/module-structure.md
9
+ ```
File without changes
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@donghyeonlee/jjamppong-harness",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Jjamppong Harness safety workflow installer and verifier.",
6
+ "license": "UNLICENSED",
7
+ "type": "commonjs",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vibedong/jjamppong-harness.git"
11
+ },
12
+ "homepage": "https://github.com/vibedong/jjamppong-harness#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/vibedong/jjamppong-harness/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "bin": {
20
+ "jjamppong": "bin/jjamppong.js"
21
+ },
22
+ "files": [
23
+ "AGENTS.md",
24
+ "README.md",
25
+ "CONTEXT.md",
26
+ "handoff.md",
27
+ "bin/",
28
+ "harness/",
29
+ "module-template/",
30
+ "modules/",
31
+ "proposals/",
32
+ "scripts/"
33
+ ],
34
+ "scripts": {
35
+ "test:contracts": "powershell -NoProfile -ExecutionPolicy Bypass -File tests/contracts/run-all.ps1"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ }
40
+ }
@@ -0,0 +1,16 @@
1
+ # Proposals
2
+
3
+ This folder stores unapproved harness and workflow rule change proposals.
4
+
5
+ Project module structure changes do not live here. They go through `harness/rules/module-types.md` and are recorded in `harness/state/module-structure.md`.
6
+
7
+ Codex reads this folder only when the user asks to create, review, approve, or reflect a proposal.
8
+
9
+ Proposal lifecycle:
10
+
11
+ 1. Create proposal after user approval.
12
+ 2. Review proposal with the user.
13
+ 3. Reflect approved changes into live files such as `README.md`, `harness/rules/workflow.md`, `harness/rules/rules.md`, or `harness/rules/module-types.md`.
14
+ 4. Delete the proposal file after reflection.
15
+
16
+ Do not keep approved proposal files as archive.
@@ -0,0 +1,62 @@
1
+ param(
2
+ [Parameter(Mandatory = $true, Position = 0)]
3
+ [string]$TemplateSource,
4
+
5
+ [Parameter(Mandatory = $true, Position = 1)]
6
+ [string]$TargetPath,
7
+
8
+ [string]$ProjectRepo,
9
+ [string]$Owner,
10
+ [switch]$AllowOverwrite,
11
+ [switch]$SkipGitHubRepo
12
+ )
13
+
14
+ Set-StrictMode -Version Latest
15
+ $ErrorActionPreference = 'Stop'
16
+
17
+ function Normalize-TargetPath {
18
+ param([string]$PathText)
19
+
20
+ $text = $PathText.Trim()
21
+ if ($text -match '^[A-Za-z]:[^\\/].+') {
22
+ $text = $text.Substring(0, 2) + [IO.Path]::DirectorySeparatorChar + $text.Substring(2)
23
+ }
24
+ return [IO.Path]::GetFullPath($text)
25
+ }
26
+
27
+ $target = Normalize-TargetPath $TargetPath
28
+ $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
29
+ $templateRoot = (Resolve-Path -LiteralPath (Join-Path $scriptRoot '..')).Path
30
+
31
+ if ($TemplateSource -and (Test-Path -LiteralPath $TemplateSource)) {
32
+ $templateRoot = (Resolve-Path -LiteralPath $TemplateSource).Path
33
+ }
34
+
35
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
36
+ throw 'Node.js is required for the Jjamppong Harness installer.'
37
+ }
38
+
39
+ if (-not $SkipGitHubRepo) {
40
+ Write-Output 'GitHub repo creation is disabled by default. This wrapper will not create remotes, commits, or pushes.'
41
+ }
42
+
43
+ if ($ProjectRepo) {
44
+ Write-Output "ProjectRepo argument was received but not applied by install-only wrapper: $ProjectRepo"
45
+ }
46
+ if ($Owner) {
47
+ Write-Output "Owner argument was received but not used because GitHub repo creation is disabled: $Owner"
48
+ }
49
+ if ($AllowOverwrite) {
50
+ Write-Output 'Existing managed files are backed up before overwrite; install still stops after verification.'
51
+ }
52
+
53
+ $cli = Join-Path $templateRoot 'bin\jjamppong.js'
54
+ if (-not (Test-Path -LiteralPath $cli)) {
55
+ throw "Jjamppong CLI not found at $cli"
56
+ }
57
+
58
+ Write-Output "Installing Jjamppong Harness into $target"
59
+ & node $cli install --target $target --template $templateRoot
60
+ if ($LASTEXITCODE -ne 0) {
61
+ throw "Jjamppong Harness install failed with exit code $LASTEXITCODE"
62
+ }