leerness 1.8.0 → 1.9.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/CHANGELOG.md +96 -0
- package/LICENSE +1 -1
- package/README.md +94 -56
- package/bin/harness.js +910 -245
- package/docs/PUBLISH_PRECHECK.md +93 -0
- package/package.json +26 -23
- package/scripts/e2e.js +158 -0
- package/docs/AX_CONSISTENCY_GUIDE.md +0 -9
- package/docs/AX_MIGRATION_GUIDE.md +0 -9
- package/docs/AX_PLAN_GUIDE.md +0 -9
- package/harness.js +0 -2
- package/skill-packs/ads-analytics/README.md +0 -6
- package/skill-packs/ai-verified-skill-publisher/README.md +0 -6
- package/skill-packs/appstore-review/README.md +0 -6
- package/skill-packs/commerce-api/README.md +0 -6
- package/skill-packs/crawling/README.md +0 -6
- package/skill-packs/firebase/README.md +0 -6
- package/skill-packs/office/README.md +0 -6
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Publish Pre-check (leerness 1.9.0)
|
|
2
|
+
|
|
3
|
+
> ⚠ **Owner 권한 필수**: npm `leerness`의 메인테이너는 `gytlrgpfl <gytlrgpfl96@gmail.com>` 입니다. 이 계정으로 로그인되어 있거나 collaborator로 등록되어 있어야 publish가 통과합니다. 권한이 없으면 `E403 Forbidden`이 떨어집니다.
|
|
4
|
+
|
|
5
|
+
## 1. 메타데이터 보강 (선택)
|
|
6
|
+
|
|
7
|
+
`package.json`의 다음 필드는 비어 있거나 일반적입니다. 사용자 정보로 채우는 것을 권장합니다.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"author": "leerness contributors",
|
|
12
|
+
"repository": { /* 비어있음 */ },
|
|
13
|
+
"bugs": { /* 비어있음 */ },
|
|
14
|
+
"homepage": ""
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
예:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
"author": "Your Name <you@example.com>",
|
|
22
|
+
"repository": { "type": "git", "url": "git+https://github.com/<user>/leerness.git" },
|
|
23
|
+
"bugs": { "url": "https://github.com/<user>/leerness/issues" },
|
|
24
|
+
"homepage": "https://github.com/<user>/leerness#readme"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
publish 필수는 아니지만, npm 페이지 신뢰도와 사용자 경험을 위해 채워두면 좋습니다.
|
|
28
|
+
|
|
29
|
+
## 2. 권한 확인
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm whoami
|
|
33
|
+
# → 응답이 leerness의 owner인지 확인
|
|
34
|
+
npm owner ls leerness
|
|
35
|
+
# → 자신의 계정이 목록에 있는지 확인
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
권한이 없으면 collaborator 추가를 owner에게 요청해야 합니다.
|
|
39
|
+
|
|
40
|
+
## 3. 로컬 검증
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
node ./bin/harness.js --version # → 1.9.0
|
|
44
|
+
npm pack --dry-run # 패키지 내용 미리보기
|
|
45
|
+
node ./scripts/e2e.js # 30+개 시나리오 통과
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 4. 1.8.0 → 1.9.0 자동 업그레이드 시연 (선택)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
mkdir /tmp/lr-old; cd /tmp/lr-old
|
|
52
|
+
npx -y leerness@1.8.0 init . --language ko --skills recommended
|
|
53
|
+
LEERNESS_OFFLINE=1 npx -y -p /path/to/leerness-1.9.0.tgz leerness update . --yes
|
|
54
|
+
cat .harness/HARNESS_VERSION # → 1.9.0
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 5. publish dry-run
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm publish --dry-run
|
|
61
|
+
# 또는
|
|
62
|
+
npm publish leerness-1.9.0.tgz --dry-run
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
확인:
|
|
66
|
+
- `package.json#files`와 실제 tarball 내용 일치
|
|
67
|
+
- 총 크기 (≈30~40 kB)
|
|
68
|
+
- bin → harness.js 매핑
|
|
69
|
+
|
|
70
|
+
## 6. 실 publish
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm login # 권한 있는 계정으로
|
|
74
|
+
npm publish --access public
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 7. publish 후 검증
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm view leerness version # → 1.9.0
|
|
81
|
+
npx -y leerness@latest --version # → 1.9.0
|
|
82
|
+
mkdir test-install && cd test-install
|
|
83
|
+
npx -y leerness@latest init . --yes --language ko --skills recommended
|
|
84
|
+
npx -y leerness@latest verify .
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 8. 롤백
|
|
88
|
+
|
|
89
|
+
24시간 이내라면 `npm unpublish leerness@1.9.0` 가능. 이후엔 1.9.1 패치 또는 deprecate.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm deprecate leerness@1.9.0 "Use 1.9.1+"
|
|
93
|
+
```
|
package/package.json
CHANGED
|
@@ -1,49 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Leerness: 비파괴 마이그레이션,
|
|
3
|
+
"version": "1.9.1",
|
|
4
|
+
"description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
|
7
7
|
"ai",
|
|
8
8
|
"agent",
|
|
9
9
|
"harness",
|
|
10
10
|
"context-engineering",
|
|
11
|
+
"claude",
|
|
12
|
+
"claude-code",
|
|
13
|
+
"cursor",
|
|
14
|
+
"copilot",
|
|
11
15
|
"skill-library",
|
|
12
16
|
"project-memory",
|
|
13
17
|
"task-tracking",
|
|
14
18
|
"planning",
|
|
19
|
+
"handoff",
|
|
20
|
+
"anti-laziness",
|
|
21
|
+
"encoding",
|
|
22
|
+
"secret-scan",
|
|
23
|
+
"auto-update",
|
|
15
24
|
"design-system",
|
|
16
|
-
"developer-tools"
|
|
25
|
+
"developer-tools",
|
|
26
|
+
"korean"
|
|
17
27
|
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"author": "leerness contributors",
|
|
30
|
+
"type": "commonjs",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
18
34
|
"bin": {
|
|
19
|
-
"leerness": "
|
|
35
|
+
"leerness": "bin/harness.js"
|
|
20
36
|
},
|
|
21
37
|
"files": [
|
|
22
|
-
"bin
|
|
23
|
-
"
|
|
24
|
-
"docs
|
|
25
|
-
"harness.js",
|
|
38
|
+
"bin",
|
|
39
|
+
"scripts",
|
|
40
|
+
"docs",
|
|
26
41
|
"README.md",
|
|
42
|
+
"CHANGELOG.md",
|
|
27
43
|
"LICENSE"
|
|
28
44
|
],
|
|
29
45
|
"scripts": {
|
|
30
|
-
"test": "node ./bin/harness.js --
|
|
46
|
+
"test": "node ./bin/harness.js --version && node ./scripts/e2e.js",
|
|
47
|
+
"test:smoke": "node ./scripts/e2e.js",
|
|
31
48
|
"prepack": "node ./bin/harness.js --version"
|
|
32
49
|
},
|
|
33
|
-
"repository": {
|
|
34
|
-
"type": "git",
|
|
35
|
-
"url": "git+https://github.com/gugu9999gu/leerness.git"
|
|
36
|
-
},
|
|
37
|
-
"bugs": {
|
|
38
|
-
"url": "https://github.com/gugu9999gu/leerness/issues"
|
|
39
|
-
},
|
|
40
|
-
"homepage": "https://github.com/gugu9999gu/leerness#readme",
|
|
41
|
-
"author": "gugu9999gu",
|
|
42
|
-
"license": "MIT",
|
|
43
|
-
"type": "commonjs",
|
|
44
|
-
"engines": {
|
|
45
|
-
"node": ">=18"
|
|
46
|
-
},
|
|
47
50
|
"publishConfig": {
|
|
48
51
|
"access": "public"
|
|
49
52
|
}
|
package/scripts/e2e.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const cp = require('child_process');
|
|
8
|
+
|
|
9
|
+
const CLI = path.resolve(__dirname, '..', 'bin', 'harness.js');
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-e2e-'));
|
|
11
|
+
let failed = 0; let total = 0;
|
|
12
|
+
|
|
13
|
+
function run(label, args, opts = {}) {
|
|
14
|
+
total++;
|
|
15
|
+
const r = cp.spawnSync(process.execPath, [CLI, ...args], { cwd: opts.cwd || tmp, encoding: 'utf8' });
|
|
16
|
+
const ok = (r.status === 0) === !opts.expectFail;
|
|
17
|
+
process.stdout.write(`${ok ? '✓' : '✗'} ${label} (exit=${r.status})\n`);
|
|
18
|
+
if (!ok) { failed++; process.stdout.write(r.stdout || ''); process.stderr.write(r.stderr || ''); }
|
|
19
|
+
return { ok, stdout: r.stdout || '', stderr: r.stderr || '', status: r.status };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`# leerness e2e smoke @ ${tmp}`);
|
|
23
|
+
|
|
24
|
+
run('init', ['init', tmp, '--yes', '--language', 'ko', '--skills', 'recommended']);
|
|
25
|
+
run('status', ['status', tmp]);
|
|
26
|
+
run('verify', ['verify', tmp]);
|
|
27
|
+
run('debug', ['debug', tmp]);
|
|
28
|
+
run('check', ['check', tmp]);
|
|
29
|
+
run('audit', ['audit', tmp]);
|
|
30
|
+
run('scan secrets', ['scan', 'secrets', tmp]);
|
|
31
|
+
run('encoding check', ['encoding', 'check', tmp]);
|
|
32
|
+
|
|
33
|
+
const secretFile = path.join(tmp, 'fake-config.json');
|
|
34
|
+
fs.writeFileSync(secretFile, JSON.stringify({ openai: 'sk-' + 'A'.repeat(48) }));
|
|
35
|
+
run('scan secrets (detect)', ['scan', 'secrets', tmp], { expectFail: true });
|
|
36
|
+
fs.unlinkSync(secretFile);
|
|
37
|
+
|
|
38
|
+
run('plan add 1', ['plan', 'add', '마일스톤 A', '--status', 'planned', '--path', tmp]);
|
|
39
|
+
run('plan add 2', ['plan', 'add', '마일스톤 B', '--status', 'in-progress', '--path', tmp]);
|
|
40
|
+
run('task add', ['task', 'add', '사용자 요청 X', '--status', 'requested', '--path', tmp]);
|
|
41
|
+
run('task update T-0001', ['task', 'update', 'T-0001', '--status', 'in-progress', '--next', '검증 실행', '--path', tmp]);
|
|
42
|
+
run('task update T-0001 done', ['task', 'update', 'T-0001', '--status', 'done', '--evidence', 'review-evidence:e2e', '--path', tmp]);
|
|
43
|
+
|
|
44
|
+
const tracker = fs.readFileSync(path.join(tmp, '.harness/progress-tracker.md'), 'utf8');
|
|
45
|
+
const t1Count = (tracker.match(/^\| T-0001 \|/gm) || []).length;
|
|
46
|
+
total++;
|
|
47
|
+
if (t1Count === 1) console.log('✓ B1 in-place update: T-0001 row count = 1');
|
|
48
|
+
else { failed++; console.log(`✗ B1 in-place update FAILED: T-0001 row count = ${t1Count} (expected 1)`); }
|
|
49
|
+
|
|
50
|
+
fs.appendFileSync(path.join(tmp, '.harness/review-evidence.md'),
|
|
51
|
+
'\n## e2e\nTask: T-0001\nCommand: npm test\nExit: 0\nNote: e2e smoke\n');
|
|
52
|
+
|
|
53
|
+
run('session close', ['session', 'close', tmp]);
|
|
54
|
+
run('handoff', ['handoff', tmp]);
|
|
55
|
+
run('audit (post close)', ['audit', tmp]);
|
|
56
|
+
run('lazy detect', ['lazy', 'detect', tmp]);
|
|
57
|
+
run('memory search', ['memory', 'search', '마일스톤', '--path', tmp]);
|
|
58
|
+
|
|
59
|
+
const offline = Object.assign({}, process.env, { LEERNESS_OFFLINE: '1' });
|
|
60
|
+
total++;
|
|
61
|
+
{
|
|
62
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'update', tmp, '--check'], { env: offline, encoding: 'utf8' });
|
|
63
|
+
const ok = r.status === 0 && /up to date|migration available/.test(r.stdout || '');
|
|
64
|
+
console.log(ok ? '✓ update --check (offline)' : `✗ update --check (offline) exit=${r.status}`);
|
|
65
|
+
if (!ok) { failed++; console.log(r.stdout || ''); console.error(r.stderr || ''); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
run('auto-update install', ['auto-update', 'install', tmp]);
|
|
69
|
+
total++;
|
|
70
|
+
{
|
|
71
|
+
const settingsFile = path.join(tmp, '.claude/settings.local.json');
|
|
72
|
+
if (fs.existsSync(settingsFile)) {
|
|
73
|
+
const s = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
74
|
+
const hasHook = (s.hooks?.SessionStart || []).some(h => h.command && h.command.includes('leerness update'));
|
|
75
|
+
if (hasHook) console.log('✓ SessionStart hook for update --check installed');
|
|
76
|
+
else { failed++; console.log('✗ SessionStart hook missing'); }
|
|
77
|
+
} else { failed++; console.log('✗ .claude/settings.local.json missing'); }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 1.9.1 P6 회귀: evidence가 plan:M-XXXX + 검증 키워드면 lazy detect가 통과해야 한다.
|
|
81
|
+
total++;
|
|
82
|
+
{
|
|
83
|
+
const trackerPath = path.join(tmp, '.harness/progress-tracker.md');
|
|
84
|
+
let cur = fs.readFileSync(trackerPath, 'utf8');
|
|
85
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | tests:32/32 (plan:M-0002) | next | 2026-05-08 |');
|
|
86
|
+
fs.writeFileSync(trackerPath, cur, 'utf8');
|
|
87
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lazy', 'detect', tmp], { encoding: 'utf8' });
|
|
88
|
+
const ok = r.status === 0;
|
|
89
|
+
console.log(ok ? '✓ B(P6) lazy detect: plan:M-XXXX + 검증 키워드 → pass' : `✗ B(P6) FAIL exit=${r.status}\n${r.stdout}`);
|
|
90
|
+
if (!ok) failed++;
|
|
91
|
+
}
|
|
92
|
+
// 1.9.1 P6 negative: plan:M-XXXX 단독은 여전히 경고
|
|
93
|
+
total++;
|
|
94
|
+
{
|
|
95
|
+
const trackerPath = path.join(tmp, '.harness/progress-tracker.md');
|
|
96
|
+
let cur = fs.readFileSync(trackerPath, 'utf8');
|
|
97
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | plan:M-0002 | next | 2026-05-08 |');
|
|
98
|
+
fs.writeFileSync(trackerPath, cur, 'utf8');
|
|
99
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lazy', 'detect', tmp], { encoding: 'utf8' });
|
|
100
|
+
const ok = r.status === 1 && /done row without verifiable evidence/.test(r.stdout);
|
|
101
|
+
console.log(ok ? '✓ B(P6 neg) lazy detect: plan:M-XXXX 단독 → warn 유지' : `✗ B(P6 neg) FAIL exit=${r.status}`);
|
|
102
|
+
if (!ok) failed++;
|
|
103
|
+
// restore
|
|
104
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | tests:32/32 (plan:M-0002) | next | 2026-05-08 |');
|
|
105
|
+
fs.writeFileSync(trackerPath, cur, 'utf8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 1.9.1 P1 회귀: legacy leerness-plus hook이 있으면 auto-update install이 정리해야 한다.
|
|
109
|
+
total++;
|
|
110
|
+
{
|
|
111
|
+
const settingsFile = path.join(tmp, '.claude/settings.local.json');
|
|
112
|
+
const s = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
113
|
+
s.hooks.SessionStart = [{ matcher: '*', command: 'leerness-plus update --check' }];
|
|
114
|
+
fs.writeFileSync(settingsFile, JSON.stringify(s, null, 2));
|
|
115
|
+
cp.spawnSync(process.execPath, [CLI, 'auto-update', 'install', tmp], { encoding: 'utf8' });
|
|
116
|
+
const after = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
117
|
+
const hasLegacy = (after.hooks?.SessionStart || []).some(h => /leerness-plus update/.test(h.command || ''));
|
|
118
|
+
const hasNew = (after.hooks?.SessionStart || []).some(h => /\bleerness update\b/.test(h.command || ''));
|
|
119
|
+
const ok = !hasLegacy && hasNew;
|
|
120
|
+
console.log(ok ? '✓ B(P1) legacy leerness-plus hook 자동 제거' : '✗ B(P1) legacy hook 남음');
|
|
121
|
+
if (!ok) failed++;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 1.9.1 P4 회귀: NA 마커가 있으면 audit이 placeholder 경고를 스킵.
|
|
125
|
+
total++;
|
|
126
|
+
{
|
|
127
|
+
const ds = path.join(tmp, '.harness/design-system.md');
|
|
128
|
+
fs.appendFileSync(ds, '\n<!-- leerness:na CLI 프로젝트라 디자인 토큰 없음 -->\n');
|
|
129
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'audit', tmp], { encoding: 'utf8' });
|
|
130
|
+
const ok = !/design-system\.md tokens not customized/.test(r.stdout) && /marked NA/.test(r.stdout);
|
|
131
|
+
console.log(ok ? '✓ B(P4) NA 마커 인식: design-system 경고 스킵' : '✗ B(P4) NA 마커 미작동');
|
|
132
|
+
if (!ok) failed++;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 1.9.1 P7 회귀: init 직후 progress에 M-0001 연결 row가 자동으로 있다.
|
|
136
|
+
total++;
|
|
137
|
+
{
|
|
138
|
+
const tmp2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-e2e-p7-'));
|
|
139
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmp2, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8' });
|
|
140
|
+
const tracker = fs.readFileSync(path.join(tmp2, '.harness/progress-tracker.md'), 'utf8');
|
|
141
|
+
const ok = /M-0001/.test(tracker);
|
|
142
|
+
console.log(ok ? '✓ B(P7) init: M-0001 → progress row 자동 생성' : '✗ B(P7) progress에 M-0001 row 없음');
|
|
143
|
+
if (!ok) failed++;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
run('viewwork install', ['viewwork', 'install', tmp]);
|
|
147
|
+
run('viewwork emit', ['viewwork', 'emit', tmp, '--action', 'note', '--note', 'e2e ping']);
|
|
148
|
+
run('route planning', ['route', 'planning']);
|
|
149
|
+
run('route bugfix', ['route', 'bugfix']);
|
|
150
|
+
run('skill list', ['skill', 'list']);
|
|
151
|
+
run('skill info', ['skill', 'info', 'office']);
|
|
152
|
+
run('readme sync', ['readme', 'sync', tmp]);
|
|
153
|
+
run('consistency check', ['consistency', 'check', tmp]);
|
|
154
|
+
run('--version', ['--version']);
|
|
155
|
+
run('--help', ['--help']);
|
|
156
|
+
|
|
157
|
+
console.log(`\nE2E result: ${total - failed}/${total} passed`);
|
|
158
|
+
if (failed > 0) process.exit(1);
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# AX Migration Guide
|
|
2
|
-
|
|
3
|
-
마이그레이션은 비파괴가 원칙입니다.
|
|
4
|
-
|
|
5
|
-
- 기존 문서는 먼저 `.harness/archive/`에 보존합니다.
|
|
6
|
-
- `.env.example`과 `.gitignore`는 덮어쓰기보다 병합합니다.
|
|
7
|
-
- `.harness/`와 관련 지침 파일은 삭제하지 않습니다.
|
|
8
|
-
- 기존 designguide 계열 문서는 `.harness/design-system.md`로 병합합니다.
|
|
9
|
-
- 마이그레이션 후 `leerness verify .`, `leerness debug .`, `leerness session close .`를 실행합니다.
|
package/docs/AX_PLAN_GUIDE.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# AX Plan Guide
|
|
2
|
-
|
|
3
|
-
AI 에이전트는 사용자 요청을 받으면 먼저 `plan.md`의 범위와 `progress-tracker.md`의 상태를 확인합니다.
|
|
4
|
-
|
|
5
|
-
1. 기존 계획의 일부인지 확인합니다.
|
|
6
|
-
2. 새 범위면 `plan.md`와 `progress-tracker.md`에 추가합니다.
|
|
7
|
-
3. 사용자가 제외한 작업은 삭제하지 않고 `dropped`로 기록합니다.
|
|
8
|
-
4. 완전히 신규 프로젝트라면 구현보다 계획표 작성이 우선입니다.
|
|
9
|
-
5. 작업 종료 시 `session close` 기준으로 완료/진행/미완료/예정/대기/보류/차단/드랍을 표시합니다.
|
package/harness.js
DELETED